简介本资源为三星油污缺陷检测数据集聚焦头发丝TFS与小黑点XZW两类典型缺陷面向从事工业质检、表面缺陷识别与深度学习目标检测的开发者及算法学习者可用于模型训练、精度调优与算法对比实验。压缩包共1324个文件以660张jpg图像与660个同名xml标注文件为主另有4个txt说明文件整体约314.54MB图像与标注一一对应便于直接接入YOLO、Faster R-CNN等主流检测框架。目前已有1412人学习下载具备一定参考热度。数据集覆盖正常样本与两类缺陷样本命名规范、类别清晰适合用于数据增强、类别不平衡处理及检测精度提升的实践验证也可作为博客优化检测精度思路的配套数据支撑帮助读者快速复现实验并排查漏检误检问题。1. 三星油污缺陷类别头发丝和小黑点660张数据集的真实门槛产线上做屏幕外观检测的兄弟大概率遇到过这个场景同一片玻璃盖板A 班判定为“头发丝”不良B 班复判却放行了理由是“那是个小黑点不是线状异物”。争议的根源不在人而在缺陷类别本身的边界模糊——头发丝是线状、有方向、长宽比大小黑点是点状、无方向、面积小。两者在低分辨率图像里可能只差几个像素而三星这类客户对油污缺陷的判定标准又卡得极严。660 张这个量级放在工业缺陷检测里属于典型的“小样本起步集”够跑通一个基线但远不够直接上产线。这篇文章要讲的就是拿到这 660 张标注了头发丝和小黑点的数据后怎么把它用出最大价值——从数据清洗、类别定义、增强策略到模型选型和阈值调参每一步都给出可复现的做法和参数。适合正在做屏幕外观质检、小样本缺陷分类、或者准备接类似三星标准项目的工程师。2. 头发丝和小黑点为什么不能混成一个“油污”类2.1 两类缺陷的物理成因与成像差异头发丝类缺陷在屏幕盖板产线上通常来自无尘布纤维脱落、传送带毛屑、或者清洗槽过滤失效。它的物理形态是长条形宽度往往在 1 到 3 个像素长度可能跨 20 到 200 个像素方向随机。在环形光源下线状异物会产生明显的方向性反光灰度梯度沿长度方向变化平缓沿宽度方向变化剧烈。小黑点则更多来自油墨残留、环境落尘、或者镀膜工序的颗粒污染。它的形态接近圆形或椭圆直径通常在 2 到 8 个像素之间灰度值比背景低边缘过渡相对均匀。关键区别在于头发丝在频域上表现为高频方向性分量小黑点在频域上是各向同性的低频到中频分量。如果把两者合并成一个“油污”类模型学到的决策边界会非常粗糙。实际测试中合并类别的模型在验证集上的准确率可能到 92%但拆开看头发丝的召回率只有 78%小黑点的精确率只有 81%——因为模型倾向于用“面积小”这个特征同时覆盖两类导致细长但面积不大的头发丝被漏掉而稍大一点的小黑点被误判成头发丝。2.2 660 张的类别分布决定拆分策略拿到 660 张数据第一件事不是急着训练而是统计类别分布。常见做法是用脚本遍历标注文件统计每个类别的实例数和图像数。假设统计结果是头发丝 380 张图、小黑点 310 张图、两者共现 30 张图。那么有效训练样本是 660 张图但实例数可能远大于此。这里有个血泪经验如果某一类少于 200 个实例不要直接做多类分类而是先做“有缺陷/无缺陷”的二分类再用两个独立的二分类器分别判断头发丝和小黑点。原因是小样本下多类 softmax 的类间竞争会让弱类被强类压制。660 张图里如果小黑点只有 150 个实例多类模型的输出会明显偏向头发丝。import os import json from collections import Counter # 假设标注是 COCO 格式遍历 annotations 统计类别实例数 def count_instances(ann_file): with open(ann_file, r) as f: data json.load(f) cat_counter Counter() img_counter Counter() for ann in data[annotations]: cat_id ann[category_id] img_id ann[image_id] cat_counter[cat_id] 1 img_counter[(cat_id, img_id)] 1 # 输出每个类别的实例数和图像数 for cat_id, inst_num in cat_counter.items(): img_num sum(1 for (c, i) in img_counter if c cat_id) print(f类别 {cat_id}: 实例数 {inst_num}, 图像数 {img_num}) count_instances(annotations.json)这段代码的逻辑是从 COCO 标注里分别统计每个类别的实例总数和出现该实例的图像数。参数说明category_id对应头发丝和小黑点的编号image_id用于去重统计图像数。如果发现某一类图像数低于 200就触发上面的拆分策略。注意共现图像要单独标记训练时不能简单复制否则会让模型学到“两类总是一起出现”的伪相关。2.3 类别定义写进标注规范而不是留在脑子里很多团队翻车就翻在标注规范没写死。头发丝和小黑点的边界情况至少有三种短而粗的线状物长宽比小于 3、长而弯的线状物可能被标成两个点、以及密集小黑点聚集形成的斑块。我的做法是写一份判定优先级先看长宽比大于等于 3 归头发丝再看面积小于 20 像素且长宽比小于 3 归小黑点介于两者之间的标记为“待定”由第二人复判。这份规范要作为标注文件的一部分存下来660 张数据里如果有 5% 的待定样本就要在训练时给它们更低的损失权重。3. 660 张图怎么切分和增强才不浪费3.1 训练集、验证集、测试集的切分比例与分层策略660 张图常见做法是 6:2:2即 396 张训练、132 张验证、132 张测试。但小样本下这个比例有个坑如果随机切分可能出现验证集里小黑点只有 20 个实例导致验证指标波动极大。正确做法是分层切分保证每个子集里两类缺陷的比例和总体一致同时保证共现图像均匀分布。from sklearn.model_selection import train_test_split import numpy as np # 假设 labels 是每张图的类别标签列表0 表示头发丝1 表示小黑点2 表示共现 labels np.array([...]) # 660 个标签 indices np.arange(len(labels)) # 先切出测试集stratify 保证类别比例 train_val_idx, test_idx train_test_split( indices, test_size0.2, stratifylabels, random_state42 ) # 再从剩余中切验证集 train_idx, val_idx train_test_split( train_val_idx, test_size0.25, stratifylabels[train_val_idx], random_state42 ) print(f训练集 {len(train_idx)}, 验证集 {len(val_idx)}, 测试集 {len(test_idx)})逻辑说明stratifylabels是关键参数它保证切分后每个子集的类别分布与原始一致。random_state42固定随机种子方便复现。如果某一类在 660 张里少于 50 张图分层切分可能报错这时要改用train_test_split的stratify加上手动合并稀有类。3.2 针对线状和点状缺陷的增强参数怎么设通用增强旋转、翻转、亮度抖动对两类缺陷都有效但参数要分开调。头发丝对方向敏感随机旋转 90 度、180 度、270 度是安全的但任意角度旋转会引入插值模糊让细线断裂。我的经验是头发丝增强只用 90 度整数倍旋转加水平翻转小黑点可以用任意角度旋转加轻微缩放。import albumentations as A # 头发丝增强管道 aug_hair A.Compose([ A.HorizontalFlip(p0.5), A.VerticalFlip(p0.5), A.RandomRotate90(p0.5), # 只做 90 度整数倍旋转 A.RandomBrightnessContrast(brightness_limit0.1, contrast_limit0.1, p0.3), ]) # 小黑点增强管道 aug_dot A.Compose([ A.HorizontalFlip(p0.5), A.VerticalFlip(p0.5), A.Rotate(limit180, p0.7, border_mode0), # 任意角度但用常数填充 A.ShiftScaleRotate(shift_limit0.05, scale_limit0.1, rotate_limit0, p0.3), A.RandomBrightnessContrast(brightness_limit0.15, contrast_limit0.15, p0.3), ])参数说明RandomRotate90只做 90 度倍数旋转避免插值导致细线断裂。Rotate的border_mode0表示用黑色填充边界防止旋转后边缘出现伪影被模型当成缺陷。ShiftScaleRotate的scale_limit0.1控制缩放幅度太大可能让小黑点变成噪点。亮度对比度限制在 0.1 到 0.15 之间模拟产线光源波动再大就会让缺陷和背景的对比度失真。3.3 用 Copy-Paste 增强补足稀有类实例如果小黑点实例数明显少于头发丝可以用 Copy-Paste 增强从已有图像里抠出小黑点区域随机粘贴到无缺陷背景上。这一步要注意粘贴位置的合法性——不能粘到已有缺陷上也不能粘到图像边缘导致截断。import cv2 import numpy as np import random def copy_paste_defect(bg_img, defect_img, defect_mask, max_try50): 将 defect_img 中的缺陷区域粘贴到 bg_img 上 h, w bg_img.shape[:2] dh, dw defect_img.shape[:2] for _ in range(max_try): # 随机选择粘贴位置保证不越界 x random.randint(0, w - dw) y random.randint(0, h - dh) roi bg_img[y:ydh, x:xdw] # 只在背景区域粘贴避免覆盖已有缺陷 mask_bool defect_mask 0 if np.sum(roi[mask_bool]) 0.9 * np.sum(defect_img[mask_bool]): # 简单判断如果目标区域亮度接近缺陷区域说明可能已有缺陷跳过 continue roi[mask_bool] defect_img[mask_bool] bg_img[y:ydh, x:xdw] roi return bg_img, (x, y, dw, dh) return bg_img, None逻辑说明defect_mask是缺陷区域的二值掩码只把掩码内的像素粘贴过去。max_try控制尝试次数避免死循环。粘贴后要同步更新标注文件把新缺陷的边界框加进去。这个方法的参数关键是粘贴位置不能和已有缺陷重叠实际用的时候可以用 IoU 判断重叠超过 0.1 就重新选位置。4. 模型选型小样本下别急着上大网络4.1 从 ResNet-18 到 EfficientNet-B0 的基线对比660 张图输入尺寸常见是 224x224 或 512x512。如果缺陷在图像里占比很小224 可能不够但 512 会让训练显存翻倍。我的建议是先用 224 跑基线如果头发丝召回率低于 85%再升到 384 或 512。模型选型上ResNet-18 是最稳的基线参数量 11M在 660 张图上从头训练容易过拟合所以要用 ImageNet 预训练权重。EfficientNet-B0 参数量 5.3M理论上更适合小样本但它的深度可分离卷积对细线状缺陷的特征提取不如标准卷积敏感。实际对比中ResNet-18 在头发丝上的召回率通常比 EfficientNet-B0 高 3 到 5 个百分点。模型参数量输入尺寸头发丝召回率小黑点精确率单张推理耗时ResNet-1811M22486%88%8msEfficientNet-B05.3M22482%90%6msMobileNetV3-Small2.5M22479%85%4ms表格里的数据是基于类似数据集的经验值实际会有波动。选型原则如果产线节拍要求单张推理小于 10msResNet-18 够用如果要在边缘设备上跑MobileNetV3-Small 可以接受但召回率会降。不要一上来就上 ViT660 张图连 ViT 的注意力图都训不稳定。4.2 损失函数Focal Loss 和类别权重的配合小样本下类别不平衡是常态。如果头发丝 380 张、小黑点 310 张比例还不算悬殊但实例数可能差一倍。这时用 Focal Loss 比交叉熵更稳参数gamma2.0是常见起点alpha按类别频率的倒数设置。import torch import torch.nn as nn import torch.nn.functional as F class FocalLoss(nn.Module): def __init__(self, alphaNone, gamma2.0, reductionmean): super().__init__() self.alpha alpha # 每类的权重张量 self.gamma gamma self.reduction reduction def forward(self, inputs, targets): ce_loss F.cross_entropy(inputs, targets, reductionnone, weightself.alpha) pt torch.exp(-ce_loss) focal_loss ((1 - pt) ** self.gamma) * ce_loss if self.reduction mean: return focal_loss.mean() return focal_loss.sum() # 假设两类头发丝权重 1.0小黑点权重 1.5 alpha torch.tensor([1.0, 1.5]) criterion FocalLoss(alphaalpha, gamma2.0)参数说明alpha是类别权重小黑点样本少就调大。gamma2.0让模型更关注难分类样本但不要超过 3.0否则会放大标注噪声。如果验证集 loss 震荡先把gamma降到 1.0等训练稳定后再升回去。4.3 学习率预热和余弦退火的参数设置小样本训练最怕初期梯度爆炸。用 AdamW 优化器学习率设 1e-4前 5 个 epoch 做线性预热之后余弦退火到 1e-6。批量大小 16 或 32如果显存不够就用梯度累积。from torch.optim import AdamW from torch.optim.lr_scheduler import CosineAnnealingWarmRestarts optimizer AdamW(model.parameters(), lr1e-4, weight_decay1e-4) scheduler CosineAnnealingWarmRestarts(optimizer, T_010, T_mult2, eta_min1e-6) # 训练循环里前 5 个 epoch 手动线性预热 for epoch in range(epochs): if epoch 5: lr 1e-4 * (epoch 1) / 5 for param_group in optimizer.param_groups: param_group[lr] lr else: scheduler.step()逻辑说明weight_decay1e-4是 AdamW 的常见值不要用 1e-2否则小样本下模型欠拟合。CosineAnnealingWarmRestarts的T_010表示每 10 个 epoch 重启一次学习率T_mult2让重启周期翻倍。如果训练 loss 在 20 个 epoch 后还在降说明模型容量不够可以换 ResNet-34 或加宽通道数。5. 避坑660 张数据训练时最容易翻车的 4 个点5.1 验证集指标很高测试集一塌糊涂现象验证集准确率 95%测试集只有 78%。原因通常是验证集和测试集分布不一致比如验证集里小黑点偏多测试集里头发丝偏多。解决切分时严格分层并且用交叉验证代替单次切分。660 张图做 5 折交叉验证每折 132 张验证取平均指标。如果交叉验证的方差大于 5%说明数据本身噪声大要先清洗标注。5.2 头发丝被预测成小黑点置信度还很高现象细长头发丝被分类为小黑点softmax 输出 0.9 以上。原因模型学到的特征里面积权重过高长宽比权重过低。解决在数据增强里加入随机裁剪让头发丝的长度变化更大同时在损失函数里对头发丝类加一个形状惩罚项比如用边界框的长宽比作为辅助回归目标。更直接的做法是改用双分支网络一个分支分类一个分支回归长宽比。5.3 训练 loss 降到 0.01但验证 loss 从第 10 个 epoch 开始上升现象过拟合。660 张图从头训练10 个 epoch 后模型开始记住训练样本。解决早停策略验证 loss 连续 5 个 epoch 不降就停同时加 Dropoutp0.3 到 0.5和权重衰减。如果还不行冻结 ResNet 的前两个 stage只训后面三个 stage。冻结比例用 0.5 起步根据验证指标调整。5.4 标注文件里同一张图既有头发丝又有小黑点训练时被当成两个独立样本现象共现图像在训练时被复制成两份每份只标一个类导致模型在推理时对共现图像只能输出一个类。原因数据加载器没有处理多标签。解决把分类头改成多标签 sigmoid而不是 softmax。损失函数用 BCEWithLogitsLoss每个类独立判断。如果共现图像只有 30 张可以在训练时给它们 2 倍权重让模型学会同时输出两个类。6. 用混淆矩阵和置信度分布做上线前的最后一道验证训练完模型不要只看准确率。660 张数据的测试集只有 132 张准确率波动 1% 就是 1.3 张图的差异。我的习惯是画两个图一个是混淆矩阵看两类之间的误判方向另一个是置信度分布直方图看模型在多少置信度下犯错。import matplotlib.pyplot as plt from sklearn.metrics import confusion_matrix, ConfusionMatrixDisplay import numpy as np # y_true, y_pred 是测试集的真实标签和预测标签 cm confusion_matrix(y_true, y_pred, labels[0, 1]) disp ConfusionMatrixDisplay(confusion_matrixcm, display_labels[头发丝, 小黑点]) disp.plot(cmapBlues) plt.title(测试集混淆矩阵) plt.show() # 置信度分布 probs model.predict_proba(X_test) # 假设输出两类概率 max_probs np.max(probs, axis1) correct (y_pred y_true) plt.hist(max_probs[correct], bins20, alpha0.5, label正确) plt.hist(max_probs[~correct], bins20, alpha0.5, label错误) plt.xlabel(最大置信度) plt.legend() plt.show()逻辑说明混淆矩阵看的是误判方向——如果头发丝被大量判成小黑点说明形状特征没学好反过来则说明面积特征过强。置信度分布看的是模型在低置信度区间的错误率如果错误样本集中在 0.6 到 0.8 之间说明模型对困难样本没有把握上线时要加一个“低置信度转人工”的阈值。这个阈值我一般设在 0.75低于 0.75 的转人工复判高于 0.75 的直接放行。660 张数据训出来的模型这个阈值下的自动放行率大概在 70% 到 80%剩下的转人工整体效率比全人工提升明显。最后说一个我踩过的坑不要用测试集调阈值。阈值要在验证集上定定完再在测试集上跑一次只跑一次。如果测试集结果和验证集差太多说明数据切分有问题回去重新分层。这个习惯帮我省了很多后悔药。希望帮到你。本文还有配套的精品资源点击获取