ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

工业缺陷检测中的图像分类实战:从数据到部署的完整方案

工业缺陷检测中的图像分类实战:从数据到部署的完整方案 简介基于图像分类的工业缺陷检测项目以视频讲解完整代码的形式覆盖从数据准备到模型训练、预测的全过程。资源面向工业质检工程师、自动化设备调试人员及图像识别人工智能学习者重点解决产品表面划痕、裂纹、异物、颜色偏差等缺陷的自动分类与识别问题。压缩包共1823个文件包含1802张BMP格式工业缺陷样本图像、6个MP4分阶段讲解视频、5个XML标注文件、4个Python训练/预测脚本及模型权重、配置文件等总大小约346MB按项目介绍、数据集介绍、自定义数据集、模型定义、定义训练过程、定义预测过程六个阶段清晰分目便于按需跳转和对照实践。已有86人学习下载项目不仅从原理上解释了CNN等图像分类模型的应用还通过自定义数据集环节指导用户按工业场景收集和标注图像配合实际调参、训练和评估流程可帮助学习者快速构建并部署可用的工业缺陷检测系统提升产线自动化质检效率。1. 工业缺陷检测为什么说图像分类够用很多工程师一提到工业缺陷检测第一反应就是上YOLO。但在实际产线里有大量需求只是“判断这块产品有没有缺陷”或者“把缺陷归成哪几类”这种场景用图像分类就够用了而且比目标检测稳定得多也更容易落地。这个标题打包的正是这样一套方案视频讲解负责把思路和调参逻辑讲清楚代码把数据加载、模型训练、验证和部署串成一条线。适合刚接手质检项目的工程师也适合手上有几千张样本、想快速搭一个分类模型评估效果的团队。下面按数据、模型、训练、避坑、部署的顺序把每个环节的工程细节和参数选择展开讲。2. 数据决定90%分类样本采集、标注与划分的工程细节2.1 先搞清楚什么场景该用分类什么场景该用检测图像分类解决的是“这张图属于哪一类”的问题。工业缺陷检测里最常见的分类任务是二分类正常/缺陷其次是按缺陷形态分类划伤、压痕、脏污、气泡等。适用条件有两个缺陷类别互斥且不需要给出位置。如果你关心的只是“这批产品能不能放行”分类就够了如果你需要告诉机械手“缺陷在右下角”那就应该去用目标检测。很多团队在这里没想清楚直接上检测模型标注成本翻了几倍效果还不一定更好。为什么分类方案在工业界比检测更稳这是血泪经验。目标检测要画框标注一致性很难保证同一个缺陷十个人能画出十种框分类只要把样本放进对应文件夹标注质量高得多。而且兼顾“在哪里”和“是什么”时模型复杂度上升训练和调参翻车概率也明显增加。所以先想清楚需求边界再决定技术路线。2.2 采集与标注用文件夹当标签按批次划分数据采集时必须固定工位。相机、镜头、光源和产品位姿在训练和推断时保持一致。很多项目死因不是模型而是采集环境变化训练时用实验室照片产线现场换成白光模型准确率立刻下滑。这属于数据问题不是算法问题后面怎么调参都救不回来。标注直接用目录结构做标签常见做法是data/ train/ ok/ scratch/ stain/ val/ ok/ scratch/ stain/ test/ ok/ scratch/ stain/代码说明PyTorch 的 ImageFolder 会按子目录字母顺序生成类别编号目录名就是语义标签训练代码里通过dataset.classes可以读出来。划分数据时有一个容易踩的坑不要随机划分。同一块产品被裁成多个 patch 时随机划分会让同一个产品同时出现在训练集和验证集里这叫数据泄露验证集准确率会虚高。正确做法是按“产品批次”划分一批产品要么全进训练集要么全进验证集。数量方面我一般要求每个类别最少 300 张能到 500 张以上最好。缺陷类不足时先补拍再考虑合成缺陷最后才是数据增强。对分类来说每类 100 张也能跑但泛化能力很差基本是在背训练集。还有一个容易被忽略的事标注定义的边界要写清楚。划伤和磨损如果肉眼都分不清建议直接合并成一个“表面损伤”类否则模型会学到标注员的随机噪声评测指标很好看落地就露馅。类别定义文档比代码本身还重要这是做过几个项目之后才真正理解的。2.3 样本量不够增强分两层在线增强与缺陷样本扩充训练时最常见的增强是翻转、旋转、颜色抖动。但工业图像和 ImageNet 里的自然图不一样背景固定缺陷可能只有几十个像素增强做过头会把“稀疏缺陷”增强成“满屏花纹”。我用下来比较稳的一套是train_tf transforms.Compose([ transforms.Resize((224, 224)), transforms.RandomHorizontalFlip(p0.5), transforms.RandomRotation(degrees15), transforms.ColorJitter(brightness0.3, contrast0.2, saturation0.2), ])说明这里故意不用RandomResizedCrop。随机裁剪和缩放会破坏缺陷的尺度特征让模型去学产品纹理而不是缺陷本身。旋转和颜色抖动模拟的是产品摆放角度和光源亮度波动这两个才是产线上最常见的干扰源。第二层增强是离线扩充。针对缺陷样本少的情况把每张缺陷图旋转 90、180、270 度加上水平翻转另存为新的样本。好处是训练集直观变大代码容易读也方便给客户验收缺点是有可能过拟合到固定的增强操作。想要更精细用 albumentations 做在线增强也可以但工业项目里离线增强的接受度往往更高。动手训练前我先做一次数据巡检检查项建议值原因每类图像分辨率是否一致一致或记录后统一缩放分辨率不一致会让模型学到尺寸特征亮度均值是否稳定类间差异小于 20%亮度差异大说明光源不稳定图片文件是否损坏逐个解码单张坏图会让训练中途崩溃这个巡检脚本十分钟能写完但能避免后面两天的无效训练。我见过最典型的问题某类全是 1920×1080另一类全是 800×600模型学到的是分辨率特征不是产品特征。3. 模型选择与预训练ResNet、EfficientNet、MobileNetV2 怎么挑3.1 输入尺寸与缺陷尺度的关系整图 224 还是切 patch分类模型的标准输入是 224×224但产线相机拍出来的图往往有几千像素。缺陷如果占图片面积还不到 1%直接把整图缩放到 224×224缺陷只剩两三个像素模型再怎么训练也学不到。常见做法是分两步走先用传统图像处理或固定坐标标定锁定缺陷可能出现的 ROI 区域再把 ROI 缩放进 224×224。如果缺陷位置随机就切成互相重叠的小 patch每个 patch 单独分类相当于把分类器当滑动窗口用。切 patch 的代码不复杂我一般这么写import numpy as np def extract_patches(image, patch_size224, overlap0.25): h, w image.shape[:2] stride int(patch_size * (1 - overlap)) patches, positions [], [] for y in range(0, h - patch_size 1, stride): for x in range(0, w - patch_size 1, stride): patches.append(image[y:y patch_size, x:x patch_size]) positions.append((x, y)) return patches, positions代码说明overlap让相邻 patch 之间保留重叠区域避免缺陷正好落在切缝上。步长等于patch_size * (1 - overlap)重叠率 0.25 时步长是 168 像素。整图推理时把每个 patch 的分类概率做平均或取最大再判断整图是否含缺陷效果比单张缩放好很多。判断该不该切 patch有一个直观标准把原图缩成 224×224你作为人肉能不能看出缺陷如果看不出必须切。切 patch 后单张图推理次数增加了但每次推理的像素总量反而少了因为跳过了大量背景实际部署时这是一个值得做的权衡。3.2 模型对比表与选型结论从 ResNet18 到 EfficientNet-B0工业部署里最常被比较的是下面几类模型参数量ImageNet Top-1参考CPU 推理耗时224x224适合场景ResNet1811.7M69.8%约 10ms 级小数据、快速验证ResNet5025.6M76.1%约 20ms 级准确率优先且有 GPUMobileNetV23.5M72.2%约 5ms 级边缘盒子、低算力EfficientNet-B05.3M77.1%约 8ms 级数据量较多时取精度数值都是公开 ImageNet 上的参考值不直接代表你的缺陷数据集。真正的差距要在自己的数据上跑出来但选型方向可以参考。我的结论是只有几百张样本时ResNet18 做迁移学习比 EfficientNet 稳因为参数少不容易把训练集的噪声背下来。算力只有 CPU 边缘盒时MobileNetV2 是综合成本最低的选择这也是很多项目里能看到 mobilenetv2 代码示例的原因。GPU 充足、类别又多先跑 EfficientNet-B0训练量不大性价比高。顺带提一句最新的图像分类模型如 ViT 在小数据上并不占优工业场景里的图像分类算法黄金组合依然是“CNN 加迁移学习”。不要为了追新把项目变成论文复现。如果缺陷样本真的少到补无可补还有人会去复现 PatchCore 这类无监督方案这是另一条技术路线它更接近特征提取加距离判断和图像分类的训练流程完全不同先别混着用。3.3 迁移学习固定套路先冻结后解冻学习率分层用 ImageNet 预训练权重是分类任务的默认动作。有人会怀疑工业图和自然图差异太大预训练权重没用实际上第一层卷积学到的边缘、纹理、颜色斑块是通用的转移过来能省大量训练时间。完全从零训练在小样本上会非常慢准确率也常常连预训练微调的一半都达不到。标准操作分两个阶段。第一阶段冻结 backbone只训练最后的全连接层让分类头先适应新类别学习率可以给到 1e-3。第二阶段解冻 backbone用 1e-4 到 3e-4 的学习率整体微调。注意第二阶段学习率一定要比第一阶段低不然会把预训练权重冲掉模型反而变笨。如果数据量中等但类别不均衡损失函数里加类别权重如果数据量很小每类只有一百张左右冻结阶段多训几个 epoch再用更小的学习率解冻。这套流程是行业内的标准做法只要你做的项目是图像分类必然绕不开。关于预训练权重加载直接用 torchvision 的 weights 参数最省事代码会按需自动下载。内网部署时提前把权重文件放到本地缓存目录避免产线服务器联网失败卡住。这一点在项目环境里很容易被忽略但遇到过一次就会长记性。3.4 混淆矩阵与评估指标别只盯着 accuracy分类模型的 accuracy 在缺陷检测里极具迷惑性。假设正常产品占 95%缺陷只占 5%模型把所有图都判成正常accuracy 也有 95%但上线后全是漏检。缺陷检测更关心漏检率也就是假阴性而且不同缺陷的漏检代价不一样气泡漏检和划伤漏检通常不能等价比较。所以在验证阶段要打印混淆矩阵和 precision/recall。召回率最低的类别就是模型最容易漏的类别这个信息直接影响后面阈值怎么调。from sklearn.metrics import confusion_matrix, classification_report y_true [] # 从验证集推理时收集真实标签 y_pred [] # 从验证集推理时收集预测标签 print(confusion_matrix(y_true, y_pred)) print(classification_report(y_true, y_pred, target_namesclass_names))代码说明classification_report会输出每个类别的精确率、召回率和 F1。正常类召回率高但缺陷类召回率低说明模型在牺牲少数类换取整体准确率下一步要做的是类别加权或阈值调整而不是换更强的网络。4. 用 PyTorch 跑通训练增强、损失函数与四个关键超参数4.1 用 ImageFolder 加载数据和增强 pipeline数据准备好了训练端直接用 PyTorch 的 ImageFolder目录结构就是标签不需要额外写 CSV。增强 pipeline 这样配from torchvision import datasets, transforms from torch.utils.data import DataLoader train_transform transforms.Compose([ transforms.Resize((224, 224)), transforms.RandomHorizontalFlip(p0.5), transforms.RandomRotation(degrees15), transforms.ColorJitter(brightness0.3, contrast0.2), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]), ]) val_transform transforms.Compose([ transforms.Resize((224, 224)), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]), ]) train_dataset datasets.ImageFolder(data/train, train_transform) val_dataset datasets.ImageFolder(data/val, val_transform) train_loader DataLoader(train_dataset, batch_size32, shuffleTrue, num_workers4, pin_memoryTrue) val_loader DataLoader(val_dataset, batch_size32, shuffleFalse, num_workers4, pin_memoryTrue)代码说明ImageFolder 会根据子目录名字母序生成标签比如scratch是 0stain是 1。Normalize 的均值方差用的是 ImageNet 统计量训练和推理必须保持一致否则颜色分布被破坏模型效果直接跳水。DataLoader 参数里num_workers4让 CPU 用多个进程做图片解码pin_memoryTrue减少主机到 GPU 的拷贝时间。这两个参数对训练速度影响很大很多人训练慢就是卡在数据加载而不是 GPU。如果缺陷样本非常少不要用shuffleTrue换成权重采样from torch.utils.data import WeightedRandomSampler class_counts torch.bincount(torch.tensor(train_dataset.targets)) class_weights 1.0 / class_counts.float() sample_weights class_weights[torch.tensor(train_dataset.targets)] sampler WeightedRandomSampler(sample_weights, num_sampleslen(sample_weights), replacementTrue) train_loader DataLoader(train_dataset, batch_size32, samplersampler, num_workers4, pin_memoryTrue)代码说明sample_weights按照每个类别的样本数倒数计算正常类权重低缺陷类权重高。每次迭代采样时网络更大概率抽到缺陷图比手动给 loss 加权更平滑缺点是同一张缺陷图可能在一个 epoch 里重复出现所以训练 epoch 数要适当减小。参数推荐值说明batch_size32 - 64太小 loss 震荡太大显存受限num_workers4 - 8解码和增强交给 CPU别让 GPU 空等pin_memoryTrue减少数据传输时间训练提速learning rate1e-4 - 3e-3从头训练用 1e-3迁移学习用 1e-44.2 训练循环CrossEntropy 与 AdamW 的关键搭配以 MobileNetV2 为例完整训练循环这样写import torch import torch.nn as nn import torch.optim as optim from torch.optim.lr_scheduler import CosineAnnealingLR import torchvision.models as models num_classes len(train_dataset.classes) model models.mobilenet_v2(weightsmodels.MobileNet_V2_Weights.IMAGENET1K_V1) model.classifier[1] nn.Linear(model.last_channel, num_classes) model model.cuda() criterion nn.CrossEntropyLoss() optimizer optim.AdamW(model.parameters(), lr1e-4, weight_decay1e-4) scheduler CosineAnnealingLR(optimizer, T_max30) best_acc 0 for epoch in range(30): model.train() train_loss 0.0 for images, labels in train_loader: images, labels images.cuda(), labels.cuda() optimizer.zero_grad() outputs model(images) loss criterion(outputs, labels) loss.backward() optimizer.step() train_loss loss.item() * labels.size(0) model.eval() correct total 0 with torch.no_grad(): for images, labels in val_loader: images, labels images.cuda(), labels.cuda() outputs model(images) preds torch.argmax(outputs, dim1) correct (preds labels).sum().item() total labels.size(0) acc correct / total if acc best_acc: best_acc acc torch.save(model.state_dict(), best_model.pth) print(fepoch{epoch} train_loss{train_loss / len(train_loader.dataset):.4f} valid_acc{acc:.4f})代码说明CrossEntropyLoss 内部自带 softmax所以模型最后不需要再接 Softmax 层。AdamW 比 Adam 更适合做权重衰减weight_decay1e-4相当于给大权重视惩罚能有效压过拟合。CosineAnnealingLR 的T_max30和总 epoch 数保持一致学习率从 1e-4 平滑降到接近 0。best_acc只保存验证集上最好的权重而不是最后一个 epoch 的权重。最后一步加载最佳权重做进一步评估model models.mobilenet_v2(weightsNone) model.classifier[1] nn.Linear(model.last_channel, num_classes) model.load_state_dict(torch.load(best_model.pth))代码说明保存 state_dict 后加载时必须先构造相同结构再 load_state_dict。这里weightsNone没问题因为真正要用的权重是从 best_model.pth 里读进来的。4.3 只有几百张样本冻结 backbone 与早停样本量不足时第一步先把 backbone 冻结for name, param in model.features.named_parameters(): param.requires_grad False optimizer optim.AdamW(model.classifier.parameters(), lr1e-3)代码说明model.features是 MobileNetV2 的 backbone冻结之后只有分类头更新。这里学习率可以给到 1e-3因为分类头是随机初始化需要学得快一点。训练三到五个 epoch 后解冻全部层for name, param in model.features.named_parameters(): param.requires_grad True optimizer optim.AdamW(model.parameters(), lr1e-4)代码说明解冻后整个网络一起微调学习率必须降到 1e-4用来精调预训练特征而不是推翻重学。早停可以直接看验证集 loss连续五个 epoch 不下降就停。还有一个便宜好用的技巧nn.CrossEntropyLoss(label_smoothing0.1)标签平滑让模型不要对训练集过于自信对小样本场景有实打实的好处。4.4 用 loss 曲线看一眼模型到底在学什么训练过程中把每个 epoch 的 train_loss 和 val_acc 记下来画成曲线。常见情况有几种train_loss 一直降val_acc 先升后降这是过拟合早停或加强增强train_loss 和 val_acc 都降得慢学习率太低train_loss 震荡很厉害batch_size 太小或者学习率太高。特征图也是一个好帮手。MobileNetV2 的model.features输出的是最后一层特征图直接取通道均值可视化能看到模型注意力是落在缺陷上还是落在背景纹理上。这个步骤一开始看起来有点像玄学但真的能帮你判断增强策略是否有效。5. 避坑笔记分类做缺陷检测的 5 个典型问题与排查这些坑是我在项目里反复遇到过的现象、原因、解决一套写清楚遇到类似症状可以直接对照。5.1 现象val 准确率 97%产线误杀率还是高原因训练集和产线的光照、工业相机型号、镜头光圈不一致模型学到的是室内灯光下的颜色分布不是真正的缺陷特征。很多团队遇到这个情况会反复调模型但问题在数据分布不在模型结构。解决从产线抽 200 张真实缺陷图和 200 张正常图单独固定成一个“野外测试集”不参与训练和调参只在最后做一次测试。只要这个测试集准确率不达标就不要改模型先回产线补数据、调整光源位置再做增强。5.2 现象模型把所有东西都判成“无缺陷”原因正常样本数量远大于缺陷样本CrossEntropy 的梯度被多数类主导网络发现全预测成正常类之后 loss 已经很低。表面上看 val 准确率很高其实正常类占 95% 的时候闭眼猜都能有 95% 准确率。解决使用 WeightedRandomSampler或者直接在 loss 里加权重class_counts torch.bincount(torch.tensor(train_dataset.targets)) weight class_counts.float().min() / class_counts.float() criterion nn.CrossEntropyLoss(weightweight.cuda())代码说明这里 weight 让样本少的类别获得更大损失权重。比如正常类 5000 张、缺陷类 500 张正常类权重是 0.1缺陷类权重是 1.0模型每犯一次缺陷类错误都要付出更大代价。权重差超过 10 倍时容易训练震荡需要配合学习率下调。5.3 现象运行代码提示“找不到 msvcp140.dll 无法继续执行代码是什么原因”原因Windows 环境缺少 Microsoft Visual C Redistributable很多编译好的 Python 依赖包需要这个运行库最常见的就是 opencv-python、pycocotools。解决安装 Microsoft Visual C Redistributable 2015-2022 x64装完重启终端。如果是 conda 环境直接用conda install -c conda-forge opencv也能绕开一部分运行库问题。从 zip 包开始跑项目的人十个里至少有三个卡在这里。5.4 现象GPU 利用率只有 40%推理延迟不达标原因模型 forward 太快瓶颈在数据加载和预处理。单张图推理时 batch_size1GPU 大部分时间在等待 CPU 把图准备好这是把训练时的坏习惯带到推理阶段造成的。解决先量化预处理耗时。把图片解码、Resize、Normalize 放到多进程里做推理阶段再把多张图拼成一个 batch一次前向处理 4 到 8 张。如果 CPU 单线程推理已经达标就不要为了 GPU 硬上 TensorRT避免把系统搞成一个小黑匣子出了问题难排查。5.5 现象一个产品上同时出现两种缺陷分类模型必然翻车原因分类模型假设每张图只有一个标签。产线里划伤和脏污同时出现很常见标注时你只能选一个主缺陷模型推理时也只能输出一个另一个漏检就会被算成误判。解决先统计真实场景里同一张图出现多缺陷的比例超过 5% 就不要用单标签分类。切到多标签分类输出层用 Sigmoid 加 BinaryCrossEntropy如果还需要位置信息直接切目标检测。这个决策要在项目一开始做进设计文档别等模型上线了才发现标签体系不合理。这些问题的共同特点是模型结构和训练代码基本不用大改修改数据分布和数据处理逻辑才是关键。指标不对的时候先别急着换网络结构把上面 5 条逐一核对。6. 部署三板斧ONNX 导出、阈值校准与流水线验证分类模型训练完模型文件只是第一步。真正上线前我会做三件事导出 ONNX、校准 softmax 阈值、按批次做穿越测试。先用 ONNX 固定推理框架model.cpu().eval() dummy torch.randn(1, 3, 224, 224) torch.onnx.export( model, dummy, defect.onnx, input_names[input], output_names[prob], dynamic_axes{input: {0: batch}, prob: {0: batch}}, opset_version12, )代码说明dynamic_axes让 batch 维可变产线上一次可以塞多张图。导出后先用 onnxruntime 跑一遍单张图和 PyTorch 输出对比误差小于 1e-5 再使用。加载推理import onnxruntime as ort sess ort.InferenceSession(defect.onnx, providers[CPUExecutionProvider]) probs sess.run([prob], {input: np.float32(x)})[0]代码说明CPU 推理用CPUExecutionProvider有 GPU 就换成CUDAExecutionProvider。注意输入的数值类型必须是 float32归一化参数要和训练时一致。第二步是阈值校准。分类模型输出的概率不能默认按 0.5 切。缺陷检测的漏检代价远高于误杀代价所以阈值应该向召回率倾斜from sklearn.metrics import precision_recall_curve valid recall 0.995 threshold thresholds[valid].max()代码说明先要求召回率不低于 99.5%再在满足条件的阈值里取最大值目的是保证漏检率可控的前提下把误杀压到最低。这个阈值会直接写进产线检测配置。第三步是批次穿越测试。按产品批次顺序取连续 10 批数据模拟真实生产顺序跑一遍看误检是不是集中在某一批。我最早用这个办法抓到过一次光源老化导致的亮度整体下降训练集完全没覆盖后来在增强里补了亮度抖动才解决。这个习惯让我明白了一件事模型的泛化能力不只是靠结构更靠数据分布里有没有覆盖现场的变化。希望帮到你。本文还有配套的精品资源点击获取
返回列表