一提到“工业缺陷检测”,现在很多人的第一反应就是上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报错。导出时看清楚默认的opset,C#端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 List<Detection> Detect(Bitmap image) { // 1. Letterbox预处理,返回tensor及padX/padY/scale var (tensor, padX, padY, scale) = Preprocess(image); // 2. 推理 var inputs = new List<NamedOnnxValue> { NamedOnnxValue.CreateFromTensor("images", tensor) }; using (var results = _session.Run(inputs)) { var output = results.First().AsTensor<float>(); // 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会利用多线程做矩阵运算,只要不跑超高分分辨率,一般检测时间都能控制在几十毫秒以内。要加速就上CUDA,OnnxRuntime有对应的Execution Provider,启用后GPU跑的效果在工业级完全够用。
部署层面落定了,接下来要解决的是和传统视觉工具Halcon的配合问题。
5. Halcon和YOLO的正确协作方式
5.1 两者各自的擅长点
Halcon做工业视觉年头长,生态成熟,但它擅长的事情和YOLO并不完全重合。我做了个对比,帮大家理清边界:
| 任务类型 | YOLO | Halcon |
|---|---|---|
| 复杂背景下找目标 | 擅长,泛化能力强 | 弱,模板匹配容易误报 |
| 缺陷分类(划痕/脏污/凹坑) | 擅长,可训练 | 靠手写规则,难维护 |
| 亚像素尺寸测量 | 不擅长,输出框精度有限 | 非常擅长,边缘亚像素提取 |
| 几何关系计算(角度/圆心/距离) | 不擅长 | 强项 |
| 相机标定、畸变校正 | 不做 | 完整的标定工具箱 |
很多人问YOLO是不是要取代Halcon,我的答案很明确:在视觉任务上,YOLO负责“找”,Halcon负责“量”,它俩是前后道关系,不是替代关系。
5.2 一个典型的协作流程
我最近做的一个零件定位抓取项目,完整流程是这样的:
第一步,相机采图后,先用YOLO在全图范围内做目标区域搜索,输出一个包含目标的ROI矩形框。这一步的优势是把Halcon从“全图找模板”的鸡肋工作里解放出来,因为复杂背景、光照变化下模板匹配特别容易误报,而YOLO对这种变化的适应能力强得多。
第二步,把ROI传给Halcon,Halcon在这个小区域内做亚像素边缘提取,计算工件的精确中心坐标、角度、边缘尺寸。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是“精确认”,两者配合之后,误报率明显下降,产线停机次数大幅减少。模型精度只是入场券,稳定可靠的系统工程才是真正值钱的地方。希望这篇能把你在工业现场省下几个月的试错时间。