工业缺陷检测实战:小样本训练与漏检控制全流程

工业缺陷检测实战:小样本训练与漏检控制全流程

工业缺陷检测和"通用目标检测"是两回事:缺陷通常极小、样本极少、漏检代价极高。

用通用检测的套路(堆数据、只看 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),
    }

调阈值的取舍:置信度阈值调低 → 漏检少、误检多;调高 → 反之。缺陷场景通常优先保召回(压漏检),用人工复核消化多出来的误检。这个取舍要跟业务方明确,别自己拍。

七、卡住时的排查顺序

  1. 先看漏检在哪 —— 把漏检的图挑出来,是"没检出"还是"检出但类别错";
  2. 再看缺陷是不是太小 —— 小缺陷先考虑提高输入分辨率或切图检测;
  3. 再看增强有没有帮倒忙 —— 关掉增强跑一版对比,尤其 mosaic 和翻转;
  4. 再看稀有类 —— 逐类 AP,稀有类是不是被忽略了;
  5. 再看阈值 —— 是不是为了压误检把召回也压掉了;
  6. 最后看未知缺陷 —— 测试集里有没有训练时没见过的缺陷类型,那要靠异常检测兜底。

一个必须接受的现实:缺陷检测没有"训一次管很久"。产线换料、换灯、换相机,分布就变了。上线后要有持续收集新样本、定期重训的机制,否则模型会随时间衰减。

最后一句:缺陷检测的目标不是 mAP 好看,是"次品不流出"。 把评估口径从"平均精度"换成"漏检率 + 逐类召回",你会发现很多"效果好"的模型其实不达标——而这个发现,比多调几轮参数有价值得多。


说明:文中代码基于 ultralytics 与 opencv-python 的通用用法,具体 API 与参数名以所用框架版本为准。增强策略、损失权重、阈值取舍均为工程经验方向,必须结合自己的产线数据回归确定。异常检测方法(PatchCore、PaDiM 等)请参考其原始论文与官方实现。贴片增强仅使用真实标注缺陷,不引入程序合成的假缺陷。

posted @ 2026-09-28 00:37  橘和柠  阅读(7)  评论(0)    收藏  举报