简介:面向 C# 与计算机视觉学习者的 OpenCV 颜色识别示例包,以 OpenCvSharp 调用 HSV 颜色空间完成颜色筛选与目标区域提取,适合需要理解颜色识别流程、准备课程设计或做图像处理练习的开发者参考。压缩包共 59 个文件,约 35.14MB,内含 17 个 cs 源码、8 个 dll 依赖、6 个 resx 与多个 resources 资源文件,并配有 sln 解决方案、csproj 工程、config 配置及 jpg/png 示例图片;文件按 Visual Studio 项目组织,覆盖窗体界面、HSV 参数设置、图像显示与识别逻辑等模块。已有 1723 人学习或下载。可参考的内容包括 BGR 转 HSV、颜色上下限区间设定、InRange 生成二值掩码、按掩码保留目标颜色区域,以及调参窗体和多窗体展示等实现,能帮助读者快速把握 OpenCvSharp 颜色识别的工程结构,并在此基础上进行二次开发与参数调试。
1. 从一张偏色的传送带照片说起:opencv颜色识别.zip 到底能干什么
传送带上的红色料盒被顶灯照得发白,用 C# 调 OpenCvSharp 做 opencv颜色识别,第一版阈值直接翻车——RGB 三个通道一起飘,换成 HSV 才稳住。这个压缩包就是围绕这条链路做的示例工程:读图或接摄像头,把 BGR 转成 HSV,按颜色区间生成掩膜,再做形态学去噪和轮廓筛选,最后把目标框和中心点画回原图。它适合刚接触机器视觉的 C# 开发者、要做颜色分拣/计数/定位前置验证的人,也适合拿 WinForms 或 WPF 快速搭一个调试界面。别把它当成万能识别器,它更像一套可改参数的底子,光照、材质、相机白平衡一变,阈值就得跟着调。
2. 技术选型与工程骨架:C# + OpenCvSharp 的 HSV 识别链路怎么搭
这个包没有走 Python 那条路,而是 C# + OpenCvSharp。原因很实际:很多上位机、工控界面、WinForms/WPF 工具本来就是 C# 写的,识别逻辑如果能在同一个进程里跑,省掉跨语言通信和额外部署。OpenCvSharp 把 OpenCV 的 Mat、CvtColor、InRange、FindContours 这些 API 包了一层,写法接近原生 OpenCV,调试时又能直接用 C# 断点看中间变量。下面这张表是这类工程最常见的选型组合,拿到包后可以对着 csproj 和引用逐项核对。
| 层 | 常见选型 | 作用 | 注意点 |
|---|---|---|---|
| 语言/运行时 | C# / .NET 6、8 或 .NET Framework 4.8 | 承载界面和识别逻辑 | 老项目常见 Framework,新项目建议 .NET 6+ |
| 视觉库 | OpenCV + OpenCvSharp4 | 图像读取、色彩转换、阈值、轮廓 | 主包和运行时包版本要一致 |
| 图像互操作 | OpenCvSharp4.Extensions | Bitmap 与 Mat 互转 | 通道顺序和 PixelFormat 要确认 |
| 界面 | WinForms 或 WPF | 显示原图、掩膜、识别结果 | 别在 UI 线程里跑重循环 |
| 配置 | JSON 或 App.config | 存 HSV 范围、最小面积 | 阈值外置后调参不用重编译 |
2.1 为什么颜色识别优先用 HSV 而不是 RGB
RGB 不是不能做颜色识别,而是它把颜色和亮度揉在三个通道里。白天红色料盒的 R 通道高,下午顶灯偏暖,R 和 G 一起升高,原来卡好的R>180 && G<80就可能把橙色也放进来。HSV 把色相 H 单独拎出来,S 管饱和度,V 管亮度,调阈值时先卡 H 找颜色大类,再用 S/V 过滤灰白和暗部,逻辑清楚很多。
OpenCV 的 8 位 HSV 有一个必须记住的坑:H 的范围是 0 到 180,不是常见的 0 到 360。因为 8 位图一个通道最多 255,OpenCV 把角度除以 2 存进去。S 和 V 都是 0 到 255。红色又正好跨在 H 的零点两侧,所以红色通常要写两段:0 到 10 一段,170 到 180 一段,再用按位或合并掩膜。只写一段的话,相机白平衡稍微偏一点,红色就会从另一边漏掉。
| 颜色 | H 主区间 | S 建议下限 | V 建议下限 | 备注 |
|---|---|---|---|---|
| 红 | 0-10 / 170-180 | 120 | 70 | 跨零点,必须两段 |
| 绿 | 35-85 | 80 | 80 | 区间宽,容易混入黄绿 |
| 蓝 | 90-130 | 80 | 80 | 深蓝要降 V 下限 |
| 黄 | 20-35 | 100 | 100 | 和暖光容易混 |
这张表只能当起点,不能当标准答案。不同相机、镜头、光源下,同一个红色料盒的 H 可能落在 2 到 8,也可能落在 175 到 179。我一般会先拍一张现场图,用取色工具读几个目标像素的 HSV,再取最小值和最大值往外扩 3 到 5 个 H 单位,S/V 下限先保守一点,后面用面积过滤兜底。
2.2 一个最小识别链路的模块划分
这类工程拆开看并不复杂,关键是每个模块只做一件事,出问题能定位。图像源负责读图或取帧;预处理负责缩放、模糊、ROI 裁剪;颜色分割用InRange生成单通道掩膜;形态学去掉噪点和填补空洞;轮廓分析算面积、外接矩形、质心;结果输出负责画框、写日志或给下游传坐标。模块之间用 Mat 和简单的结果对象传递,不要让 UI 代码混进识别类里。
常见做法是把颜色范围抽成一个HsvRange类,把单帧识别抽成一个ColorDetector类,多颜色再包一层MultiColorClassifier。这样后面加新颜色只是加一条配置,不用复制一整段if。摄像头采集和界面显示放在入口层,识别类只接收Mat,返回结果和可视化图。调试时可以先绕过摄像头,直接用静态图喂给ColorDetector,把阈值调稳再接实时流。
提示:先静态图后摄像头。摄像头有自动曝光、自动白平衡、第一帧偏黑这些变量,静态图都没跑通就接摄像头,排查时根本分不清是阈值问题还是采集问题。
2.3 运行环境与依赖版本怎么对齐
C# 项目跑 OpenCvSharp,最常见的翻车不是代码写错,而是依赖没对齐。主包OpenCvSharp4提供托管 API,非托管 DLL 在运行时包里,Windows 通常要加OpenCvSharp4.runtime.win,Bitmap 互转再加OpenCvSharp4.Extensions。这三个包的版本最好保持一致,不要主包用新版本、运行时包还是旧版本,否则容易出现找不到入口点或者DllNotFoundException。
dotnet add package OpenCvSharp4 dotnet add package OpenCvSharp4.runtime.win dotnet add package OpenCvSharp4.Extensions这三条命令不指定版本时,NuGet 会取当前源里的最新稳定版。装完打开 csproj 看一眼,三个PackageReference的版本号应该一致;不一致就手动改成同一版本再还原。平台目标建议固定 x64,尤其是老项目从 AnyCPU 切过来时,容易在 32 位进程里加载不到 64 位非托管 DLL。运行后如果报Unable to load DLL 'OpenCvSharpExtern',先去输出目录看有没有这个文件,再看运行时包有没有被正确复制。
3. 目录结构与跑通步骤:从解压到看到第一块掩膜
拿到压缩包先别急着改阈值,先确认项目能不能编译、能不能出图。这类 C# 视觉 Demo 的目录组织通常不会太复杂,按解决方案文件、项目文件、识别类、样图这几类去找就行。不同版本的命名可能略有差异,但入口和依赖关系基本一致。
opencv颜色识别/ ├─ ColorRecognition.sln ├─ src/ │ └─ ColorRecognition/ │ ├─ ColorRecognition.csproj │ ├─ Program.cs │ ├─ MainForm.cs │ ├─ Vision/ │ │ ├─ HsvRange.cs │ │ ├─ ColorDetector.cs │ │ └─ MultiColorClassifier.cs │ └─ Assets/ │ └─ samples/ ├─ README.md └─ .gitignore上面是常见组织方式,实际包里的文件夹名可能叫Forms、Detector或者直接平铺。找的时候抓住几个关键点:.sln是入口,.csproj里看TargetFramework和包引用,Program.cs或MainForm.cs看启动方式,Assets或images下看有没有测试图。识别类一般在名字里带Color、Hsv、Detector的文件中。
3.1 拿到压缩包先看哪几类文件
第一眼看.csproj,确认是.NET 6/8还是.NET Framework 4.x。如果是新式项目,文件里会有<TargetFramework>net6.0-windows</TargetFramework>、<UseWindowsForms>true</UseWindowsForms>这类节点;如果是老式项目,可能会看到packages.config和一堆HintPath。这决定你用dotnet命令还是用 Visual Studio 的旧版还原方式。
第二眼看入口。WinForms 项目通常从Program.cs的Application.Run(new MainForm())进,WPF 项目看App.xaml和MainWindow.xaml。入口里如果写死了图片路径,先确认那个路径在解压后的目录里存在;如果写死了摄像头索引0,先确认机器上有摄像头或者改成图片模式。
第三眼看样图。Assets/samples下如果有红色、绿色、蓝色的测试图,先用这些图跑,不要一上来就拿现场图。样图能跑通,说明依赖和基本链路没问题;样图都跑不通,先查环境,别怀疑 HSV 参数。
3.2 还原依赖与配置运行时
新式项目在解决方案目录下执行还原和编译。第一次跑先restore,再build,最后run,每一步报错都单独看,不要一条命令混到底。
dotnet restore dotnet build -c Release dotnet run --project .\src\ColorRecognition\ColorRecognition.csprojrestore负责拉 NuGet 包,build负责编译并复制非托管 DLL,run启动程序。如果build成功但run报 DLL 找不到,重点看输出目录下的OpenCvSharpExtern.dll和runtimes文件夹。老式.NET Framework项目用 Visual Studio 打开.sln,右键解决方案点“还原 NuGet 包”,再把平台切到 x64 编译。不要同时混用dotnet build和旧版 MSBuild,输出目录可能对不上。
注意:如果项目里有
AnyCPU和x64两个配置,先选x64。OpenCvSharp 的非托管 DLL 是分平台的,32 位进程加载 64 位 DLL 会直接崩,报错还不一定指向真正原因。
3.3 用静态图片和摄像头分别验证
静态图验证的代码很短,核心是读图、建检测器、出可视化结果。路径按你解压后的实际样图改,ImreadModes.Color保证读成三通道 BGR,不要用灰度模式。
using OpenCvSharp; using var src = Cv2.ImRead("Assets/samples/red_box.jpg", ImreadModes.Color); if (src.Empty()) throw new FileNotFoundException("样图路径不对,检查 Assets/samples 下有没有这张图"); using var detector = new ColorDetector(DefaultRanges.Red, minArea: 300, kernelSize: 5); using var result = detector.Detect(src); Cv2.ImShow("color-result", result.Visualized); Cv2.WaitKey(0);ImRead返回空 Mat 通常不是代码问题,而是工作目录不对。dotnet run的当前目录和 exe 所在目录可能不同,保险做法是用AppContext.BaseDirectory拼绝对路径。minArea: 300是像素面积,样图分辨率小的时候够用,换成 1280x720 的相机图就要往上调。kernelSize: 5表示 5x5 椭圆核,先小后大,别一上来就 11x11。
摄像头验证把图像源换成VideoCapture,其余识别逻辑不变。Windows 下常用VideoCaptureAPIs.DSHOW,Linux 下换成CAP_V4L2。刚打开时前几帧可能是黑的,先读几帧丢掉再进识别循环。
using var cap = new VideoCapture(0, VideoCaptureAPIs.DSHOW); if (!cap.IsOpened()) throw new InvalidOperationException("摄像头没打开,检查索引或被其他程序占用"); using var frame = new Mat(); while (true) { cap.Read(frame); if (frame.Empty()) break; using var result = detector.Detect(frame); Cv2.ImShow("camera-color", result.Visualized); if (Cv2.WaitKey(1) == 27) break; // Esc 退出 }VideoCapture(0, ...)里的 0 是默认摄像头索引,外接 USB 相机可能是 1 或 2。WaitKey(1)不只是等按键,它还负责刷新 HighGUI 窗口,少了它画面会卡住。result用using释放,避免长时间跑循环时 Mat 越积越多。如果画面颜色明显不对,先查摄像头是不是输出 MJPG/YUY2 导致通道错位,再查Bitmap转换那一步。
4. 核心代码拆解:HSV 阈值、形态学与轮廓判定逐段讲
颜色识别的核心代码其实就三段:把 BGR 转成 HSV,用InRange生成掩膜,从掩膜里找轮廓并过滤。真正花时间的是参数和边界情况。下面按一个可编译的ColorDetector拆开讲,代码里的默认值可以直接跑,但不要当成现场万能值。
4.1 颜色范围定义与红色双区间处理
先定义颜色范围。Scalar在 OpenCvSharp 里是double四元组,InRange只用前三个分量,顺序是 H、S、V。红色用Lower1/Upper1和Lower2/Upper2两段,其他颜色第二段留空。
using OpenCvSharp; public sealed class HsvRange { public string Name { get; set; } = ""; public Scalar Lower1 { get; set; } public Scalar Upper1 { get; set; } public Scalar? Lower2 { get; set; } public Scalar? Upper2 { get; set; } } public static class DefaultRanges { public static HsvRange Red => new HsvRange { Name = "red", Lower1 = new Scalar(0, 120, 70), Upper1 = new Scalar(10, 255, 255), Lower2 = new Scalar(170, 120, 70), Upper2 = new Scalar(180, 255, 255) }; public static HsvRange Green => new HsvRange { Name = "green", Lower1 = new Scalar(35, 80, 80), Upper1 = new Scalar(85, 255, 255) }; public static HsvRange Blue => new HsvRange { Name = "blue", Lower1 = new Scalar(90, 80, 80), Upper1 = new Scalar(130, 255, 255) }; }红色的0-10和170-180两段必须都保留。只写0-10时,偏紫的红或冷光下的红会落到 170 以上,掩膜直接空掉;只写170-180时,正红又会从 0 附近漏掉。S下限 120 是过滤灰白和反光,V下限 70 是过滤暗部。现场如果红色料盒表面有高光,S 会掉下去,可以把 S 下限降到 80 到 100 试试,但别降太低,否则白色标签也会被当成红色。
绿色和蓝色的 H 区间比较宽,容易把相邻颜色吃进来。绿色35-85会覆盖黄绿,蓝色90-130会覆盖青蓝。实际调的时候先把区间收窄到目标颜色附近,再用面积和长宽比过滤。老项目如果是 .NET Framework,把new HsvRange改成显式new HsvRange { ... },Scalar?用可空结构体没问题。
4.2 单帧识别主流程
ColorDetector把转换、阈值、形态学、找轮廓串起来。形态学核在构造函数里创建一次,不要每帧都GetStructuringElement,实时循环里这也是开销。
using OpenCvSharp; public sealed class DetectItem { public double Area { get; set; } public Point Center { get; set; } public Rect Bounds { get; set; } public Point[] Contour { get; set; } = Array.Empty<Point>(); } public sealed class DetectResult : IDisposable { public Mat Visualized { get; } public IReadOnlyList<DetectItem> Items { get; } public DetectResult(Mat visualized, IReadOnlyList<DetectItem> items) { Visualized = visualized; Items = items; } public void Dispose() => Visualized.Dispose(); } public sealed class ColorDetector : IDisposable { private readonly HsvRange _range; private readonly int _minArea; private readonly Mat _kernel; public string Name => _range.Name; public ColorDetector(HsvRange range, int minArea = 300, int kernelSize = 5) { _range = range; _minArea = minArea; int k = kernelSize % 2 == 1 ? kernelSize : kernelSize + 1; _kernel = Cv2.GetStructuringElement(MorphShapes.Ellipse, new Size(k, k)); } public DetectResult Detect(Mat bgr) { if (bgr.Empty()) throw new ArgumentException("输入图像为空"); if (bgr.Channels() != 3) throw new ArgumentException("需要 BGR 三通道图像"); using var hsv = new Mat(); Cv2.CvtColor(bgr, hsv, ColorConversionCodes.BGR2HSV); using var mask = new Mat(); Cv2.InRange(hsv, _range.Lower1, _range.Upper1, mask); if (_range.Lower2.HasValue && _range.Upper2.HasValue) { using var mask2 = new Mat(); Cv2.InRange(hsv, _range.Lower2.Value, _range.Upper2.Value, mask2); Cv2.BitwiseOr(mask, mask2, mask); } Cv2.MorphologyEx(mask, mask, MorphTypes.Open, _kernel); Cv2.MorphologyEx(mask, mask, MorphTypes.Close, _kernel); Cv2.FindContours(mask, out var contours, out _, RetrievalModes.External, ContourApproximationModes.ApproxSimple); var items = FilterContours(contours, _minArea); var visualized = bgr.Clone(); foreach (var item in items) { Cv2.Rectangle(visualized, item.Bounds, Scalar.Red, 2); Cv2.Circle(visualized, item.Center, 4, Scalar.Lime, -1); Cv2.PutText(visualized, $"{_range.Name} {item.Area:F0}", new Point(item.Bounds.X, Math.Max(12, item.Bounds.Y - 6)), HersheyFonts.HersheySimplex, 0.5, Scalar.White, 1); } return new DetectResult(visualized, items); } private static List<DetectItem> FilterContours(Point[][] contours, int minArea) { var items = new List<DetectItem>(); foreach (var contour in contours) { double area = Cv2.ContourArea(contour); if (area < minArea) continue; var m = Cv2.Moments(contour); if (Math.Abs(m.M00) < 1e-6) continue; items.Add(new DetectItem { Area = area, Center = new Point((int)(m.M10 / m.M00), (int)(m.M01 / m.M00)), Bounds = Cv2.BoundingRect(contour), Contour = contour }); } return items; } public void Dispose() => _kernel.Dispose(); }CvtColor的BGR2HSV是固定入口,输入必须是 BGR 三通道。OpenCvSharp 读图默认就是 BGR,摄像头帧一般也是 BGR,但如果你从Bitmap手动拷字节,可能是 RGB 或 BGRA,这里就会错。InRange输出单通道 8 位掩膜,白色是命中,黑色是未命中。红色第二段用BitwiseOr合并,不能用加法,加法会在重叠区溢出。
MorphologyEx先开后闭:开运算去小噪点,闭运算填小空洞。_kernel是椭圆核,比矩形核更贴合圆润目标。FindContours用External只取外轮廓,ApproxSimple压缩点集,面积计算够用。bgr.Clone()是为了不污染输入帧,返回的Visualized由调用方释放。
4.3 轮廓过滤与中心点计算
FilterContours里做三件事:面积过滤、质心计算、外接矩形。Cv2.ContourArea返回像素面积,minArea是绝对像素值,所以换分辨率会失效,后面避坑章会专门讲。Moments的M00是零阶矩,等于轮廓面积;M10/M00是 x 质心,M01/M00是 y 质心。M00接近 0
本文还有配套的精品资源,点击获取