C# + ONNX + Halcon实战:工业缺陷检测中YOLO的正确使用方法
一提到“工业缺陷检测”现在很多人的第一反应就是上YOLO。开源仓库拉下来训练一版评估集精度显示九十几信心满满往产线上一放结果要么漏检要么误报现场工程师直接把工单甩你脸上。这种事我见得太多了老实说不是YOLO不行是很多人从一开始就把它的位置摆错了。这篇文章不聊花哨的技术选型就聊我在C# ONNX Halcon这套组合下做完真实项目后总结下来的那些“没写在开源文档里”的坑和正确用法。适合正在搞工业视觉、或者准备把深度学习引入产线的兄弟参考。1. 先说结论YOLO在工业检测里不是算法是视觉部件1.1 工业检测项目的完整链路很多做算法出身的人容易陷入一个思维定式模型精度就是一切。但在工业视觉项目里YOLO从来都不是“完整的视觉方案”它只是视觉系统中的一个部件。完整链路是触发信号传感器到位、PLC下发指令相机采图工业相机SDK、曝光、增益、触发模式图像预处理尺寸转换、灰度化、归一化、去畸变检测模型推理YOLO 前向传播输出候选框后处理置信度过滤、NMS、坐标反算结果通讯通过TCP/IP、串口或PLC协议下发执行动作剔除机构动作、机械臂抓取、报警灯闪烁你仔细看这条链路YOLO只负责“检测模型推理”中间那一小段前后全部要靠工程手段接起来。很多项目翻车不是模型没训好而是链路没打通。我见过一个项目模型检测率能到98%但是因为相机触发延迟产品在流水线上已经跑过去了才拍照最后漏检率反而比原来模板匹配还高。算法再准架不住前端的触发和采图拖后腿。1.2 为什么很多人一开始就把YOLO用错了总结这几年看到的失败案例我归纳了三种“一开始就错”的典型表现你看看自己有没有踩过第一种直接拿公开权重跑现场。公开权重是在COCO这类自然图像数据集上训练的里面是行人、汽车、猫狗这些通用类别。产线上要检测什么划痕、焊点、脏污、毛刺。完全没有交集。有些人为了快速看效果拿公开权重对着一张零件图跑一下发现什么都框不出来于是得出“YOLO不适合工业检测”的结论。这就像拿一本英文教材去考中文听力考不好不能说听力题本身有问题。第二种对成像系统没有控制意识。相机随便一装光源随便一调光圈快门自动曝光畸变也不管。YOLO的输入是像素但像素背后的物理过程——光照角度、焦距、景深、曝光时间——决定了你喂给模型的图像质量。成像不稳定模型再强也是垃圾进垃圾出。第三种输出不做坐标落地。YOLO输出的是像素坐标系下的矩形框。但你要给机械臂的是机器人坐标系下的坐标要给PLC的是“有缺陷/无缺陷”的布尔信号。这个从像素到物理坐标的标定关系很多人根本没建。框出来了然后呢没有然后了。YOLO的定位搞清楚之后接下来我就按照项目推进的顺序把每个环节里最容易踩的坑逐个拆开。2. 图像采集与预处理才是真正的“上半场”2.1 相机、镜头、光源你说怪模型的锅其实是硬件的锅有个兄弟项目做金属表面划痕检测。他拿YOLO迭代了快两周召回率怎么都上不去。我让他把现场照片发过来一眼就看出问题——光源用的是普通环形白光大面积照射划痕方向随机部分划痕在图像里根本没有对比度。人眼都看不见YOLO凭什么能检测出来这不是模型的锅这是成像方案不对。工业检测首选要解决的就是“看得见”的问题。划痕、凹陷这类缺陷关键在光源角度。低角度环形光能让表面微小的凹凸纹理产生明显阴影人眼看着图像都更“立体”模型学起来自然更容易。字符、标签这类容易反光的场景用同轴光消除反光图像对比度会大不一样。透明件、定位外轮廓背光才是王道。我把这几类常见场景列了个表实际选型的时候可以直接参考检测类型推荐光源原因金属表面划痕、凹坑低角度环形光利用阴影突出微小凹凸字符、条码、标签同轴光消除反光增强对比透明物体、轮廓定位背光板边缘锐利背景干净大面积均匀表面穹顶光/漫射光减少反光不均光照均匀还有个分辨率的问题。很多人在标注的时候意识不到一个缺陷在图像里如果只占几个像素模型是学不出稳定特征的。我给自己定的经验值是最小目标至少要覆盖20×20像素以上低于这个值就要提高相机分辨率或者缩小视场。你给一个人看一张只有5个像素的人脸他也不可能认得出来YOLO也一样。2.2 Letterbox与归一化90%的框偏移都出在这一步模型训练和推理通常固定输入尺寸比如640×640。但工业图片的宽高比五花八门有的是1240×1000有的是4096×3000直接把图Resize到640×640会把图像拉变形。目标形状一旦变形模型检测会不稳定框的位置也会跟着漂移。正确做法是Letterbox按比例缩放到目标尺寸内剩余的边用灰色128填充保持原始宽高比不变。这个操作本身不难但很多人在部署环节处理得太随意导致坐标对不齐。Letterbox的关键在于缩放比例scale、填充宽度padX、填充高度padY这三个值在后处理反算原图坐标时还要用到。很多人图省事先把这个参数丢了后处理直接用640坐标标结果框自然全部偏移。2.3 预处理代码实测我贴一段C#里的Letterbox实现这段代码我在实际项目里跑得很稳定// 假设原图已经由相机SDK拿到存为Bitmap Bitmap src Bitmap.FromFile(part.jpg); // 目标尺寸要和模型输入一致 int targetSize 640; // 计算缩放比例保证短边能完整放进640内 float scale Math.Min((float)targetSize / src.Width, (float)targetSize / src.Height); int newW (int)(src.Width * scale); int newH (int)(src.Height * scale); // 计算填充偏移居中布局 int padX (targetSize - newW) / 2; int padY (targetSize - newH) / 2; // 创建letterbox画布 Bitmap letterboxed new Bitmap(targetSize, targetSize); using (Graphics g Graphics.FromImage(letterboxed)) { g.Clear(Color.FromArgb(128, 128, 128)); g.DrawImage(src, padX, padY, newW, newH); } // 把letterboxed转成CHW的float数组像素值归一化到0~1 // 这一步在C#里可以用指针循环做效率比较高这里有个细节要提醒padX和padY的计算很多人的写法是用(targetSize - newW)直接除以2但遇到奇数尺寸时会丢掉小数。推理端看起来无所谓但对坐标反算来说误差哪怕只有0.5个像素乘以缩放比例后可能被放大最终框的边缘就不贴目标了。我现在的做法是全用float计算不要提前用整数截断。预处理搞对了接下来才是模型训练的大头。3. 数据标注与训练精度上不去问题多半不在模型3.1 类别设计与标注框规范工业缺陷检测里类别设计是一门容易被忽略的学问。我见过有人把十几类缺陷全部合成一个“NG”类模型学完之后只知道“有异常”但不知道“是哪一种异常”。产线工艺工程师拿到结果根本没法处理——到底是划痕要打磨还是脏污要擦拭不同的缺陷对应不同的返工策略类别必须分开标。但类别也不是越细越好。有些缺陷本身形态就非常接近比如“气泡”和“凹陷”人眼区分都费劲模型训练容易混淆。我的建议是先按工艺处理方式划分大类别如果两个类别的返工策略完全一样那在算法层面就没必要硬拆。标注框的规范同样重要。很多人标注时把背景框进去大半导致模型在学习时目标特征里混入了大量背景信息。框应该紧贴目标外轮廓宁可紧一点不要松一点。同一张图里如果有多个目标全部都要标全漏标就是给模型喂错误标签。我个人的经验是标注宁可少图不要脏图。几千张高质量的标注图比几万张粗标图都好用。3.2 数据增强Mosaic不是万能的YOLO训练管线里默认带Mosaic增强把四张图拼在一起训练能显著提升模型对复杂背景的适应能力。但工业场景下Mosaic有时候反而有害——工业缺陷往往是小目标、小面积Mosaic会把目标截断、缩小本来所占像素就少再切一下就彻底学不出来了。我在小缺陷检测项目里做过对比实验默认开启Mosaic的模型在小目标召回率上反而比关闭Mosaic的模型低了两三个点。后来我把训练配置里Mosaic关掉换上更贴近现场实际工况的增强方式随机亮度扰动、对比度调整、轻微高斯模糊、随机旋转和平移。这些增强模拟的是产线上光照波动、相机微小振动的真实变化比花哨的Mosaic实用得多。3.3 训练参数怎么选训练参数的选择逻辑我直接给几个实测结论输入分辨率。小目标缺陷多就提高输入分辨率比如从640提升到1280。我在一个划痕检测项目里把输入分辨率从640提到1280小缺陷的召回率从84%涨到93%效果显著。代价是推理时间增加原来一张图8毫秒现在要18毫秒。产线节拍允许的前提下分辨率能高就高。批次大小。工业数据集通常不大几百上千张的规模很常见批次大小建议设在8到16之间。批次太小会导致BN层参数不稳定模型波动大。训练轮数。不用盲目拉几百轮。小数据集上我一般控制在100到200轮同时开启早停机制验证集损失不再下降就停。置信度阈值。这个非常关键很多人一上来就默认0.5实际上要在“漏检”和“误检”之间权衡。如果缺陷漏检会流出产线造成客户投诉那阈值就调低一点让更多候选框通过交给后道Halcon做二次确认如果误检导致产线频繁停机、浪费大量人工复检阈值就调高一点。阈值不是拍脑袋定的是拿验证集样本画Precision-Recall曲线再结合产线误检和漏检的实际代价来选。模型训练这一关过了真正考验工程能力的部署阶段就到了。4. C# ONNX 部署从Demo到产线的关键一跳4.1 导出ONNX前要做的几件事训练好模型之后Pytorch模型不能直接给C#用要先导出成ONNX。这个过程看似简单其实有几个坑。第一输入尺寸尽量固定。ONNX可以支持动态输入尺寸但动态shape在推理时会增加额外的内存分配开销而且某些算子在动态shape下会跑得慢。工业检测场景下一个工位一个固定尺寸就够了没必要动态。导出时直接把输入尺寸固定比如[1,3,640,640]。第二导出后先用Python的onnxruntime验证一遍输出确认输出张量的维度。不同版本的YOLO输出排布不一样有的是[1,84,8400]有的是[1,8400,84]。这两种格式对应的后处理代码完全不一样写反了检测结果就是一坨屎。拿到模型之后第一件事就是打印输出的shape再跑通一个最小验证别急着接C#。第三确认算子版本。ONNX的opset版本对C#端OnnxRuntime兼容性有影响opset太高而C#端库版本太老会出现Unsupported Operator报错。导出时看清楚默认的opsetC#端NuGet包版本跟着对齐能省很多折腾。4.2 C#推理代码骨架与坐标反算C#端调用ONNX Runtime用Microsoft.ML.OnnxRuntime这个NuGet包就行。核心逻辑我贴一个骨架using Microsoft.ML.OnnxRuntime; using Microsoft.ML.OnnxRuntime.Tensors; public class YoloDetector : IDisposable { private readonly InferenceSession _session; private const int InputSize 640; private readonly float _minConfidence 0.25f; private readonly float _iouThreshold 0.45f; public YoloDetector(string modelPath) { _session new InferenceSession(modelPath); } public ListDetection Detect(Bitmap image) { // 1. Letterbox预处理返回tensor及padX/padY/scale var (tensor, padX, padY, scale) Preprocess(image); // 2. 推理 var inputs new ListNamedOnnxValue { NamedOnnxValue.CreateFromTensor(images, tensor) }; using (var results _session.Run(inputs)) { var output results.First().AsTensorfloat(); // 3. 后处理过滤低置信度框、NMS、坐标反算 return Postprocess(output, padX, padY, scale); } } }后处理里最关键的就是坐标反算核心逻辑如下// 假设output shape是[1, 4 classes, anchors] // 遍历每个anchor for (int i 0; i anchors; i) { float objConf ...; // 目标置信度 float classScore ...; // 最大类别得分 float finalScore objConf * classScore; if (finalScore _minConfidence) continue; // 模型输出的x, y, w, h是letterbox坐标系下的值 // 反算到原图先减去padding偏移再除以缩放比例 float x_center (output[0, 0, i] - padX) / scale; float y_center (output[0, 1, i] - padY) / scale; float w output[0, 2, i] / scale; float h output[0, 3, i] / scale; // 得到原图坐标下的检测框 }这个反算逻辑看起来简单但我在项目评审时看过太多人在这里翻车。很多人后处理代码里用了某一个版本的解析逻辑后来又换了模型结构没有同步更新后处理坐标自然全偏。我现在的习惯是模型文件和后处理代码当成一个整体来管理换模型的时候必须要跑一遍同一张图的Python和C#对拍测试两边输出的框几乎一致才算通过。4.3 为什么别用Python写服务端很多人的部署思路是Python训练然后把YOLO封装成一个Web服务C#这边用HTTP请求去调用。这种做法在Demo阶段跑得很欢但到产线上我真的强烈不建议。多一个进程就多一个故障点。Python服务崩了、端口被占用、并发请求卡死任何一个问题现场工程师都处理不了。而且图像数据要转成JSON或base64传给Python服务高分辨率图片的传输开销非常大一张4096×3000的灰度图转base64后有几MB每张图都传一次延迟和带宽都吃不消。C#进程内直接调用OnnxRuntime才是产线该有的形态。没有网络传输没有进程间通信一张图进去直接出结果延迟最低稳定性最高。CPU推理的话OnnxRuntime会利用多线程做矩阵运算只要不跑超高分分辨率一般检测时间都能控制在几十毫秒以内。要加速就上CUDAOnnxRuntime有对应的Execution Provider启用后GPU跑的效果在工业级完全够用。部署层面落定了接下来要解决的是和传统视觉工具Halcon的配合问题。5. Halcon和YOLO的正确协作方式5.1 两者各自的擅长点Halcon做工业视觉年头长生态成熟但它擅长的事情和YOLO并不完全重合。我做了个对比帮大家理清边界任务类型YOLOHalcon复杂背景下找目标擅长泛化能力强弱模板匹配容易误报缺陷分类划痕/脏污/凹坑擅长可训练靠手写规则难维护亚像素尺寸测量不擅长输出框精度有限非常擅长边缘亚像素提取几何关系计算角度/圆心/距离不擅长强项相机标定、畸变校正不做完整的标定工具箱很多人问YOLO是不是要取代Halcon我的答案很明确在视觉任务上YOLO负责“找”Halcon负责“量”它俩是前后道关系不是替代关系。5.2 一个典型的协作流程我最近做的一个零件定位抓取项目完整流程是这样的第一步相机采图后先用YOLO在全图范围内做目标区域搜索输出一个包含目标的ROI矩形框。这一步的优势是把Halcon从“全图找模板”的鸡肋工作里解放出来因为复杂背景、光照变化下模板匹配特别容易误报而YOLO对这种变化的适应能力强得多。第二步把ROI传给HalconHalcon在这个小区域内做亚像素边缘提取计算工件的精确中心坐标、角度、边缘尺寸。YOLO输出的框坐标是像素精度当参考可以做精密定位不够Halcon在这个环节能精确到亚像素级。第三步Halcon算出的结果经过标定矩阵转换成机械臂坐标系坐标通过Socket或Modbus协议发给机械臂控制器。这套组合我测下来定位精度从YOLO像素框的±2mm左右提升到±0.1mm以内完全满足装配精度要求。5.3 标定与坐标转换的实操建议标定这个问题视项目精度要求不同区别对待。如果只是做有无判断、缺陷分类不需要把像素坐标映射到物理坐标标定可以先不做。但要注意现场如果要统计缺陷在工件上的具体位置比如“这个产品左上角有缺陷”还是需要一个粗略的像素到工件的坐标映射否则产线没法做定向打磨和返工。如果涉及机械臂抓取、精密测量标定就必须做。我的建议是用Halcon自带的标定板流程先做相机内参标定再做手眼标定。Halcon的标定算子封装得很好照着手册跑一遍就能拿到相机内参和外参拿到之后把像素坐标转成机器人坐标就是几个矩阵相乘的事。还有一个容易被忽略的细节如果相机安装在运动轴上比如机械臂末端相机标定结果会随机械臂位姿变化而变化这时候要做动态标定或者分区域标定。我见过一个项目就是因为忽略了这个问题同一个目标在视野中心和视野边缘的定位误差差了好几毫米。6. 工业现场常见问题速查与避坑清单6.1 几个最常见的翻车现场我挑四个现场高频问题说说基本每个项目都会遇到其中之一。现场光照变了。训练时采集的图像是静态光照到了产线上旁边的设备、门窗透进来的自然光都会影响图像亮度。解决办法是把训练数据的亮度、对比度扰动打开并固定现场的光源和相机曝光参数不要让相机开启自动曝光。自动曝光会让图像亮度随环境自适应图像的绝对灰度值不稳定模型学到的特征分布就被打乱了。小目标缺陷漏检。前面说过输入分辨率的作用。现场漏检严重时先别急着加数据先看一眼模型实际输入尺寸。如果当前用的是640直接换1280重训一版往往比盲目堆数据见效快。检测框在图像边缘截断。当一个目标在图像边缘时Letterbox填充区域会有灰色边。如果目标跑到灰色区域内模型就没法正确识别。解决方法有两个一是采集时把产品放在视野中央避免目标出界二是后处理时对边缘区域做一次镜像填充或边界扩展。在线检测延迟突然变大。产线上偶尔会卡顿排查发现是工控机上同时开了多个推理会话CPU被吃满每次推理的线程竞争导致延迟飙升。OnnxRuntime实例要复用不要每一帧都new一个Session那会反复加载模型延迟直接翻倍。6.2 排查思路与速查表我把常见问题的排查顺序做成一个速查表兄弟们可以直接贴在工位旁边问题现象可能原因排查建议漏检率高现场光照与训练不一致固定光源和曝光数据增强加光照扰动小缺陷检不出输入分辨率不足提升到1280或对ROI区域二次放大检测框整体偏移Letterbox反算丢失padX/padY检查预处理与后处理是否共用参数误检多置信度阈值太低按误检/漏检代价重新选阈值在线延迟抖动Session重复初始化/多进程竞争复用InferenceSession限制线程数换模型后结果异常后处理解析格式不匹配用同一张图在Python和C#端对拍排查顺序我一般固定为先看图像质量亮度、对焦、曝光再看预处理代码是否和训练一致再检查模型输出解析最后查阈值和标定关系。按这个顺序走百分之八九十的问题都能定位。7. 实测总结我踩过坑之后的做法最后分享一套我现在做项目的标准动作算是踩过无数坑之后的肌肉记忆。固定一切能固定的变量光源角度定死、相机曝光增益定死、图像预处理逻辑写进配置表、标定参数存文件、模型和后处理代码绑定版本。凡是能提前写死的东西绝不给现场留调参空间。数据标注让工艺工程师参与。算法人员不了解工艺标出来的框可能不符合产线认知。现场工艺最清楚哪些缺陷影响功能、哪些只是外观瑕疵让他们一起审数据集模型才真正贴合需求。部署之前先写对拍测试再上线。同一张图Python推理结果和C#推理结果要几乎一致才能证明部署环节没有引入偏差。这个测试跑通了产线维护时才不会出现“算法说没检测到但评测集里明明检测到了”的尴尬局面。YOLO的候选结果一定要让Halcon二次确认。现在我的每个项目中YOLO是“粗筛选”Halcon是“精确认”两者配合之后误报率明显下降产线停机次数大幅减少。模型精度只是入场券稳定可靠的系统工程才是真正值钱的地方。希望这篇能把你在工业现场省下几个月的试错时间。