ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

660张三星油污缺陷图:头发丝与小黑点分类实战与调优

660张三星油污缺陷图:头发丝与小黑点分类实战与调优 简介本资源为三星油污缺陷检测数据集聚焦头发丝TFS与小黑点XZW两类典型缺陷面向工业质检方向的算法工程师、深度学习研究者及高校相关专业学生可用于缺陷分类、目标检测模型的训练与验证帮助解决油污场景下细粒度缺陷样本不足的问题。压缩包共1324个文件包含660张jpg图像与660个同名xml标注文件另有4个txt说明文件整体约314.54MB图像与标注一一对应便于直接接入YOLO、Faster R-CNN等主流框架进行训练。目前已有1412人学习下载具备一定参考热度。数据集覆盖normal、tfs2等多种命名样本类别划分明确可直接用于模型微调、数据增强实验与检测精度对比配合博客中的优化思路能帮助读者快速搭建基线、排查漏检误检问题适合作为工业缺陷检测项目的实战数据支撑。1. 660 张三星油污缺陷图里头发丝和小黑点为什么总被模型搞混产线上做外观质检的兄弟大概率都遇到过这个场景一批手机盖板或屏幕模组过完 AOI 设备算法把「头发丝」判成了「小黑点」或者反过来复判工位一天要手动改几百张。你手里拿到的正是这样一个典型数据集——三星油污缺陷类别核心就两类头发丝和小黑点总共 660 张。数量不大类别又极度相似这正是很多工业缺陷检测项目最真实的起点。这个标题背后要解决的问题很具体在样本量只有几百张、两类缺陷在灰度图上高度接近的前提下怎么把分类或检测模型做到产线可用。它适合两类人一是刚接手小样本工业质检、想快速跑通 baseline 的算法工程师二是手里有类似数据集、纠结要不要上深度学习、怎么标注和增强的现场技术人员。660 张不算多但也绝不是不能做关键看你把力气花在哪——是盲目堆模型还是先把数据本身的坑填平。下面我按自己踩过的路子把选型、实现、参数和翻车点讲清楚。2. 先搞清楚头发丝和小黑点在像素层面差在哪2.1 两类缺陷的形态差异与混淆根源头发丝和小黑点放在一起做分类最反直觉的一点是它们在低分辨率下几乎无法区分。头发丝是细长的曲线结构宽度往往只有几个像素方向随机小黑点则是近似圆形或椭圆形的团块面积小但灰度更集中。问题出在成像环节——如果光源是环形光加同轴光头发丝边缘会产生高光中心偏暗看起来就像一串断续的小黑点而某些油污形成的小黑点边缘发虚拉长后又有几分像短头发丝。我一般会先做一件事把 660 张图按缺陷外接框的宽高比统计一遍。头发丝的宽高比通常大于 3:1小黑点大多在 0.5:1 到 2:1 之间。这个统计不用写复杂脚本用标注文件里的 bbox 就能算。如果发现两类宽高比分布重叠严重说明单靠形状特征不够必须引入纹理或灰度梯度特征。这一步决定了你后面是走分类路线还是检测路线——如果缺陷位置固定、每张图只有一个目标分类就够如果一张图里可能同时出现多个缺陷必须上检测。2.2 660 张的分布决定了你该怎么切分660 张这个量级最忌讳的是随机切分。工业图像往往来自连续采集相邻帧高度相似随机切分会导致训练集和验证集里出现几乎一样的图验证指标虚高上线就翻车。我的习惯是按采集批次或时间顺序切前 70% 做训练中间 15% 做验证最后 15% 做测试。如果不知道采集顺序至少按文件名的序号切不要用随机种子打乱。另外要确认两类各多少张。如果头发丝 500 张、小黑点 160 张这就是典型的不平衡。不平衡本身不可怕可怕的是你用了 accuracy 当指标。后面我会讲怎么用 F1 和混淆矩阵来看真实表现。先记住一个数每类至少留出 30 张做测试低于这个数指标波动会大到没有参考意义。2.3 标注质量比模型结构更影响最终精度我见过太多项目模型换了一轮又一轮最后发现是标注框把头发丝的两端截断了。头发丝标注必须包住整条曲线包括两端渐隐的部分小黑点标注则要贴紧边缘不要留太多背景。如果标注规范不统一模型学到的就是标注员的习惯而不是缺陷本身。建议在动手训练前抽 50 张图做一次标注复核重点看两类缺陷的边界是否一致。有条件的话让两个人独立标同一批图算一下 IoU 一致性低于 0.7 就说明规范需要重新对齐。这一步花半天能省后面几天调参的时间。3. 用 PyTorch 搭一个能跑通 660 张的最小分类基线3.1 数据加载与增强别一上来就上重增强660 张图做分类数据增强是必须的但方向要对。头发丝的方向随机所以随机旋转、水平垂直翻转是安全的小黑点旋转后还是小黑点也没问题。但颜色抖动要慎用因为油污缺陷的灰度对比是重要特征大幅改变亮度对比度会让两类更混淆。我一般只用轻度亮度调整幅度控制在 0.1 以内。下面是一个基于torchvision的数据加载示例假设你的数据按train/头发丝、train/小黑点这样的文件夹组织import torch from torch.utils.data import DataLoader from torchvision import datasets, transforms # 训练增强几何变换为主灰度扰动为辅 train_tf transforms.Compose([ transforms.Resize((224, 224)), # 统一尺寸便于批量训练 transforms.RandomHorizontalFlip(p0.5), # 水平翻转头发丝方向随机安全 transforms.RandomVerticalFlip(p0.3), # 垂直翻转概率略低 transforms.RandomRotation(degrees15), # 小角度旋转避免引入黑边 transforms.ColorJitter(brightness0.1, contrast0.1), # 轻度灰度扰动 transforms.ToTensor(), transforms.Normalize(mean[0.5], std[0.5]) # 灰度图单通道归一化 ]) # 验证/测试只做尺寸统一和归一化 val_tf transforms.Compose([ transforms.Resize((224, 224)), transforms.ToTensor(), transforms.Normalize(mean[0.5], std[0.5]) ]) train_ds datasets.ImageFolder(data/train, transformtrain_tf) val_ds datasets.ImageFolder(data/val, transformval_tf) train_loader DataLoader(train_ds, batch_size16, shuffleTrue, num_workers2) val_loader DataLoader(val_ds, batch_size16, shuffleFalse, num_workers2) print(train_ds.class_to_idx) # 确认类别索引映射这段代码的关键参数有三个。Resize((224, 224))是因为后面用的骨干网络通常接受 224 输入如果你用更小的网络可以改成 128但不要小于 96否则头发丝会细到看不见。RandomRotation(degrees15)角度不要开大超过 30 度会引入大量黑色填充区域反而干扰训练。batch_size16是针对 660 张的量级太大容易过拟合太小梯度不稳16 或 32 都可以试。3.2 选 ResNet18 还是自己搭小网络660 张图我不建议上来就上 ResNet50。参数越多过拟合风险越大。ResNet18 配合预训练权重是性价比最高的起点。如果你没有 ImageNet 预训练权重或者环境不允许下载那就自己搭一个 4 层卷积加全局平均池化的小网络参数量控制在 50 万以内。用预训练模型时要注意第一层卷积默认是 3 通道输入而油污图往往是灰度图。两种改法一是把灰度图复制成 3 通道二是把第一层卷积改成单通道并重新初始化。我一般选第一种改动最小预训练权重的其他部分都能复用。import torch.nn as nn from torchvision import models # 方案一灰度图复制为3通道复用预训练权重 model models.resnet18(pretrainedTrue) model.fc nn.Linear(model.fc.in_features, 2) # 两类输出 # 方案二如果必须单通道输入改第一层 # model models.resnet18(pretrainedTrue) # model.conv1 nn.Conv2d(1, 64, kernel_size7, stride2, padding3, biasFalse) # model.fc nn.Linear(model.fc.in_features, 2) device torch.device(cuda if torch.cuda.is_available() else cpu) model model.to(device)model.fc换成 2 输出是必须的因为你的类别就两类。如果后面要加类别改这个数字就行。学习率方面预训练部分用 1e-4新加的全连接层用 1e-3这种分层学习率在小样本上很管用。优化器选 AdamW权重衰减设 1e-4能明显抑制过拟合。3.3 训练循环里必须盯住的三个数训练循环本身不复杂但小样本训练有三个数必须每轮记录训练损失、验证损失、验证集上的 F1 分数。只看准确率会被不平衡数据骗。如果某一类只有 50 张全预测成另一类也有 80% 以上的准确率但 F1 会暴露真相。import torch.optim as optim from sklearn.metrics import f1_score criterion nn.CrossEntropyLoss() # 分层学习率骨干网络小分类头大 optimizer optim.AdamW([ {params: model.conv1.parameters(), lr: 1e-4}, {params: model.layer1.parameters(), lr: 1e-4}, {params: model.layer2.parameters(), lr: 1e-4}, {params: model.layer3.parameters(), lr: 1e-4}, {params: model.layer4.parameters(), lr: 1e-4}, {params: model.fc.parameters(), lr: 1e-3} ], weight_decay1e-4) best_f1 0.0 for epoch in range(30): model.train() for imgs, labels in train_loader: imgs, labels imgs.to(device), labels.to(device) optimizer.zero_grad() outputs model(imgs) loss criterion(outputs, labels) loss.backward() optimizer.step() # 验证阶段 model.eval() all_preds, all_labels [], [] with torch.no_grad(): for imgs, labels in val_loader: imgs, labels imgs.to(device), labels.to(device) outputs model(imgs) preds outputs.argmax(dim1) all_preds.extend(preds.cpu().numpy()) all_labels.extend(labels.cpu().numpy()) f1 f1_score(all_labels, all_preds, averagemacro) print(fEpoch {epoch}, Val F1: {f1:.4f}) # 保存最佳模型只认F1不认loss if f1 best_f1: best_f1 f1 torch.save(model.state_dict(), best_model.pth)这段代码里averagemacro是关键它让两类权重相同不会被多数类带偏。保存模型时只认 F1不认验证损失因为损失低不代表分类边界对。30 个 epoch 是针对 660 张的保守设置如果验证 F1 在 15 轮后就不涨了可以提前停。4. 小样本下把 F1 从 0.7 拉到 0.9 的四个动作4.1 用混淆矩阵定位到底是哪一类在拖后腿F1 是宏观指标它不告诉你错在哪。每次验证后打印混淆矩阵你会看到是头发丝被误判成小黑点还是反过来。如果是头发丝误判多说明模型没学到细长结构这时候要检查输入分辨率是不是太低或者旋转增强是不是把头发丝转成了近似团块。如果是小黑点误判多往往是标注框太大把周围油污背景也框进去了。我一般会在验证集上把误判样本单独存出来人工看一遍。十有八九能发现标注问题或成像问题而不是模型问题。这个动作花 20 分钟比调一天参有用。4.2 针对头发丝做方向敏感的增强头发丝的核心特征是方向性。普通的随机旋转虽然增加了样本多样性但也可能把一条清晰的头发丝转成模糊的斜线。更好的做法是限制旋转角度在 ±30 度以内同时加入随机裁剪让模型学会关注局部曲线而不是整图布局。另外可以试一个技巧把图像转成灰度后做 Sobel 边缘检测把边缘图作为额外通道拼进去。这样输入变成 2 通道模型能直接看到梯度信息对细长结构更敏感。代价是数据加载要改但效果在头发丝这类缺陷上通常有提升。4.3 用类别权重和阈值调整处理不平衡如果两类数量差超过 3:1在损失函数里加类别权重是最直接的办法。CrossEntropyLoss(weighttorch.tensor([1.0, 3.0]))这种写法能让少数类获得更大梯度。权重怎么定我一般用反频率但不会完全按比例否则少数类会被过度放大导致另一类崩掉。通常取反频率的平方根再手动微调。还有一个更细的手段调整分类阈值。模型输出的是两类概率默认取 0.5 为界。如果头发丝召回低可以把头发丝的判定阈值降到 0.4牺牲一点精确率换召回。这个阈值要在验证集上扫一遍找到 F1 最高的点然后固定下来用于测试。4.4 交叉验证代替单次切分660 张做单次切分验证集只有 100 张左右指标波动大。如果条件允许做 5 折交叉验证每折都留出独立的验证集最后看平均 F1 和标准差。标准差大于 0.05 说明模型不稳定这时候不要急着上线先检查数据里有没有异常样本。交叉验证的代价是训练时间翻 5 倍但 660 张图用 ResNet18 在单卡上跑一折也就几分钟总体可控。我通常会在项目初期做一次 5 折确认模型稳定性后再用全量数据训练最终模型。5. 避坑这五类翻车现场我几乎每次都遇到5.1 验证集 F1 很高上线后一塌糊涂现象验证集 F1 到 0.95产线测试只有 0.6。原因训练集和验证集来自同一批次图像高度相似模型记住了背景而不是缺陷。解决按采集时间或批次切分确保验证集和训练集来自不同时间段。如果做不到至少在验证集里混入不同光照条件的图。5.2 头发丝被大量漏检现象混淆矩阵显示头发丝召回率只有 0.5。原因输入分辨率太低头发丝在 224 图上只有 1 到 2 个像素宽卷积后特征消失。解决把输入尺寸提到 320 或 448或者改用检测模型在更高分辨率特征图上做预测。分类模型对细长小目标天然不友好这是结构决定的。5.3 训练损失降到接近零验证损失开始上升现象训练 20 轮后训练 loss 0.01验证 loss 0.8。原因过拟合。660 张图配 ResNet18 已经接近容量上限。解决加 dropout、加权重衰减、减少全连接层宽度或者直接换更小的网络。早停也是必须的验证 F1 连续 5 轮不涨就停。5.4 标注框把两类缺陷都框进去了现象一张图里既有头发丝又有小黑点标注时只标了一个框。原因标注规范没考虑多缺陷共存。解决如果一张图里可能同时出现两类分类路线就不适用了必须上检测。检测模型能输出多个框和对应类别不会强迫你二选一。660 张做检测偏少但可以用分类模型做预筛再人工复判。5.5 灰度归一化参数用错现象模型训练不收敛loss 震荡。原因用了 ImageNet 的 RGB 均值方差去归一化灰度图通道数对不上。解决灰度图归一化用单通道 mean0.5, std0.5或者先统计你自己数据集的均值和方差。这个坑很隐蔽但一旦踩了训练曲线会非常难看。6. 用 TTA 和阈值扫描把最后 3 个点抠出来前面五章把数据、模型、训练和坑都过了一遍最后一章讲一个我每次都会用的收尾技巧测试时增强TTA加阈值扫描。这两个手段不改变模型结构也不增加训练成本但在 660 张这种小样本场景下经常能把 F1 从 0.87 推到 0.90 以上。TTA 的做法很简单对同一张测试图做多次变换比如原图、水平翻转、垂直翻转、小角度旋转分别推理后把 softmax 概率平均再取 argmax。代码上就是在验证循环里套一层变换def tta_predict(model, img, device): 对单张图做4种变换平均概率 model.eval() transforms_list [ lambda x: x, # 原图 lambda x: torch.flip(x, dims[3]), # 水平翻转 lambda x: torch.flip(x, dims[2]), # 垂直翻转 lambda x: torch.rot90(x, 1, dims[2, 3]) # 旋转90度 ] probs [] with torch.no_grad(): for tf in transforms_list: aug tf(img).to(device) out model(aug) probs.append(torch.softmax(out, dim1)) return torch.stack(probs).mean(dim0) # 平均后返回这段代码里torch.flip和torch.rot90都是张量操作不依赖额外库。四种变换覆盖了头发丝的主要方向变化平均后能抵消单次推理的随机误差。注意旋转 90 度对小黑点也安全因为圆点旋转后不变。如果你担心旋转引入黑边可以只保留翻转。阈值扫描是另一个动作。模型输出两类概率后默认取较大者为预测类。但如果头发丝召回低可以手动设一个阈值当头发丝概率大于 0.4 时就判为头发丝而不是等它超过 0.5。这个阈值要在验证集上扫从 0.3 到 0.7步长 0.05找 macro F1 最高的点。扫完之后把阈值固定再用测试集验证一次。注意不要在测试集上扫阈值那样指标会虚高。还有一个我自己的习惯每次项目收尾把验证集里所有误判样本按置信度排序看置信度最低的那 20 张。如果这 20 张里超过一半是标注模糊或成像异常的说明模型已经学到边界了剩下的问题在数据不在模型。这时候继续调参收益很低不如把这几张图重新标一遍或者直接剔除。这个习惯帮我省了很多无效调参的时间。最后说一句660 张做两类油污缺陷分类能做到 0.9 左右的 F1 已经可以上线试运行了。但上线后一定要留一个复判通道让产线工人能快速纠正误判这些纠正数据攒起来下一轮迭代就是免费的增量样本。希望帮到你。本文还有配套的精品资源点击获取
返回列表