☰
HSV颜色识别实战:OpenCvSharp中InRange与轮廓框选全流程
2026/10/21 2:45:03 网站建设 项目流程

简介:基于 OpenCvSharp 的 HSV 颜色识别示例工程,专为需要借助 OpenCV 进行颜色处理的 C# 开发者设计,可应用于自动化检测、机器人视觉、物体跟踪等场景。项目以 WinForm 窗体组织,借助 HSV 色彩模型直观展示颜色识别核心流程:读取图像后完成 BGR 到 HSV 的转换,再通过设置色调、饱和度、亮度阈值,调用 InRange 得到掩码,最终从原图中提取目标色区域。压缩包共 59 个文件,包括 C# 源文件、OpenCvSharp 动态库、窗体布局与资源文件、项目配置以及可直接运行的 exe,整体约 35.14MB;开发者既可打开工程查看代码,也能快速运行观察效果。资源还配套颜色区间调参窗口、图像对比与目标显示等功能模块,并附有 HSV 颜色体系示意图,便于理解不同色相与阈值范围的关系。目前已有 1719 人学习下载,适合作为颜色识别入门实践或后续功能扩展的基础。

1. 颜色识别不是「调个 RGB 范围」:为什么我劝你先转 HSV

打开这份「opencv颜色识别」资源包之前,我劝你先接受一个结论:用 OpenCV 做颜色识别,别死磕 RGB 区间,尽早把图像转成 HSV 再设上下限,这是整套代码能稳定跑起来的前提。这个 zip 里的核心工程用 C# 编写,基于 OpenCvSharp,覆盖了 BGR 转 HSV、InRange 生成二值掩码、轮廓提取和框选目标的全流程。它适合两类人:一是刚接触视觉、想在桌面程序里快速做出颜色识别 Demo 的开发者,二是手头项目需要按颜色做分拣、定位、跟踪,却被环境光变化反复折磨的工程师。下面我会从 HSV 三通道的物理含义拆起,把参数选型讲透,再给你可以照抄的 C# 代码和你大概率会踩的坑。

2. HSV 的六个参数先讲清:色相范围、饱和度阈值与 OpenCvSharp 的坐标系

2.1 OpenCvSharp 里的色相只有 0~180,这和设计软件里的 HSV 不是一回事

很多第一次接触颜色识别的人会直接照搬设计软件里的 HSV 数值,比如「红色 0°、绿色 120°、蓝色 240°」,在 OpenCV 里却死活识别不出来。原因很简单:OpenCV 为了把色调塞进一个 8 位无符号整数里,把标准的 0~360° 色相环直接除以 2,所以 OpenCV 里红色大约是 0~10 或 170~180,绿色在 60~80 上下,蓝色在 100~130 左右。

这份资源包里的 Demos 同样遵守 OpenCV 的这一套坐标系,你在 C# 里写Cv2.CvtColor(mat, matHsv, ColorConversionCodes.BGR2HSV)之后,得到的 HSV 三通道范围分别是:

通道取值范围说明
H(色相)0 ~ 180表示色相环上的角度折半,红色绕在 0 附近,所以红色经常要用两段范围
S(饱和度)0 ~ 255颜色偏离灰白的程度,越接近 255 颜色越纯
V(明度)0 ~ 255整体亮度,受环境光和曝光影响最大

调参时候最容易犯的错,是把饱和度 0~100、明度 0~100 这种百分比思维带进来。OpenCvSharp 的Scalar和Mat都是字节图像,S 和 V 直接就是 0~255,不是百分比。如果你按 50% 去写new Scalar(0, 50, 200),实际用的是极低的饱和度阈值,白色、浅灰会被成片误判进来,掩码会糊成一大块。

我一般会先在代码里把鼠标所在像素的 HSV 值打印出来,对着实际画面读数,再落到 InRange 的上下限上。资源包里如果只给了固定的识别参数,那只是给大家验证流程用的,真实项目里的范围必须结合现场光照重新标定。

2.2 C# 里组上下限的两种写法:Scalar 和数组,别混着用

在 C# 里调用Cv2.InRange时,上下限要分别封装成Scalar。我见过有人把数组和 Scalar 混着写,编译能过,运行结果却千奇百怪。这里统一建议用Scalar保存一组 HSV 范围:

// 定义蓝色的 HSV 下界和上界 Scalar lowerBlue = new Scalar(100, 80, 60); Scalar upperBlue = new Scalar(130, 255, 255); Mat hsvImage = new Mat(); Cv2.CvtColor(colorImage, hsvImage, ColorConversionCodes.BGR2HSV); Mat mask = new Mat(); Cv2.InRange(hsvImage, lowerBlue, upperBlue, mask); // 显示掩码,方便直观判断识别效果 Cv2.ImShow("Blue Mask", mask);

