
简介面向C#开发者的OpenCV颜色识别示例工程基于OpenCvSharp封装HSV色彩模型帮助读者快速掌握色调、饱和度、亮度三个维度的阈值设定与掩码提取方法。配套完整Visual Studio解决方案包含窗体界面、图像处理逻辑、颜色范围设置模块以及测试图片可在实际项目中直接参考配色或移植代码。工程共五十九个文件以C#源码为主十七个cs辅以程序集dll、资源resx、配置文件config、可执行exe及调试pdb整体35.14MB目录结构清晰适合新手按入口逐步调试也适合有经验者快速提取颜色识别功能。目前已有1719人学习下载资源热度较高。通过HSV转换、InRange掩码、像素级过滤等核心步骤读者可掌握绿色等特定颜色的识别流程并理解OpenCV与C#接口的调用差异为自动化分拣、目标检测等场景打下基础。工程中cs文件覆盖HSV参数设置、主界面交互与图像显示等模块便于按需拆解复用。1. 颜色识别不难难的是把 HSV 阈值调到不误检拿到一个 OpenCV 颜色识别的项目需求很多人第一反应是写个 RGB 范围判断就交差。但实际放到现场红色和橙色分不开、反光区域被当成白色、光照一变所有阈值全部失效——这些才是真正消耗时间的部分。这个项目把 OpenCvSharp 的 HSV 颜色识别做了完整落地从摄像头取帧、颜色分割、形态学去噪到轮廓框选全部用 C# 实现适合想做视觉分拣、机器人巡线、颜色追踪的开发者。它不只是一个 Demo而是一套能直接改颜色范围、调形态学参数、换视频源的基础框架读懂了它二次开发可以少走很多弯路。2. 项目解剖从摄像头到颜色分类的完整链路2.1 整体架构与目录结构这个项目是基于 OpenCvSharp 4.x 的 Windows 窗体应用核心链路是摄像头/视频文件 → Mat 帧 → 转 HSV → 按阈值生成掩膜 → 形态学处理 → 轮廓查找 → 画框标注 → 显示结果。整个结构分为三个层采集层、处理层、展示层。OpenCV颜色识别/ ├── CameraCapture.cs // 摄像头封装负责帧读取和释放 ├── ColorDetector.cs // 核心颜色识别逻辑封装 HSV 分割 ├── MainForm.cs // 界面显示含滑动条调参 ├── Program.cs // 程序入口 └── ColorRange.cs // 颜色范围定义类模块之间的耦合很低ColorDetector不依赖界面CameraCapture不关心图像内容。这样做的好处是后面你想把摄像头换成视频文件、网络流或者把识别结果改成输出坐标到串口只需要替换或扩展对应模块不用动主逻辑。2.2 为什么用 HSV 而不是 RGB用 RGB 做颜色识别的坑在于RGB 三个通道和亮度强相关同一物体在阴影下和阳光下R、G、B 值能差出一倍还多。HSV 把色相Hue单独拆出来饱和度和亮度作为辅助维度这样只要物体的色相稳定就能靠 H 值准确切出目标颜色。项目里ColorRange类就是为此设计的它保存了一个颜色在 HSV 空间的最小/最大三元组public class ColorRange { public Scalar Lower { get; set; } // HSV 下界 public Scalar Upper { get; set; } // HSV 上界 public string Name { get; set; } // 颜色名称用于标注 public ColorRange(string name, Scalar lower, Scalar upper) { Name name; Lower lower; Upper upper; } }这段代码是后续所有识别的基础。Lower和Upper直接传给Cv2.InRangeOpenCV 会逐像素判断如果像素的 H、S、V 三个值都在这个区间内掩膜对应位置就被置为白色255否则是黑色0。也正因为这个设计颜色的精细化调整不需要改代码只改这两个Scalar参数就行。2.3 主流程从原始帧到掩膜ColorDetector的Detect方法是整个项目的心脏它按顺序完成颜色识别链条中的每一步。下面这段是它的主干实现public Mat Detect(Mat frame, ColorRange range) { // 1. 高斯模糊降低摄像头噪声和颜色跳变 Mat blurred new Mat(); Cv2.GaussianBlur(frame, blurred, new Size(5, 5), 0); // 2. 从 BGR 转到 HSVOpenCV 默认以 BGR 存储 Mat hsv new Mat(); Cv2.CvtColor(blurred, hsv, ColorConversionCodes.BGR2HSV); // 3. 按阈值生成二值掩膜 Mat mask new Mat(); Cv2.InRange(hsv, range.Lower, range.Upper, mask); // 4. 开操作去噪先腐蚀后膨胀去掉孤立白点 Mat kernel Cv2.GetStructuringElement(MorphShapes.Ellipse, new Size(5, 5)); Mat morphed new Mat(); Cv2.MorphologyEx(mask, morphed, MorphTypes.Open, kernel); // 5. 闭操作填充空洞把同一物体断裂的部分连起来 Mat closed new Mat(); Cv2.MorphologyEx(morphed, closed, MorphTypes.Close, kernel); return closed; }我标注的几步里最容易忽略的是第 1 步高斯模糊。摄像头采集到的像素往往存在邻域抖动如果直接取 HSV掩膜会出现密集的小噪点模糊之后相邻像素趋于一致后面的形态学操作才能起作用。第 4、5 步的核大小直接决定处理效果核太小噪点去不干净核太大会吞掉小目标一般物体面积在画面里占比不小时用 5x5 椭圆核就够了。3. 核心模块拆解轮廓查找与目标框选3.1 从掩膜到轮廓拿到干净的掩膜后下一步是指出「哪里有这个颜色」。OpenCvSharp 里用FindContours查找白色区域的边缘再通过BoundingRect得到外接矩形。这部分涉及到一个容易被忽略的坑FindContours在版本演进中返回值变了老代码里常见的Cv2.FindContours重载在新版本下需要传输出参数out才能拿到Point[][]和HierarchyIndex[]。public ListRect GetBoundingBoxes(Mat mask) { // 查找所有外轮廓 Point[][] contours; HierarchyIndex[] hierarchy; Cv2.FindContours(mask, out contours, out hierarchy, RetrievalModes.External, ContourApproximationModes.ApproxSimple); ListRect boxes new ListRect(); foreach (Point[] contour in contours) { // 面积过滤去掉小于 500 像素的噪声块 double area Cv2.ContourArea(contour); if (area 500) continue; Rect rect Cv2.BoundingRect(contour); boxes.Add(rect); } return boxes; }面积过滤这一步是血泪经验。相机里经常出现一小片高光、皮屑、灰尘的影子会被误判成目标颜色加了ContourArea下限后这些零碎区域直接被跳过。实际项目里下界值要根据摄像头的安装位置和物体距离来调镜头离目标近面积下限可以设大一些离得远则需要调小否则小目标会被丢掉。3.2 绘制结果拿到矩形框后在原始帧上直接画线并标注颜色名称。注意这里不是在掩膜上画因为掩膜已经被二值化没有原始纹理信息。public Mat DrawResults(Mat frame, ListRect boxes, string colorName) { foreach (Rect box in boxes) { // 画矩形框颜色用 BGR厚度 2 Cv2.Rectangle(frame, box, new Scalar(0, 255, 0), 2); // 在框顶部写文字 string label ${colorName} {box.Width}x{box.Height}; Cv2.PutText(frame, label, new Point(box.X, box.Y - 5), HersheyFonts.HersheySimplex, 0.5, new Scalar(0, 255, 0), 1, LineTypes.Line8); } return frame; }画框逻辑里HersheySimplex是 OpenCV 内置字体不会依赖系统字体库所以发布到别的机器也不会出现文字乱码。文字位置放在矩形框的左上角偏上如果Y - 5小于 0比如目标在画面最顶端文字会被截断常见做法是加一个Math.Max(0, box.Y - 5)的夹取。3.3 界面与预览线程主窗体MainForm里跑着一个Timer或者后台线程定时读取摄像头帧并调用Detect和DrawResults。关键在于Cv2.ImShow不能在非 UI 线程直接调用这个项目用的是PictureBox显示每次从 Mat 转 Bitmap 时会有一次像素拷贝。这个拷贝是帧率瓶颈之一常见优化是复用 Bitmap 对象避免每次 new 造成 GC 压力private Bitmap _buffer; private void UpdateFrame(Mat frame) { if (_buffer null || _buffer.Width ! frame.Width || _buffer.Height ! frame.Height) { _buffer new Bitmap(frame.Width, frame.Height); } // Mat 到 Bitmap 的转换OpenCvSharp 直接提供扩展方法 Bitmap bmp OpenCvSharp.Extensions.BitmapConverter.ToBitmap(frame); pictureBox.Image?.Dispose(); pictureBox.Image bmp; }这里有一个取舍直接ToBitmap每次都产生新 Bitmap简单但频繁分配内存复用_buffer则需要处理 Mat 内存对齐问题。对于 640x480 的分辨率其实直接 ToBitmap 就够用只有在 1080p 以上才需要真正意义的共享内存优化。4. 跑通项目环境配置与运行参数4.1 环境准备这个项目用的技术栈是 .NET Framework 或 .NET 6/8取决于你的 OpenCvSharp 版本。推荐直接用 Visual Studio 打开.csproj通过 NuGet 安装 OpenCvSharp4 和 OpenCvSharp4.runtime.win。需要注意运行时包必须安装否则打开会报DllNotFoundException因为 OpenCvSharp 是 P/Invoke 封装找不到原生 dll 就会砸掉。如果你的机器上有多个 OpenCvSharp 版本残留NuGet 还原时容易冲突。我一般会先在项目里清一遍packages目录再重新还原比较干净。4.2 运行步骤与界面操作打开MainForm.cs找到摄像头初始化代码默认是Cv2.VideoCapture(0)也就是第一个 USB 摄像头。如果你的摄像头索引不是 0改成 1、2 试试。在界面加载事件里设置默认颜色范围比如红色识别需要给ColorRange传两组值因为红色在 HSV 中跨越 0 和 180 两端项目里通常用两个 range 做 OR 合并。运行程序后画面左侧是原始视频右侧是掩膜结果下方滑动条分别控制 H、S、V 的上下界。拖动滑动条观察掩膜中目标区域是否完整、背景是否被误选。调完之后点击「保存参数」会生成一个config.xml或 JSON 文件记录当前阈值。4.3 参数调优滑动条调试滑动条是这个项目最实用的部分它把 HSV 阈值从「改代码-编译-运行」循环里解放出来。OpenCvSharp 里滑动条绑定的方法非常简单关键点在于它是静态回调需要把当前帧和掩膜都存成类字段回调里才能实时刷新预览。private void TrackbarHMin_ValueChanged(int pos) { colorRange.Lower new Scalar(pos, colorRange.Lower[1], colorRange.Lower[2]); UpdatePreview(); }调参是有套路的别乱拖。先固定 S 和 V 为最大范围只调 H 把目标颜色的色相圈出来然后逐步缩小 S 下限把低饱和度的背景排除最后调 V应对过曝或过暗。每一步都看右侧掩膜而不是看最终画框效果因为画框的轮廓过滤会掩盖小的阈值错误。5. 避坑指南HSV 颜色识别那些玄学问题5.1 红色分量的回绕问题现象设置红色 HSV 阈值(0, 100, 100)到(10, 255, 255)识别出的红色目标偶尔缺失尤其当物体颜色偏深红或粉红时。原因HSV 色相 H 是一个环形红色分布在 0 附近既包括 0-10也包括 170-180。如果你只设置了下限 0、上限 10那么偏紫红色H 接近 170的像素全部被丢弃。解决对红色做两次InRange一次覆盖(0, minS, minV)到(10, maxS, maxV)另一次覆盖(170, minS, minV)到(180, maxS, maxV)然后用Cv2.BitwiseOr合并两张掩膜。Mat mask1 new Mat(); 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); Cv2.BitwiseOr(mask1, mask2, mask);5.2 环境光变化导致识别不稳定现象上午调好的阈值下午太阳角度一变掩膜里出现大量噪点或者目标区域明显缩小。原因家用摄像头通常有自动白平衡和自动曝光它会根据环境动态调整色彩增益导致同一块颜色在不同时刻的 HSV 值漂移。解决如果现场是固定光源把摄像机的自动曝光关闭固定曝光时间。如果是户外场景不能关闭自动曝光就要在算法层增大 S 和 V 的容差并把高斯模糊的核从 5x5 改成 7x7。注意模糊核增大后面积过滤下限也要相应提高。5.3 OpenCvSharp 的 Mat 与 Bitmap 转换异常现象程序运行到BitmapConverter.ToBitmap时提示ArgumentException: Parameter is not valid。原因Mat 的通道数或位深不满足 Bitmap 要求常见于把灰度掩膜Mat直接 ToBitmap但 Bitmap 需要 8UC3 或 8UC1 格式有时你会拿到一个 8UC4 的 BGRA Mat转换器不支持。解决先在下游强制Cv2.CvtColor成ColorConversionCodes.GRAY2BGR或确认是 8UC3另外检查 Mat 是不是空对象frame.Empty()时不要进入转换逻辑。我习惯在转换前加个断言。5.4 摄像头帧率低到没法用现象识别结果像是幻灯片每秒只有 3-4 帧。原因没有限制PictureBox.Image的 Disposal 频率每次更新都触发 UI 重绘加上Mat频繁分配垃圾回收把主线程拖住了。解决把预览更新降到 30fps 以内用一个Stopwatch控制只有超过 33ms 才刷新界面同时将分辨率从 1920x1080 降到 640x480处理时间能缩短到原来的三分之一。5.5 形态学核大小对不同距离的目标不通用现象目标在近处时识别正常人后退两步目标消失了。原因固定 5x5 的形态学核在远距离时会把小物体整个腐蚀掉因为远处目标像素面积小一次腐蚀就没了。解决不要用固定核按轮廓面积动态决定是否做开操作或者把面积过滤与形态学核解耦——小目标不做开操作只做闭操作。更稳的办法是先做连通域标记然后用面积过滤而不是先形态学再面积。6. 二次开发思路从单色识别到多目标与输出6.1 多颜色并行检测当前项目一次只识别一种颜色实际分拣场景往往要求同时框出红、蓝、绿。常见做法是定义一个ListColorRange对每一帧做循环检测把不同颜色的掩膜合并到一张图里绘制时用不同颜色的框。注意合并掩膜不能用简单加和用两张图填充不同灰度值如蓝100绿200最后按像素值区分颜色否则轮廓查找会把相邻颜色合并。Mat combinedMask new Mat(mask.Size(), MatType.CV_8UC1, Scalar.All(0)); // 蓝色填充 100绿色填充 200 Cv2.InRange(hsv, blueRange.Lower, blueRange.Upper, maskBlue); combinedMask.SetTo(100, maskBlue); Cv2.InRange(hsv, greenRange.Lower, greenRange.Upper, maskGreen); combinedMask.SetTo(200, maskGreen);6.2 输出目标坐标到外部设备识别框坐标本身只是起点机器人抓取需要把像素坐标换算成相机坐标系下的物理位置。这里有个常用技巧在画面上取四个标定点用GetPerspectiveTransform做一个单应性矩阵把像素点投影到实际平面坐标。C# 侧计算完成后通过 SerialPort 输出X:123,Y:456这样的文本帧下位机解析即可。6.3 接入视频文件与网络流VideoCapture的参数可以传文件路径也可以传网络流地址。改造成本非常低只需要把构造参数从整数索引改成字符串。要注意的是网络流解码耗时比本地文件高很多必须把处理线程和 UI 线程彻底分开否则一个网络抖动就卡死界面。// 摄像头 using (var capture new VideoCapture(0)) // 视频文件 using (var capture new VideoCapture(test.mp4)) // RTSP 网络流 using (var capture new VideoCapture(rtsp://192.168.1.100:554/stream1))改造完后建议用一段带有复杂背景的视频做回归测试观察掩膜中是否有环境物体被误判。我自己的习惯是每次改完阈值或形态学参数都先跑一段录制好的视频回放而不是直接拿摄像头试因为回放可以反复比对参数效果。从那以后我每次调完参数都强制走一遍回放流程省下的现场调试时间不是一点点。希望这个项目的拆解对你有所帮助跑起来后再动手改几个参数你会比看文档学得快得多。本文还有配套的精品资源点击获取