简介:面向C#开发者和计算机视觉入门者,提供基于Emgu.CV在.NET平台识别条码的完整示例工程。项目以图像预处理、条码定位与解码为主线,覆盖灰度化、高斯滤波等常用操作,并演示BarcodeReader等识别接口的调用步骤,适合用于零售、物流、仓储等自动识别场景的预研与学习。资源包共53个文件,压缩后容量39.19MB,包含C#源代码、Visual Studio解决方案和工程文件、可直接运行的可执行程序、依赖的动态链接库,以及用于深度学习条码识别的模型与配置文件。各类型文件分工明确,既支持打开工程直接编译调试,也便于按需替换模型或扩展识别逻辑。已有266人浏览学习。通过这份资料,读者既能获得一个可运行的条码检测程序,也能从源码中理解图像预处理和Emgu.CV的基本用法,还可借助附带模型探索更复杂的检测思路,是入门C#计算机视觉的实用参考。
1. Emgu.CV 条码检测:相机拍照找码,不是等扫码枪
手持扫码枪要近距离对准、一次只能扫一个,产线一停就是成本。换成工业相机拍摄,一帧画面里的所有条码可以同时定位并解码,这就是 Emgu.CV 条码检测解决的问题。Emgu.CV 作为 OpenCV 的 .NET 封装,把图像预处理、条码候选区域定位、内容解码完整地收进 C# 上位机程序里,不依赖扫码枪厂商的私有 SDK,也不需要额外购买读码硬件。自动化设备、视觉检测、仓储分拣这三个方向最容易遇到这个需求,承接 C# 上位机开发、或想把人工扫码升级成自动读码的工程师都可以直接参考。下面先拆模块,再给一条从预处理到解码的可运行管线,最后落在参数调优和部署验证上。
2. 搭建 Emgu.CV 条码检测环境与模块选型
2.1 NuGet 依赖:Emgu.CV 与 ZXing.Net 的匹配关系
在 Visual Studio 里新建一个 .NET Framework 4.8 或 .NET 6 的 C# 项目,先装两个包:Emgu.CV(核心托管 API 和 x64 native 库)和ZXing.Net(条码解码库)。如果你的目标框架不是 .NET 6 而是 .NET Framework 4.7.2,还需要额外确认System.Drawing的引用是否完整,因为 ZXing 解码接口收到的对象是Bitmap,而Mat.ToBitmap()的转换结果直接依赖 System.Drawing 的类型定义。
版本搭配上有两个实际踩过的坑。第一,Emgu.CV 较新版本封装了 OpenCV 的 barcode 检测模块,对应命名空间是Emgu.CV.Barcode,里面的BarcodeDetector可以独立完成定位和解码,但它对一维码(EAN-13、Code 128)的识别率不如 ZXing,对 QR Code 的纠错能力也有限,所以产线上更常用的分工是:Emgu.CV 负责把条码从复杂背景里分离出来,ZXing 负责把分离出来的图像变成字符串。第二,Emgu.CV 的 native 库分 x64 和 x86,ZXing.Net 是纯托管程序集,二者在 CPU 架构上通常没有冲突,但一旦项目里同时引用了相机 SDK(比如海康、Basler 的官方库),就必须把所有库统一到同一个位数,否则程序会在第一次调用 native 方法时直接抛DllNotFoundException,表现和没装运行库一模一样,排查起来很费时间。
提示:安装 Emgu.CV 后首次运行报
Unable to load DLL 'cvextern',先检查 NuGet 还原是否完整,再把项目平台目标从 AnyCPU 改成 x64,最后确认输出目录里有cvextern.dll和opencv_videoio_ffmpeg*.dll。
2.2 BarcodeDetector 与 ZXing 各管哪一段
把条码检测拆成两步,思路会清楚很多:第一步回答“条码在哪里”,第二步回答“条码内容是什么”。第一步输入原始图像,输出若干候选矩形框;第二步输入裁剪后的小图,输出文本和条码格式。这里把职责列出来看:
| 步骤 | 工具 | 输入 | 输出 | 优势 |
|---|---|---|---|---|
| 条码定位 | Emgu.CV 图像处理 / BarcodeDetector | 原始 Mat | 矩形候选框集合 | 速度快、可控、不依赖条码类型 |
| 条码解码 | ZXing.Net | Bitmap / LuminanceSource | 文本 + BarcodeFormat | 一维/二维通用、纠错能力强 |
BarcodeDetector直接调用 OpenCV 的 barcode 模块时,Detect方法只负责定位,DetectAndDecode一步到位返回内容。但工业场景里光照多变、条码可能有倾斜和残缺,一步到位的方案在失败时没有中间结果可看,你分不清是“定位没找到”还是“解码没成功”,排错成本很高。我一般只把DetectAndDecode当验证工具用,产线上的算法仍以自己控制的预处理管线为主线,每一步都能把中间图像存下来。
这里要说明为什么不做“直接拿原图丢给 ZXing”这种更省事的路径。ZXing 对清晰、正对、光照均匀的图像确实一次就能出结果,但产线相机拍到的图往往有阴影、运动模糊、反光带,直接送解码器的成功率会明显下降,而且失败时不提供任何定位线索,问题非常难追踪。先用 OpenCV 把条码区域从背景里分离出来,再针对性裁剪、增强、送解码器,每一步的中间结果都能存档观察,出现漏检时能很快定位是哪一环出了问题,这也是 Emgu.CV 条码检测方案的核心价值所在。
3. 用 Emgu.CV 定位条码 + ZXing 解码的完整管线
3.1 灰度化、Sobel 梯度与二值化:把条码的纹理转成白块
直接给出一段能跑通的主流程代码。以下示例从文件读取图像,实际项目中把source换成相机采集到的Mat即可,后续步骤完全一致。
using Emgu.CV; using Emgu.CV.CvEnum; using Emgu.CV.Structure; using Emgu.CV.Util; using System.Drawing; Mat source = new Mat("barcode_sample.jpg", ImreadModes.Color); // 1. 转灰度 Mat gray = new Mat(); CvInvoke.CvtColor(source, gray, ColorConversion.Bgr2Gray); // 2. 高斯滤波,削弱传感器噪点 CvInvoke.GaussianBlur(gray, gray, new Size(5, 5), 1.2); // 3. 水平方向 Sobel 梯度 Mat gradX = new Mat(); CvInvoke.Sobel(gray, gradX, DepthType.Cv32F, 1, 0, 3); CvInvoke.ConvertScaleAbs(gradX, gradX, 1, 0); // 4. 固定阈值二值化 Mat binary = new Mat(); CvInvoke.Threshold(gradX, binary, 100, 255, ThresholdType.Binary);这段代码的关键在第三步。先转灰度是为了去掉彩色噪声对梯度计算的干扰;高斯滤波核取 5×5、σ 取 1.2,能削弱传感器噪点,又不至于把条码的黑白边界磨平。Sobel 在 x 方向求一阶导,条码的竖条在水平方向上有密集的灰度跳变,梯度响应值很高,背景区域则接近 0,这一操作把“条形码”变成了“亮条区域”。阈值 100 是可调参数,强光环境可以提高到 150 以上,暗光环境降到 60 左右,调参标准是二值图里条码区域必须连成白色块且背景没有大面积白斑。
3.2 形态学闭运算与轮廓过滤:定位候选条码区域
Sobel 梯度图上的条码区域是许多细密的白色竖线,直接找轮廓会得到上百个碎片,所以要用闭运算把这些竖线“焊”成一块整体。闭运算先膨胀后腐蚀,核的大小直接决定合并效果:这里用 21×7 的矩形核,水平方向把 21 像素内的白色条纹连成一片,垂直方向只允许 7 像素内的竖条合并,既尊重条码的长条形状,又不会把上下相邻的文字或图形也连进来。
// 5. 闭运算 + 膨胀,把条纹合并成候选块 Mat kernel = CvInvoke.GetStructuringElement( ElementShape.Rectangle, new Size(21, 7), new Point(-1, -1)); CvInvoke.MorphologyEx(gradX, gradX, MorphOp.Close, kernel, new Point(-1, -1), 3, BorderType.Default, new MCvScalar()); CvInvoke.Dilate(gradX, gradX, kernel, new Point(-1, -1), 4, BorderType.Default, new MCvScalar()); // 6. 轮廓提取 + 长宽比过滤 VectorOfVectorOfPoint contours = new VectorOfVectorOfPoint(); Mat hierarchy = new Mat(); CvInvoke.FindContours(binary, contours, hierarchy, RetrType.External, ChainApproxMethod.ChainApproxSimple); List<Rectangle> candidates = new List<Rectangle>(); for (int i = 0; i < contours.Size; i++) { Rectangle rect = CvInvoke.BoundingRectangle(contours[i]); double aspect = rect.Width / (double)rect.Height; if (rect.Width >= 60 && rect.Height >= 12 && aspect >= 2.5) candidates.Add(rect); }迭代两次闭运算再加四次膨胀,目的是把相邻条纹彻底连通,并把候选区域向外扩一圈,防止条码边缘的零碎梯度在轮廓提取时把区域切开。MorphologyEx的迭代次数和核尺寸是最值得调的参数,条码密集、条纹细时,核宽度可以缩小到 15;条码稀疏、占幅大时,核宽度可以加到 31。后面的过滤条件里,宽度下限 60 像素过滤小噪点,高度下限 12 像素过滤偶然形成的长条噪线,宽高比 2.5 是条码的固有形状特征——一维码不管放得多正、字段多短,横向尺寸都显著大于纵向,用这个条件可以筛掉绝大多数文字、按钮、图形类干扰。
3.3 ZXing.Net 解码与 Mat 转 Bitmap 的两个坑
获取到候选框之后,从原图上裁剪对应区域,转成Bitmap后交给 ZXing 解码:
var reader = new ZXing.BarcodeReader(); foreach (Rectangle rect in candidates) { using (Mat roi = new Mat(source, rect)) using (Bitmap bmp = roi.ToBitmap()) { ZXing.Result result = reader.Decode(bmp); if (result != null) { Console.WriteLine( $"rect=[{rect.X},{rect.Y}] " + $"format={result.BarcodeFormat} text={result.Text}"); } } }new Mat(source, rect)创建的是一个指向原图内存区域的新 Mat 头,不会复制像素数据,开销很小;真正耗时的是roi.ToBitmap(),它会申请新内存并完成整块像素拷贝。这里有两个实际工作中经常踩的坑。第一个坑是ToBitmap()在热路径上被频繁调用,单帧几十个候选框时视觉上可能不明显,但如果循环采集和 UI 刷新在同一个线程,画面就会明显卡顿,也就是大家常搜的“c# 循环数据采集和 UI 刷新卡顿”问题,解法见 4.4 节。第二个坑是 ZXing 对横向排布的一维码识别最稳,如果条码是竖着的,矩形框里的条纹方向垂直,解码率明显下降,这时要先按矩形角度旋转原图再裁剪,旋转方案见 4.2 节。
4. 工业现场参数调优:光照、模糊、多码与帧率
4.1 光照不均时的自适应阈值与参数含义
固定阈值最怕的画面是:条码区域被阴影斜切,一半亮一半暗,阈值取高了暗部变黑,取低了亮部过曝。这种场景把固定阈值换成局部自适应阈值,效果立竿见影:
Mat adaptive = new Mat(); CvInvoke.AdaptiveThreshold(gray, adaptive, 255, AdaptiveThresholdType.GaussianC, ThresholdType.Binary, 11, 2);自适应阈值不是用一个全局值切分图像,而是对每个像素取它邻域的加权平均作为阈值,邻域半径由blockSize决定,这里的 11 表示 11×11 窗口;param1的 2 是常量偏置,最终阈值是邻域均值减 2。窗口越大对光照变化的跟随越慢,窗口越小越容易把条码内部的白色间隙误判成背景,一般推荐 9 到 15 之间取奇数值。产线上如果条码周围有大面积深色包装图案,还可以把GaussianC换成MeanC,前者用高斯加权平均、抗噪更好,后者计算快、边缘更锐利,二者在条码场景下差异不大,优先以现场画面为准。
4.2 倾斜条码的旋转校正与运动模糊的去噪策略
条码贴歪是产线常态,水平 Sobel 对倾斜超过 15 度的条码响应会减弱。先用MinAreaRect拿到最小外接矩形和角度,再做旋转:
RotatedRect minRect = CvInvoke.MinAreaRect(contours[i]); float angle = minRect.Angle; if (Math.Abs(angle) > 5) { PointF center = minRect.Center; Mat rotMat = CvInvoke.GetRotationMatrix2D(center, angle, 1.0); Mat rotated = new Mat(); CvInvoke.WarpAffine(source, rotated, rotMat, source.Size); // 在 rotated 上重新执行 3.1 和 3.2 的定位逻辑 }注意MinAreaRect返回的角度范围是 -90 到 0 度,旋转时应取angle + 90还是直接取angle,取决于条码的朝向和你观察到的正方向,建议先打印角度和画面,确认旋转方向后再固化逻辑。运动模糊是另一个常见问题,产线传送带速度超过每秒 1 米时,卷帘快门相机拍出来的条码边缘会有重影,此时先做一步FastNlMeansDenoising再去 Sobel 往往比换更高快门更快见效,代价是单帧耗时增加 10 到 30 毫秒,对 30 帧以下的检测任务完全可接受。
4.3 多条码同时检出:ROI 裁剪与干扰过滤
一箱多码在仓储场景里很常见,外箱一个条码、内部每个产品各一个条码。定位逻辑天然支持多目标,FindContours会把所有满足过滤条件的轮廓都返回。真正要处理的是两个问题:一是重叠矩形,二是误检框。重叠矩形可以按 IoU 合并,两个候选框交集面积占较小框面积超过 60% 时,保留较大的一个;误检框靠的是上下文规则,比如条码面积占比、候选框内部有没有连续的梯度周期,最实用的方法是把result.Text拿去做格式校验,EAN-13 有校验位,Code 128 有起始符约束,ZXing 解码失败或格式不符的候选框直接丢弃。
裁剪 ROI 时还有一个小技巧:不要把矩形原样裁剪,而是向外扩展 5 到 10 个像素,把条码两端的空白区包含进来。ZXing 内部解码时需要一定的前后留白来定位起始符和结束符,裁剪过紧是解码失败的常见原因。
4.4 连续检测不卡 UI:线程模型与画面帧率
上位机里最容易出现的问题就是采集、检测、显示挤在 UI 线程里,帧率一高界面直接假死。标准做法是把采集到显示的链路拆成两个线程:
Task.Run(() => { List<Rectangle> rects = BarcodePipeline.Locate(mat); List<string> texts = BarcodePipeline.DecodeCandidates(mat, rects); BeginInvoke((Action)(() => { // 只有最终结果回到 UI 线程 txtResult.Text = string.Join(", ", texts); })); });Task.Run里的定位和解码全部在后台线程执行,BeginInvoke只把最终结果传回 UI 线程做显示,这是 WinForms 的写法,WPF 里对应Dispatcher.BeginInvoke,原理一样。这里要克制的是不要在BeginInvoke里回传Mat或Bitmap,大对象跨线程传递既慢又容易触发内存抖动,只传字符串或矩形坐标就够。如果相机帧率是 60 帧而检测算法只能跑到 25 帧,可以做帧丢弃策略,只处理最新帧,保证 UI 始终显示最近的检测结果,而不是积压一队待处理图像让延迟越来越大。
5. 验证检测率与部署避坑
5.1 用测试图片集和 IoU 统计检测率
调参不能靠感觉,要在现场采集图片之前先用测试集把算法能力量化清楚。准备 100 张左右有代表性的图,每张手工标注条码的真实矩形框,格式用最简单的 JSON 或 XML 存坐标,然后跑一个统计脚本:
int tp = 0, fp = 0, fn = 0; foreach (string file in Directory.GetFiles(testDir, "*.png")) { List<Rectangle> dets = BarcodePipeline.Locate(new Mat(file)); List<Rectangle> truths = LoadTruth(file); // 判定规则:检测框与标注框的 IoU > 0.5 视为正确检出 // 统计 tp / fp / fn 并汇总精确率和召回率 }IoU 阈值 0.5 是目标检测领域常用的默认值,但条码场景建议放宽到 0.4,因为人工标注框的边界本身有 2 到 3 像素的误差。统计完精确率和召回率后,优先看漏检样本,把漏检图按失败原因分类:光照不均、模糊、倾斜、过小、遮挡,每一类对应一个专门的调参方向,而不是盲目改全局参数。
5.2 部署时最容易漏的三样东西
算法验证完成后部署到工控机,最容易出问题的不是代码本身,而是运行环境。第一,确认目标机的 VC++ 运行库版本,Emgu.CV 的 native 库依赖 Visual C++ Redistributable,工控机通常比较干净,装机后第一次跑就报缺少 DLL 的案例非常多。第二,确认 native 文件完整,把cvextern.dll、opencv_videoio_ffmpeg*.dll这些库连同 exe 一起发布,不要只拷托管 DLL。第三,确认平台位数和相机 SDK 一致,x64 相机配 x86 的 Emgu.CV native 库会在初始化时静默失败或抛出随机异常。
部署完再用生产环境的同一张标定图做一次端到端验证,从相机采图到结果输出全链路走一遍,确认没有依赖开发机的路径或环境变量。之后把检测结果通过事件或消息队列抛给上位机的业务流程模块,替代原先扫码枪的触发信号,PLC 联动逻辑无需改动即可平滑切换。
本文还有配套的精品资源,点击获取