工业视觉检测项目做到第三年,我越来越确信一件事:模型选型本身并不难,难的是把“论文指标”翻译成“产线节拍和良率报表”。YOLOv6 这个检测框架我是从它开源版本早期就开始跟的,前后在五金件、注塑件、线束端子、包装外观四类场景里落地过多套方案。它的骨干和颈部结构对工业场景的多尺度缺陷非常友好,训练收敛也快,但真把它塞进工控机、跑到 30 FPS 以上、并且连续三个月不掉精度,中间要补的工程细节远比想象得多。这篇就是我这几套项目攒下来的完整复盘:从选型判断、数据集构建、训练调参,到 INT8 量化、推理加速、剔除机构联动和长期稳定性设计,全部按实战顺序展开,给正在做 YOLOv6 工业视觉检测优化与部署的同行一份能直接抄作业的参考。不管你是刚接手第一个检测项目的新人,还是正在为产线误检率头疼的老手,下面这些内容应该都能对上你的坑。
1. 选型之前先看产线:YOLOv6 在工业检测里的真实定位
1.1 工业检测和通用检测到底差在哪
很多人拿通用检测的思路套产线,第一步就会踩空。工业视觉检测和通用目标检测有四条本质差异,这四条决定了后面所有的优化方向。第一是类别极其不均衡:一条产线上 99% 是良品,缺陷样本可能一天只出几十个,正负样本比动辄 1:1000,通用的 focal loss 参数直接照搬会训出一个“全部预测背景”的废模型。第二是目标尺度和形态固定但微小:相机、镜头、光源都是固定死的,缺陷出现的位置和尺寸范围是可预估的,这既是限制也是红利,你可以用固定 ROI 裁剪把有效分辨率拉高 3 到 4 倍。第三是评价口径完全不同:算法同学看 mAP,产线班长看漏检率和过杀率,这两个指标和 mAP 不是单调对应的,mAP 0.85 的模型在某个特定缺陷上可能一个都检不出。第四是延迟是硬约束:节拍多少就是多少,不存在“等一等”这个选项,超出 5ms 就是丢件。
把上面四条想清楚,你在选型阶段就不会被各种榜单带跑偏。我个人判断一个检测框架值不值得上产线,只看三个维度:小目标召回的天花板、在目标硬件上的真实延迟、以及量化和导出链路的成熟度。第三条尤其关键,很多框架训练效果好,但导出 ONNX 之后算子融合失败,落地时精度掉 5 个点,前面的功夫全白费。
1.2 为什么在几个候选里选 YOLOv6
我把实际比较过的几个方向做了个对照,注意这里说的是“适不适合我的场景”,不是绝对优劣。
| 候选方向 | 优势 | 在工业场景的短板 | 我的采用情况 |
|---|---|---|---|
| YOLOv6 系列 | 重参数化骨干,训练/推理解耦;提供 n/s/m/l 完整档位;官方自带量化与导出脚本 | 大模型档位在低端边缘设备上偏重,需要自己剪枝 | 主力方案,覆盖 80% 项目 |
| YOLOv5 系列 | 生态最成熟,资料最多,改一行就能跑 | 骨干结构较老,同精度下延迟高 20%~30% | 老项目维护,新项目不再选 |
| 两阶段检测器 | 小目标精度好,可解释性强 | 延迟太高,工控机端基本不可行 | 仅用于离线抽检比对 |
| 传统视觉 + 特征工程 | 稳定、可解释、算力极低 | 换一种缺陷就要重做规则,维护成本爆炸 | 作为算法兜底分支保留 |
选 YOLOv6 最核心的理由是重参数化结构在训练和推理上的分离。训练时用多分支提升梯度流动,推理时融合成单路卷积,这个特性对工业场景特别值——我的训练环境有 GPU 集群可以慢慢磨,但现场工控机的算力是死的,两者需求天然冲突,重参数化正好把矛盾解开了。另外它的 n 档位参数量在 5M 量级,s 档位在 20M 量级,这两个档位刚好卡在“能检出 0.3mm 级缺陷”和“能在中端边缘设备上跑 30 FPS”的平衡点上。
注意:不要因为“版本号更大”就盲目上更高档位的模型。我见过一个项目直接拿最大档位去跑,精度确实高了 2 个点,但单帧 90ms,节拍完全撑不住,最后退回小档位 + 高分辨率输入的组合,效果反而更好。
1.3 我把整套方案拆成五层
动手之前先在纸上分层,这是我做每个项目的固定动作,能省掉大量返工。整个 YOLOv6 工业视觉检测系统我拆成五层:光学成像层(相机、镜头、光源、工装夹具)、数据层(采集、标注、增广、难例回流)、模型层(结构选择、训练策略、蒸馏剪枝)、推理层(导出、量化、算子优化、后处理)、控制层(通信、剔除、报警、日志)。
分层的价值在于定位问题。上线后精度掉了,先看是哪一层:图像对比度变了是光学层,标注漂移是数据层,某些类别突然崩了是模型层,延迟抖动是推理层,偶尔漏剔除是控制层。没有分层,所有人都在猜,排查一个 bug 能耗掉一周。这套分层我在四个项目里都用了,最直观的收益是平均故障定位时间从 4 小时压到 40 分钟以内。
2. 数据层:模型的天花板在标注台上就定死了
2.1 工业数据的三类“脏”,以及对应的清洗手法
做工业检测,数据脏的方式和公开数据集完全不是一回事。我总结下来主要是三类。第一类是成像不一致:同一批产品在不同时间段拍的图,亮度、对比度、色温明显不同,原因可能是环境光漏进来、光源老化、镜头有灰尘。这类脏数据解决不了靠增强,必须回光学层解决,加遮光罩、固定光源电流、定期擦镜头。第二类是标注口径漂移:上午标的人认为轻微划痕不算缺陷,下午换个人来标,同样程度就算缺陷了,模型学到的是两套标准,精度自然上不去。第三类是长尾类别严重缺失:某些缺陷一个月才出现两次,样本量个位数,模型根本学不会。
针对第二类,我的做法是先做一致性对齐实验:随机抽 200 张图,让三个标注员独立标一遍,算两两之间的 IoU 和类别一致率。如果类别一致率低于 90%,说明标注规范本身有歧义,得先改规范再标,不能硬标。这一步只花两小时,但能省掉后面两轮返工。针对第三类,我的做法是合成 + 迁移:用已有的缺陷做局部裁剪,通过泊松融合贴到不同背景上,同时用相近类别的缺陷做迁移学习预热。
2.2 标注规范怎么写才有实际约束力
我见过太多标注规范写了二十页,标注员根本不看。真正有用的规范要满足三个条件:有边界图、有反例图、有强制判定顺序。边界图就是把“算”和“不算”的两个极端案例并排放,标注员卡住的时候对照着看;反例图是把常见错标单独列出来,比如把反光误标成划痕、把结构线误标成裂纹;强制判定顺序是给多缺陷重叠的情况定优先级,避免同一个区域被标成两个类别。
具体到字段设计,我一般只保留这几个:类别名、外接框、严重等级(致命/主要/次要)、可忽略标记。严重等级这一项很多人不加,但它对后面调阈值太重要了——产线对不同等级的容忍度差十倍,全塞进一个类别里,你连“漏检的是致命缺陷还是次要缺陷”都分不清。
实操心得:标注工具里我会加一个“不确定”标签,标注员拿不准的一律进这个桶,不要强行归类。每周由工艺工程师集中裁决一次,裁决结果立刻回灌成新的边界图。这个流程跑三个月,标注一致率能从 82% 提到 96%。
2.3 数据增强的边界:哪些增广在工业场景是纯亏本
通用检测那套增强策略搬过来,至少一半是负收益。我说几个我实测过的。
Mosaic 增强要不要开?开,但比例要降。标准配置是 0.5 到 1.0,我在工业场景一般设 0.3 到 0.5,并且关掉它的随机缩放部分。原因是 Mosaic 会拼出真实产线上不存在的目标尺寸分布,模型学到一堆小碎片,反而对完整缺陷的召回下降。随机裁剪我基本禁用,缺陷常常贴着边缘,裁掉一半就成了无效样本。HSV 色相扰动要收敛到 ±10 以内,工业光源是固定的,色相大幅变化会让模型对颜色特征过度敏感,换个批次的产品颜色就崩。
真正值得加的是这几类:亮度/对比度的窄幅扰动(模拟光源老化,±15% 以内)、高斯噪声(模拟高感光度下的传感器噪声)、轻微运动模糊(模拟传送带速度波动)、随机擦除小块区域(模拟遮挡,但面积不超过目标的 20%)。这四类才是贴合现场分布的。配置上我会这样写:
# 工业场景增强配置(示意,具体字段名以你用的框架为准) augment: mosaic: 0.35 mixup: 0.0 # 工业目标密集,mixup 容易造出伪标签 hsv_h: 0.008 # 强烈建议压到 0.015 以下 hsv_s: 0.3 hsv_v: 0.25 degrees: 0.0 # 相机固定,旋转无意义,反而引入伪样本 translate: 0.05 scale: 0.15 shear: 0.0 perspective: 0.0 fliplr: 0.0 # 关键:有方向性的缺陷不能翻转 flipud: 0.0 erasing: 0.2 erasing_area: 0.2里面最容易被忽略的是fliplr。划痕、裂纹这类缺陷是有方向的,左右翻转之后模型学到的方向特征就乱了,同时文字、Logo 类目标翻转后会变成另一类东西。除非你确认所有缺陷都是无方向性的,否则一律设 0。
3. 训练与调优:把 mAP 换成现场真正关心的指标
3.1 指标口径必须先跟产线对齐
训练前一定要做的事,是把评价口径写成文档,让工艺、质量、算法三方签字。我用的指标体系是这样的:
| 指标 | 定义 | 我们的验收线 | 为什么重要 |
|---|---|---|---|
| 致命缺陷漏检率 | 未检出的致命缺陷数 / 致命缺陷总数 | ≤ 0.1% | 直接影响客户投诉和召回成本 |
| 主要缺陷漏检率 | 同上,主要等级 | ≤ 1% | 影响后道工序良率 |
| 过杀率 | 误判为缺陷的良品数 / 良品总数 | ≤ 2% | 过高会让人工复检扛不住 |
| 单帧端到端延迟 | 从触发到输出结果 | ≤ 35ms | 决定能不能跟上节拍 |
| 连续运行精度漂移 | 30 天内同批样本的召回变化 | ≤ 1.5% | 决定要不要频繁重训 |
这张表里最容易吵架的是过杀率。算法想保守,宁可多报不可漏报;产线不干,过杀 5% 就要多养两个复检工。我的经验是把过杀率和漏检率做成一条曲线给产线看,让他们自己选工作点——通常选在过杀 2% 附近,这个点上致命缺陷召回已经能到 99.5% 以上了。
3.2 输入分辨率和模型宽度的算账方式
这是最需要掰着手指算的地方。假设你的最小可检缺陷在物理尺寸上是 0.5mm,相机视场 200mm×150mm,分辨率 2448×2048,那么一个缺陷在原始图里大概占 6 个像素。检测网络的特征图 stride 通常是 8 起步,6 个像素的目标在 stride 8 的特征图上已经不足 1 个格子了,网络根本学不到。
算法很简单:目标在输入图上的像素数 ÷ 最小检测层的 stride ≥ 3,才能有稳定的响应。上面这个例子,你需要把输入分辨率放大到让缺陷占 12 到 18 像素,也就是需要 2 到 3 倍放大。两个办法:整体输入放大到 1280,代价是算力涨 4 倍;或者先用固定 ROI 裁出缺陷高发区域再送进网络,代价是丢失全局上下文。
我一般走中间路线:固定 ROI 裁剪 + 640 输入。ROI 的位置在工装上做机械定位,相机每次拍到的是同一块区域,裁出来再缩放到 640,等效分辨率提升 3.8 倍,缺陷能占到 20 个像素以上,算力还压在可接受的范围。这个方案的前提是工装夹具必须做得好,产品位置偏差不能超过 ±3mm,否则 ROI 会裁偏。夹具这块钱不能省。
算力这边我给个估算公式,方便你反推需要什么档位的硬件:
所需标称算力(TOPS) = 单帧FLOPs(G) × 目标帧率(FPS) ÷ 有效利用率有效利用率是硬件实际能跑出来的比例,CPU 上大概 5%~15%,边缘 GPU 模组上大概 20%~35%,专用加速卡能到 40% 以上。举个实例:s 档位模型在 640 输入下单帧约 45 GFLOPs,目标 30 FPS,边缘 GPU 有效利用率按 25% 算:
45 × 30 ÷ 0.25 = 5400 GOP/s = 5.4 TOPS所以标称算力 8 TOPS 以上的模组才留得住余量,这个结论和我实测的结果吻合——8 TOPS 的模组跑 s 档位在 640 输入下稳定在 28 到 33 FPS 之间。
3.3 学习率、蒸馏和 EMA 的实操设置
微调工业数据集的参数,我有一套用了很多次的起点配置,能省掉大部分瞎试:初始学习率设万分之五到千分之一,warmup 3 个 epoch,cosine 衰减到底。批大小按显存能吃多少给多少,但不要低于 16,否则 BatchNorm 的统计量不稳。权重衰减设万分之五。冻结骨干训练 2 个 epoch 再解冻全量训练,这个“两段式”在小数据集上特别管用,能防止一开始就把预训练权重冲掉。
EMA(指数滑动平均)一定要开,衰减系数设 0.999 到 0.9999。工业数据噪声大,模型每个 epoch 的波动可达 1 到 2 个点,EMA 能把这条曲线压平。我做过对照,开 EMA 的验证集指标比不开稳定 1.3 个点左右,而且最终收敛值也略高。
知识蒸馏是我最近一年用得最多的技巧。思路很直接:先用大档位模型在充足数据上训出一个“老师”,再用它去指导小档位“学生”训练。工业场景尤其适合,因为缺陷样本少,小模型学不到的东西正好从大模型那里“抄”过来。实操上蒸馏损失权重设 0.5,温度参数设 1,只在 neck 和 head 阶段加蒸馏,backbone 不加。实测下来,小档位学生能从 0.79 mAP 提到 0.83,延迟一点没变,性价比非常高。
# 蒸馏训练要点(伪代码,具体接口按你用的框架来) teacher.eval() for images, targets in loader: with torch.no_grad(): t_feats, t_logits = teacher(images) s_feats, s_logits = student(images) # 学生正常前向 loss_det = detection_loss(s_logits, targets) # 检测主损失 # 只在 neck / head 层做特征对齐 loss_kd = 0.0 for s_f, t_f in zip(s_feats[2:], t_feats[2:]): loss_kd += F.mse_loss(s_f, t_f.detach()) loss = loss_det + 0.5 * loss_kd loss.backward() ema.update(student) # EMA 必须保留注意:老师模型和学生模型的输入分辨率必须一致,否则特征图尺寸对不上,蒸馏直接报错。我踩过这个坑,花了一个下午才发现是两边 img_size 写的不一样。
3.4 难例回流:让模型自己去现场找题做
这一块是长期维持精度的关键。上线后我做的是主动学习闭环:模型对每张图输出置信度,把“置信度在 0.3 到 0.6 之间”的图自动存到待审队列,每天下班前人工过一遍,确认为新缺陷的加入训练集,确认为误报的加入负样本集。每周重训一次,用增量方式微调,学习率降到原来的十分之一。
这套闭环跑起来之后,效果比一次性的“完美训练”好太多。我在一个注塑件项目上做过统计:第一版模型上线时主要缺陷召回 92%,跑满 8 周主动学习之后升到 98.4%,全程没有增加标注人力,就是每天花 40 分钟过一下队列。这个投入产出比,比堆标注量高得多。
4. 推理加速:量化、图优化和后处理三件事
4.1 导出链路:从训练框架到推理引擎
导出这一步是整个部署里最容易出问题的环节。我固定的流程是:训练框架权重 → ONNX(做图简化)→ 推理引擎的序列化模型。ONNX 导出时有两个开关必须开:--simplify做图简化,--inplace让重参数化在导出时完成融合。导出命令大概长这样:
python deploy/ONNX/export_onnx.py \ --weights runs/train/exp/weights/best.pt \ --img-size 640 640 \ --batch 1 \ --simplify \ --inplace \ --end2end False导出完立刻做一致性验证,这是硬性动作:拿 50 张固定图,分别跑训练框架和 ONNX 运行时,逐图对比输出框坐标和分数,最大偏差超过 1e-3 就得回查是哪层算子没对齐。我遇到过的典型问题有三个:后处理算子(比如某些实现的 grid 采样)不被支持,需要改成标准算子组合;动态 batch 维度导致部分融合失效,得固定 batch=1;anchor 或 strides 被写死在推理脚本里,和导出的模型不匹配。这三个坑每一个都能让你多花一天。
4.2 INT8 量化的校准集怎么挑
INT8 量化能带来 1.8 到 2.5 倍的加速,代价是精度可能掉 1 到 3 个点。掉多少,几乎完全取决于校准集选得好不好。我的经验是校准集必须满足三个条件:数量 300 到 500 张、覆盖所有缺陷类别、覆盖所有光照和产品批次。常见错误是随便从训练集里抽 100 张,结果抽到的全是良品,量化后的激活值范围完全按良品分布来定,缺陷特征的精度直接被截断。
校准算法我一般选熵校准或者百分比校准(99.99% 分位),前者对多峰分布更稳,后者实现简单。逐层混合精度也要用上:把 head 层和最后一层卷积保持 FP16,其余层 INT8,这样通常能把精度损失压到 0.5 个点以内,速度损失不到 5%。下面是校准流程的示意:
# INT8 校准流程示意 calib_loader = build_calib_loader( data_dir="datasets/calib", num=400, # 400 张足够,再往上收益递减 balance=True, # 按类别均衡采样 cover_shifts=True, # 覆盖不同班次/光照 ) config = builder_config() config.set_flag(INT8) config.set_calibrator(EntropyCalibrator2( calib_loader, cache_file="calib.cache", # 缓存一定要存,重复校准很浪费时间 )) config.set_mixed_precision(["head.*", "conv_last"], dtype=FP16) engine = build_engine(onnx_path, config) evaluate(engine, val_loader) # 必须跑一遍完整验证集我做过一组对照,同样的模型同样的硬件,不同校准集配置下的精度表现:
| 校准集配置 | 数量 | mAP 变化 | 备注 |
|---|---|---|---|
| 训练集随机抽 | 100 | -2.8 | 缺陷类别覆盖不足,典型失败案例 |
| 训练集随机抽 | 500 | -1.9 | 数量够了但分布偏 |
| 按类别均衡 + 覆盖光照 | 300 | -0.7 | 性价比最高 |
| 按类别均衡 + 覆盖光照 | 500 | -0.5 | 边际收益很小 |
| 上者 + 混合精度 | 500 | -0.2 | 我的默认配置 |
4.3 NMS 是隐藏的性能杀手
很多人在优化推理速度时只盯着网络前向,忽略了后处理。标准 NMS 在密集目标场景下时间复杂度接近 O(n²),我一测就发现一个中型模型单帧推理 18ms,后处理居然吃掉 7ms。
三个优化方向。第一是降候选框数量:置信度阈值从 0.01 提到 0.05,候选框直接从 8000 降到 1500,NMS 耗时降 80%,精度损失只有 0.1 个点,因为低于 0.05 的框本来也进不了最终结果。第二是分类别并行 NMS:不要所有类别一起做,按类别拆分后每个类别的框数量少,而且能并行,实测能提速 40%。第三是换成更高效的实现:如果目标检测框之间重叠很少(工业场景通常是这样,缺陷是分散的),可以用快速 NMS 变体,把 IoU 计算向量化。
# 后处理优化要点 def postprocess(preds, conf_thres=0.05, iou_thres=0.45, max_det=100): # 1. 先做粗筛,把置信度极低的框早点扔掉,减少后续计算量 mask = preds[..., 4] > conf_thres preds = preds[mask] if preds.shape[0] == 0: return empty_result() # 2. 限制最大候选数,防止极端情况爆内存 if preds.shape[0] > 1000: preds = preds[preds[:, 4].argsort(descending=True)[:1000]] # 3. 按类别分组做 NMS,而不是全局做 results = [] for cls_id in preds[:, 5].unique(): cls_preds = preds[preds[:, 5] == cls_id] keep = fast_nms(cls_preds[:, :4], cls_preds[:, 4], iou_thres) results.append(cls_preds[keep][:max_det]) return torch.cat(results)提示:
max_det一定要设上限。我见过一次因为一张图的光照异常,模型输出了 4000 多个框,后处理卡了 300ms,整个节拍直接崩了。设成 100 就够用了,工业检测一张图里的缺陷不可能超过这个数。
4.4 内存拷贝和线程:被忽视的一半开销
网络前向优化到极致之后,你会发现瓶颈跑到别的地方去了。我在一个项目上做过完整的时间分解,单帧 33ms 的延迟里,前向只占 19ms,剩下 14ms 全是数据搬运。具体是:图像从相机缓冲区拷到主机内存 3ms,主机到设备 4ms,预处理(letterbox、归一化、通道转换)5ms,结果拷回 2ms。
优化手法有几个见效很快的。预处理放到 GPU 上做,用 CUDA 算子实现 letterbox 和归一化,能把 5ms 压到 1.2ms。用固定内存(pinned memory)加异步拷贝,主机到设备的时间能砍掉一半。零拷贝读取相机,直接让采集卡往 GPU 可访问的内存里写,省掉一次全图拷贝。输入张量预分配,不要每帧 new 一个,避免频繁申请释放内存带来的抖动。
这一套组合下来,33ms 能压到 22ms 左右,效果比换硬件还明显。很多人一上来就想换更贵的模组,其实先把搬运开销理干净,往往就够了。
5. 上线部署:工控机里的那些工程细节
5.1 硬件的选型账,别只看算力
选硬件只看 TOPS 是典型的入门错误。我吃过一次亏:两款标称算力相同的模组,A 款跑出来 31 FPS,B 款只有 19 FPS。查了两天才发现 B 款的内存带宽只有 A 款的一半,而这个模型的瓶颈恰好就在特征图的访存上。
选型时我固定要看四个参数:标称算力、内存带宽、功耗和散热设计、驱动和推理引擎的成熟度。内存带宽这条对检测模型特别关键,因为 neck 部分要做大量的特征拼接,访存量很大。经验值是每 TOPS 算力至少配 8GB/s 带宽,低于这个比例模型就跑不满。散热也不能忽略,工控机柜里温度经常到 45 度以上,风冷的模组会降频,实际算力打七折。
成本上我算过一笔账:中端边缘模组 + 一台四核工控机的组合,单工位硬件成本大概在几千元级,能覆盖 90% 的检测场景。真要跑大模型或者多路视频,才需要上更高档位的加速卡。不要为了“未来扩展”一开始就买最贵的,产线往往只用一个固定工位,扩展性根本用不上。
5.2 端到端流水线和延迟拆解
我把整条链路的延迟拆开测了一遍,这个表建议每个项目都自己做一遍,因为你的硬件和相机跟我不一样,数字会差很多:
| 环节 | 耗时 | 优化手段 | 优化后 |
|---|---|---|---|
| 传感器触发 + 曝光 | 6.0ms | 缩短曝光时间,提高光源亮度补偿 | 4.0ms |
| 图像传输到主机 | 3.0ms | 用高速接口,减少中转 | 1.5ms |
| 预处理(缩放/归一化) | 5.0ms | 移到 GPU 上做 | 1.2ms |
| 网络前向 | 19.0ms | INT8 量化 + 混合精度 | 11.5ms |
| 后处理 NMS | 4.0ms | 提阈值 + 分类并行 | 1.5ms |
| 结果判定 + 通信 | 2.0ms | 结果打包精简 | 1.0ms |
| 合计 | 39.0ms | 20.7ms |
从 39ms 压到 20.7ms,节拍能力从 25 件/秒提升到 48 件/秒,翻了一倍。这里面量化贡献最大,但预处理和通信这两项的收益也不能小看——它们加起来省了 5.3ms,而且没有任何精度代价。
流水线结构上我采用三级流水:采集线程、推理线程、控制线程各跑各的,用环形缓冲队列通信。这样即使某一帧推理慢了,后面的帧也能继续采集,不会连锁阻塞。队列长度设 3 到 5,太长了会引入额外延迟,太短了起不到缓冲作用。
5.3 和剔除机构的联动时序
这一块是纯工程问题,但翻车率极高。核心是时序对齐:模型判定“这是缺陷”之后,从当前帧到剔除气缸动作,中间有个固定延迟,必须精确补偿,否则会把后面的良品打掉。
我的做法是用编码器位置而不是时间做基准。每帧图像采集时记录编码器计数,判定为缺陷后,计算目标位置 = 当前计数 + 补偿量,编码器走到这个位置时触发剔除。补偿量 = 相机到剔除工位的物理距离 ÷ 编码器每脉冲对应的位移。这个方案的好处是传送带速度变化时不用改参数,比定时触发可靠得多。
时序上还有一个坑:判定延迟超过了剔除窗口。假设从相机到剔除工位只有 300mm,传送带速度 0.5m/s,那么窗口只有 600ms,看起来够。但如果产线提速到 1m/s,窗口就只剩 300ms,再加上通信和执行机构响应,就来不及了。所以设计阶段就要把最坏情况的节拍算进去,留 50% 以上的余量。
5.4 长期运行:别指望它一直不出问题
产线要连续跑三个月,这中间的事情比上线那一刻复杂得多。我做了三层防护。
进程级看门狗:推理进程定时往共享内存写心跳,外部监控进程发现心跳超过 2 秒没更新,就重启推理进程,同时记录一条日志。这个简单机制在四个项目里救过至少十次。
# 心跳写入(推理进程侧,每帧调用) import time, mmap, struct def heartbeat(shm): shm.seek(0) shm.write(struct.pack("d", time.time())) # 监控侧(独立进程) def watchdog(shm_path, timeout=2.0): with open(shm_path, "r+b") as f: mm = mmap.mmap(f.fileno(), 8) while True: mm.seek(0) last = struct.unpack("d", mm.read(8))[0] if time.time() - last > timeout: restart_inference_process() log_alert("inference hang detected") time.sleep(0.5)数据级漂移监控:每 1000 帧抽一帧存盘,每周算一次这批图和训练集的特征分布距离(简单点用亮度均值、对比度、边缘密度这几个统计量就够)。偏离超过阈值就告警,通常是光源老化或者镜头脏了,先让现场处理光学问题,别急着怀疑模型。
模型级兜底:对于致命缺陷,我永远保留一个基于传统视觉的兜底分支(阈值分割 + 形态学 + 面积判定),和模型结果做“或”逻辑。这个分支误报率高,但召回稳定,能挡住模型偶发的崩盘。两个分支同时失效的概率很低,这个设计让客户对我们的信任度提高了不少。
6. 常见问题速查与排查思路
6.1 精度类问题的排查顺序
精度不达标时,别急着调模型,先按这个顺序过一遍:先看图——把漏检的样本调出来看,很多时候一眼就能看出是图像模糊、过曝、缺陷被遮挡,这些模型再怎么调也救不了。再看标注——漏检样本的标注是不是有问题,有没有标错类别、框不准、漏标。我遇到过一批数据被标注员整体偏移了 15 个像素,模型精度怎么都上不去,查了两天才发现。然后看数据分布——漏检集中在哪个类别、哪个时段、哪个产品批次,如果是集中在某个批次,那是新出现的缺陷形态,得补数据。最后才动模型——加大输入分辨率、换更高档位、加蒸馏。
我把常见精度问题的速查表整理成这样:
| 现象 | 可能原因 | 首要排查动作 |
|---|---|---|
| 某类缺陷完全检不出 | 该类样本过少,或标注缺失 | 统计该类样本量,少于 50 个就先补数据 |
| 小目标漏检严重 | 输入分辨率不足,或 ROI 裁偏 | 算目标像素数,检查是否 ≥ 12 |
| 过杀率高但漏检低 | 置信度阈值偏低 | 做阈值-过杀率曲线,找平衡点 |
| 换了批次就崩 | 光学条件变化 | 对比新旧批次的图像统计量 |
| 检出的框偏移 | 标注框本身偏移,或 letterbox 参数不一致 | 用固定图跑一遍端到端坐标验证 |
6.2 速度类问题的定位手法
延迟高的排查我有个固定套路:分段计时,把整条链路每一段的耗时打出来。经验上,如果你发现前向只占 60% 以下,那问题就在搬运或后处理;如果前向占 85% 以上,才考虑换模型或换硬件。
几个高频原因:量化没生效,很多情况下模型导出成功但实际还在跑 FP16/FP32,用推理引擎的 profiler 一看每层的数据类型就能确认;动态输入尺寸,某些引擎对动态 shape 支持很差,固定成 640×640 能快 20%;批大小设置不当,工业检测通常是 batch=1,但有些引擎在 batch=1 时算子利用率低,可以攒 2 帧一起跑,能提 15%;CPU 降频,工控机散热不好,跑几分钟就降频,实测能慢 40%,加装风扇或者限制功耗墙能解决。
6.3 跑一段时间就漂移怎么办
这是最让人头疼的问题,因为“漂移”本身不可复现。我的排查清单是这样的:先确认是真漂移还是误报增加——如果是过杀率上升而漏检率不变,多半是光学问题,不是模型问题。然后查光源——用标准色卡每周拍一次,看亮度和色温有没有变化,这个是最常见的元凶。再查相机——镜头脏、传感器坏点增加、自动增益被误打开,都会导致图像分布变化。最后查模型和服务——是不是重启加载了错误的模型文件,是不是预处理参数被改过。
我一般要求项目上线第一天就建立“基线图像集”:固定标准样品,每天开机时拍一组,做统计量对比。这条基线看起来简单,但它能让你在精度掉下去的当天就发现问题,而不是等一个月后客户投诉。
6.4 上线验收清单
最后把我一直用的验收清单列出来,照着过一遍基本不会漏项:
| 检查项 | 通过标准 |
|---|---|
| 致命缺陷召回 | 用独立验证集(≥500 张,含各类缺陷)测,≥99.5% |
| 过杀率 | 同批次良品 ≥2000 件,≤2% |
| 端到端延迟 | 连续 1 小时统计,P99 ≤ 35ms |
| 内存占用 | 连续运行 24 小时,无持续增长 |
| 异常恢复 | 手动 kill 推理进程,10 秒内自动恢复 |
| 断电重启 | 冷启动到可检测状态 ≤ 90 秒 |
| 数据留存 | 每帧图像和结果可追溯,保留 ≥30 天 |
| 报警联动 | 连续 N 帧检出致命缺陷时,触发停线信号 |
注意:测试集一定要和训练集按产品批次划分,不能随机划分。同一个批次的产品图像高度相似,随机划分会让验证结果虚高 3 到 5 个点,上线就打脸。这个坑我在第一个项目上踩得特别惨,验证集 96%,现场实测 88%。
我个人在这几个项目里最深的体会是:模型只占整个系统的三成权重,剩下七成全是数据、光学和工程。我见过太多团队把 90% 的精力投在调网络上,反复换骨干、换损失函数,收益从 0.5 个点挤到 0.1 个点;而真正把漏检率从 5% 压到 0.5% 的,往往是重新设计了一次背光、把 ROI 裁准了、或者补了 200 张难例样本。做工业视觉这一行,算法的上限是数据给的,落地的下限是工程兜的,两边都不能偏。如果你正在做类似的项目,第一件事建议不是改模型,而是先去产线上站两个小时,看看光是怎么打的、产品是怎么流的、缺陷到底长什么样——那两个小时的信息量,比读十篇论文都大。