这段代码先做了一次颜色空间转换,再用 InRange 遍历 hsvImage 每个像素,同时满足 H、S、V 三个区间才置为白,否则置为黑。逻辑不复杂,但三个参数背后的含义要清楚:S 下限 80 表示我们要排除掉接近灰白的浅蓝;V 下限 60 表示太暗的深蓝区域也不参与识别,这两个下限是抑制噪声的关键。

如果项目里有多种颜色要识别,我会把它封装成一个ColorRange类,把下界和上界对象存进List,而不是在回调里反复 new Scalar。这样后面做配置化、界面滑块联动都方便。

3. 在 C# 里跑通第一个 HSV 识别:从加载图片到输出目标框

3.1 工程里引入 OpenCvSharp 的两种方式,以及我推荐哪一种

拿到这个 zip 之后,第一步是确保工程能编译。代码里到处是OpenCvSharp的命名空间,缺依赖的话错误列表会一片红。网上初始化 OpenCvSharp 项目的主流方式有两种:一是通过 NuGet 搜索OpenCvSharp4和OpenCvSharp4.runtime.win安装;二是把别人打包好的OpenCvSharp.dll和对应 native 依赖直接放到工程目录手动引用。

我在本机测试时倾向于用 NuGet,原因是它能自动带上对应平台的 native 文件,不容易出现「DLL 找不到」的玄学报错。手动手引用也不难,但要注意把包含OpenCvSharpExtern.dll的目录放到输出目录,并且保证 x64 和 x86 的库选得一致。资源包里的源码默认是 Any CPU 还是 x64,你拿到手之后先看一眼项目配置,别一上来就改平台。

3.2 加载图片、转 HSV、出掩码、画轮廓,完整可抄流程

下面这段是资源包最核心的演示逻辑,基于某图像处理 Demo 的流程整理,我用一张实拍图片做输入,输出带目标框的结果图:

