
简介一套完整的OpenCV颜色识别C#示例工程基于OpenCvSharp封装面向需要做颜色检测、图像分割的C#开发者与计算机视觉初学者。资源围绕HSV色彩模型展开清晰演示了BGR到HSV转换、InRange掩码生成、按颜色范围提取目标区域等关键步骤适合快速上手工业视觉、机器人识别等场景。压缩包共59个文件约35.14MB内含17个cs源码文件、8个dll运行库、6个resx界面资源、2张示例图片及配置和项目文件结构完整可直接用Visual Studio打开调试。目前已有1719人学习下载。通过这套工程可掌握OpenCvSharp颜色识别通用流程理解Hue/Saturation/Value各通道对识别效果的影响并借助配套的HSV颜色体系图和窗体调整代码灵活设置目标颜色的上下限阈值。1. 把 OpenCV 颜色识别跑通HSV 才是颜色识别的正确打开方式做图像处理的人几乎都踩过同一个坑直接用 RGB 判断颜色结果换个光照就全线崩溃。OpenCV 颜色识别opencv颜色识别.zip这个项目资源提供的就是一套基于 HSV 颜色空间的完整识别方案配合 OpenCvSharp 在 C# 环境下的封装能让你从像素级颜色判断直接跨越到连通域分析、轮廓过滤和框选定位。适合正在做视觉定位、颜色分拣、质检判色的开发者也适合刚接触 OpenCV 但不想碰 Python、想用 C# 做桌面工具的人。这套资源的核心思路把图像从 RGB 转到 HSV用 H色调、S饱和度、V明度三个维度去圈定目标颜色的范围再用 inRange 函数生成二值掩膜最后通过轮廓提取拿到目标的位置和大小。对于彩色物体识别来说HSV 比 RGB 直观得多因为颜色本身由 H 通道主导S 和 V 只负责饱和度和亮度这意味着你可以只调 H 的阈值范围而不用担心环境亮度波动把识别结果带偏。2. HSV 颜色空间为什么 RGB 不适合做颜色识别2.1 RGB 的三个通道都受光照影响而 HSV 把颜色和亮度拆开了RGB 颜色空间里每个像素的红、绿、蓝三个分量都携带亮度信息也就是说同一个物体在强光和弱光下RGB 三个值会同时变化这就导致用固定 RGB 阈值做颜色判断非常不稳定。比如一个红色纸盒在 500 lux 光照下可能是 (180, 40, 30)到了 2000 lux 下就变成 (230, 80, 60)单纯用阈值范围去卡要么漏检要么误检。HSV 空间把这个问题拆解成了三个独立维度Hue色调0-180 在 OpenCV 中、Saturation饱和度0-255、Value明度0-255。H 通道描述的是这是什么颜色红色的 H 值大约在 0 到 10 以及 170 到 180 之间因为色相环是环形的S 通道描述的是颜色有多纯越低越接近灰色V 通道描述的是有多亮这个通道受光源强度影响最大。所以只要光照变化没有夸张到让颜色本身失真你只需要把阈值范围放宽在 V 通道上H 通道的范围几乎不需要大动。在 OpenCvSharp 中从 BGR 转到 HSV 就一行代码Cv2.CvtColor(src, hsv, ColorConversionCodes.BGR2HSV);这里src是原始 Bgr 图像hsv是输出图像。注意 OpenCvSharp 里默认是 BGR 通道顺序不是 RGB如果不小心把通道顺序搞反颜色识别结果会完全乱掉。ColorConversionCodes.BGR2HSV这个枚举值就是 OpenCV 标准转换的标志位整个转换过程不会对数据做缩放取值范围跟输入图像一致通常是 8UC1 或 8UC3。2.2 OpenCV 的 H 范围只有 0-180别拿 0-360 去套这是一个高频翻车点。日常我们说到色相习惯用 0-360 度但 OpenCV 为了把 HSV 塞进 8 位无符号整数把 H 通道压缩成了 0-180也就是角度值除以 2。所以红色的 H 值在 0-10 和 170-180 两段绿色的 H 值大约在 60-80蓝色的 H 值大约在 100-130。如果你从网上下载了一个 HSV 颜色对照表很多是 0-360 的直接抄数字进代码那识别结果大概率是空的或者乱的。正确做法是把查到的角度值除以 2 再填入阈值范围。举个例子如果你看到某个颜色表的红色范围是 345°-360° 和 0°-9°换算到 OpenCV 就是 172-180 和 0-4合并写成new Scalar(0, 100, 100)到new Scalar(10, 255, 255)这种区间。2.3 用 inRange 生成掩膜二值化的核心一步HSV 范围确定之后用Cv2.InRange把图像中落在范围内的像素变成白色255其余变成黑色0这就得到了掩膜图。掩膜图后续可以直接用来找轮廓也可以做形态学操作去噪。Mat mask new Mat(); Scalar lowerRed new Scalar(0, 100, 100); Scalar upperRed new Scalar(10, 255, 255); Cv2.InRange(hsv, lowerRed, upperRed, mask);InRange的判断逻辑是当像素的 H、S、V 三个通道都同时落在下限和上限之间时输出 255否则输出 0。这意味着阈值范围设得越大容忍的颜色变化越多但误检概率也越高范围设得太窄又容易漏检。通常的做法是先取一个适中的 S 下限比如 80-120V 下限取 40-80这样可以过滤掉接近灰色和过暗的像素让识别聚焦在有实际色彩饱和度的区域。3. 用 OpenCvSharp 搭颜色识别流程从掩膜到轮廓定位3.1 完整代码骨架打开图像 → 转 HSV → inRange → 轮廓提取这是整个资源的核心用法。下面这一套代码能处理静态图片把指定颜色的目标用矩形框画出来。先看代码结构再逐个参数讲解。using OpenCvSharp; class ColorDetector { public static void DetectColor(string imagePath) { Mat src Cv2.ImRead(imagePath, ImreadModes.Color); if (src.Empty()) { Console.WriteLine(图片加载失败); return; } Mat hsv new Mat(); Cv2.CvtColor(src, hsv, ColorConversionCodes.BGR2HSV); Scalar lower new Scalar(35, 60, 60); // 绿色下限 Scalar upper new Scalar(85, 255, 255); // 绿色上限 Mat mask new Mat(); Cv2.InRange(hsv, lower, upper, mask); // 形态学操作去掉噪点填补空洞 Mat kernel Cv2.GetStructuringElement(MorphShapes.Rect, new Size(5, 5)); Cv2.MorphologyEx(mask, mask, MorphTypes.Open, kernel); Cv2.MorphologyEx(mask, mask, MorphTypes.Close, kernel); // 找轮廓 Cv2.FindContours(mask, out Point[][] contours, out HierarchyIndex[] hierarchy, RetrievalModes.External, ContourApproximationModes.ApproxSimple); Mat result src.Clone(); foreach (Point[] contour in contours) { double area Cv2.ContourArea(contour); if (area 500) continue; // 过滤小面积噪点 Rect rect Cv2.BoundingRect(contour); Cv2.Rectangle(result, rect, new Scalar(0, 0, 255), 2); } Cv2.ImShow(Result, result); Cv2.WaitKey(0); } }这段代码的流程分四步读取并转换颜色空间用 inRange 出掩膜用开运算和闭运算清理掩膜最后找轮廓并框选。我在实际项目中碰到过很多次FindContours 返回的轮廓数量可能成百上千大部分都是噪点或者碎块所以面积过滤是必须的。ContourArea算的是轮廓围成的面积单位是像素面积500 这个值不是固定不变的如果你的摄像头距离目标比较远目标在画面中很小阈值就要往下调。RetrievalModes.External表示只取最外层轮廓这特别关键。如果你用了RetrievalModes.List或者CComp同一个目标可能会被拆出多个嵌套轮廓导致一个目标画好几个框。用 External 模式可以避免嵌套问题。3.2 参数调优S 和 V 的上下限是光照鲁棒性的关键很多人调参只盯着 H 通道其实 S 和 V 才是决定是否稳定的关键。S 下限控制的是最小饱和度也就是颜色最少要浓到什么程度才被接受。如果设成 0纯灰色区域也会被包含进来设太高比如 180颜色稍微偏淡一点就被拒绝。我一般把 S 下限设在 60 到 100 之间具体取决于你的目标颜色饱和度。V 下限控制的是最小亮度这个值如果设得太低暗光环境下背景里偏暗的非目标区域也会混进来设太高则目标只要稍微背光就找不到了。比较稳的做法是 V 下限设在 50 左右上限直接拉满 255因为上限限制很少会用到。另外强调一点H 的上下限组合可能出现跨越 0 度的情况。比如红色既包含 170-180 又包含 0-10如果直接写Scalar(170, 100, 100)到Scalar(10, 255, 255)OpenCV 会认为上限小于下限导致空结果。正确做法是拆成两个区间分别做 inRange然后做一次按位或合并。在 OpenCvSharp 中合并用Cv2.BitwiseOrMat mask1 new Mat(), mask2 new Mat(); Cv2.InRange(hsv, new Scalar(0, 100, 100), new Scalar(10, 255, 255), mask1); Cv2.InRange(hsv, new Scalar(170, 100, 100), new Scalar(180, 255, 255), mask2); Mat mask new Mat(); Cv2.BitwiseOr(mask1, mask2, mask);3.3 从静态图到视频流加上帧处理循环静态图片识别的逻辑可以直接平移到视频或者摄像头流。唯一的区别是数据源变成了逐帧捕获以及每帧之间要做内存释放防止 Mat 对象堆积导致内存暴涨。下面这段是摄像头连续识别的常见写法VideoCapture capture new VideoCapture(0); if (!capture.IsOpened()) { Console.WriteLine(摄像头无法打开); return; } using (Window window new Window(Live Detection)) { Mat frame new Mat(); while (true) { capture.Read(frame); if (frame.Empty()) break; Mat hsv new Mat(); Cv2.CvtColor(frame, hsv, ColorConversionCodes.BGR2HSV); Mat mask new Mat(); Cv2.InRange(hsv, new Scalar(35, 60, 60), new Scalar(85, 255, 255), mask); // 找轮廓 框选这里省略重复代码 window.ShowImage(frame); int key Cv2.WaitKey(30); if (key 27) break; // ESC 退出 } }视频处理的帧率核心瓶颈不在 inRange而在 FindContours。如果一帧图像是 1920x1080FindContours 的耗时可能达到 30-50ms会直接压到 20 FPS 以下。如果对实时性有要求通常做法是先把帧尺寸缩小到一半或者 640x480 再处理识别框再按比例放大回去。4. 从单色识别到多色识别拆区间、合并掩膜的实战策略4.1 多颜色识别的三种常见方案多色识别在真实项目中几乎是刚需流水线上要同时区分红、绿、蓝三个颜色的零件或者做颜色分拣时几个颜色同时出现在画面里。实现多色识别有三种常见实现路径第一种是顺序遍历每个颜色单独做一次 inRange 和 FindContours然后各画各的框。优点是代码直白调试时能单独看每个颜色的掩膜缺点是同一帧要做多次完整流程耗时线性增长。第二种是合并掩膜先给一个目标颜色生成掩膜然后把所有颜色的掩膜用Cv2.BitwiseOr合并成一张再统一做一次 FindContours。这个方法只做一次轮廓提取性能更好但代价是哪个轮廓对应哪个颜色需要额外记录因为合并之后原来的颜色信息丢失了。要解决这个问题合并前先给每个颜色的掩膜做连通域标记或者直接用不同颜色掩膜分别找轮廓、画不同颜色的框。第三种是给每个颜色整一个专属掩膜分别 FindContours但是每个都降采样处理。这里我给出一种折中写法// 定义颜色区间表 var colorRanges new List(Scalar lower, Scalar upper, Scalar boxColor) { (new Scalar(0, 100, 100), new Scalar(10, 255, 255), new Scalar(0, 0, 255)), // 红 (new Scalar(35, 60, 60), new Scalar(85, 255, 255), new Scalar(0, 255, 0)), // 绿 (new Scalar(100, 100, 100), new Scalar(130, 255, 255), new Scalar(255, 0, 0)), // 蓝 }; Mat hsv new Mat(); Cv2.CvtColor(frame, hsv, ColorConversionCodes.BGR2HSV); foreach (var range in colorRanges) { Mat mask new Mat(); Cv2.InRange(hsv, range.lower, range.upper, mask); // 形态学清理 Cv2.MorphologyEx(mask, mask, MorphTypes.Open, Cv2.GetStructuringElement(MorphShapes.Rect, new Size(3, 3))); Cv2.FindContours(mask, out Point[][] contours, out HierarchyIndex[] hierarchy, RetrievalModes.External, ContourApproximationModes.ApproxSimple); foreach (Point[] contour in contours) { double area Cv2.ContourArea(contour); if (area 800) continue; Rect rect Cv2.BoundingRect(contour); Cv2.Rectangle(frame, rect, range.boxColor, 2); Cv2.PutText(frame, $Area:{rect.Width * rect.Height}, rect.TopLeft, HersheyFonts.HersheySimplex, 0.5, range.boxColor); } }这段代码里有个值得注意的细节每个颜色独立用RetrievalModes.External找轮廓就不用担心不同颜色的嵌套问题。HersheyFonts.HersheySimplex是内嵌字体数值 0.5 是字号缩放因子rect.TopLeft是文字起点。4.2 多色场景下掩膜互相污染的问题多色识别中常见的翻车现场是红色和橙色经常混在一起绿色和蓝色在某些光照下发灰发白导致掩膜相互覆盖。本质上这是因为色相阈值设得太宽把相邻色相的颜色也圈进来了。解决方案是收窄 H 区间每个颜色只保留它最典型的色相段红色0-10 或 170-180 二选一尽量别同时开橙色10-25黄色25-35绿色35-85 内再细分亮绿 45-70蓝色100-130紫色130-160另外还有一个通用手段把 S 下限提高比如从 60 提到 120可以过滤掉因为反光导致颜色变淡的区域。这个操作在多色识别里尤其有用因为不同颜色交界处的混叠像素通常饱和度偏低。4.3 光照不均的补丁方案先做亮度归一化如果画面里目标区域的亮度差异大到同一块颜色一部分过曝一部分过暗单纯调 V 下限很难兼顾。常见的优化方法是转 HSV 之前先做一次 CLAHE 直方图均衡。但注意CLAHE 应该在 BGR 空间做也可以转到 YCrCb 对 Y 通道做做完再转 HSV。代码大致是这样Mat lab new Mat(); Cv2.CvtColor(src, lab, ColorConversionCodes.BGR2Lab); Mat[] planes Cv2.Split(lab); Cv2.CLAHE(2.0, new Size(8, 8)).Apply(planes[0], planes[0]); Cv2.Merge(planes, lab); Cv2.CvtColor(lab, src, ColorConversionCodes.Lab2BGR);Cv2.CLAHE(2.0, new Size(8, 8))里的 2.0 是对比度限制参数越大增强越明显普遍取值 1.5-3.0Size(8, 8)是分块大小图像越大块可以设大些。这个操作能明显缓解阴影边缘带来的漏检但别指望它让过曝区域复原那需要另外做动态范围压缩。5. 避坑指南HSV 颜色识别最常见的五个翻车现场5.1 现象识别结果全是噪点找不到目标原因形态学操作没做或者 inRange 的 S/V 阈值太低把背景里的淡色像素全包进来了。另外摄像头 CMOS 传感器的暗部噪点在普通光照下也很容易触发 HSV 范围。解决先做一次开运算去小颗粒噪点再做一次闭运算填补目标内部细小空洞核大小建议 3x3 开始目标小就用 3x3大目标就 5x5过大的核会把相邻目标连成一片。同时把 S 下限从 60 提到 100V 下限设为 60 以上。5.2 现象同一个目标出现多个重叠框原因没有用RetrievalModes.External轮廓被拆成内外多层。另一个可能原因是同一颜色类别的多个色相区间分别识别后被重复框选。解决统一使用RetrievalModes.External只取最外层轮廓同一个颜色做多掩膜合并时不要对同一目标重复画框。如果仍然拆框检查轮廓近似参数ApproxSimple已经足够。5.3 现象识别框明显偏离目标中心原因轮廓太重、面积过滤只用了ContourArea而目标本身被阴影或遮挡切成了多块FindContours 返回了多个小轮廓每个小轮廓的 BoundingRect 都覆盖不了完整目标。解决用Cv2.MinAreaRect和Cv2.BoxPoints旋转外接矩形来框选或者对多块小轮廓做合并把所有轮廓的顶点集合成一个大轮廓再计算外接矩形。这个做法在零件遮挡场景特别实用。5.4 现象换了摄像头型号后识别率骤降原因不同摄像头对颜色的采样、白平衡处理差异很大同一个颜色在相机 A 和相机 B 的 HSV 值可能相差 20-30 个单位的 H 值。代码里写死的阈值只适用于开发时的摄像头。解决做一次校准。用目标颜色在不同光源下采集 10-20 个样本用 HSV 直方图把每个像素的 H 值分布打出来上下限取分布区间的 2%-98% 分位再留 10%-20% 的余量比如算出来的 H 范围是 30-70就放宽到 25-75。5.5 现象V 通道太低导致黑色物体被识别为任意颜色原因当 V 值很低比如低于 40颜色信息基本丢失S 值也变得很小整个物体接近纯黑。此时 inRange 可能恰好卡进多个颜色的 H 区间导致误判。解决在 inRange 之前加一个判断V 低于某个阈值的像素直接排除掉。可以先用Cv2.Threshold对 V 通道做一个下限掩膜再与 inRange 的结果做BitwiseAnd。这会减少黑色物体的误检。6. 进阶技巧用 HSV 直方图反推阈值不再靠肉眼调参调参从来都不是一遍就完成的。我现在的工作流是先拍一张包含目标颜色的代表性图像然后计算目标区域的 HSV 直方图查看 H 值和 S 值的分布根据分布区间反过来生成阈值范围而不是在代码里反复试数字。下面这段代码可以打印一个图像所有像素的 H 通道分布用于反推阈值Mat hsv new Mat(); Cv2.CvtColor(src, hsv, ColorConversionCodes.BGR2HSV); Mat hChannel new Mat(); Cv2.ExtractChannel(hsv, hChannel, 0); // 提取 H 通道 int histSize 180; float[] ranges { 0, 180 }; Mat histogram new Mat(); Cv2.CalcHist(new Mat[] { hChannel }, new int[] { 0 }, null, histogram, 1, new int[] { histSize }, new float[][] { ranges });Cv2.CalcHist的参数里histSize180对应 0-180 的每个整数值一个 bin输出数组下标就是 H 值。把直方图打到控制台或者存成图片肉眼就能看出有哪些高峰峰值区间就是阈值该覆盖的位置。这个方法虽然多一步但比盲调靠谱太多。另一个习惯是每次调试完都把最终使用的区间存成一个配置文件用 JSON 或者 ini 维护方便切换颜色方案的时候不用重新编译。配置项就三个颜色名、下限H, S, V上限H, S, V再加一个面积过滤阈值。实际部署时只需要改配置不动代码这个习惯帮我省了不知道多少麻烦。最后再说一个容易踩的细节C# 里 OpenCvSharp 的Scalar赋值顺序是 BGR 还是 HSV 取决于实际使用场景在 inRange 里它就是 HSV但画框时new Scalar(0, 0, 255)又是 BGR。这两个顺序完全不同曾经有一次我把它俩搞混了红色被识别对了但画出来的框是蓝色排查了半天才发现是在画框那行把顺序写成了 HSV。从那以后我每次写完画框相关的代码都会强制检查一遍 Scalar 参数的通道顺序哪怕再熟练也照查。希望帮到你。本文还有配套的精品资源点击获取