简介:图像处理与深度学习模型部署是工业视觉的核心环节。在复杂的产线环境中,条形码检测常受反光、倾斜和背景干扰影响,传统图像处理方法难以稳定定位。DBNet作为一种可微二值化分割网络,原本用于文本检测,其强大的纹理分割能力同样适用于条形码区域提取。通过ONNX Runtime,开发者可以在C#环境下高效部署深度学习模型,无需依赖Python服务或云端,实现本地快速推理。这种技术路线不仅简化了上位机集成流程,还能利用像素级分割结果进行角度校正与质量判断。本文基于C#与ONNX Runtime,结合DBNet模型,完整演示了条形码检测的源码实现,涵盖预处理、推理、后处理与性能调优,为工业自动化识别提供了一套可直接参考的解决方案。 条形码检测这个需求,在很多实际项目里都会碰到:扫码枪能解的条码,在图像里就是要靠视觉算法去定位。我在做C#上位机视觉项目时,接过一个需求——在产线上识别标贴上的条形码,但现场环境复杂,条码有倾斜、反光、打印质量参差不齐,用传统图像处理去找条码并不是很稳定。后来我把OCR领域常用的DBNet文本检测模型用到了条形码定位上,配合ONNX Runtime部署到C#环境下,效果意外地好。这篇文章就把我整套C# Onnx DBNet条形码检测源码的思路、踩坑过程、核心代码全部整理出来,给遇到同类需求的朋友一个能直接参考的完整示例。
如果你在做的项目也是C#上位机、工业质检、自动化识别相关的,并且需要一个不依赖云端、能在本机跑、推理速度够快的条码定位方案,那这套源码思路大概率适合你。整个方案以C#为主体语言,模型推理走ONNX Runtime,深度学习模型用DBNet(Differentiable Binarization),专门用来从图像里分割出条形码区域。跟传统的“找边缘、算梯度”方案相比,它的抗干扰能力明显更强,尤其适合复杂背景下的条码定位。
1. 方案背景与整体设计思路
1.1 为什么用DBNet检测条形码
传统条码定位最常用的做法是形态学操作,比如先转灰度、再求梯度、然后二值化、找连通域。这种方式代码简单、推理快,但有个致命弱点:一旦背景有复杂的纹理、打印字体、logo、表格线,二值化之后很容易把很多干扰区域也当成条码候选,最终要写大量的规则去过滤。我在现场就吃过这种亏——标签上有几行文字和网格线,传统方法检测出来的候选区域一屏都是,调阈值调得头皮发麻。
DBNet原本是做文本检测的,它的核心优势在于“可微二值化”(Differentiable Binarization)。简单理解,它不是一个简单的固定阈值分割,而是同时预测一个概率图和一个阈值图,再把两者结合得到最终的二值分割结果。这个特性让模型对光照不均、对比度低、背景复杂的场景有更强的鲁棒性。而条形码本质上就是一组密集的黑白条纹,从视觉特征上讲,它跟文本区域有非常相似的地方——都是局部纹理有明显梯度变化、区域内有连续的边缘结构。所以把DBNet迁移到条形码检测上,在模型结构上不需要大改,自己标注一批条码数据微调一下,或者直接用通用文本检测模型也能有一定效果。
当时我对比了三种方案,最终选DBNet:
| 方案 | 优点 | 缺点 | 适合场景 |
|---|---|---|---|
| 传统形态学 | 速度极快、无需模型 | 环境鲁棒性差、调参困难 | 背景干净、条码占比较大的固定场景 |
| YOLO系列目标检测 | 速度快、部署成熟 | 需要较多标注数据、条码倾斜时矩形框不准 | 只需要定位框、不需要精细分割的场景 |
| DBNet分割检测 | 能输出像素级分割掩码、抗背景干扰 | 模型参数相对大、后处理略复杂 | 复杂背景、条码倾斜/变形、需要精确区域 |
DBNet在条码检测这个场景下还有一个额外好处:它的输出是像素级预测,所以我可以进一步分析条码区域的纹理特征,比如黑白条纹的宽度比例、方向角度,用来做条码质量判断。这个对工业场景很有价值。
1.2 为什么选C# + ONNX Runtime
很多视觉项目团队会用Python来做模型推理,因为Python写起来快、OpenCV调库也方便。但到了实际产线上,尤其是C#写的上位机软件,用Python通常有两种方式:一种是起一个Python服务,C#通过HTTP或者Socket去请求;另一种是用Python.NET嵌入。这两个方式我都试过,各有各的坑。起服务的话,部署和开机自启麻烦,而且在交换机断连、服务挂了的时候要额外做监控;Python.NET嵌入的话,版本兼容问题让人头疼,DLL地狱了解一下。
ONNX Runtime的好处是它天然就是为跨语言部署设计的。微软官方的Microsoft.ML.OnnxRuntime包直接在NuGet上就能装,C#里调用跟调用普通库一样简单。而且ONNX Runtime对CPU的优化做得很好,支持Intel的MKLDNN、支持多线程,在工业电脑那种没有高端GPU的环境下也能跑出可接受的帧率。
我把整个方案定成C# + ONNX Runtime之后,项目部署就变得非常清爽:一个exe,一堆DLL,再加一个.onnx模型文件,拷到工控机上就能跑。不需要装Python环境,不需要配conda,故障排查也简单。
1.3 整体技术链路
整个检测流程可以拆成三步:
- 图像读取与预处理:加载图片、缩放、归一化、转通道顺序(HWC转CHW)
- ONNX Runtime推理:加载DBNet模型,前向计算得到概率图
- 后处理:对概率图做二值化、找轮廓、获取外接矩形/旋转矩形、映射回原图坐标,在原图上绘制结果
后面所有源码都是围绕这三条链路展开的。项目用到的最核心依赖就是两个:OpenCvSharp4(图像处理)和Microsoft.ML.OnnxRuntime(模型推理)。这两个库在NuGet上直接拉,不需要额外配置什么特殊环境。
2. 环境准备与模型转换
2.1 开发环境与NuGet依赖
我用的开发环境是Visual Studio 2022,目标框架.NET 6.0(用.NET Framework 4.8也可以,只要NuGet包版本选对)。项目类型建议直接用控制台应用,调试好了再迁移到WinForms/WPF或者你的上位机框架里。
需要安装的NuGet包如下:
Install-Package OpenCvSharp4 -Version 4.8.0.20230708 Install-Package OpenCvSharp4.runtime.win -Version 4.8.0.20230708 Install-Package Microsoft.ML.OnnxRuntime -Version 1.17.1这里有个细节,OpenCvSharp4.runtime.win这个包是运行库,里面包含了OpenCV原生DLL,必须装。如果你用.NET Framework版本,注意包版本要选对,有些新版本包已经不支持.NET Framework了。
2.2 DBNet模型准备与ONNX导出要点
DBNet模型本身的训练可以使用PaddleOCR的DBNet实现,也可以使用MMOCR的实现。因为本文重点是C#部署,所以我默认你已经有一个训练好或者下载好的DBNet模型。
如果是从PaddleOCR导出ONNX模型,有几点要特别注意:
- 输入节点名称通常是
x,输出节点名称一般是sigmoid_0.tmp_0(有的版本叫save_infer_model/scale_0.tmp_0),但转出来的ONNX输出节点名可能会变。 - 输入张量的shape是
[1, 3, H, W],注意DBNet一般训练时输入尺寸是640x640或者1152x1152,推理时最好像训练时保持一致或者等比例缩放。 - 如果用PaddleOCR的导出脚本,它会自动包一层预处理(归一化、通道转换),但我建议自己控制预处理,导出时用
--opset 11,并把动态轴打开,这样输入尺寸可以灵活一点。
下面是我常用的导出命令参考,如果你用PaddleOCR:
python tools/export_model.py \ -c configs/det/det_mv3_db.yml \ -o Global.pretrained_model=./pretrain/det_mv3_db_train/best_accuracy \ Global.save_inference_dir=./inference/det_db导出后再用paddle2onnx转换:
paddle2onnx \ --model_dir ./inference/det_db \ --model_filename inference.pdmodel \ --params_filename inference.pdiparams \ --save_file ./det_db.onnx \ --opset_version 11 \ --enable_onnx_checker True如果你是自己用PyTorch训练的DBNet,导出更简单,直接torch.onnx.export就行。关键是要弄清楚模型输出的结构:DBNet最终输出的是一个跟输入分辨率相同大小的概率图(经过sigmoid),拿到这个概率图后继续做后处理。有些训练源码里会输出二值图或者阈值图,注意导出的其实是期望中间哪个分支——标准做法是导出经过sigmoid的二值概率图。
2.3 确认模型的输入输出张量
拿到ONNX模型后,别急着写代码,先确认一下模型的输入输出信息。最省事的工具是Netron,网页版直接拖进去就能看。也可以用ONNX Runtime在C#里自己打印:
using var session = new InferenceSession("det_db.onnx"); foreach (var input in session.InputMetadata) { Console.WriteLine($"Input: {input.Key}, Shape: {string.Join(",", input.Value.Dimensions)}, Type: {input.Value.ElementDataType}"); } foreach (var output in session.OutputMetadata) { Console.WriteLine($"Output: {output.Key}, Shape: {string.Join(",", output.Value.Dimensions)}, Type: {output.Value.ElementDataType}"); }打印出来你会看到输入一般是[1,3,640,640]的float数组,输出一般是[1,1,640,640]的float数组。如果输出名不是你期望的,就把上面打印的名字记下来,后面后处理代码里直接用。
这里我踩过一个坑:如果模型的输出是[1,640,640]而不是[1,1,640,640],索引数据的时候要多留一个心眼,否则读数据的时候越界或者读出来全乱。建议写代码之前先把shape确认死,后面就能少很多调试时间。
3. 核心源码解析
3.1 工程结构与整体流程
我用一个控制台演示项目来说明,工程结构如下:
BarcodeDBNetDemo/ ├── Program.cs # 入口,读图、调用检测、保存结果 ├── BarcodeDetector.cs # 核心检测类:预处理、推理、后处理 └── models/ └── det_db.onnx # DBNet ONNX模型主流程的伪代码如下:
static void Main(string[] args) { string modelPath = @"models/det_db.onnx"; string imagePath = @"test.jpg"; using var detector = new BarcodeDetector(modelPath); Mat src = Cv2.ImRead(imagePath, ImreadModes.Color); List<RotatedRect> barcodes = detector.Detect(src); foreach (var rect in barcodes) { Cv2.Polylines(src, new Point[] { /*矩形的四个角点*/ }, true, Scalar.Red, 2); } Cv2.ImWrite("result.jpg", src); Console.WriteLine($"Detected {barcodes.Count} barcode(s)"); }实际项目里,Detect方法里返回的RotatedRect可以直接送给你自己的解码库(比如ZXing)去做解码,也可以只用来做区域标记。
3.2 图像预处理:从Mat到Tensor
DBNet的输入是一张归一化后的CHW浮点张量。OpenCvSharp读进来的Mat是HWC的BGR格式,因此要先转换颜色空间、缩放到模型输入尺寸、再调整布局。
下面是我写的预处理代码:
private Tensor Preprocess(Mat src, int inputSize, out float scale, out int padW, out int padH) { int h = src.Rows; int w = src.Cols; // 等比例缩放,短边补齐到inputSize,保持宽高比 float ratio = Math.Min((float)inputSize / w, (float)inputSize / h); int newW = (int)Math.Round(w * ratio); int newH = (int)Math.Round(h * ratio); Mat resized = new Mat(); Cv2.Resize(src, resized, new Size(newW, newH), 0, 0, InterpolationFlags.Linear); // 生成画布,用黑色填充(注意:如果模型训练时用均值填充,这里要跟着用均值) Mat canvas = new Mat(inputSize, inputSize, MatType.CV_8UC3, Scalar.All(0)); resized.CopyTo(canvas[new Rectangle(0, 0, newW, newH)]); // BGR转RGB Cv2.CvtColor(canvas, canvas, ColorConversionCodes.BGR2RGB); // Mat转float数组再转Tensor int channels = 3; float[] inputData = new float[channels * inputSize * inputSize]; unsafe { byte* ptr = (byte*)canvas.Data; for (int y = 0; y < inputSize; y++) { for (int x = 0; x < inputSize; x++) { int offset = (y * inputSize + x) * channels; for (int c = 0; c < channels; c++) { inputData[c * inputSize * inputSize + y * inputSize + x] = ptr[offset + c] / 255.0f; } } } } var tensor = new DenseTensor<float>(inputData, new[] { 1, 3, inputSize, inputSize }); return tensor; }几点说明:
canvas[new Rectangle(0, 0, newW, newH)]是OpenCvSharp里取子区域的方式,直接操作子区域做CopyTo可以避免多余拷贝,效率更高。- 把HWC转CHW的时候,上面这个写法是最直接的三层循环。如果你对性能有要求,可以用
Cv2.Split把三通道拆开再分别拷贝,但实测在640x640这种尺寸下,三层循环的开销也就几毫秒,完全够用。 - 归一化除以255是通用的做法。如果你转出来的模型,训练时用的是
mean=[0.485, 0.456, 0.406]、std=[0.229, 0.224, 0.225]这种ImageNet标准归一化,要按标准归一化处理。但DBNet训练时通常只用/255归一化,所以一般不需要减均值除方差。
3.3 ONNX Runtime推理
推理部分的代码非常简单,ONNX Runtime把前面的复杂度都封装掉了。但有几个关于RunOptions和RunResult的细节值得注意。
private Mat RunInference(Tensor inputTensor) { var inputs = new List<NamedOnnxValue> { NamedOnnxValue.CreateFromTensor("x", inputTensor) }; using var results = _session.Run(inputs); var outputTensor = results.First().AsTensor<float>(); int outH = outputTensor.Dimensions[2]; int outW = outputTensor.Dimensions[3]; // 把输出的[1,1,h,w]转成OpenCV的Mat,方便后处理 var outputData = outputTensor.ToArray(); Mat probMap = new Mat(outH, outW, MatType.CV_32FC1); Marshal.Copy(outputData, 0, probMap.Data, outputData.Length); return probMap; }注意这里的NamedOnnxValue.CreateFromTensor的第一个参数名"x",必须和你前面用Netron或者代码打印出来的输入节点名完全一致。如果不一致,推理时会报异常或者输出莫名其妙的结果。
另外,_session.Run(inputs)默认会跑完所有输出。如果模型有多个输出,只想要第一个,可以用RunOptions配合OutputNames来指定:
var runOptions = new RunOptions(); var outputNames = new[] { "sigmoid_0.tmp_0" }; using var results = _session.Run(inputs, outputNames, runOptions);这样能减少不必要的计算量。虽然DBNet通常只有一个输出,但养成指定输出名的习惯,在换其他模型时会少踩很多坑。
3.4 DBNet后处理:从概率图到条形码区域
后处理是整个流程里最影响最终效果的部分。DBNet原始论文里的后处理逻辑大致是:
- 对概率图做Sigmoid,得到每个像素属于条码区域的概率(有的onnx导出已经自带sigmoid)
- 用阈值(通常0.3)做二值化
- 对二值图做膨胀操作,把断裂的区域连通起来
- 查找轮廓,过滤掉太小的区域
- 对每个轮廓求最小外接旋转矩形
我封装后的后处理代码如下:
private List<RotatedRect> Postprocess(Mat probMap, float threshold, float scale, int padW, int padH, int srcW, int srcH) { // 1. 二值化 Mat binary = new Mat(); Cv2.Threshold(probMap, binary, threshold, 255, ThresholdTypes.Binary); binary.ConvertTo(binary, MatType.CV_8UC1); // 2. 膨胀,让条码区域连起来 Mat kernel = Cv2.GetStructuringElement(MorphShapes.Rect, new Size(3, 3)); Cv2.Dilate(binary, binary, kernel, new Point(-1, -1), 1); // 3. 查找轮廓 Cv2.FindContours(binary, out Point[][] contours, out _, RetrievalModes.External, ContourApproximationModes.ApproxSimple); List<RotatedRect> results = new List<RotatedRect>(); foreach (var contour in contours) { double area = Cv2.ContourArea(contour); if (area < 50) // 过滤噪声小区域 continue; RotatedRect rect = Cv2.MinAreaRect(contour); // 过滤太窄的长条(可能是文字行) float minSide = Math.Min(rect.Size.Width, rect.Size.Height); float maxSide = Math.Max(rect.Size.Width, rect.Size.Height); if (minSide < 10 || maxSide / Math.Max(minSide, 1) > 15) continue; // 把检测框的坐标从模型输入尺寸映射回原图尺寸 Point2f[] points = rect.Points(); for (int i = 0; i < 4; i++) { points[i].X = (points[i].X - padW) / scale; points[i].Y = (points[i].Y - padH) / scale; } // 校正成正常顺序的RotatedRect RotatedRect mappedRect = Cv2.MinAreaRect(points); results.Add(mappedRect); } return results; }这段代码里有两个小技巧:
- 膨胀操作很关键。如果条码有一定角度或者中间有断裂,膨胀可以把它连接成一个完整的整体。但膨胀核不能太大,否则会把附近的文字也连进来。
- 过滤条件里的长宽比限制很重要。因为我用通用文本检测模型的时候,经常会框出一些单独的文字行,这类文字行通常是长条状,而条形码相对来看更方正一些(大多数情况)。当然这只是个经验值,具体要看你现场条码的形态。
3.5 完整检测类封装
把前面几段串起来,完整的BarcodeDetector.cs核心方法如下:
public class BarcodeDetector : IDisposable { private readonly InferenceSession _session; private const int InputSize = 640; public BarcodeDetector(string modelPath) { _session = new InferenceSession(modelPath); } public List<RotatedRect> Detect(Mat src) { int srcW = src.Cols; int srcH = src.Rows; // 预处理 float scale = Math.Min((float)InputSize / srcW, (float)InputSize / srcH); int newW = (int)Math.Round(srcW * scale); int newH = (int)Math.Round(srcH * scale); int padW = 0; int padH = 0; Tensor inputTensor = Preprocess(src, InputSize, out scale, out padW, out padH); // 推理 Mat probMap = RunInference(inputTensor); // 后处理 List<RotatedRect> results = Postprocess(probMap, 0.3f, scale, padW, padH, srcW, srcH); return results; } public void Dispose() { _session?.Dispose(); } }这个类已经可以直接拿去做单元测试或者集成到上位机里。需要说明的是,上面为了示例清晰,预处理里的padW、padH计算并没有实际参与画布贴图,如果你做等比例缩放补齐画布,坐标映射时要把padding也考虑进去。我后处理里的坐标映射代码是有处理padding的,但如果你用的是纯缩放不补齐的方式,则需要对应调整。
4. 实操过程与性能实测
4.1 完整可运行的演示程序
我直接贴一段可以编译运行的完整入口代码,方便你在本地验证:
using OpenCvSharp; namespace BarcodeDBNetDemo { internal class Program { static void Main(string[] args) { string modelPath = @"models/det_db.onnx"; string inputImagePath = @"test.jpg"; string outputImagePath = @"result.jpg"; using var detector = new BarcodeDetector(modelPath); using var src = Cv2.ImRead(inputImagePath, ImreadModes.Color); if (src.Empty()) { Console.WriteLine("Failed to load image."); return; } var sw = System.Diagnostics.Stopwatch.StartNew(); var results = detector.Detect(src); sw.Stop(); Console.WriteLine($"Detected {results.Count} barcode(s), cost {sw.ElapsedMilliseconds} ms"); foreach (var rect in results) { Point2f[] points = rect.Points(); Point[] pts = new Point[4]; for (int i = 0; i < 4; i++) { pts[i] = new Point((int)points[i].X, (int)points[i].Y); } Cv2.Polylines(src, new[] { pts }, true, new Scalar(0, 0, 255), 2); } Cv2.ImWrite(outputImagePath, src); Console.WriteLine($"Result saved to {outputImagePath}"); } } }在我的测试电脑上(Intel i5-12400,无GPU,ONNX Runtime CPU模式,640x640输入),单帧推理耗时大概在180ms到250ms之间。在工业场景里这个速度不算快,但对“静态拍照识别”这种场景完全够用。如果你需要实时视频流检测,建议走下面的性能优化路线。
4.2 性能优化:输入尺寸与线程配置
ONNX Runtime的CPU推理时间跟输入分辨率直接相关,输入越大,耗时越高。我从320、480、640、736这几个尺寸都测过,结果差异非常明显:
| 输入尺寸 | 单帧耗时(ms) | 检测效果 |
|---|---|---|
| 320x320 | ~55 | 小条码容易漏检 |
| 480x480 | ~110 | 中等条码基本能检到 |
| 640x640 | ~210 | 常规条码检测稳定 |
| 736x736 | ~300 | 效果好但性能压力大 |
具体选哪个,完全取决于你现场条码占画面的大小。如果条码在画面里占得比较大(比如区域宽度超过图像宽度的30%),我用320x320或者480x480就够了。如果条码很小,或者有多个条码,那还是640起步。
另外,ONNX Runtime在CPU上可以配置线程数,默认会使用所有逻辑核。在多线程的上位机软件里,这可能会导致其他UI线程卡顿。建议设置成不跟主线程抢资源:
var sessionOptions = new SessionOptions { GraphOptimizationLevel = GraphOptimizationLevel.ORT_ENABLE_ALL }; sessionOptions.AppendExecutionProvider_CPU(1); // 参数是CPU执行线程数 _session = new InferenceSession(modelPath, sessionOptions);AppendExecutionProvider_CPU里的参数表示线程数。设成1或者2对UI线程更友好,但推理时间会相应变长。我用4线程跑640x640大概需要250ms,但整体系统响应更流畅。
4.3 输出结果与解码联动
检测出条形码区域之后,下一步往往是把这块区域交给解码库识别条形码内容。如果你用的是ZXing.Net,可以直接把检测框内图像裁出来,转成ZXing的LuminanceSource,再走解码流程:
using ZXing; using ZXing.Common; using ZXing.Windows.Compatibility; private static string DecodeBarcode(Mat src, RotatedRect rect) { // 根据旋转矩形裁出正视角图像 var matrix = Cv2.GetRotationMatrix2D(rect.Center, rect.Angle, 1.0); Mat rotated = new Mat(); Cv2.WarpAffine(src, rotated, matrix, src.Size()); // 重新计算旋转后的矩形区域并裁剪 Size rectSize = new Size((int)rect.Size.Width, (int)rect.Size.Height); Rect cropRegion = new Rect( (int)(rect.Center.X - rectSize.Width / 2.0), (int)(rect.Center.Y - rectSize.Height / 2.0), rectSize.Width, rectSize.Height ); using Mat cropped = rotated[cropRegion].Clone(); var barcodeReader = new BarcodeReader(); var bmp = OpenCvSharp.Extensions.BitmapConverter.ToBitmap(cropped); var result = barcodeReader.Decode(bmp); return result?.Text ?? string.Empty; }这条链路把“检测”和“识别”完全打通了,基本上可以做到从图像输入到内容输出一条龙。不过要提醒一下,ZXing对透视畸变比较敏感,如果你检测到的Rect是倾斜角度很大的,先用仿射变换矫正再解码的成功率会明显提高。
5. 常见问题与排查技巧
5.1 ONNX Runtime相关的几个报错
这张表里是我实际部署中遇到的高频报错以及处置办法:
| 报错场景 | 常见原因 | 解决办法 |
|---|---|---|
DllNotFoundException: onnxruntime | 缺少onnxruntime原生DLL | 确认安装了Microsoft.ML.OnnxRuntime包,并把运行库拷贝到输出目录 |
Exception: Input namexnot found | 输入节点名不匹配 | 用Netron或代码打印输入节点名,改成实际的名称 |
Shape mismatch | 输入张量维度不对 | 确认输入是[1,3,640,640],Chat tensor创建时的长度也要对应 |
No execution provider found | CPU执行提供程序未初始化 | 检查是否调用了AppendExecutionProvider_CPU |
最典型的是“Input name not found”。同一套DBNet模型,不同版本的PaddleOCR导出的ONNX输入名可能会不一样,有的是x,有的是inputs,有的是image。我强烈建议在代码里写一个日志直接打印模型元信息,不要肉眼猜。
5.2 检测结果为空或者区域不对
最麻烦的不是报错,是代码跑通了,但检测结果不对。我遇到过的空结果原因包括:
- 阈值太高。如果
threshold设置成0.5,很多时候条码区域的概率值达不到这个值(尤其模型没有专门用条码数据微调时)。我用0.3比较多,效果稳定。 - 图像输入尺寸太小。800x1000的图上有个很小的条码,如果缩到320x320,条码本身可能就剩几十个像素了,模型很难识别。可以先把小区域放大或者裁切。
- 模型本身的问题。通用文本检测模型在条码上效果可能一般,建议用条码数据微调一下,哪怕几百张图,效果都会有质的提升。
如果检测出来的区域是一个不完整的碎块,大概率是膨胀参数太小或者阈值太高导致条码区域没有完全连通。把膨胀核从3x3调成5x5,或者把阈值降一点,一般能解决。
5.3 坐标映射不准
坐标映射是最容易出错的后处理环节。很多初学者在模型输出图上画框没问题,但映射回原图就飘了。这通常是因为没有考虑到缩放比例、padding偏移,或者把模型输出尺寸当成原图尺寸。
我的建议是你在实现坐标映射时,严格按照这个公式来:
原图X = (模型输出图X - paddingLeft) / scale 原图Y = (模型输出图Y - paddingTop) / scale其中scale = min(InputSize / srcW, InputSize / srcH)。记住一点:模型输出图尺寸等于InputSize,不等于newW或newH。如果你用等比例缩放后补边的做法,padding计算要仔细。我在文章开头那段预处理里用的是“缩放后放在画布左上角”的方案,如果模型图是640x640,newW是540,newH是640,那么paddingLeft是0,paddingTop是0。如果你用居中放置,则要额外计算偏移量。
5.4 OpenCvSharp和上位机框架的兼容性
如果你把这段代码放到WinForms里,注意OpenCvSharp的Mat和WinForms的Bitmap互转,需要引用OpenCvSharp.Extensions命名空间。另外,在高DPI的显示器上,OpenCvSharp的窗口显示可能模糊,但这对图像处理结果没有影响,不用担心。
在.NET Framework 4.8上如果出现Could not load file or assembly 'OpenCvSharp, Version=...',通常是因为包版本不同导致的程序集版本冲突。最简单的办法是统一NuGet包版本,或者用app.config里的bindingRedirect。
6. 扩展方向:训练自己的DBNet条码检测模型
6.1 数据准备:标注与增强
如果你手头的条码形态跟通用文本模型差异较大(比如条码是DotCode、DataMatrix这类点阵型,或者背景有特殊底纹),强烈建议自己微调一个DBNet模型。
标注工具用LabelMe或者PPOCRLabel都行。PPOCRLabel对检测这种多边形标注更友好,而且可以导出成DBNet训练需要的格式。标注的时候注意:
- 条形码区域要多边形框(因为条码可能倾斜,矩形框会导致混入背景)
- 区域边界尽量贴着条码边缘,不要留太多空白
- 一图多码的情况正常标注,不用特别处理
数据增强方面,我实际用下来最有效的是随机旋转(-15度到+15度)、随机亮度对比度扰动、随机添加噪声。因为这些场景在产线上太常见了——标签可能在传送带上歪着拍,也可能因为反光导致局部过曝。
6.2 微调训练要点
DBNet微调不需要从头训练,加载预训练权重后冻结backbone的前几层,只训练后面几层也能收敛。学习率建议设在1e-4到5e-5之间,batch size根据显存来定。如果只有几百张数据,训练几十个epoch就会有效果。
我之前用大概800张现场标签图微调之后,检测率从通用模型的70%左右提升到了95%以上。通用模型能检到条码,但对反光、遮挡、复杂背景的适应能力弱,微调后就明显好很多。训练好的模型再转成ONNX,直接用我前面写的C#代码跑,完全无缝衔接。
7. 一些经验心得
这套C# Onnx DBNet条形码检测方案,在我自己的项目里已经稳定跑了大半年。回想起来,最值得说的经验就两条:
第一,模型选型要往下游需求倒推。我当时手里有现成的YOLO检测方案,为什么还要换DBNet?因为我的下游要做条码质量判断和角度校正,只有像素级分割能给出精准的条码边界,矩形框做不到。如果你的下游只是“框出来给人看”或者“送去识别”,那YOLO完全够用,没必要上分割模型。
第二,C#部署ONNX模型比想象中简单,真正的复杂度全在后处理和现场容错上。ONNX Runtime把跨平台、跨语言的推理封装得很好了,但你要处理的是真实世界的图像——光照、角度、遮挡、分辨率变化,这些才是项目成败的关键。所以我在后处理里加了各种过滤和映射逻辑,就是为了让检测结果在不同现场条件下都尽量稳定。
最后分享一个调试小技巧:当检测结果不准时,把模型输出的概率图直接保存成图片看看,基本就能定位问题是出在模型、预处理,还是后处理。保存概率图只需要一个Cv2.ImWrite("prob.jpg", probMap),但看到那幅图,你就能直观地知道模型到底学到了什么、哪里没检出来。这一步,比查一整天代码都管用。
如果你正在做类似的条码检测项目,希望这篇文章和源码能帮你少走点弯路。后续如果你在部署中还有其他问题,欢迎在评论区把报错信息贴出来,我看到会回复。
本文还有配套的精品资源,点击获取