机器视觉工业缺陷检测实战:从成像选型到算法部署
简介:机器视觉工业缺陷检测的那些事是一份面向机器视觉工程师、自动化测试人员及工业检测入门者的PDF资料,围绕工件尺寸测量、表面缺陷检测和Logo标识检测识别等典型场景,系统梳理了视觉检测系统的完整构成。内容从基础的光源选型讲起,对比LED、萤光灯、卤素灯等常见光源的寿命与适用场景,并介绍了背向照明、前向照明、结构光、同轴光等照明方式在划痕、轮廓、字符检测中的实际取舍;随后给出工业相机分辨率、帧率、色彩深度等关键参数的选择思路,以及镜头、图像采集卡的配套要点。图像处理部分覆盖预处理中的去噪、增强、校正,以及边缘检测、模板匹配、灰度变换等检测识别算法,并补充了常用算法库信息,可直接用于项目方案梳理或部门内部分享。资源包共1个PDF文件,大小约1.26MB,内容以图文形式呈现,便于查阅和打印。该资料至今已有1188人学习,适合在工业视觉选型、算法选型或缺陷检测项目启动前快速建立整体认知。
1. 机器视觉工业缺陷检测:算法只占三成,剩下七成在成像与现场工程
做机器视觉工业缺陷检测的人,迟早会承认一件事:算法只占项目成败的三成,剩下的七成在成像、光源和现场工程。表面划痕、脏污、缺料、尺寸超差,这些原本靠老师傅目检的活儿,换成人眼替代方案后,真正的难点不是“能不能检测”,而是“换一个光照、换一个角度、换一条产线节拍后还稳不稳定”。这篇文章写给正在选型、准备上手第一个检测项目、或者已经被过杀率折磨到想砸设备的工程师。我会按照从需求拆解、硬件选型、算法实现到现场部署的路径,把可复现的代码、参数和踩坑记录一次讲清楚。
2. 拆解检测需求:成像方案、相机选型与算法边界
2.1 光源和相机先于算法:四种常用光源的适用场景
做缺陷检测项目,现场最常被问的一句话是“用什么算法”,但实际项目里,最先定的是“用什么光”。算法看到的图像质量,在图像采集那一刻就已经被决定了。同一个划痕,用低角度环形光能打出高对比度的暗场效果,换成正光可能完全隐没在纹理里。光源选型很多时候靠玄学,但基础规律是可以用一套经验框架的。
环形光安装在镜头周围,光线从四周打向工件。高角度环形光适合表面字符、定位标记的照明;低角度环形光专门做金属表面划伤检测,划痕会在暗背景下形成亮线,对比度极高。同轴光适合高反光平面,光路经过半反半透镜垂直打在物体上再原路返回,能看清玻璃、晶圆、手机屏表面的细微色差,缺点是对倾斜表面几乎没有效果。条形光适合大视野和倾斜照射,经常两三根组合使用,用来检出塑料件表面的凹凸和褶皱。穹顶光也叫球积分光源,光线经过球面内壁多次漫反射再均匀落在物体上,专门对付曲面和高反光物体,避免镜面反射造成的亮斑。
光源颜色不是随便选的。白光最通用,但红光打在蓝色或绿色材料上会形成强烈反差,蓝光能增强金色表面的氧化痕迹。更讲究的做法是加偏振片,把工件表面的镜面反光滤掉,只保留漫反射信息,这一步在检测拉丝金属和覆膜材料时几乎是必须的。下面这张表是我的常用选型参考:
| 光源类型 | 适用缺陷 | 典型场景 | 主要注意点 |
|---|---|---|---|
| 低角度环形光 | 划伤、压伤、凹凸 | 金属、轴承、螺钉端面 | 对安装高度敏感,要调 |
| 同轴光 | 色差、污点、玻璃裂纹 | 屏幕、晶圆、镜面 | 曲面表面会发暗 |
| 条形光组合 | 褶皱、纹理异常、缺料 | 塑料件、木材、纸张 | 角度不固定,需支架 |
| 穹顶光 | 曲面反光、整体均匀照明 | 药瓶盖、曲面外壳 | 亮度偏低,曝光要拉长 |
光源的亮度稳定性和寿命,往往决定一个检测系统能平稳跑多久。我一般会选带恒流驱动的光源控制器,而不是让光源直接接开关电源,否则LED一发热,电流漂移,图像灰度跟着飘,算法的阈值就得天天改。
2.2 相机选型与分辨率计算:面阵、线阵和镜头的搭配逻辑
光源定完,第二件事是选相机。大多数工件检测用面阵相机,拍一张图就能覆盖整个视野。如果检测对象是大幅面卷材,比如布匹、薄膜、钢板,或者产线运动速度很高、一次只能拍一条窄带,就要用线阵相机连续扫行拼接。判断依据很简单:工件能停住或低速通过,用面阵;工件高速不停、画面宽度又有几十厘米以上,用线阵。
分辨率的计算是每次都要跟客户确认的核心环节。假设视野是 100mm × 80mm,要求检测精度 0.1mm,那么宽度方向至少需要 100 / 0.1 = 1000 像素,高度方向 80 / 0.1 = 800 像素。这只是理论下限,实际选型要留边缘余量和亚像素定位空间,我习惯按 1.5~2 倍余量来选,也就是至少 200 万像素,直接上 500 万像素更省心。分辨率越高,图像数据量越大,算法耗时越长,所以不要无脑买高像素,够用就行。
镜头这块,焦距由工作距离、传感器靶面尺寸和视野共同决定,公式是 焦距 = 工作距离 × 传感器宽度 / 视野宽度。如果做的是尺寸测量类项目,比如测零件外径、内孔位置,强烈建议用远心镜头,它只接收平行光,物体前后移动时影像大小不变,能消除视差误差。普通镜头在物体高度方向有起伏时,测量结果会漂,这在缺陷检测里也会导致同一个缺陷在不同高度的表现不一致。
2.3 算法选型边界:传统视觉和深度学习的适用分界线
选算法的第一原则是:能用传统视觉解决,就不要上深度学习。传统视觉是 Blob 分析、边缘提取、模板匹配这一路,毫秒级出结果,参数透明,出了问题能一行一行查逻辑。深度学习的检测模型表现更鲁棒,但它是黑匣子,样本要求高、推理要算力、误判原因难追溯。具体可以按下面这张表来判断:
| 对比维度 | 传统视觉 | 深度学习 |
|---|---|---|
| 样本需求 | 少量样本即可调参 | 每类缺陷建议 300 张以上 |
| 推理速度 | 1~5 毫秒 | 10~50 毫秒(视模型和硬件) |
| 可解释性 | 阈值、面积、位置都清楚 | 特征图可看,但决策路径说不清 |
| 光照敏感度 | 高,固定阈值会漂 | 中,前提是训练数据覆盖充分 |
| 适合场景 | 尺寸超差、固定位置毛刺、定位 | 纹理复杂、缺陷形态多变、背景杂乱 |
我的判断逻辑一般是三步。第一步,缺陷能不能用面积、长度、灰度差这类数字描述清楚,能就用传统视觉,比如药瓶盖缺料、螺丝端面划痕、继电器引脚弯曲。第二步,缺陷形态变化大、背景纹理复杂,比如布匹上的脏污、注塑件的流痕,这就要用深度学习的目标检测或分割模型。第三步,如果你连缺陷长什么样都说不全,那就直接考虑无监督异常检测,只拿正常样本训模型,让模型自己发现“不正常”,这个思路我放在最后一章展开。
2.4 最小检测方案的工作流程
一套最小可落地的缺陷检测流程,通常分七个环节。产品进入检测工位,光电传感器触发相机曝光;相机把图像传给工控机;检测程序先做 ROI 裁剪,再滤波增强;算法完成缺陷判定;系统向 PLC 输出 OK/NG 信号;NG 品在后续工位被剔除机构吹掉;图像和结果写进本地数据库备查。
节拍是这里面算得最细的指标。检测节拍 = 曝光时间 + 图像传输时间 + 算法耗时 + IO 延迟,实际设计要在这基础上留 20%~30% 余量。举个例子,产线节拍要求每秒检 2 件,即 500 毫秒一件,曝光 5 毫秒,传输 30 毫秒,算法占 300 毫秒,IO 加 PLC 响应 30 毫秒,总耗时约 365 毫秒,还有 135 毫秒余量,这个方案才敢上线。算出来如果超过节拍,反馈给前面的相机选型,要么降低分辨率,要么裁剪 ROI,而不是等到现场跑不动了再想办法。
3. 跑通第一个检测程序:OpenCV 骨架与合成缺陷扩展样本
3.1 OpenCV 实现表面缺陷检测的最小骨架
如果工件表面和背景灰度差异明显,用 OpenCV 就能搭出第一个可用的检测程序。以药瓶盖划痕检测为例,常见的做法是:读取图像、转灰度、裁剪 ROI、滤波、阈值分割、形态学处理、按面积筛选缺陷。
import cv2
import numpy as np
# 读取图像,先裁剪ROI,缩小后续计算范围
img = cv2.imread("cap_001.png")
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
roi = gray[100:600, 150:700]
# 高斯滤波去噪,避免把CMOS传感器噪声当成缺陷
blur = cv2.GaussianBlur(roi, (5, 5), 0)
# 固定阈值分割:缺陷区域与背景灰度差异明显时最有效
_, thresh = cv2.threshold(blur, 120, 255, cv2.THRESH_BINARY)
# 形态学闭运算,把断裂的划痕连接成完整区域
kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (3, 3))
closed = cv2.morphologyEx(thresh, cv2.MORPH_CLOSE, kernel)
# 找轮廓并按面积过滤,min_area相当于忽略噪点
contours, _ = cv2.findContours(closed, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
min_area = 50
defects = [c for c in contours if cv2.contourArea(c) > min_area]
print(f"defect count: {len(defects)}")
这里有几个参数要解释清楚。threshold 的 120 是灰度阈值,像素灰度低于 120 的被认为是缺陷区域,这个值依赖光源亮度,光源稍有变化就要重新标定。min_area 等于 50,意思是面积小于 50 像素的连通域当作灰尘或噪点忽略,这就是商业视觉软件里“忽略点数”参数的同一个思路,点掉了反而更干净。高斯滤波核 (5, 5) 对高斯噪声抑制效果好,如果用中值滤波,应对椒盐噪声更合适,但会稍微抹掉细小划痕的对比度,我一般默认高斯滤波。闭运算的核大小也要调,核太大,两个相邻缺陷会被连成一个,核太小,断裂划痕接不上。
固定阈值最大的问题是扛不住光照变化。如果现场曝光偶尔波动,改用 Otsu 自适应阈值会稳很多:
# OTSU阈值自动计算,光照变化时比固定阈值更稳
_, thresh = cv2.threshold(blur, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)
OTSU 的原理是自动寻找一个让前景和背景类内方差最小的灰度值,不需要人工设定。代价是如果图像里缺陷面积占比太小,比如小于 5%,OTSU 会偏向背景。这时候可以给 ROI 加一个高亮背景垫片,强制缺陷占比变大,再用 OTSU 就能稳定分割。
3.2 样本不够时用合成缺陷撑起训练集
深度学习不是天生比传统视觉强,它比传统视觉更“吃”样本。产线上的缺陷率往往在千分之一以下,收集几百张真实缺陷图像可能要跑一两个星期。常见做法是:用大量正常样本加上人工合成的缺陷,把训练集撑起来。合成缺陷不是随便画几条线,而是要模拟真实成像结果。
import numpy as np
import cv2
import random
def add_scratch(img, max_len=80, max_width=4):
"""在正常图像上随机画一道划痕,模拟表面划伤缺陷"""
h, w = img.shape[:2]
mask = np.zeros((h, w), dtype=np.uint8)
x1, y1 = random.randint(0, w), random.randint(0, h)
angle = random.uniform(0, np.pi)
length = random.randint(20, max_len)
x2 = int(x1 + length * np.cos(angle))
y2 = int(y1 + length * np.sin(angle))
cv2.line(mask, (x1, y1), (x2, y2), 255, random.randint(1, max_width))
# 模糊让划痕边缘更接近真实成像效果
mask = cv2.GaussianBlur(mask, (5, 5), 0)
# 与图像叠加,并加入传感器高斯噪声
noise = np.random.normal(0, 25, img.shape).astype(np.float32)
result = img.astype(np.float32) + mask[..., None] * random.uniform(0.5, 1.0) + noise
return np.clip(result, 0, 255).astype(np.uint8)
画一条线再高斯模糊,这一步很关键。真实划痕在成像中是边缘柔和的亮带或暗带,如果直接用硬边线条叠加,模型会学到“有硬边就是缺陷”这个假规律,到了现场边缘锐利的纹理都会触发误报。叠加高斯噪声,是为了模拟相机暗光下的噪声特性,防止模型只认无噪声的干净图像。扰动幅度 random.uniform(0.5, 1.0) 控制划痕的灰度对比度,值是 1.0 时划痕最明显,0.5 时接近真实弱缺陷。建议真实缺陷和合成缺陷的比例控制在 1:3 以内,全用合成样本训出来的模型,一到现场就会被真实成像差异打回原形。
数据增强还可以继续加:随机翻转、90 度旋转、亮度对比度扰动、弹性形变。亮度和对比度扰动在缺陷检测里尤其重要,它让模型对光源波动不敏感。OpenCV 里可以直接用 cv2.convertScaleAbs 实现:
def random_brightness_contrast(img, brightness_delta=40, contrast_alpha=1.2):
"""随机亮度对比度扰动,模拟光源波动下的成像变化"""
brightness = random.randint(-brightness_delta, brightness_delta)
contrast = random.uniform(1.0, contrast_alpha)
return cv2.convertScaleAbs(img, alpha=contrast, beta=brightness)
这类增强不能做过头,否则会改变缺陷本身的可辨识度。我一般把亮度偏移控制在 ±40 灰度以内,对比度倍数不超过 1.2,保证增强后的图像还属于“同一现场”的分布范围。
3.3 转向 YOLO 检测:标注格式与训练命令
当缺陷形态复杂、背景纹理杂乱,传统视觉撑不住,就该上 YOLO 这类目标检测模型了。YOLO 的标注格式是每张图对应一个 txt 文件,每行代表一个缺陷框,格式为“类别 class x_center y_center width height”,所有坐标都归一化到 0~1。这个归一化是最容易翻车的点,很多初学者直接把像素坐标喂进去,loss 不收敛也找不到原因。
def voc_to_yolo(xml_path, img_w, img_h):
"""把VOC xml标注转为YOLO格式,坐标归一化到0-1"""
import xml.etree.ElementTree as ET
tree = ET.parse(xml_path)
root = tree.getroot()
lines = []
for obj in root.iter("object"):
cls = obj.find("name").text
box = obj.find("bndbox")
x1 = float(box.find("xmin").text)
y1 = float(box.find("ymin").text)
x2 = float(box.find("xmax").text)
y2 = float(box.find("ymax").text)
x_c = (x1 + x2) / 2 / img_w
y_c = (y1 + y2) / 2 / img_h
w = (x2 - x1) / img_w
h = (y2 - y1) / img_h
lines.append(f"{cls} {x_c:.6f} {y_c:.6f} {w:.6f} {h:.6f}")
return "\n".join(lines)
width 和 height 用的是缺陷框的真实像素宽度除以图像宽度,不是左上右下坐标差的一半,这一点看代码容易混淆。归一化后,无论训练图像是 640×640 还是 1280×1280,框坐标都在同一个尺度下,模型才能做多尺度训练。
数据准备好后,用一个 defects.yaml 描述数据集路径和类别,然后启动训练:
# defects.yaml
path: ./datasets/defects
train: images/train
val: images/val
nc: 2
names: ["scratch", "dent"]
# YOLOv5 系列训练命令,s模型在速度与精度之间平衡最好
python train.py --data defects.yaml --img 640 --batch 16 --epochs 200 \
--weights yolov5s.pt --device 0 --patience 50
训练参数有几个值得说明。img 640 意味着图像会缩放到 640×640,如果缺陷很小,建议先用原图 ROI 裁剪再训练,而不是盲目加大训练尺寸,因为分辨率翻倍,显存和时间也翻倍。batch 16 在显存允许范围内尽量大,大 batch 让梯度更稳定。patience 50 表示验证集指标连续 50 个 epoch 不提升就提前停止,这比硬跑满 200 个 epoch 更实际,能避免后期过拟合。yolov5s 是轻量模型,工业检测以毫秒级节拍为目标时,s 模型通常是性价比最高的起点,跑不动再考虑更小更快的 n 版本或做剪枝。
4. 现场避坑笔记:五个让检测系统翻车的常见问题
4.1 现象:训练集准确率 99%,产线上一测就崩
模型在验证集上 mAP 很高,一装到产线误报率暴增,OK 品被大量判成 NG。原因是训练集和现场分布不一致。实验室里用固定光源、固定角度、固定背景拍出来的数据,到了现场换了日光灯、换传送带颜色、工件位置稍有偏移,模型见过的“正常”就不再是现场的正常。
解决思路分三层。第一层,现场至少采集 1000 张正常样本加进训练集,重新划分训练集和验证集,让模型真正见过现场的正常形态。第二层,训练时把亮度扰动范围调大,用前面提到的 random_brightness_contrast 让模型对光照变化脱敏。第三层,上线前先跑影子模式,程序照常检测并记录结果,但不实际触发剔除,统计一天的误报率,用数据决定要不要正式启用。这个步骤能避免一次灾难性的质量事故。
4.2 现象:光源一换,检测结果集体漂移
同一套程序上午检得好好的,下午换了一根同型号的光源棒,过杀率从 1% 跳到 15%。即使同型号 LED 光源,批次不同发光强度和色温也有差异,再加上 LED 本身会衰减,光源亮度随时间缓慢下降。所有写了固定灰度阈值的地方,比如第 3 章那个 threshold=120,都会在这一刻集中爆发。
解决的办法有三个。硬件上,光源控制器必须带恒流驱动,别直接接开关电源。算法上,把固定阈值全部改成 OTSU 自适应阈值,或者基于背景灰度相对值计算阈值。管理上,每次开机用标准板拍照,计算整图的平均灰度并记录到日志,灰度值偏离初始基线超过设定值,系统直接报警提示光源需要更换。这就是让“光源衰减”尽早暴露,而不是等检测结果先崩。
4.3 现象:过杀率居高不下,老师傅拒绝用系统
算法把很多看起来像缺陷、但实际不影响使用的纹理、油污、水渍全标成 NG,产线频繁停线,操作工只能绕过系统手动放行。本质原因是缺陷定义没有和生产标准对齐。质量部门认可的是“功能性缺陷”,算法学到的是“视觉异常”。比如塑料件上的模流纹,老师傅知道这是正常工艺纹理,算法却当成划痕。
我的做法是把判定结果分成三级:NG、NG-Low 和 OK。NG 是明确的功能性缺陷,NG-Low 是疑似缺陷需要人工复检。给每个缺陷区域算一个综合评分,特征是面积、长宽比、灰度对比度和位置,评分超过高阈值才判 NG,在高低阈值之间判为 NG-Low。同时和质检团队一起收集被误杀的样本,单独建一个“可接受纹理”的类别加入训练集,模型下一次更新时就会把这些正常纹理学进去。
4.4 现象:缺陷类别不平衡,模型忽略少数类
训练集里 OK 样本和划痕占了 95%,凹坑只有 2%,训练出来的模型对凹坑几乎不报,即使报了置信度也低。原因是目标检测的损失在类别极度不平衡时被多数类主导,模型策略性把所有难分样本都推给多数类。
解决办法是给少数类加权重,YOLOv5 里可以用 cls_pw 参数调整类别损失权重;或者对少数类做过采样,每个 epoch 重复使用,让凹坑样本数量达到多数类的 30% 以上。另外一个更实用的手段是专门为少数类合成样本,比如凹坑可以用高斯斑点模拟:在正常图像上随机画圆斑点,乘以高斯衰减,再模糊叠加,做法和 3.2 节画划痕完全一致。血泪经验是,合成样本必须和真实样本混合训练,纯合成模型到了现场会比预想中更早翻车。
4.5 现象:检测速度跟不上产线节拍
产线要求每秒检测 2 个工件,程序算完一个要 1 秒,传送带已经过去了,只能降速生产。原因通常是图像分辨率过大、ROI 没裁剪、模型太重,或者相机传输没有开硬件加速。很多项目到这一步才反过来说硬件选型有问题,但大多数情况还有优化空间。
优化顺序按性价比排列:先裁剪 ROI,把计算量缩到一半以上;深度学习输入尺寸从 1280 降到 640,精度损失往往很小,速度提升明显;模型导出为 TensorRT 或 ONNX Runtime,推理通常可以提速 1.5~2 倍;最后把采集和计算流水线化,相机在采集第 N+1 帧时,算法正在处理第 N 帧。大部分项目靠前两步就能把 1 秒压到 300 毫秒以内,真正需要换 GPU 的是极少数。
5. 部署到产线:结果输出、触发控制与模型迭代闭环
5.1 检测结果如何落到现场系统:JSON、TCP 与数据库三选一
算法跑出结果后,下一步是把“这个工件是 NG”这件事告诉现场设备。常见做法有三种:通过 TCP 把 JSON 发给上位机,直接写数据库,或者把信号通过 IO 模块送给 PLC。如果现场有 MES 或 SCADA,首选 TCP + JSON,因为 MES 侧可以反序列化直接入库,不需要约定二进制协议。
import json
import socket
def send_result(ip, port, result_dict):
"""将检测结果打包成JSON,通过TCP发送给上位机"""
data = json.dumps(result_dict, ensure_ascii=False).encode("utf-8")
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
s.connect((ip, port))
s.sendall(data)
# 等上位机ACK,确认结果已送达
ack = s.recv(16)
return ack == b"OK"
result = {
"camera_id": "C001",
"product_id": "SN-20210817-001",
"result": "NG",
"defect_type": "scratch",
"score": 0.87,
"image_path": "/data/cap_001.jpg"
}
send_result("192.168.1.50", 9000, result)
这里必须等上位机回 ACK。现场网络偶尔抖动,发完就忘会造成 NG 结果丢失,那是漏检事故,不是性能问题。image_path 字段要存原始图像路径,否则后续追溯缺陷时找不回当时的画面。如果对接的是 PLC,通常不用 TCP,而是让工控机通过 IO 网关把结果映射成数字量输出,NG 信号直接参与剔除机构的控制逻辑,PLC 再记录事件。
5.2 相机触发与节拍匹配:硬触发为什么更稳
相机触发方式分成软触发和硬触发两种。软触发由软件通过 SDK 命令让相机曝光,流程简单,适合实验室和单工位。但软件调用有毫秒级抖动,一旦操作系统卡顿还会漏拍。硬触发是光电传感器或编码器信号直接连到相机的硬件触发引脚,工件到位瞬间就给相机一个电平信号,曝光在微秒级延迟内完成,完全不依赖工控机负载。
| 对比维度 | 软触发 | 硬触发 |
|---|---|---|
| 延迟 | 毫秒级,抖动明显 | 微秒级,稳定 |
| 可靠性 | 程序卡顿会漏拍 | 不依赖软件 |
| 成本 | 无需额外线缆 | 需要传感器和接线 |
| 适用场景 | 实验、节拍要求低 | 高速产线、运动物体 |
除了触发方式,还要算曝光时间是否会产生运动模糊。工件移动速度 v = 100 mm/s,检测精度要求 0.1 mm,那么曝光时间必须小于 0.1 / 100 = 1 毫秒,否则拍出来的图像是拖影的,算法再强都救不回来。如果算出来曝光时间不够,那就得换更强的光源来缩短曝光,或者加快门相机。
5.3 现场样本回流与模型迭代:避开“越改越差”的陷阱
上线不是终点,模型迭代才是长期工作。现场每天都在产生新的正常样本和少量缺陷样本,正确的做法是把这些样本收集起来,每周或每月合并进旧训练集,整体重训一次。模型文件命名要带上版本号和训练日期,部署时保留上一个版本的备份,这样新模型出问题时可以一键切回。这就是给自己留后悔药,我在一线项目里吃过不止一次没有回滚机制的亏。
不要在线用现场数据直接继续训模型。数据分布一旦突变,增量训练会触发灾难性遗忘,模型会突然丢失已经学过的缺陷类型。我一般的做法是:新数据合入旧训练集,整体重训,虽然慢,但结果可控。模型迭代前还要做一次回归测试,拿一批覆盖所有缺陷类型的固定样件跑一遍,确认新模型没有让任何一类缺陷的召回率下降,否则不上线。
6. 进阶:用无监督异常检测兜住“没见过”的缺陷
很多缺陷检测项目的真正难点根本不是某一种缺陷检不出,而是缺陷类别永远说不全。换了批次材料、模具磨损、工艺参数调整,随时可能出现一种从未见过的缺陷形态。监督学习需要每类几百张样本,迭代速度永远追不上现场变化。这个问题的解法是换成无监督异常检测的思路:只用正常样本训练一个重建模型,模型没见过“缺陷长什么样”,但它知道“正常长什么样”,当输入偏离正常分布时,重建误差就会变大,误差大的像素区域就是缺陷位置。
具体实现可以用一个轻量的卷积自编码器,输入 128×128 灰度图,输出重建图像,用重建图和原图的像素差定位缺陷。
import torch
import torch.nn as nn
class ConvAE(nn.Module):
def __init__(self):
super().__init__()
self.encoder = nn.Sequential(
nn.Conv2d(1, 16, 3, stride=2, padding=1), nn.ReLU(),
nn.Conv2d(16, 32, 3, stride=2, padding=1), nn.ReLU(),
nn.Conv2d(32, 64, 3, stride=2, padding=1), nn.ReLU(),
)
self.decoder = nn.Sequential(
nn.ConvTranspose2d(64, 32, 4, stride=2, padding=1), nn.ReLU(),
nn.ConvTranspose2d(32, 16, 4, stride=2, padding=1), nn.ReLU(),
nn.ConvTranspose2d(16, 1, 4, stride=2, padding=1), nn.Sigmoid(),
)
def forward(self, x):
return self.decoder(self.encoder(x))
训练时只用 OK 样本,损失函数用均方误差,目标是让重建图像尽量接近输入。推理时,把重建误差图做阈值分割,误差超过阈值的连续区域就是异常位置。这个方案的优点是没有标注成本,新缺陷也能被兜住,缺点是正常样本里如果混入了瑕疵,模型会把瑕疵也当正常学进去,所以训练数据必须人工筛选干净。当前沿技术路线走到这里时,你会发现缺陷检测的规模化能力其实不取决于标注了多少缺陷,而取决于你对“正常”建模得有多准。
我自己的习惯是,无论选哪条路线,先挑产线上最痛的那一种缺陷,把方案跑通、跑稳、跑到被老师傅认可,再横向复制到其他缺陷类型。每个项目保留一份现场参数备忘,光源型号、曝光时间、阈值、节拍、误报率变化全部记下来,因为这类项目的坑,大部分不是算法写不出来,而是环境变了你不知道。希望帮到你。
更多推荐

所有评论(0)