简介:面向工业质检领域算法工程师与产线技术人员的技术文档,聚焦YOLOv11与多模态数据融合在复杂缺陷检测中的落地路径。内容共28页,从工业质检背景与挑战切入,系统讲解YOLOv11的网络架构、核心算法与训练策略,并围绕数据层、特征层、决策层展开多模态融合方案,覆盖汽车零部件、航空航天、电子元器件、钢铁冶金、木材加工五大行业案例,同时提供项目规划、模型调优、系统部署与运维等实战经验。资源包为1个PDF,大小2.12MB,目录支持章节跳转,排版完整清晰,便于按图索骥快速定位所需内容。目前已有213人学习下载,适合希望深入掌握目标检测工程化应用及多模态融合策略的进阶读者参考使用。
1. 工业质检为什么盯上 YOLOv11+多模态数据融合:先从复杂缺陷说起
产线上的金属件表面缺陷检测,老师傅一眼能看出的问题,传统机器视觉方案却经常翻车。不是算法笨,而是单一光源下一张灰度图能提供的信息量太少:划伤和纹理边界分不开,针孔和油污长得一模一样,反光区域里缺陷彻底隐身。把多个成像通道——RGB、深度、红外甚至不同角度光源下的画面——当成多个模态一起喂给检测模型,缺陷与背景的可区分度才会真正拉开。YOLOv11 这类 anchor-free 结构的实时检测器在精度和速度上能压住产线节拍,配合多模态数据融合,正好把复杂缺陷检测拉回工程可行域。这篇文章按一线工业视觉项目最常见的落地方案讲清楚选型理由、融合做法、训练参数和部署细节,适合正在评估 AI 视觉工业质检方案、准备在自己产线上试点验证的工程师。
2. YOLOv11 选型与训练环境:从网络结构改动点到显卡上能跑的配置
2.1 从网络结构看 YOLOv11 为工业质检准备了什么
YOLOv11 是 Ultralytics 在 YOLOv8 之后推出的目标检测系列,结构上仍然走 backbone、neck、head 三件套,但内部替换了不少关键模块。对工业质检来说,我第一眼关注的是它把检测头改成了 anchor-free 解耦结构:分类分支和回归分支分开输出,这让低对比度缺陷的定位和分类不再互相干扰。第二个值得注意的点是 C3k2 模块替代了之前常见的 C3,计算量下降但感受野保持得不错,在小目标缺陷上表现比同类轻量网络稳。neck 里的 C2PSA 带了自注意力,对背景纹理复杂、目标形态不规则的缺陷有一定抑制作用,像金属拉丝表面上的细微划伤,这类“背景强干扰”场景正好吃这一套。
从落地角度讲,YOLOv11 把过往 YOLO 系列里那些“看起来厉害但跑不动”的结构做了删减,n/s/m/l/x 几个规格覆盖了从 Jetson Nano 到工控机独显的算力区间。很多刚接触 AI 视觉工业质检的朋友一上来就问“哪个模型最强”,我的经验是先选 s 或 m:产线缺陷检测不是打榜,漏杀和过杀都要用真实数据反复磨,结构太重的模型训一轮就要多花几倍时间,调参周期拉长后反而不划算。
把网络结构打出来看一眼是熟悉模型最快的方式,用 Ultralytics 自带的 Python API 就能直接打印,不需要翻训练的源码。
from ultralytics import YOLO # 载入预训练权重,会自动下载 yolo11s.pt model = YOLO("yolo11s.pt") # 打印完整的网络结构,逐层看 backbone/neck/head 的模块组成 print(model.model) # 只筛选关键模块名,快速确认 C3k2、SPPF、Detect head 是否按预期加载 for name, module in model.model.named_modules(): cls_name = type(module).__name__ if "C3k2" in cls_name or "SPPF" in cls_name or "Detect" in cls_name: print(name, cls_name)这段代码的作用是把网络结构从“黑匣子”变成可检查的清单。第一次拿到 YOLOv11 时先跑一遍,确认预训练权重加载正常,再开始改配置。实际项目中我还会顺手把每个模块的参数量打印出来,方便对比不同规格模型在部署设备上的内存占用。需要注意 Ultralytics 的版本要统一,训练、导出、部署用同一套版本,否则容易出现权重兼容问题。
2.2 YOLOv11 环境配置与最小训练命令
YOLOv11 的环境配置在目标检测框架里算最省心的那档,核心依赖就是 PyTorch 和 Ultralytics 包。建议用 Python 3.9 以上版本,PyTorch 按自己显卡的 CUDA 版本安装,然后 pip 装 ultralytics 即可。很多现场工程师的环境里同时有别的深度学习项目,我一般会建独立 conda 环境,避免依赖相互打架。
# 创建独立环境,规避与其他项目的依赖冲突 conda create -n yolov11 python=3.10 -y conda activate yolov11 # 安装 PyTorch,这里按 CUDA 12.1 的版本示例,实际以你的驱动为准 pip install torch torchvision --index-url https://download.pytorch.org/whl/cu121 # 安装 Ultralytics pip install ultralytics # 验证是否装好,用一张任意图片做推理测试 yolo predict model=yolo11s.pt source=./sample.jpg推理测试能通过,说明环境基本没问题。接着要确认训练链路通不通,直接用官方 COCO 预训练权重跑一个极短的训练任务,这一步能尽早暴露数据集路径、标注格式、显存这些基础问题。
yolo detect train \ model=yolo11s.pt \ data=defect.yaml \ epochs=20 \ imgsz=640 \ batch=8 \ device=0命令里几个参数是必须说清楚的:epochs=20只是验证链路用的迭代数,正式训练后文会给完整参数;imgsz=640是默认分辨率,小目标缺陷建议往上加,但显存成本同步上升;batch=8要根据显卡显存动态调整,显存不够就降到 4 或 2,训练慢一点总比 OOM 中断好。跑通一次哪怕损失函数曲线再难看,也比零报错更重要,因为这条链路后续要反复用。
2.3 小目标缺陷的改进方向:切块推理与 P2 检测头
复杂缺陷里小目标占比普遍不低,金属表面的针孔、微小裂纹、焊点气泡往往只有十几个像素。YOLOv11 默认下采样倍率到 32 倍时,这些缺陷在深层特征图上只剩一两个像素点,检测头能拿到的信息近乎为零。解决小目标问题最基本的两个手段,一是切块推理,二是增加 P2 检测头。P2 检测头需要改动模型 yaml 结构,不是改一行配置就能搞定;切块推理则可以在不改模型的前提下立刻见效,适合先验证产线对小目标的真实需求。
from ultralytics import YOLO model = YOLO("yolo11s.pt") def infer_tile(img, tile=640, overlap=64, conf=0.25): """对大图做切块推理,解决小目标因下采样丢失导致的漏检。""" h, w = img.shape[:2] detections = [] for y0 in range(0, h, tile - overlap): for x0 in range(0, w, tile - overlap): crop = img[y0:y0 + tile, x0:x0 + tile] results = model.predict(crop, imgsz=tile, conf=conf) for box in results[0].boxes: x1, y1, x2, y2 = box.xyxy[0].tolist() detections.append([ x0 + x1, y0 + y1, x0 + x2, y0 + y2, float(box.conf[0]), int(box.cls[0]) ]) return detections这段代码的思路是把原图切成多个 640×640 的块分别推理,再还原到大图坐标系下。切块后小目标在输入图里的相对尺寸变大,网络能提取到的特征更充足,漏检率通常明显下降。overlap=64是给相邻块留的重叠区域,避免缺陷恰好被切在两块边界上导致重复检测或漏检。代价是推理次数成倍增加,节拍压力大时要把 tile 和 overlap 调小来平衡。我在产线项目里一般先测切块推理,确认收益后再考虑改网络结构,这样改动风险更小。
3. 多模态数据融合的落地做法:成像对齐、特征融合与数据管道
3.1 先想清楚模态:哪些数据值得融,哪些只是凑数
多模态数据融合这个词听起来高级,但工业现场的“模态”必须落到成像设备上。我见过最务实的分类是按信息维度划分:RGB 提供颜色和纹理,深度图提供高度和凹凸信息,红外热像提供温度分布,多角度光源提供不同辐照条件下的表面反射特性。选择哪些模态取决于缺陷本身的物理特性——划伤和凹坑在深度图上区分度极高,色差类缺陷在 RGB 下更明显,虚焊和热应力裂纹则要靠红外。
模态组合和适用缺陷的对应关系,我整理成一张表方便评估时快速对号入座。
| 模态组合 | 适合的缺陷类型 | 设备成本 | 落地难度 |
|---|---|---|---|
| RGB + 多角度光源 | 划伤、反光表面缺陷、纹理干扰 | 低 | 低 |
| RGB + 深度图 | 凹坑、凸起、虚焊、胶路断胶 | 中 | 中 |
| RGB + 红外热像 | 虚焊、热裂纹、电阻类缺陷 | 高 | 高 |
| RGB + 高光谱 | 材质混料、极轻微色差 | 很高 | 很高 |
我的建议是先从低成本模态组合开始,把多光源当成多模态融合的第一步。很多缺陷在特定角度光源下会现出原形,而单张图里它就是一片灰。等确认了缺陷确实需要额外维度的信息,再上深度或红外,避免项目一上来就堆设备,最后发现部分通道一点贡献都没有。别被“多模态”三个字架住,它本质是“给模型更多能区分缺陷的信号”。
3.2 数据对齐比融合更重要:像素级配准与离线校验
多模态融合最隐蔽的坑不是网络结构,而是模态之间的像素对齐。RGB 相机和深度相机安装位置不同,视野有偏差,直接把两个通道拼在一起喂给网络,模型的收敛会被不一致的图像内容干扰。我见过不止一个项目在融合阶段折腾了两个月,最后发现深度图和 RGB 图之间存在几个像素的平移,缺陷位置在两幅图里根本没对准。
处理对齐的常见做法是先做相机标定,算出两个传感器之间的单应性矩阵,再对深度图做透视变换,把深度图映射到 RGB 图的坐标系下。这一步的产出应该是一张拼合图像,方便肉眼直接检查对齐效果——不建议直接跳到模型训练,人眼确认过的对齐质量才可靠。
import cv2 import numpy as np # 读入同一时刻采集的 RGB 图和 16bit 深度图 rgb = cv2.imread("rgb.png") depth_raw = cv2.imread("depth.png", cv2.IMREAD_UNCHANGED) # 深度图归一化:避免全图 min-max 把前景细节压扁 roi = depth_raw[100:900, 100:1200] depth_norm = cv2.normalize(roi, None, 0, 255, cv2.NORM_MINMAX).astype(np.uint8) depth_bgr = cv2.applyColorMap(depth_norm, cv2.COLORMAP_JET) # 加载标定得到的单应性矩阵,把深度图投影到 RGB 视角 H = np.load("align_homography.npy") depth_aligned = cv2.warpPerspective(depth_bgr, H, (rgb.shape[1], rgb.shape[0])) # 拼成一张图做人工校验;同时保留 6 通道数据用于后续训练 multi = np.concatenate([rgb, depth_aligned], axis=-1) cv2.imwrite("check_alignment.png", np.hstack([rgb, depth_aligned]))这段代码里最值得注意的细节是深度图归一化。很多人的第一反应是直接对整张深度图做 min-max 归一化,结果背景区域站了大部分灰度范围,前景缺陷的微弱高度差被彻底压没。我习惯先框定目标出现的 ROI,在 ROI 内做归一化,这样缺陷的高度差异才能保留下来。warpPerspective之后还要人工抽查几个特征点,确保障碍物边缘、工件轮廓在两张图里位置一致,再进入下一步。
这一步的数据流设计,决定了后续融合的上限。像素级配准是地基,地基没打好,融合网络设计得再精巧也只能学到模态之间的噪声对应关系。
3.3 两种融合结构:输入级拼接与特征级注意力融合
多模态融合在算法层面通常分成输入级和特征级,两种我都跑过,落地节奏差别很大。输入级融合最直接:把 RGB 和深度/红外在通道维度拼接成 6 通道输入,模型第一层卷积从 3 通道改成 6 通道,其他结构完全不动。这个方案改动小、训练快,特别适合项目前期验证多模态到底有没有价值。
用 Ultralytics 加载 YOLOv11 后,直接替换第一个卷积层,就能把输入从 3 通道扩展成 6 通道,预训练权重的前三个通道可以保留复用,新增通道用原权重的平均复制初始化。
import torch from ultralytics import YOLO model = YOLO("yolo11s.pt") # 第一层卷积原来是 3 通道输入,改成 6 通道 old_conv = model.model.model[0] new_conv = torch.nn.Conv2d( 6, old_conv.conv.out_channels, kernel_size=old_conv.conv.kernel_size, stride=old_conv.conv.stride, padding=old_conv.conv.padding ) with torch.no_grad(): # 前 3 个通道继承原权重,后 3 个通道用前 3 通道的均值初始化 new_conv.weight[:, :3] = old_conv.conv.weight new_conv.weight[:, 3:] = old_conv.conv.weight.mean( dim=1, keepdim=True ).expand(-1, 3, -1, -1) model.model.model[0] = new_conv这段代码的关键在于权重继承策略。直接把新通道权重置零会导致训练初期梯度异常,置成原权重的平均值可以让新通道在初始阶段输出相对平滑的特征,网络更容易稳定收敛。训练时要注意关掉或降低 mosaic 增强,因为 mosaic 会把四张不同图拼在一起,多模态通道的对应关系会被打乱,模型等于在学错误的对齐模式。
特征级融合是更进阶的做法:RGB 和深度各自过 backbone 编码,在 neck 位置把特征层拼接起来,再用可学习的权重或注意力机制决定每个模态的贡献。这里我常用的一个简洁结构是给两个模态的特征各接一个 1×1 卷积投影到相同维度,然后引入可学习的模态权重做加权求和。类似 HCANet 那种把注意力机制用在跨尺度融合上的思路,在工业缺陷场景下比简单拼接更稳。
import torch.nn as nn class ModalFusion(nn.Module): """特征级模态融合,用可学习权重动态调整 RGB 与深度的贡献。""" def __init__(self, rgb_ch, depth_ch, out_ch): super().__init__() self.rgb_proj = nn.Conv2d(rgb_ch, out_ch, 1) self.depth_proj = nn.Conv2d(depth_ch, out_ch, 1) self.modal_weight = nn.Parameter(torch.ones(1, 2, 1, 1)) def forward(self, rgb_feat, depth_feat): rgb_feat = self.rgb_proj(rgb_feat) depth_feat = self.depth_proj(depth_feat) w = torch.softmax(self.modal_weight, dim=1) return w[:, 0] * rgb_feat + w[:, 1] * depth_feat特征级融合的优点是模型能根据缺陷类型自适应地选择信息更丰富的模态,在复杂场景下的上限更高;缺点是需要改造网络结构,调试难度大,训练时间也长。我一般建议项目按“输入级验证价值、特征级提升上限”的节奏走:先用输入级融合快速跑通实验确认多模态有收益,再切换到特征级融合做精度优化。直接上特征级融合而跳过输入级验证,等于在还没确认原材料有价值的时候就开始做精细加工,出了问题很难定位是融合结构的问题还是模态本身的问题。
4. 复杂缺陷检测实战:标注策略、训练参数与部署评测
4.1 标注策略:类别怎么定,背景负样本怎么加
多模态数据准备好了,标注策略直接决定模型能否收敛出可用的检测器。复杂缺陷的标注第一原则是:按形态分,不按成因分。同样一条划痕,可能是来料磨损造成的,也可能是加工过程磕碰造成的,但从图像上看就是一条线状缺陷,机器没有能力、也没有必要区分成因。类别设计越贴近视觉形态,模型学起来越容易。我经手的项目里,类名一般压到 3~6 个,超过 6 类,标注一致性和模型精度都会明显下降。
典型的数据集组织和数据集配置文件如下所示,path指向数据集根目录,train和val指向图片目录,names按类别序号排列。标注文件每行内容为“类别 x1 y1 x2 y2”,坐标是归一化后的值。
# defect_multi.yaml path: /data/defect_plus_depth train: images/train val: images/val names: 0: scratch 1: pinhole 2: bubble 3: contaminant标注圈框的原则是宁紧勿松,框要紧贴缺陷边缘,背景多圈进去一个像素,模型就多学一份噪声。这里有个新手容易踩的坑:只标“有缺陷”的样本,不标“无缺陷”的负样本。深度学习的检测器在训练时会把没有标注框的区域当作背景,如果整个数据集全是正样本,模型对背景的建模就严重不足,上线后误报率会高到没法用。我一般会在训练集里混入 20%~30% 的纯负样本图,并且这些负样本图的采集工况要和正样本一致,否则模型会靠“是不是这条产线拍的”来做判断,泛化能力大打折扣。
4.2 训练参数:从收敛曲线到置信度阈值
YOLOv11 训练参数比 YOLOv5 时代多了不少,但工业质检真正需要反复调的其实就那几个。下面是多模态缺陷检测项目里比较稳的一组训练配置,以 s 模型、单卡 24GB 显存为基准。
yolo detect train \ model=yolo11s.pt \ data=defect_multi.yaml \ epochs=200 \ imgsz=640 \ batch=16 \ lr0=0.01 \ optimizer=AdamW \ mosaic=0.5 \ close_mosaic=10 \ patience=30 \ workers=8 \ device=0 \ project=runs/defect \ name=rgbd_v1| 参数 | 我常用的值 | 说明 |
|---|---|---|
imgsz | 640~1024 | 小目标多就调大,显存不够先减 batch |
mosaic | 0.5 | 多模态场景不要开满,模态配对齐会被拼图打乱 |
close_mosaic | 10 | 最后 10 个 epoch 关掉 mosaic,让模型在真实分布上收敛 |
patience | 30 | 验证集指标连续 30 个 epoch 不涨就早停 |
optimizer | AdamW | 收敛稳,对 lr 的敏感度弱于 SGD |
lr0 | 0.005~0.01 | batch 越大 lr 可以越高,16 时 0.01 起步安全 |
训练过程要盯的不是训练集 loss,而是验证集上的 P 和 R 曲线。训练集 loss 降到低位但验证集指标不动,大概率是过拟合或者标注噪声太大;验证集指标上下乱跳,可能是模态对齐问题,也可能是某个类别样本量太少。我在每次 epoch 结束后会看一眼保存下来的预测样例图,比任何指标都直观。
4.3 评估不止看 mAP:过杀率、漏杀率与样本级指标
工业质检的评估体系和学术目标检测不一样。mAP 高不代表能上线,产线上真正让人头疼的是过杀和漏杀:过杀是把合格品当缺陷拦下来,造成产线拥堵;漏杀是缺陷品混出去,这是质量事故。我习惯用样本级指标来评估,不只看框级 mAP。
import json with open("val_pred.json") as f: preds = json.load(f) with open("val_gt.json") as f: gts = json.load(f) # 把 GT 和预测都按“图+类别”分组,做样本级统计 def group(items): groups = {} for it in items: groups.setdefault((it["img_id"], it["cls"]), []).append(it) return groups pred_g, gt_g = group(preds), group(gts) missing, overkill = 0, 0 for key, gt_items in gt_g.items(): pred_items = pred_g.get(key, []) hit = any(p["conf"] >= 0.3 for p in pred_items) missing += not hit for key, pred_items in pred_g.items(): if key not in gt_g: overkill += 1 print("漏杀率:", missing / max(len(gt_g), 1)) print("过杀率:", overkill / max(len(pred_g), 1))这段代码的核心逻辑是:按“哪张图、哪个缺陷类别”做维度,只要该图该类下存在置信度超过阈值的预测框,就认为缺陷被命中了,不再关心框级 IoU 是否完美贴合。这样算出来的漏杀率、过杀率,直接对应质检员和产线管理者能听懂的指标。框级 IoU 匹配需要在框与框之间做分配,stage 级的评估在项目早期更实用。
我在项目里真正盯的接受标准是过杀率压到 3% 以下、漏杀率压到 1% 以下。达不到就按缺什么补什么来迭代:漏杀高优先加难例样本、调小目标优化;过杀高优先压置信度阈值、加负样本、检查标注一致性。
4.4 部署到 Jetson Nano 或工控机:导出与加速
训练收敛后进入部署环节。常见部署设备是 NVIDIA Jetson Nano 这类边缘盒子或工控机加 GPU 卡。无论哪种,导出成 TensorRT 引擎都是提速的关键一步。PYTHON 环境下直接推理 .pt 权重虽然简单,但动态尺寸和 FP32 精度对 Jetson Nano 这种小显存设备是双重负担,推理速度往往只有 TensorRT FP16 的一半不到。
# 导出 TensorRT 引擎,固定输入尺寸并开启半精度加速 yolo export model=runs/defect/rgbd_v1/weights/best.pt \ format=tensorrt \ imgsz=640 \ half=True导出的 .engine 文件直接替换原来的 .pt 权重做推理即可。如果在 Jetson Nano 上部署,我会特别强调固定输入尺寸有多重要:动态尺寸会触发 TensorRT 重新选择 kernel,每次推理都多出几十毫秒开销,产线上这就是按秒累积的。用 yolo11n 加 TensorRT FP16 通常能跑到实时,还不够的话就回到 2.3 节的切块方案,把分辨率和块尺寸一起压下来。部署后的又一次验证必须用产线实拍图重新跑一遍,不能用训练集和验证集的图,这部分脏数据带来的精度损失往往超出很多人的预期。
5. 工业质检避坑指南:YOLOv11+多模态融合的常见问题排查
5.1 验证集 mAP 很高,过杀率却压不下来
这是一个非常迷惑人的现象:模型在验证集上精度漂亮,一上产线就疯狂报警,质检员一天到晚在跟误报搏斗。原因多数出在置信度阈值和数据分布上。验证集的标注框全是“相对标准的缺陷”,而产线上大量存在的是边缘形态缺陷——半截划痕、轻微压痕、可疑脏点,这些样本在验证集里没有对应标注,模型就倾向于把它们也当作目标框出来。
解决思路要从两个方向同时下手。第一,把置信度阈值从默认的 0.25 往上提到 0.4 甚至 0.5,先看过杀率是否回落,这一步成本最低。第二,把产线采集到的“难负样本”加进训练集,这类样本的特征是“像缺陷但不是缺陷”,它们就是过杀率降不下来的根源。我见过一个密封圈检测项目,加了 2000 张难负样本后过杀率直接从 25% 降到 4%,模型结构一行代码没改。
5.2 加了深度或红外通道,精度反而比单模态更差
多模态融合并不是通道越多越好。深度图跟 RGB 图没有对齐,红外图里有大量与缺陷无关的环境热噪声,这些脏信号被网络当成有效特征学进去,精度自然往下掉。遇到这种情况,先别急着怪融合网络,返回去查数据质量。
第一步检查像素级对齐,用 3.2 节的可视化脚本把拼接图逐张翻一遍,确认工件边缘、定位孔、纹理特征在模态之间完全重合。第二步是逐个模态做消融实验:只用 RGB 训一版,只用深度训一版,再用融合通道训一版,哪一版精度最低就说明那个模态在拖后腿。如果深度图单独训的效果很差,融合后把 RGB 的精度也带崩了,这个模态在现阶段就是负资产,果断去掉比硬融更有工程价值。
5.3 Jetson Nano 上推理从演示变卡顿
开发机上推理流畅,部署到 Jetson Nano 后直接卡成幻灯片,这是边缘部署最常见的摔跤。原因基本是三点:没导出 TensorRT、用了动态尺寸、开了 FP32。开发机的 GPU 显存和算力远强于 Jetson Nano,同样的模型在两端性能差距可能有四五倍。
按照 4.4 节的导出命令重新生成 TensorRT 引擎,固定 imgsz 为 640,开启 half 精度,通常能让推理时间降到原来的三分之一左右。如果还嫌慢,把模型从 yolo11s 降到 yolo11n,或者对输入图做缩放加切块,牺牲一点精度换节拍。这里我的经验是部署板子到手的第一天就装上 TensorRT 跑一个基准测试,别等模型训完再折腾,那时候时间成本已经很贵了。
5.4 小目标缺陷一批一批漏检,指标全被拖垮
小目标缺陷漏检在多模态融合项目里同样常见,因为新增的深度或红外通道在浅层特征上的分辨率往往更低,小目标的信息被进一步稀释。现象是第一轮验证时中大型缺陷全检出来了,单独看小目标类别的召回率只有三四成。
原因有两层:一是下采样倍数太高,小目标在深层特征图上只剩零星几个激活值;二是小目标样本在训练集里占比天然偏低,正样本数量不足,模型学不好。解决方向按顺序来,先用 2.3 节的高重叠切块推理验证能不能把小目标召回提上来,同时把训练集里小目标样本的占比人为抬高,比如对小目标区域做复制增强。P2 检测头是更彻底的手段,但需要改网络结构并做更长的调参周期,建议放到前两步做完仍然不够时再上。
5.5 换了一个班次的光源,模型效果就漂移
工业现场的光照稳定性永远是个玄学问题。白天自然光透过窗户漏进来,夜班换了不同色温的照明灯,甚至产品批次表面颜色有细微差异,都会让模型精度肉眼可见地下降。原因只有一个:训练数据的成像分布和实际产线不一致,模型把“光怎么打”也当成了一种特征。
解决方式从数据侧和算法侧同时做。数据侧,在采集阶段刻意覆盖多个光源角度、多个亮度等级,把光照变化当成训练集的一部分。算法侧,训练时把 HSV 增强的强度开大一些,尤其对色相和饱和度做扰动,降低模型对颜色绝对值的依赖。我现在的习惯是每次产线换光源或换班次后,先拿 100 张实拍图跑一遍离线预测,把置信度分布和之前的一个月做对比,漂移立刻就能看出来,不用等质检员来投诉。
6. 每次推理都保存结果:缺陷可视化复查与阈值校准的一个实用技巧
模型部署之后,最容易被忽视的就是推理结果的保存习惯。很多团队只保留了“合格/不合格”的判定结果,缺陷框、置信度、原图一概不存,等质检员反馈误报多了想复盘,手里什么都没有。我现在的做法是每次推理都把文本结果单独落盘,一图一 txt,后续复查和阈值校准全靠这批文件。
from ultralytics import YOLO import cv2 import os model = YOLO("best.engine") out_dir = "inspection_log" os.makedirs(out_dir, exist_ok=True) for img_path in img_list: img = cv2.imread(img_path) results = model.predict(img, imgsz=640, conf=0.3) lines = [] for box in results[0].boxes: x1, y1, x2, y2 = box.xyxy[0].tolist() cls = int(box.cls[0]) conf = float(box.conf[0]) lines.append(f"{cls} {x1:.1f} {y1:.1f} {x2:.1f} {y2:.1f} {conf:.4f}") txt_path = os.path.join(out_dir, os.path.basename(img_path)[:-4] + ".txt") with open(txt_path, "w") as f: f.write("\n".join(lines))这段代码把每张图的检测结果保存成文本,每一行对应一个缺陷框。保存格式里最关键的是置信度,不要只存坐标和类别。积累一周的推理结果后,把所有缺陷的置信度拉成直方图,再结合质检员的复判记录,就能算出每个置信度阈值对应的过杀、漏杀代价。阈值校准的活干过几次之后会发现,很多所谓的“模型不稳定”,其实是产线本身的光照和产品状态在漂移,定期用保存下来的数据做阈值复审比反复重训模型更划算。
另一个高阶用法是连续帧验证:对同一产品拍多帧,只在连续 N 帧都检出同一位置缺陷时才判为不合格,能显著压掉瞬时噪声和光源闪烁造成的误报。这需要把一个产品关联到的全部 txt 文件按时间顺序对齐分析,也是保存了结果才能做的事。
我现在的习惯是上线第一天就把推理结果保存脚本挂上,和产线数据同步积累。一个月后不管是做阈值调整还是模型更新,手里都有一批真实分布的历史数据,而不是靠记忆和感觉。这个习惯帮我挡掉过很多次“返工式调参”,也希望帮你在自己的质检项目里少踩几个同样的坑。
本文还有配套的精品资源,点击获取