工业缺陷检测实战:小样本训练与漏检控制全流程
工业缺陷检测实战:小样本训练与漏检控制全流程
工业缺陷检测和"通用目标检测"是两回事:缺陷通常极小、样本极少、漏检代价极高。
用通用检测的套路(堆数据、只看 mAP)去做缺陷检测,最常见的结局是:mAP 看着还行,上线后客户天天投诉漏检。
这篇按做事的顺序走一遍:先搞清它和通用检测的差别 → 数据怎么组织 → 建模路线怎么选 → 小样本怎么训 → 怎么评估才不骗自己。每步给可跑代码,最后附排查顺序。
系列导航:数据格式与转换看《检测数据集制作全流程》,评估指标细讲看《检测评估指标怎么看》,部署上线看《检测模型部署成 Web 服务》。

一、缺陷检测和通用检测,差在哪
三个"不一样",决定了后面所有做法:
| 维度 | 通用检测 | 工业缺陷检测 |
|---|---|---|
| 目标大小 | 通常占图 10% 以上 | 可能只有几个像素 |
| 样本量 | 成千上万 | 缺陷样本常只有几十张 |
| 类别分布 | 相对均衡 | 长尾,某些缺陷只有几张 |
| 错误代价 | 漏检可接受 | 漏检 = 次品流出,代价极高 |
| 背景 | 多变 | 相对固定(同一产线、同一相机) |
最后一条其实是好消息:背景固定,意味着可以用更针对性的方法。但也意味着换一条产线就得重新做——别指望一个模型通吃所有产线。
核心矛盾一句话:缺陷样本天然稀缺,而漏检代价极高。所有技巧都是围绕这个矛盾展开的。
二、数据:三类样本与标注规范
缺陷检测的数据要分三类来看,不能混在一起想:
- 正常样本:大量,容易拿。但不能只拿正常样本去训检测模型——检测模型需要正样本;
- 已知缺陷:少量,需人工标注。这是检测模型的学习目标;
- 未知缺陷:训练时不存在,上线才冒出来。这类只能靠异常检测路线兜底(见下一节)。
数据目录组织:
dataset/
├── images/
│ ├── train/ # 正常 + 已知缺陷
│ ├── val/
│ └── test/
└── labels/
├── train/ # 同名 txt(YOLO 格式)
├── val/
└── test/
标注规范(缺陷场景特有):
- 缺陷框紧贴缺陷外沿,但缺陷边界往往模糊——先定一个"最小可见区域"的口径,全组统一;
- 一张图有多个缺陷时,每个都要标,别只标最明显的那个;
- 类别按工艺原因分(划痕 / 凹坑 / 脏污),不要按外观大小分——按大小分会让模型学到尺度而不是缺陷本质;
- 边界样本(拿不准是不是缺陷)单独放
unclear/,别硬标。硬标等于教模型学错。
三、建模路线:检测 / 分割 / 异常检测怎么选
三条路线,对应三种数据条件:

| 路线 | 需要什么数据 | 输出 | 适合 |
|---|---|---|---|
| 目标检测(YOLO 系) | 缺陷框标注 | 缺陷位置 + 类别 | 缺陷类别已知、位置重要 |
| 语义/实例分割 | 缺陷轮廓标注 | 像素级区域 | 缺陷形状不规则、要面积 |
| 异常检测(无监督) | 只要正常样本 | 异常分数/热力图 | 缺陷罕见、类别未知、标注成本高 |
怎么选:
- 缺陷类别已知且稳定(比如就那 3 种划痕)→ 走目标检测,最直接;
- 缺陷边界模糊、要算面积占比(比如涂层不均)→ 走分割;
- 缺陷种类未知、样本极少(比如新产线,只知道"会有异常")→ 走异常检测(PatchCore、PaDiM 这类,只训正常样本)。
最稳的做法是组合:用异常检测做"有没有异常"的粗筛,再用检测模型判"是哪种缺陷"。异常检测能兜住未知缺陷,检测模型给出可解释的类别。
四、小样本训练:四招
缺陷样本只有几十张时,从零训必崩。四招按优先级来:

第一招:用预训练权重做迁移学习。 这是收益最大的一招。加载通用检测预训练权重,让模型带着"什么是边缘、什么是纹理"的先验进来,只学"缺陷长什么样"。
from ultralytics import YOLO
# 从预训练权重出发,而不是从零开始
model = YOLO("yolov8n.pt") # 通用预训练权重
model.train(
data="data.yaml",
epochs=100,
imgsz=640,
batch=8,
lr0=0.001, # 小样本用小学习率,别冲垮预训练特征
lrf=0.01,
patience=20, # 早停,防过拟合
pretrained=True,
freeze=10, # 先冻结前 10 层(骨干),只训检测头
cache=True,
seed=42,
)
第二招:冻结骨干,先训头再微调。 前若干轮冻结骨干(freeze=10),只训练检测头;等损失稳定后再解冻,用小学习率整体微调。这样能避免一开始就把预训练特征冲坏。
第三招:增强要"针对缺陷",不能照搬通用增强。 通用增强里的一些操作会破坏缺陷特征——比如大角度旋转可能把细划痕转出画面,强模糊会把小凹坑抹平。增强配置要克制:
# 缺陷检测的增强:几何变换温和,颜色扰动适度
hsv_h: 0.015 # 色调扰动小
hsv_s: 0.4
hsv_v: 0.3
degrees: 10.0 # 旋转角度别太大(小缺陷易转出画面)
translate: 0.1
scale: 0.3
fliplr: 0.5
flipud: 0.0 # 工业图像上下翻转通常不合理,关掉
mosaic: 0.5 # mosaic 对缺陷场景要慎用
注意:上下翻转(flipud)在工业场景通常不合理——划痕有方向性,上下翻转会造出现实中不存在的样本。同理,mosaic 拼接可能把缺陷拼到不该出现的背景上。增强的目的是模拟真实变化,不是制造不可能的场景。
第四招:用真实缺陷做贴片增强。 小样本时,把已标注的真实缺陷区域裁剪出来,随机贴到正常背景图上,能成倍扩大缺陷样本。关键是只用真实缺陷贴片,不要程序合成假缺陷:
import cv2, numpy as np, random
def paste_defects(bg, patches, max_n=3):
"""patches: [(patch_img, cls_id)],全部来自真实标注缺陷。"""
h, w = bg.shape[:2]
labels = []
for _ in range(random.randint(1, max_n)):
patch, cid = random.choice(patches)
ph, pw = patch.shape[:2]
if ph >= h or pw >= w:
continue
x, y = random.randint(0, w - pw), random.randint(0, h - ph)
bg[y:y + ph, x:x + pw] = patch
labels.append((cid, x + pw / 2, y + ph / 2, pw, ph))
return bg, labels
贴片时要注意边缘融合(羽化/泊松融合),否则贴片边界会成为新的"缺陷",模型学的是拼接痕迹而不是缺陷。
五、类别不平衡:重采样与损失权重
缺陷样本天然长尾——某类几百个、某类只有几个。直接训,模型会忽略稀有类。两种处理:
数据层面:过采样稀有类。 对稀有缺陷的图做额外复制(配合不同的增强),让各类在每轮里的出现次数接近。别简单复制同一张图,要配合增强产生差异。
损失层面:给稀有类加权。 让稀有类的定位和分类损失权重更高,模型不敢忽略。
# 按类别频率的倒数给权重,稀有类权重高
import numpy as np
counts = np.array([820, 96, 12]) # 三类的框数
weights = (counts.max() / counts) ** 0.5 # 开方缓和,避免权重过激
# 传给训练配置的 class 权重(框架支持时)
print(weights) # [1.0, 2.92, 8.27]
注意别过度加权:权重拉到十几倍时,模型会为了稀有类牺牲多数类,误检暴增。用开方(而不是直接取倒数)能缓和这个问题。
六、评估:漏检率比 mAP 重要
这是缺陷检测和通用检测最本质的区别。

| 指标 | 定义 | 缺陷场景里意味着什么 |
|---|---|---|
| 漏检率(FNR) | 真缺陷没被检出的比例 | 次品流出,客户投诉 |
| 误检率(FPR) | 正常区域被判成缺陷的比例 | 人工复核成本上升 |
| 各类 AP | 每类的平均精度 | 看稀有类有没有被忽略 |
| mAP | 各类 AP 的均值 | 会被多数类拉高,掩盖稀有类问题 |
必须做的两件事:
一是分类看 AP,不看总 mAP。 某类只有 12 个框时,它在 mAP 里的贡献被摊薄,就算全错也看不出来。要逐类打印 AP 和召回率。
二是按"漏检/误检"分开统计。 别只看一个总分:
def defect_report(model, test_dir, conf=0.25):
import glob, os
fn = fp = tp = total_gt = 0
for img in glob.glob(os.path.join(test_dir, "images", "*")):
gt = count_labels(os.path.join(
test_dir, "labels",
os.path.splitext(os.path.basename(img))[0] + ".txt"))
preds = len(model.predict(img, conf=conf, verbose=False)[0].boxes)
total_gt += gt
tp += min(gt, preds)
fn += max(0, gt - preds) # 漏检:真值有、预测少
fp += max(0, preds - gt) # 误检:预测多、真值没有
return {
"漏检率": fn / max(total_gt, 1),
"误检率": fp / max(total_gt, 1),
"召回率": tp / max(total_gt, 1),
}
调阈值的取舍:置信度阈值调低 → 漏检少、误检多;调高 → 反之。缺陷场景通常优先保召回(压漏检),用人工复核消化多出来的误检。这个取舍要跟业务方明确,别自己拍。
七、卡住时的排查顺序
- 先看漏检在哪 —— 把漏检的图挑出来,是"没检出"还是"检出但类别错";
- 再看缺陷是不是太小 —— 小缺陷先考虑提高输入分辨率或切图检测;
- 再看增强有没有帮倒忙 —— 关掉增强跑一版对比,尤其 mosaic 和翻转;
- 再看稀有类 —— 逐类 AP,稀有类是不是被忽略了;
- 再看阈值 —— 是不是为了压误检把召回也压掉了;
- 最后看未知缺陷 —— 测试集里有没有训练时没见过的缺陷类型,那要靠异常检测兜底。
一个必须接受的现实:缺陷检测没有"训一次管很久"。产线换料、换灯、换相机,分布就变了。上线后要有持续收集新样本、定期重训的机制,否则模型会随时间衰减。
最后一句:缺陷检测的目标不是 mAP 好看,是"次品不流出"。 把评估口径从"平均精度"换成"漏检率 + 逐类召回",你会发现很多"效果好"的模型其实不达标——而这个发现,比多调几轮参数有价值得多。
说明:文中代码基于
ultralytics与opencv-python的通用用法,具体 API 与参数名以所用框架版本为准。增强策略、损失权重、阈值取舍均为工程经验方向,必须结合自己的产线数据回归确定。异常检测方法(PatchCore、PaDiM 等)请参考其原始论文与官方实现。贴片增强仅使用真实标注缺陷,不引入程序合成的假缺陷。
浙公网安备 33010602011771号