using OpenCvSharp; static Mat RunColorDetect(Mat src, Scalar lower, Scalar upper) { // 1. 高斯模糊,降低传感器噪声对 HSV 的干扰 Mat blurred = new Mat(); Cv2.GaussianBlur(src, blurred, new Size(5, 5), 0); // 2. 转 HSV 颜色空间 Mat hsv = new Mat(); Cv2.CvtColor(blurred, hsv, ColorConversionCodes.BGR2HSV); // 3. 生成掩码 Mat mask = new Mat(); Cv2.InRange(hsv, lower, upper, mask); // 4. 形态学操作,去除小斑点 Mat kernel = Cv2.GetStructuringElement(MorphShapes.Rect, new Size(5, 5)); Cv2.MorphologyEx(mask, mask, MorphTypes.Open, kernel); Cv2.MorphologyEx(mask, mask, MorphTypes.Close, kernel); // 5. 找轮廓 Point[][] contours; HierarchyIndex[] hierarchy; Cv2.FindContours(mask, out contours, out hierarchy, RetrievalModes.External, ContourApproximationModes.ApproxSimple); // 6. 画外接矩形 Mat result = src.Clone(); foreach (var contour in contours) { if (contour.Length < 20) continue; // 过滤噪声轮廓 OpenCvSharp.Rect r = Cv2.BoundingRect(contour); Cv2.Rectangle(result, r, new Scalar(0, 255, 0), 2); } return result; }

这段代码里有几个点是实际项目里反复用到的,我说一下为什么要这么写。第 1 步的 GaussianBlur 很多人会省掉,但它对低照度环境里的 HSV 识别帮助很大,因为 H 通道对噪点敏感,先模糊再取色能明显减少掩码上的雪花点。第 4 步的开运算和闭运算组合也很关键:开运算先消除孤立白点,闭运算再填补目标内部的小黑洞,效果比单纯 InRange 干净得多。第 5 步用RetrievalModes.External只取最外层轮廓,避免目标内部纹理产生多个嵌套框。

运行完之后,你应该能看到一个绿色矩形框把目标物体圈住。如果框的位置不对,除非是参数问题,否则先检查 BGR 和 RGB 的顺序——OpenCvSharp 读取出来的 Mat 是 BGR 顺序,很多人在给识别结果上色或者转 Base64 时在这里翻车。

4. 从静态图到实时视频:摄像头帧率掉得厉害,先调这三处

4.1 实时取流的最小框架

静态图识别跑通之后,下一步通常就是挂摄像头。资源包里也放了视频识别的示例,用VideoCapture循环抓帧,再对每一帧做和上一章几乎一样的处理。我在接入摄像头后,遇到的最典型问题不是识别不出颜色,而是画面卡顿、CPU 占用高。

using OpenCvSharp; // 打开默认摄像头,若失败可以依次尝试 1、2 VideoCapture capture = new VideoCapture(0); if (!capture.IsOpened()) { Console.WriteLine("camera open failed"); return; } capture.FrameWidth = 640; capture.FrameHeight = 480; capture.Fps = 30; Mat frame = new Mat(); Mat mask = new Mat(); var lowerBlue = new Scalar(100, 80, 60); var upperBlue = new Scalar(130, 255, 255); while (true) { if (!capture.Read(frame)) break; // 读不到帧,说明摄像头被占用或已断开 Mat hsv = new Mat(); Cv2.CvtColor(frame, hsv, ColorConversionCodes.BGR2HSV); Cv2.InRange(hsv, lowerBlue, upperBlue, mask); // 这里可以做形态学处理,再 FindContours Cv2.ImShow("Frame", frame); Cv2.ImShow("Mask", mask); int key = Cv2.WaitKey(30); if (key == 27) break; // ESC 退出 }

这段代码里面,FrameWidth、FrameHeight和Fps设置了摄像头采集分辨率。我建议实时识别先别用 1920x1080,分辨率越高,每一帧 CvtColor 和 InRange 的计算量越大,帧率直线往下掉。先降到 640x480,识别效果肉眼基本无差别,但 CPU 占用能降下来一大截。

4.2 实时调参的正确姿势:把 HSV 上限下限做成 Trackbar

实时视频里你不可能改一次参数就重新编译一次,那样太折腾了。资源包如果带调参窗口就用它的滑块工具,如果没有,我建议自己在工程里加一组 Trackbar,把 H、S、V 的上限和下限都暴露出来。调参体验会好很多。

// 创建窗口并绑定六个滑块 Cv2.NamedWindow("Settings"); Cv2.CreateTrackbar("H Min", "Settings", 0, 180, null); Cv2.CreateTrackbar("H Max", "Settings", 180, 180, null); Cv2.CreateTrackbar("S Min", "Settings", 80, 255, null); Cv2.CreateTrackbar("S Max", "Settings", 255, 255, null); Cv2.CreateTrackbar("V Min", "Settings", 60, 255, null); Cv2.CreateTrackbar("V Max", "Settings", 255, 255, null); // 在循环里读取滑块值 int hMin = Cv2.GetTrackbarPos("H Min", "Settings"); int hMax = Cv2.GetTrackbarPos("H Max", "Settings"); int sMin = Cv2.GetTrackbarPos("S Min", "Settings"); int sMax = Cv2.GetTrackbarPos("S Max", "Settings"); int vMin = Cv2.GetTrackbarPos("V Min", "Settings"); int vMax = Cv2.GetTrackbarPos("V Max", "Settings");

注意一个常见误区:HSV 滑块的最大值不能写 360,你在这个坐标系里写 360 会导致红色范围怎么调都对不上。H 上限必须落在 180 以内。我一般在调参时先固定 S 上限为 255、V 上限为 255,只动 H 的两个滑块找出色相范围,再回头收紧 S 和 V 下限过滤杂点,这样定位问题更快。

5. 颜色识别避坑与排查:六个我实际翻过车的点

5.1 红色识别的拦路虎:色相环在 0 度附近断开

现象:识别红色物体时,掩码要么只出来一半,要么时有时无;饱和度调满也压不住缺块。

原因:OpenCV 的 H 通道取值为 0~180,红色分布在 0 附近的连续区间,H=5 和 H=175 实际都是红色,但数值上相差 170,单个InRange根本无法同时包含两段。

解决:红色用两段范围分别 InRange,再用Cv2.BitwiseOr合并掩码。

Mat maskRed1 = new Mat(); Mat maskRed2 = new Mat(); Cv2.InRange(hsv, new Scalar(0, 80, 60), new Scalar(10, 255, 255), maskRed1); Cv2.InRange(hsv, new Scalar(170, 80, 60), new Scalar(180, 255, 255), maskRed2); Mat maskRed = new Mat(); Cv2.BitwiseOr(maskRed1, maskRed2, maskRed);

5.2 自动曝光让 HSV 跟着漂,颜色识别成了碰运气

现象:同一套参数,上午能用,下午识别效果变差;摄像头在强光旁边和背光环境下的识别结果完全不同。

原因:消费级摄像头的自动曝光和白平衡会动态调整,导致 S 和 V 通道数值整体漂移。H 通道一般不随亮度突变,但曝光明暗变化会影响 S,比如原本饱和度 150 的颜色在过曝后变成 60,低于你的下限,目标直接消失。

解决:代码里关闭自动曝光,把曝光值固定到一个合适档位,同时在下限里留出余量。

capture.Set(VideoCaptureProperties.AutoExposure, 0.25); // 尝试关闭自动曝光 capture.Set(VideoCaptureProperties.Exposure, -6); // 固定曝光值,具体范围因摄像头而异

5.3 S 下限设太高,浅色物体全场失踪

现象:识别浅蓝色时掩码几乎全黑,把 S 下限一路降到 20 才恢复正常。

原因:很多浅色物体的饱和度本身就不高,加上环境光散射,数值可能在 40~80 之间。若直接用网上教程的 S 下限 100,等于把这些目标全部排除了。

解决:先用一个小工具显示鼠标位置的像素 HSV,观察你要识别的颜色的实际饱和度范围,再决定下限。不要盲目照搬网上给的参数。

5.4 BGR 和 RGB 搞混,识别结果整体色偏

现象:明明是红色物体,识别结果对着蓝色区域输出矩形框;或者保存下来的调试图颜色怪异。

原因:OpenCV 的Mat通道顺序是 BGR,而很多图像库或者摄像头 SDK 给的是 RGB。某些情况下,Cv2.ImRead读出来的顺序和 Bitmap 转换过来的顺序不一致。

解决:确认数据源,如果是 RGB 顺序,先转换:

Mat rgb = new Mat(); Cv2.CvtColor(bgrSource, rgb, ColorConversionCodes.BGR2RGB);

5.5 掩码上的噪点多到让人怀疑人生

现象:掩码里除了目标,边缘、地面反光处也出现大量白点;矩形框框了一堆没意义的小区域。

原因:传感器噪声、暗角、材质反光都会在 HSV 上形成杂散分布。只做了 InRange 没做形态学处理,是噪点泛滥最常见的原因。

解决:在 InRange 之后加一次高斯模糊或中值滤波,再执行一次开运算。若还是有零星噪点,用Cv2.BoundingRect得到的矩形宽度或高度做面积过滤。我一般在轮廓数小于 20 时直接跳过。

5.6 圆滑表面的高光把颜色洗白

现象:塑料件、陶瓷表面的高光区域在掩码里被当成空白,目标框中间出现空洞。

原因:镜面反光会让局部 V 接近 255、S 接近 0,也就是被识别成白色,HSV 的颜色属性被高光覆盖。

解决:V 上限不要拉到满,适当下调到 230~240,把高光区排除;如果目标太亮,也可以通过调整光源角度来减少正向反射。条件允许的话,加偏振片是我见过的最治本的方案。

6. 把 HSV 参数做成配置文件:调好一次,下次识别不用再重调

这套资源跑通之后,你会发现最耗时的不是代码,而是参数。所以我养成了一个习惯:把调好的 HSV 范围写进配置文件,程序启动时自动加载,调试窗口里还有一个「保存参数」按钮。这样换一台电脑、换一个光源环境时,只要在界面上微调一次,再存回去,代码逻辑一行都不用改。

public class HsvRange { public int HMin { get; set; } public int HMax { get; set; } public int SMin { get; set; } public int SMax { get; set; } public int VMin { get; set; } public int VMax { get; set; } } // 读取 JSON 反序列化为对象 string json = File.ReadAllText("hsv_config.json"); HsvRange cfg = System.Text.Json.JsonSerializer.Deserialize<HsvRange>(json); // 构造 Scalar Scalar lower = new Scalar(cfg.HMin, cfg.SMin, cfg.VMin); Scalar upper = new Scalar(cfg.HMax, cfg.SMax, cfg.VMax);

对应的配置文件hsv_config.json大概是这样的结构:

{ "HMin": 100, "HMax": 130, "SMin": 80, "SMax": 255, "VMin": 60, "VMax": 255 }

我一直建议把「红色两段范围」这种情况也记进配置,用HMin、HMax之外再加一个HMin2、HMax2的字段,为空时就不做第二次 InRange。这样配置文件的表达能力更强,不用为了红色单独写一份特殊代码。从那以后,我每次做颜色识别项目,都强制自己先把调参工具和配置读写写好,再写识别算法,这省下来的是连续好几天的重复劳动。希望这份整理能帮你在 HSV 颜色识别这条路上少绕几个弯。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询