首页 / 资讯中心 / 文章详情

OpenCV颜色识别落地指南:HSV阈值标定与C#实现技巧

OpenCV颜色识别落地指南:HSV阈值标定与C#实现技巧 ★ FEATURED ARTICLE
简介面向OpenCV与C#开发者的颜色识别示例项目基于OpenCvSharp在HSV色彩空间中完成指定颜色的检测与提取适用于自动化、机器人及物体检测等场景。压缩包共59个文件包含17个.cs源码文件、8个DLL依赖库、2个exe可执行程序以及sln/csproj工程文件、jpg/png示例图片等整体约35.14MB结构清晰便于直接运行学习。已有1718人学习下载。项目提供完整WinForms界面涵盖主窗体、HSV阈值设置、图像显示与对比等多个模块并附OpenCV中的HSV颜色体系参考图可直观理解H色调、S饱和度、V亮度取值范围与颜色变化关系。通过实际源码和可运行程序读者能快速掌握Cv2.CvtColor、Core.InRange、CopyTo等核心API的用法涵盖从图像读取、色彩空间转换到掩码提取的完整流程并在此基础上扩展识别逻辑适合OpenCV入门者与需要快速集成颜色识别功能的C#开发者。1. opencv 颜色识别HSV 才是那个能落地的方法做颜色识别第一脚踩进 RGB 的人十个有九个会翻车。我接过一个台球桌自动摆球项目要在深绿色桌面上锁定六颗不同颜色的球交给机械臂夹取。一开始直接对 RGB 三通道做阈值分割光源一换同一颗球的 RGB 均值能漂出 40 个灰度级阈值怎么设都压不住误检。换成 HSV 空间之后把色调从亮度里拆出来单独判断一个多小时就把六色标定做完识别率稳定到可交付水平。这套 opencv 颜色识别方案走的就是 HSV 这条路。这份资源包把整套做法整理成了一个可在 Windows 上用 OpenCvSharpC# 版 OpenCV直接跑起来的工程读图、转 HSV、用 Trackbar 实时标定阈值、轮廓筛选、画框输出每一步都有对应代码。它特别适合两类人一是机器视觉入门工程师想跳过查表试错直接看完整实现二是有 C# 项目背景、要在 WinForm 或采集卡流程里快速集成颜色识别功能的开发者。下面按我实际拆代码的顺序把这套方案里的关键环节和踩过的坑一次讲完。2. HSV 色彩空间与第一条识别通路把颜色从亮度里拆出来再画框2.1 HSV 三个参数H/S/V 分别控制什么为什么比 RGB 抗光照RGB 是三个强相关的通道。物体被阴影盖住时R、G、B 三个值一起往下掉阈值窗口跟着失效光源色温偏暖时三个通道又一起偏移。HSV 把颜色信息拆成三个独立维度其中色调 H 只描述“这是什么颜色”和亮度、饱和度解耦。这才是它能抗光照波动的根本原因。在 OpenCV 和 OpenCvSharp 里BGR 转 HSV 后各通道的取值范围是固定的一套通道取值范围含义H0 - 179色调红橙黄绿青蓝紫在这个轴上排列S0 - 255饱和度越低越接近灰色V0 - 255明度越低越接近黑色注意 H 只到 179不是 0 - 360。因为 OpenCV 用 Byte 存储 HSV 结果一个字节放不下 360 个刻度所以把色相环砍了半。很多人第一次调色时拿网上 0 - 360 的表格套进 InRange结果全是黑的就是这个原因。HSV 也不是万能的。过曝到接近纯白时S 趋近于 0H 就失去意义死黑区域也一样。所以 HSV 方案对打光的要求是“不过曝、不死黑”这句话记在脑子里后面标定会少很多折腾。2.2 第一条能跑的 HSV 识别代码转 HSV、写 InRange、看 mask先把最简单通路跑通。读一张图转 HSV然后用 InRange 圈出绿色区域把结果 mask 显示出来。这是所有后续步骤的地基。using OpenCvSharp; string path test.jpg; using var src Cv2.ImRead(path); if (src.Empty()) return; using var hsv new Mat(); Cv2.CvtColor(src, hsv, ColorConversionCodes.BGR2HSV); using var mask new Mat(); // 一组较宽的绿色阈值只圈色调S/V 下限先放宽 Cv2.InRange(hsv, new Scalar(35, 60, 60), new Scalar(85, 255, 255), mask); Cv2.ImShow(Src, src); Cv2.ImShow(Mask, mask); Cv2.WaitKey(0);CvtColor 里用的是 BGR2HSV不是 RGB2HSV这是最常见的坑之一。OpenCvSharp 从 ImRead 读进来的图像默认就是 BGR 三通道顺序转 HSV 时必须告诉它源顺序是 BGR。InRange 做的事是逐像素判断H 在 35 - 85、S 不低于 60、V 不低于 60三个条件同时满足时输出 255否则输出 0。所以 mask 是一张单通道二值图。这组阈值是绿色示例。S/V 下限先放到 60是为了先把主色调找出来如果一开始就设得很高目标颜色很容易被挤掉。等你看到 mask 里出现大块连续白色区域再回头把 S/V 下限往上收。2.3 从 mask 到结果轮廓筛选与矩形框标记mask 只是黑底白图要变成“识别到了目标”的结果还得找轮廓、算面积、画框。这一步把前面做的二值图翻译成人能看懂的输出。Cv2.FindContours(mask, out Point[][] contours, out HierarchyIndex[] hierarchy, RetrievalModes.External, ContourApproximationModes.ApproxSimple); foreach (var contour in contours) { double area Cv2.ContourArea(contour); if (area 500) continue; // 像素面积阈值噪点直接丢 Rect rect Cv2.BoundingRect(contour); Cv2.Rectangle(src, rect, new Scalar(0, 0, 255), 2); Cv2.PutText(src, $area{area:F0}, new Point(rect.X, rect.Y - 5), HersheyFonts.HersheySimplex, 0.5, new Scalar(0, 255, 0), 1); } Cv2.ImShow(Result, src); Cv2.WaitKey(0);FindContours 选 External 模式只取最外层边界避免同一个色块内部再套一堆小圈ApproxSimple 是压缩轮廓点矩形识别够用。面积阈值 500 是针对低分辨率测试图的保守值如果是 1080p 画面我一般起步给 1000 - 3000具体数值要看目标在画面里占多大。到这里从“像素颜色判断”到“目标位置输出”的整条通路就通了后面所有工作都是在让这条通路在不同光照、不同现场条件下依然稳定。3. HSV 阈值标定与形态学收尾让 mask 从“筛子”变成实心色块3.1 阈值不是查出来的是标出来的网上能搜到一堆 HSV 颜色参考表红色 0 - 10、绿色 35 - 85、蓝色 100 - 130 之类它们只能当起点不能直接抄进工程。原因很简单这些表是在标准光源、标准色卡条件下测出来的而你的现场有荧光灯、有自然光、有塑料高光、有金属反光同一个蓝色工件在两条产线上测出来的 S 和 V 能差出 80 个刻度。我整理一个常用的起点参考表标定前先用它圈定大方向目标颜色H 参考范围S/V 初始建议红0 - 10 与 170 - 179S 80V 60橙10 - 25S 100V 60黄25 - 35S 80V 80绿35 - 85S 60V 60蓝100 - 130S 80V 60紫130 - 160S 80V 60注意红色的 H 范围是两段这个在避坑章节专门讲。标定的核心原则必须拿现场样品、在现场光源下做。你的目标不是把表抄准而是让 mask 里的白色区域恰好覆盖住目标表面、又不把背景带进来。3.2 用 CvTrackbar 搭一个实时标定工具查表不行就得自己调。OpenCvSharp 提供了原生窗口和滑块控件六根滑块分别控制 HMin、HMax、SMin、SMax、VMin、VMax拖一下立刻看到 mask 变化。这是这套资源包里最值钱的部分之一。int hMin 35, hMax 85; int sMin 60, sMax 255; int vMin 60, vMax 255; Mat sourceImage; // 从摄像头或文件读进来的 BGR 原图 void RefreshMask(int pos, object state) { using var hsv new Mat(); Cv2.CvtColor(sourceImage, hsv, ColorConversionCodes.BGR2HSV); using var mask new Mat(); Cv2.InRange(hsv, new Scalar(hMin, sMin, vMin), new Scalar(hMax, sMax, vMax), mask); Cv2.ImShow(Mask, mask); } Cv2.NamedWindow(Adjuster, WindowFlags.Normal); Cv2.CreateTrackbar(HMin, Adjuster, ref hMin, 180, RefreshMask); Cv2.CreateTrackbar(HMax, Adjuster, ref hMax, 180, RefreshMask); Cv2.CreateTrackbar(SMin, Adjuster, ref sMin, 255, RefreshMask); Cv2.CreateTrackbar(SMax, Adjuster, ref sMax, 255, RefreshMask); Cv2.CreateTrackbar(VMin, Adjuster, ref vMin, 255, RefreshMask); Cv2.CreateTrackbar(VMax, Adjuster, ref vMax, 255, RefreshMask); while (true) { RefreshMask(0, null); if (Cv2.WaitKey(1) 27) break; // ESC 退出 }RefreshMask 里的 using 是保证 Mat 释放长时间跑不会内存暴涨。滑块回调里直接读外面那几个 int 变量这是 Cv2.CreateTrackbar 在 OpenCvSharp 里的标准用法传 ref 变量进去滑块值变化时变量值跟着更新。循环里每帧都调一次 RefreshMask是为了兜底——某些窗口环境下滑块回调只在拖动瞬间触发循环刷新能保证画面一直更新。标定顺序比参数更重要这是血泪经验浓缩成的三步先只动 HMin/HMax把目标颜色的色相区间框住S/V 保持 30 - 255 这种宽范围色相区间稳了再收 SMin/SMax 把浅灰背景、白色反光剔掉最后收 VMin/VMax 处理暗部。一次只动一个通道别两个滑块一起拉。提示S/V 下限标得好不好直接决定现场稳定性。宁可让范围稍微宽一点导致偶尔带进杂色也不要收太死导致光照轻微波动就识别不到。面积过滤会帮你挡掉一部分误检。3.3 形态学修 mask先开先闭一个判断方法mask 出来通常不是干净的实心白块而是带小黑洞、边缘带毛刺的二值图像。黑洞来自目标表面的反光点和纹理毛刺来自边缘像素抖动。要修成实心色块用形态学里的开运算和闭运算。using var element Cv2.GetStructuringElement(MorphShapes.Rect, new Size(5, 5)); using var closed new Mat(); Cv2.MorphologyEx(mask, closed, MorphTypes.Close, element); using var opened new Mat(); Cv2.MorphologyEx(closed, opened, MorphTypes.Open, element);闭运算是先膨胀再腐蚀作用是把目标内部的小洞填掉开运算是先腐蚀再膨胀作用是削掉边缘毛刺和小噪点。实际场景里我默认先闭后开因为反光造成的内洞比边缘脏点更影响面积判定。如果发现目标外部杂点特别多、颜色块本身边缘倒是很干净就反过来先开后闭。kernel 取 5×5 是个稳妥中间值目标在画面里很小就降到 3×3目标大而边缘粗糙就升到 7×7。形态学不是越狠越好kernel 大了会把两个靠近的色块糊成一个。3.4 标定结果落盘把阈值写进配置文件标定完成之后的第一件事是立刻把阈值固化下来否则下次启动又要重新拖半小时滑块。我用一个简单的 JSON 配置把每种颜色的 HSV 区间和面积阈值都序列化到文件里程序启动时自动读入。public class ColorRange { public string Name { get; set; } 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; } public double MinArea { get; set; } } var range new ColorRange { Name green_ball, HMin 38, HMax 82, SMin 55, SMax 255, VMin 50, VMax 255, MinArea 1500 }; string json JsonSerializer.Serialize(new ListColorRange { range }); File.WriteAllText(color_ranges.json, json); var savedRanges JsonSerializer.DeserializeListColorRange(File.ReadAllText(color_ranges.json));把阈值放在配置而不是硬编码进代码换现场、换光源、换摄像头之后的维护成本完全不同。现场光变了打开标定工具重新拉一遍滑块把新阈值写回 JSON 就完事不用重新编译整个程序。资源包里目前用的是 System.Text.Json你要是习惯 Newtonsoft.Json 改成等价写法就行。4. 常见问题颜色识别五个高频翻车点与排查方法4.1 识别结果闪烁时而识别时而丢失现象同一帧画面不动mask 里的白色区域一会变大一会变小目标时出时没轮廓框跟着抖。原因通常是阈值卡太死尤其是 HMin/HMax 只给 2 - 3 个刻度的余量叠加摄像头自动曝光和现场光源波动色相做小幅度漂移就掉出区间。解决思路分两层识别前先对原图做一次高斯模糊滤掉传感器噪声标定收敛后手动把 H 范围往两边各放宽 3 - 5 个刻度S/V 下限再各降 10 - 20。牺牲一点点精度换稳定性对产线场景是划算的。using var blurred new Mat(); Cv2.GaussianBlur(src, blurred, new Size(5, 5), 0);4.2 红色识别不出来H 是环形的不是一条直线现象按参考表设了 H 范围 0 - 10红色目标只能识别到一半另一半断断续续怎么调都补不齐。原因OpenCV 把色相环从红色处剪开拉直成一个 0 - 179 的轴红色同时落在 0 - 10 和 170 - 179 两段一次 InRange 只能圈住一段。解决对红色做两次 InRange把两段结果按位 OR 合并成一张 mask。using var redLow new Mat(); using var redHigh new Mat(); using var redMask new Mat(); Cv2.InRange(hsv, new Scalar(0, sMin, vMin), new Scalar(10, sMax, vMax), redLow); Cv2.InRange(hsv, new Scalar(170, sMin, vMin), new Scalar(179, sMax, vMax), redHigh); Cv2.BitwiseOr(redLow, redHigh, redMask);4.3 阈值和表的数值完全一致识别结果却对不上现象用参考表里的绿色 35 - 85 做 InRangemask 却把蓝色物体识别进来了或者目标区域是黑的。原因BGR 通道顺序搞反了。OpenCvSharp 里 ImRead 和摄像头帧默认是 BGR你拿 PhotoShop、画图工具看到的色值是 RGB两者取同一个绿色算 HSV算出来的 H 数值体系不同套进代码自然对不上。解决统一以代码为准所有 HSV 参考值都用 Cv2.CvtColor 现场转换得到不要跨工具换算。自查方法也简单新建一张纯蓝色 3×1 的 Mat用 ImShow 显示如果输出的蓝色对应 BGR 排列说明你的环境和 OpenCvSharp 的默认一致。4.4 滑块怎么拖都出不了理想 mask动一点就全黑全白现象标定时六根滑块来回试了一个小时mask 要么全黑要么全白偶尔出现一点区域也一晃就没。原因一次同时调了两个以上通道H/S/V 互相牵制分不清是谁把目标挤掉了。这是标定顺序问题不是代码问题。解决严格按 3.2 的顺序来——S/V 先放宽到 30 - 255只动 H 把目标色相区间锁定H 稳了再收 S最后收 V。每次只动一根滑块观察幅度控制在 5 - 10 个刻度之间。4.5 实时识别 CPU 爆表帧率掉到个位数现象处理摄像头视频流时CPU 占用拉满画面一卡一卡连显示窗口都在掉帧。原因对整帧高清图无脑做高斯模糊、形态学、轮廓提取每一步都是像素级遍历叠在一起计算量爆炸。解决先把输入图缩到处理所需的最小尺寸或者直接在原图上定义 ROI 裁剪掉无关区域然后形态学 kernel 从 5×5 降到 3×3迭代次数固定在 1。做颜色识别不需要 1080p 的精细度640×360 往往绰绰有余。using var small new Mat(); Cv2.Resize(src, small, new Size(640, 360));5. 进阶用法把识别结果变成稳定触发的工程信号5.1 光照归一化先把亮度压平再谈颜色现场光照不均时同一目标左边亮右边暗mask 可能只出来半边这种问题靠调 HSV 阈值是补不平的。常见做法是在进 HSV 之前先把亮度通道做一次直方图均衡化。注意不要直接在 BGR 上均衡那样会破坏颜色比例正确姿势是转 YCrCb 后只对 Y 通道做。using var ycrcb new Mat(); Cv2.CvtColor(src, ycrcb, ColorConversionCodes.BGR2YCrCb); Cv2.Split(ycrcb, out Mat[] channels); Cv2.EqualizeHist(channels[0], channels[0]); Cv2.Merge(channels, ycrcb); Cv2.CvtColor(ycrcb, src, ColorConversionCodes.YCrCb2BGR);注意 EqualizeHist 是全局均衡对整体偏暗有改善但它拉不回真正过曝的区域——过曝处 S 已经趋近于 0信息已经丢了。所以这个步骤是辅助手段不能指望它逆天改命。5.2 面积判定把“有目标”变成“状态信号”识别程序最终要输出的往往不是一堆矩形框而是一个布尔信号“有没有货”“是什么颜色”“要不要触发”。做法是遍历轮廓求最大面积跟配置文件里的 MinArea 比较。double maxArea 0; foreach (var contour in contours) { double a Cv2.ContourArea(contour); if (a maxArea) maxArea a; } bool detected maxArea range.MinArea;这样输出的 detected 是干净的状态量可以直接接到 WinForm 的指示灯、报警器或者通过串口和 PLC 联动。面积阈值本身也放进配置文件换摄像头分辨率时只改数字不动代码。5.3 从标定到部署的固定流程我把这套流程固定成一条流水线每次新接一个颜色识别需求都按这个顺序走取一帧原图 → 高斯模糊 → YCrCb 均衡化 → 转 HSV → InRange 生成 mask → 形态学闭开运算 → FindContours → 最大面积判定 → 输出信号。每一步的输入输出都是明确的 Mat 或布尔量排错时从中间某一帧截图就能定位问题。我第一次把颜色识别模块交付到产线时就是直接把阈值硬编码在代码里调试间光线正常现场日光灯一开识别率当场垮掉。从那以后不管多小的识别任务我都强制在自己的程序里保留标定入口和配置文件出口先标定完再写死部署。这套 opencv 颜色识别资源包里正好把标定工具、配置读写、识别主流程都拆在了一起下载后直接把自己的目标颜色填进配置就能跑通希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?
咨询建站