简介这一压缩包面向制造业质检工程师、算法研发人员及自动化产线管理者尤其适合具有Python基础、希望将深度学习落地到产线质检的读者提供基于深度学习的工业表面缺陷检测完整方案。包内共375个文件含177张jpg、176张png图像样本可用于模型训练与验证10个Python脚本负责图像处理、特征提取与推理另有PyTorch事件日志、pt权重文件、md/docx说明文档和html展示页面压缩后约176MB。目前已有48人学习内容由浅入深适合从零搭建CNN缺陷分类模型覆盖划痕、磕碰、杂质等常见瑕疵。资源集数据、代码、可视化与说明于一体既支持高精度图像分割、边缘检测等算法复现也提供自动化质量控制和实时监控的落地参考可帮助产线减少漏检、降低人工成本。1. 工业缺陷检测系统落地先想清楚“模型不是主角”一台贴着“基于深度学习的视觉识别算法”标签的工业缺陷检测系统到了产线上真正决定成败的往往不是那套网络结构有多新而是你把样本、光照、节拍和误报处理得有多糙。很多团队第一次做这类项目会把 80% 精力放在改模型上结果到了现场发现漏检来自反光、误报来自脏污甚至一张图片在开发机跑 80ms到了工控机上变成 300ms节拍直接超时。这篇笔记按实际落地顺序讲透先解决“缺陷长什么样、数据怎么来”再谈模型选型和训练参数然后是实时链路怎么搭、PLC 怎么联动最后是产线上最常见的翻车点和我现在养成的复盘习惯。适合刚接手制造业视觉项目的算法工程师、准备做内部质检自动化的工艺工程师以及想评估“深度学习到底能不能用在我这条线”的产线管理者。先说结论这个方向能省人、能提准但它的复杂度不在算法论文里而在产线约束和脏数据里。2. 从产线到数据集缺陷数据怎么做才不坑2.1 先看缺陷长什么样光源、相机与数据口径工业缺陷检测系统的第一个决策不是模型而是“你打算让模型看到什么”。常见表面缺陷无非几类划痕、脏污、凹陷、气泡、色差、毛刺、裂纹。每类缺陷在不同材质、不同光照下的表现差异极大比如金属表面的细小划痕在暗场照明下亮度高、对比清晰在明场照明下可能完全消失而玻璃气泡用背光拍最清楚用同轴光反而容易拍成亮点误判。我一般拿到需求会先做两件事翻产线上已有的质检标准和缺陷样本照片再去现场看相机工位的实际约束——相机固定方式、视野和分辨率是否匹配最小缺陷尺寸、节拍时间有多少毫秒。最小缺陷尺寸这条尤其重要一个 0.2mm 的划痕要在 1920x1080 分辨率下可靠识别视野宽度最好不要超过 100mm视野超过了缺陷在图像里只有 3 个像素任何算法都救不回来这不是模型问题是成像系统设计问题。数据口径上常见做法是先把缺陷按“类别 材质 批次”打标。不要只给一个“缺陷/正常”二分类因为产线要的是“是哪类缺陷、是否需要停机、要通知哪个工序”。铝件划痕和铝件脏污在处置上完全不同混在一起会让后续质量控制很被动。目录结构我习惯这样组织dataset/ ├── train/ │ ├── normal/ # 正常块 │ ├── scratch/ # 划痕 │ ├── stain/ # 脏污 │ └── dent/ # 凹陷 ├── val/ └── test/2.2 把关键缺陷“抠”成训练块一个干净的数据整理脚本工业现场的原始图像往往不是深度学习直接能吃的尺寸——一张 500 万像素的相机图如果直接缩放成 224x224小缺陷基本就丢了如果整图送进网络显存又撑不住。常规做法是滑窗切块把大图切成 512x512 或者 256x256 的训练块。切块之后要做的关键一步是过滤只保留包含可疑区域的块否则训练集里正常块占比 99%模型会学会“永远输出正常”。下面这段脚本解决的是“把标注过的可疑区域图像裁成训练块并按类别归档”的问题。实际项目中标注文件可能是 VOC 格式的 XML也可能是生产厂家导出的 CSV这里我换成更通用的坐标文本来演示逻辑是等价的import os import cv2 import numpy as np # 假设每行记录格式: 图像路径 x1 y1 x2 y2 类别 # 这个脚本把可疑区域周围带背景的上下文一起裁出来 # 背景太少模型学不到“在正常背景下找差异”太多则小目标被稀释。 def crop_and_save(line, input_root, output_root, patch_size(256, 256), margin16): img_rel, x1, y1, x2, y2, label line.strip().split(,) x1, y1, x2, y2 int(x1), int(y1), int(x2), int(y2) img cv2.imread(os.path.join(input_root, img_rel)) h, w img.shape[:2] # 缺陷区域外扩 margin保留周边正常纹理作为上下文 x1 max(0, x1 - margin) y1 max(0, y1 - margin) x2 min(w, x2 margin) y2 min(h, y2 margin) patch img[y1:y2, x1:x2] # 统一缩放到训练 patch 尺寸保留宽高比后用零填充 ph, pw patch.shape[:2] scale min(patch_size[0] / ph, patch_size[1] / pw) new_w, new_h int(pw * scale), int(ph * scale) resized cv2.resize(patch, (new_w, new_h)) canvas np.zeros((patch_size[0], patch_size[1], 3), dtypenp.uint8) x_off, y_off (patch_size[1] - new_w) // 2, (patch_size[0] - new_h) // 2 canvas[y_off:y_off new_h, x_off:x_off new_w] resized save_dir os.path.join(output_root, train, label) os.makedirs(save_dir, exist_okTrue) base os.path.splitext(os.path.basename(img_rel))[0] cv2.imwrite(os.path.join(save_dir, f{base}_{x1}_{y1}_{label}.jpg), canvas, [cv2.IMWRITE_JPEG_QUALITY, 95]) # 读取标注文件 with open(annotations.csv, r) as f: for line in f: crop_and_save(line, raw_images, dataset, patch_size(256, 256))逻辑说明这段代码先按标注坐标抠出缺陷区域并外扩 16 像素作为上下文然后等比缩放并零填充到 256x256避免直接拉伸导致缺陷宽高比失真。最后按类别归档。值得注意的参数是 margin 和 JPEG 质量margin 太小会让模型只学会“看到一团异物”不会结合周边表面纹理判断JPEG 质量不要低于 90工业场景下过度压缩会磨掉浅划痕的边缘信息等于人为给检测加了难度。实际整理数据时还要处理两类问题一是同一张图里缺陷特别小切块后缺陷只占 3% 面积这类块如果送进分类网络模型很容易忽略建议对小缺陷块做过采样或者后续改用分割模型去逐像素找二是样本极度不均衡划痕几千张、凹陷只有几十张这种情况下不要硬训分类器先用少量真实样本做验证再结合 2.3 节的合成增强把凹陷扩到可训练规模。2.3 别只依赖真实样本光照不均与噪声的合成增强工业现场的真实缺陷样本永远不够尤其是新产线投产初期。常见的增强手段是亮度抖动、对比度抖动、高斯噪声、高斯模糊、JPEG 压缩伪影、随机平移旋转。注意工业场景和 ImageNet 分类的增强策略有个本质差异不要用颜色通道随机扰动因为金属和陶瓷表面的色差是重要判据把颜色改了会教坏模型。另一个工业特有的增强是光照不均模型。产线上最典型的干扰是光源衰减和角度偏移导致的图像中心亮、边缘暗这个用全局亮度抖动模拟不了得在增强函数里加一个空间缓变的亮度遮罩import random import cv2 import numpy as np def simulate_illumination_uniformity(img, strength0.25): 模拟工业现场光照不均图像中心亮、边缘暗的渐变效果 h, w img.shape[:2] ys, xs np.meshgrid(np.linspace(0, 1, h), np.linspace(0, 1, w), indexingij) # 从图像中心向边缘衰减的权重矩阵强度随机 cx, cy random.uniform(0.4, 0.6), random.uniform(0.4, 0.6) dist np.sqrt((xs - cx) ** 2 (ys - cy) ** 2) gain 1.0 - strength * dist / np.sqrt(2) img_float img.astype(np.float32) for c in range(3): img_float[:, :, c] * gain return np.clip(img_float, 0, 255).astype(np.uint8) # 在训练循环里对每个 batch 随机调用配合常规增强一起用增强参数说明strength 建议从 0.15 起调过强会让模型把亮度变化误当成缺陷特征反而拉低正常样本上的准确率光源衰减的方向也不是永远从中心向边缘如果产线上的是条形光源遮罩形状换成沿某方向线性衰减更贴近实际。另外要控制增强后的样本占比我一般控制在 30% 的训练块上做光照扰动让模型既有鲁棒性又不会因为增强过度而失去对真实缺陷的敏感性。这样整理出来的训练集会让后面选择的深度学习模型训练顺利非常多。多数团队在这个阶段翻车不是模型不收敛而是训练集里混了大量“看起来是缺陷但其实是反光/阴影”的样本导致模型上线后对着正常产品狂报缺陷。3. 模型选型与训练参数分类、分割与异常检测怎么选3.1 三类方案对比先定检测粒度再定网络结构工业表面缺陷检测按输出粒度可分为三类图像级分类、目标框检测、像素级分割。图像级分类解决“这一块有没有问题”适合缺陷面积占比较大、位置不重要的场景目标框检测解决“缺陷在哪片区域”适合离散缺陷、需要定位但不需要精确轮廓的场景像素级分割解决“缺陷有多严重、面积多大”适合缺陷面积直接影响质量等级的行业比如钢材表面夹杂、电池极片涂布缺陷。如果只有一个模糊需求说“我要做缺陷检测”我通常建议先用分类模型快速验证可行性理由有三个分类模型训练快、标注成本最低只要有块级标签、漏检率容易压住。但产线真实需求往往是“我要知道缺陷出现在哪个位置、以及缺陷面积多大”这时候分类模型做不了要切换到分割模型或带定位头的检测模型。当下工业项目里最常用的几个深度模型按范式分分类用 ResNet、EfficientNet 这类 CNN目标检测用 YOLO 系列分割用 U-Net 加轻量编解码结构。异常检测方向比如 PatchCore适合“缺陷样本极少、只知道什么是正常”的场景离线先在正常样本上学分布线上对比特征距离找异类但工业界对它的可解释性和阈值稳定性还不太满意。下面这张表列出我的选型倾向标注了适用场景和典型代价方案输出粒度标注成本适合场景主要代价分类ResNet/EfficientNet整块图类别低缺陷大、无需精确位置位置信息丢失小缺陷易漏检测YOLO 系列外接矩形框中离散缺陷、需要定位抓取细长缺陷的框不紧IOU 评估失真分割U-Net 系列像素级掩码高缺陷面积参与质量分级训练和推理显存占用高异常检测PatchCore 等像素级热力图极低正样本多、缺陷千奇百怪阈值敏感性高现场调参不容易3.2 训练一个能抗过拟合的分类基线参数怎么设以分类基线为例我给出一份在工业表面缺陷上验证过多次的参数配置。输入尺寸常见做法是 256x256 起显存允许就用 384x384比如最小缺陷目标直径约 5mm视野 100mm在 256 分辨率上大约是 13 像素能稳住。# PyTorch 训练脚本核心片段 import torch import torch.nn as nn from torch.optim import AdamW from torch.optim.lr_scheduler import CosineAnnealingWarmRestarts model torchvision.models.resnet18(weightstorchvision.models.ResNet18_Weights.IMAGENET1K_V1) model.fc nn.Linear(model.fc.in_features, num_classes) # 工业数据类别不平衡非常常见用类别权重压低多数类 class_counts torch.tensor([1200, 80, 45, 300], dtypetorch.float) weights 1.0 / torch.sqrt(class_counts 1e-6) weights weights / weights.mean() criterion nn.CrossEntropyLoss(weightweights.to(device)) optimizer AdamW(model.parameters(), lr2e-4, weight_decay1e-4) # 带热重启的余弦退火避免陷入局部平缓区 # 工业数据量小固定步长调度很容易在后期震荡。 scheduler CosineAnnealingWarmRestarts(optimizer, T_010, T_mult2, eta_min1e-5) # EMA 平滑权重推理用滑动平均参数能压住单个 epoch 的抖动 ema_model torch.optim.swa_utils.AveragedModel(model, avg_fnema_avg)参数说明类别权重用 1/sqrt(count) 而不是经典的 1/count是因为缺陷类别样本本身就少权重拉太大模型会为了少数类过度敏感正常产品误报率飙升warmup 前 3 个 epoch 用 1e-4 预热后面再放开到 2e-4。工业小数据场景不建议上来就微调全网络冻结 backbone 前几层只调后面几层等 loss 不降了再解冻能明显减少过拟合。训练收敛后要在测试集上按缺陷类别逐个看准确率和召回率不能只看总 Acc。划痕这类容易判但脏污往往和灰尘混淆混淆矩阵里会明显堆在一起。这类问题时不要急着换模型先去看样本是不是脏污样本里的背景纹理和划痕样本太像或者标注本身就不干净。3.3 分割任务的 loss 组合与后处理不只是换 U-Net如果产线需要的是缺陷面积和精确轮廓做法通常从 U-Net 这套编解码结构起步编码器用 ResNet34 或更轻的 MobileNetV3。训练分割模型有四个关键点损失函数、mask 标注噪声容忍度、类别不均衡、预测概率图转二值 mask 的阈值。单一 Dice Loss 在缺陷面积很小时会不停抖动单一 CrossEntropy 在正常像素占 99% 的情况下会让模型“全输出背景”也拿到很低的 loss。我常用的组合是 0.7 * BCE 0.3 * Dice理由是两个 loss 的梯度尺度互补BCE 在类别不均衡时稳定回传Dice 把注意力聚焦到缺陷区域上。def bce_dice_loss(pred, target, bce_weight0.7): bce nn.functional.binary_cross_entropy_with_logits(pred, target) pred_sigmoid torch.sigmoid(pred) pred_flat pred_sigmoid.reshape(-1) target_flat target.reshape(-1) dice 1 - (2 * (pred_flat * target_flat).sum() 1) / (pred_flat.sum() target_flat.sum() 1) return bce_weight * bce (1 - bce_weight) * dice影响分割效果最隐蔽的参数是转 mask 的置信度阈值0.5 只是默认值现场缺陷对比度不同这个阈值一般要在验证集上以“面积误差最小”为目标重新搜索一遍。后处理里还有一个贯穿始终的动作——对小连通域按面积过滤。常见做法是面积小于 30 像素的连通域直接丢弃因为工业相机传感器噪声造成的假阳性热点通常不会超过 8-10 像素。面积阈值别设太大否则初期的小缺陷漏检率会明显上升。做完分割模型实际部署时还要把像素级 mask 换算成“缺陷类别 位置 面积占比”的结构化信息这才能对接产线的质量判定逻辑。这部分我放到第四章一起讲。4. 实时监控链路从相机触发到 PLC 动作的工程化方案4.1 采集与缓冲不让丢帧拖慢节拍深度学习模型跑得再快如果上游相机取流环节频繁丢帧检测结果就是不完整的。工业现场最常见的架构工业相机通过 GigE 或 USB3.0 接口接工控机软件触发或硬触发拍摄。这里有个常见的坑直接用相机 SDK 的回调函数去做预处理和推理会导致采集线程被阻塞帧率上不去而且触发信号来了相机缓存堆积内存占用持续上涨。我一般会在采集和推理中间加一个有界队列作为缓冲。队列长度按节拍时间估算比如产线节拍 80ms 一件相机帧率 15fps取流线程持续写入推理 worker 按固定节拍消费import queue import threading import cv2 capture_queue queue.Queue(maxsize8) def camera_thread(camera): while True: ok, frame camera.read() if not ok: continue try: # 队列满时直接丢最旧帧保证数据新鲜度 # 不要用 put(blockTrue)那样会把取流线程拖死。 capture_queue.put(frame, blockFalse) except queue.Full: try: capture_queue.get_nowait() capture_queue.put(frame, blockFalse) except queue.Empty: pass def inference_worker(): while True: frame capture_queue.get() # 预处理 推理 后处理丢帧策略里的选择逻辑值得展开要么丢最旧帧保证实时性要么丢最新帧保证处理对象完整但引入延迟。产线节拍在 100ms 左右时我选丢旧帧因为旧帧对应的产品可能已经流到下一工位判断结果没有执行价值。如果产线节拍紧张到 50ms 内则要考虑双相机交替采集或上更高性能的推理设备。4.2 推理服务与前后处理线程把 GPU 让给模型工业现场的工控机通常配置一块中端 GPU比如 NVIDIA RTX 4060 或 A2000 级别4K 输入图像做预处理也会消耗不小 CPU 资源。常见做法是让 CPU 线程做图像缩放、归一化、拷贝到 GPUGPU 只负责模型推理避免 CPU 和 GPU 在单线程里互相等待。这里有个细节cv2.resize 在 CPU 上对 4K 图缩放到 512x512 大约 3-5ms多次调用会积少成多所以建议预处理只做一次缩放不要再额外做滤波。推理框架上PyTorch 原生态推理虽然方便但工业部署要先转成 ONNX再用 TensorRT 加速如果用的是 NVIDIA GPU。转换时最容易踩的坑是动态输入尺寸生产现场的图像宽高在不同批次可能变化但如果你的模型只支持固定 512x512 输入就要在前处理强制 resize而不是试图让模型接受任意尺寸。import numpy as np import cv2 import onnxruntime as ort session ort.InferenceSession(model.onnx, providers[CUDAExecutionProvider]) def preprocess(frame): # 工业场景保留全图纹理信息使用固定缩放而不是中心裁剪 img cv2.resize(frame, (512, 512), interpolationcv2.INTER_AREA) img img[:, :, ::-1] # BGR - RGB img img.astype(np.float32) / 255.0 img (img - np.array([0.485, 0.456, 0.406])) / np.array([0.229, 0.224, 0.225]) return np.transpose(img, (2, 0, 1))[None, ...] onnx_input preprocess(frame) logits session.run(None, {input: onnx_input})[0] scores 1.0 / (1.0 np.exp(-logits)) # 二分类时转概率推理完成后的后处理环节不要忽略分类模型的输出要转成置信度和类别分割模型的输出要连通域分析算出缺陷数量、位置、面积。所有后处理应该输出结构化 JSON给上层的 MES 系统或 PLC 联动模块消费而不是在推理代码里直接改数据库让故障排查变得困难。4.3 良率统计与报警策略阈值不是一拍脑袋定的产线对识别结果的消费方式通常分两类一是统计用途——每个班次缺陷类型分布、良率趋势、缺陷面积分布二是自动控制——发现连续多个缺陷就停线或通知复检。后者要求缺陷检测系统具备“连续 N 件异常报警”和“单件严重缺陷立即停机”两种策略。工业界普遍认为单件报警阈值调得太灵敏会变成“狼来了”操作工会关掉报警。我一般这样设计报警逻辑每条缺陷记录先打上置信度分数控制逻辑只读取分数和级别不直接读取模型输出。级别分三档低置信度观察级仅统计、中置信度复检级触发机械臂抓取或人工复检、高置信度拦截级直接报警或停线。各档位阈值要放在配置文件里工控机上随时可调alarm_config: high_conf_threshold: 0.92 mid_conf_threshold: 0.80 consecutive_defect_count: 3 defect_area_limit: 250 # 缺陷面积超过该值立即拦截单位: 像素现场调阈值有个土办法连续跑 2 小时良品和 100 件已知缺陷样件统计置信度分布将阈值设在“良品置信度最高点”和“缺陷置信度最低点”的中间位置。如果两个分布区间重叠太多说明信息量不足不是单纯调阈值能解决的要回到数据采集和模型训练去补样本。5. 落地避坑我在产线上踩过的 6 个坑5.1 光照一变模型立刻“失聪”现象模型在实验室测试准确率 99%上线后前俩小时还行下午阳光从窗户照进来误报率从 0.5% 飙到 20%。原因训练数据都是上午的光照条件拍摄阳光带来的阴影改变了缺陷周围的纹理对比模型看到的特征分布整体漂移。解决在采集端加遮光罩同时把所有产线历史图像按时间段抽样加入训练集并保留 3.5 节的照明均匀性增强。现在我在所有项目里都会把光照当作独立变量做鲁棒性检查训练完成后专门找不同时间的样本去测。5.2 样本不均衡导致小缺陷被“无视”现象模型对划痕的召回率有 95%对凹陷的召回率只有 40%而凹陷才是客户最在意的问题。原因凹陷样本总数只有划痕的十分之一虽然加了类别权重但权重只解决了“分类边界”没有解决“特征学习不充分”——模型对凹陷的深层特征根本没学会。解决对凹陷样本做过采样和针对性增强模拟不同深度的阴影变化而不是只在 loss 上加权重。后续我还专门为小样本类别单独训了一个二分类复核模型。5.3 标注不一致同一个缺陷两个人标出三种结果现象分割模型训练完发现预测的缺陷轮廓总是“歪”到一侧而且边缘毛刺多。原因数据集是多个人标注的有人把缺陷边缘严格贴着过渡区标有人习惯多包一圈背景。模型在嘈杂的标注里取折中结果谁都学不像。解决制作标注规范文档让标注人员看基准图例训练前对 mask 做一次腐蚀/膨胀的规则化消除细碎差异。这个坑最费时间但最值得花时间。5.4 推理速度达标了但端到端节拍超了现象模型在 GPU 上推理 30ms但整个系统处理一件产品要 120ms产线节拍 80ms严重超时。原因时间都花在图像解码、预处理、结果上报这些环节上模型推理只是其中一环。解决把耗时拆开测找到瓶颈。大多情况是 JPEG 解码太慢换成 raw 图传输或零拷贝图像接口能省一半时间结果上报改成异步队列不要阻塞在主流程。5.5 换了批次材料误报集中爆发现象同一个型号产品换了一批原材料后正常品误报率从 0.3% 涨到 5%。原因新材料的表面纹理和旧材料差异大模型把“纹理不熟悉”当成了“异常”。解决新批次上线前用无监督异常检测模块先跑一遍统计特征分布或者从产线上抓 50 张正常品图像做快速测试发现误报过高就触发模型快速微调。5.6 INT8 量化把小缺陷给“量化”没了现象为了提速做了 INT8 量化速度确实快了一倍但 0.3mm 的浅划痕漏检率从 0.5% 涨到 8%。原因浅划痕在图像里本来就是低对比度边缘量化把梯度信息压掉了。解决对分割模型和细小缺陷分类器保留 FP16只对面积较大的粗分类模型用 INT8。如果你对延迟没有极端要求工业场景优先上 FP16别贪 INT8 的加速比。6. 让模型持续变好用的月度复盘机制含验收指标系统上线不是终点是数据飞轮的起点。我现在的习惯是每 30 天做一次复盘流程固定从产线系统里导出当月所有误检、漏检图像按类别重新标注注意这里要工艺人员和算法工程师一起确认很多“误检”其实是确有缺陷但质检标准没写然后归入训练集或专门难例集接着用旧模型跑一遍找出特征分布偏移最大的子集针对性做增广或微调。增量训练有个细节微调时学习率要比初始训练低一个数量级比如 2e-5训练 epoch 控制在 15-20并且在训练集里保留 60% 以上原始样本否则模型会“忘掉”老缺陷。每次增量训练完要用一套不变的回归测试集验证保证旧缺陷不会漏检反弹。这一点上翻过车有一次只加了新缺陷样本结果老划痕漏检率从 1% 涨到 6%。验收标准方面我常用的量化指标表如下给刚启动项目的人一个参考锚点具体数值要结合工艺要求和误判代价调整指标参考值说明缺陷漏检率≤ 0.5%以复检工位人工判定为基准正常品误报率≤ 1 次/小时超过会让操作工失去信任端到端处理时间小于产线节拍的 80%留出缓冲给抖动月度模型迭代周期≤ 5 个工作日数据收集到新模型验证完毕缺陷分类一致率≥ 95%与质检主管人工判级对比这套指标里最难追的是漏检率因为漏检的定义本身依赖人工复检。所以数据闭环里一定要有“定期抽检已放行产品”的动作否则漏检率永远是个黑匣子出了问题只能事后追责。最后说一个我自己的习惯也是踩过很多次坑后的教训任何模型上线前我都会要求算法同学写一页纸的模型行为说明写清楚“这个模型在哪些光照、哪些材质、哪些缺陷尺寸上没验证过”比写“准确率 99%”有用得多。自动质量控制的底线不是模型有多聪明而是我们清楚它在哪里会笨。希望帮到你。本文还有配套的精品资源点击获取