资讯动态

660张工业油污缺陷图:小数据集下头发丝与小黑点的分类检测实战

发布时间:2026/10/17 0:36:42 来源:尧图企业网站定制
简介面向工业视觉缺陷检测任务的三星油污缺陷数据集聚焦头发丝与小黑点两类典型表面瑕疵对应标注类别 TFS 与 XZW包含660张真实产线采集图像适用于制造质检领域的模型训练、验证与算法研究。资源包共1324个文件其中660个jpg原始图像与660个xml标注文件一一对应另有4个txt辅助文件压缩包总大小约314.54MB结构清晰便于接入主流目标检测框架或转换为其他标注格式使用。XML中保存缺陷类别与位置框信息是训练和评估检测模型的直接依据辅助txt文件可帮助快速梳理数据划分与类别组织方式节省预处理时间。目前已有1412人浏览学习适合算法工程师、科研人员以及工业视觉竞赛团队用于油污、划痕等低对比度缺陷的检测精度优化实验。借助这些真实标注数据可系统验证模型对细小缺陷的召回能力与定位准确性也可结合配套博客的优化思路围绕数据增强、模型选择或后处理策略做进一步探究推动检测方案在复杂产线环境中落地。1. 三星油污缺陷类别660张小数据集别急着上大模型先把标题里的“三星油污缺陷类别”拆开看这里的“三星”是某供应商产线内部的质量等级代号不是某个消费电子品牌。整个任务一句话就能说清——从660张零件表面的油污图像里把头发丝和小黑点这两类缺陷识别出来建成一个能过验收的分类系统。拿到这种数据集的人第一反应通常是660张去掉正常图真正带缺陷的可能就三四百张这点量能训深度学习吗我的答案是能但前提是别把它当成通用目标检测来做。工业视觉和自然场景最大的不同是背景受控相机机位固定、光源固定、零件型号固定油污和缺陷的形态是可重复的。这个项目真正决定能否上线的是三件事标注边界是不是统一、数据增强有没有针对性、阈值有没有按产线漏检要求标定。模型反而是里面最不值钱的部分。2. 头发丝和小黑点的成像差异先定义清楚再标数据2.1 头发丝与小黑点的形态端点长宽比、对比度和边缘特征拿到数据集后的第一步不是建模型而是把图像统计做一遍。油污表面上的头发丝本质是一根细长的纤维遮挡了漫反射光在图像里呈现为几个像素宽、几十甚至上百像素长的低对比暗线边缘容易断裂。小黑点则是紧凑的暗斑直径通常在5到30像素边缘相对锐利对比度比头发丝高更像油污里的颗粒沉淀。这两类缺陷在形态上几乎是两个极端直接用统一特征去描述一定会打架。我把它们的关键差异列成一张表标注和建模前先对着这张表过一遍特征维度头发丝小黑点灰度走向细长暗线局部低对比可能有断续紧凑暗斑边缘相对锐利尺寸比例长度常超过50像素宽度小于5像素直径集中在5到30像素纹理特征沿一个方向延伸需要长距离上下文各向同性局部对比度起决定作用形态学标志长宽比极大适合骨架提取面积和圆度指标比较稳定这个差异直接影响后续选择。头发丝考验的是模型对“长距离上下文”的利用小黑点考验的是“局部对比度”。于是预处理里不能对整张图做强烈模糊否则头发丝会被洗掉也不能只做局部阈值分割否则油污背景的暗色纹理很容易混进小黑点。先想清楚这两类缺陷的成像差异再谈建模。2.2 标注工具与标签规范小数据集最怕标得乱660张图如果多人协作标注标准不统一模型训练时同一根头发丝一会儿有框一会儿没框验证集指标就会来回跳。我一般会做三条规定第一类别名固定用英文小写hair和dot正常图单独放一个normal文件夹不在同一张图里标“正常”类别。第二头发丝的框贴住缺陷两端各留2到3像素覆盖完整线条宁可把周围一点点油污背景包进来也不要截断。第三小黑点的框以缺陷边缘外扩2像素为准小于3像素的标注视为噪点直接删掉。标注导出后不要直接拿去训练先跑一个简单的校验脚本把空标注、未知标签、疑似标反方向的长条框筛出来import json import glob # 读取LabelMe导出的JSON做最基础的标注把关 label_stats {hair: 0, dot: 0} bad_files [] for json_path in glob.glob(labels/*.json): with open(json_path, encodingutf-8) as f: ann json.load(f) shapes ann[shapes] if len(shapes) 0: bad_files.append((json_path, 空标注)) continue for s in shapes: label s[label].lower() if label not in (hair, dot): bad_files.append((json_path, f未知标签 {label})) continue pts s[points] xs [p[0] for p in pts] ys [p[1] for p in pts] w max(xs) - min(xs) h max(ys) - min(ys) # 头发丝框的长宽比应该远大于1 if label hair and max(w, h) / min(max(w, h), 1) 2.0: bad_files.append((json_path, 头发丝框疑似标反方向)) # 小黑点框太小大概率是标了噪点 if label dot and max(w, h) 3: bad_files.append((json_path, 小黑点框小于3像素)) label_stats[label] 1 print(label_stats) print(异常文件, bad_files)这个脚本不负责解决标注问题它的价值是把最恶劣的错误暴露出来让标注员在训练前重新过一遍。逻辑上对每个JSON文件遍历所有标注框按类别统计数量并对明显不合理的框打标记。min(max(w, h), 1)这一句是为了防止高度为0时除零。参数上3像素这个阈值来自相机分辨率像素尺寸小于3的“缺陷”在多数工业相机视野下已经接近噪声标了反而会把模型带偏。2.3 数据增强策略先扩背景多样性再扩缺陷形态小数据集泛化靠增强但工业图的增强和自然图像不一样必须克制。产线零件的姿态是固定的旋转不能太多光照有波动但有限亮度和对比度的扰动范围也要锁住。我常用的增强管线长这样import albumentations as A from albumentations.pytorch import ToTensorV2 # 训练增强让模型适应真实的光强波动和轻微形变 train_transform A.Compose([ A.Resize(224, 224), A.RandomBrightnessContrast( brightness_limit0.1, contrast_limit0.1, p0.5 ), A.GaussNoise(var_limit(5.0, 15.0), p0.3), A.ShiftScaleRotate( shift_limit0.02, scale_limit0.05, rotate_limit10, p0.5 ), A.Cutout(num_holes4, max_h_size8, max_w_size8, p0.5), A.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]), ToTensorV2() ]) # 推理增强只做resize和归一化不做任何扰动 val_transform A.Compose([ A.Resize(224, 224), A.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]), ToTensorV2() ])参数选择有讲究。brightness_limit0.1是因为产线同一光源下的亮度波动不会超过这个范围调大了模型反而会去拟合不存在的亮度变化。rotate_limit10对应零件摆放的轻微角度偏差如果工位有定位治具这个值还可以更小。Cutout的作用是模拟小黑点被油污部分遮挡的情况但num_holes4不能再多否则会把细长的头发丝切成两段让模型误以为“断裂的线”才是正常形态。数据增强的正确方向是先扩背景多样性。光照变化、噪声、轻微位移都比旋转裁剪重要因为这些才是产线真实存在的变化。如果发现验证集上头发丝的漏检集中在对比度较低的图上再把RandomBrightnessContrast的权重调高一点。3. 小数据集模型的选型与训练二分类网络比YOLO更稳3.1 分类、检测还是分割660张图的路口同样面对缺陷模型选型有三种常见路线图级分类、目标检测、像素分割。它们的标注成本和对数据量的要求差距很大。路线标注成本小数据适应性输出适合条件分类网络只需要图级标签好660张够用类别、置信度只报警不定位检测网络需要画框差建议1000张以上目标框、类别、置信度需要坐标下游要抓取分割网络像素级标注中缺陷掩膜、面积、长度需要量化缺陷尺寸这个项目如果甲方只要“这批料里有没有混入头发丝或小黑点”直接上分类网络代价最小、稳定性最高。检测网络不是不能用而是660张图里头发丝框的长宽比极大YOLO默认的anchor几乎都是正方形比例需要单独做anchor聚类训练难度凭空高了一截。分割网络只有一种情况值得选后续要统计头发丝的长度、小黑点的面积比如用来划分批次等级。所以我的路线是先用分类网络跑通报警如果验收单里明确要求输出缺陷坐标再在这个分类模型基础上叠加一个轻量的目标检测模块。660张的数据量硬走检测路线属于给自己挖坑。3.2 用迁移学习跑通基线ResNet18微调的完整骨架分类网络也不是随便训。660张图直接随机初始化一个ResNet训练集上过拟合几乎必然。常见做法是用ImageNet预训练权重冻结骨干只训练最后的全连接层。代码骨架如下import torch import torch.nn as nn from torchvision import datasets, models from torch.utils.data import DataLoader, WeightedRandomSampler # 数据目录要求 # train/hair、train/dot、train/normal、val/... 按ImageFolder结构组织 dataset datasets.ImageFolder(data/train, transformtrain_transform) # 类别不均衡时用加权采样让模型每个epoch都能看到少量类 class_counts torch.bincount(dataset.targets) total len(dataset.targets) weights [1.0 / (class_counts[c] / total) for c in range(len(class_counts))] sampler WeightedRandomSampler(weights, num_sampleslen(dataset), replacementTrue) train_loader DataLoader(dataset, batch_size16, samplersampler, num_workers2) # 加载预训练ResNet18冻结卷积层 model models.resnet18(pretrainedTrue) for name, param in model.named_parameters(): if fc not in name: param.requires_grad False model.fc nn.Linear(512, 3) # 三个类别normal、hair、dot criterion nn.CrossEntropyLoss() optimizer torch.optim.AdamW( filter(lambda p: p.requires_grad, model.parameters()), lr1e-3 ) scheduler torch.optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max30)关于冻结骨干我的判断是前几层卷积提取的是边缘、纹理这类基础特征预训练权重已经足够好小数据下全量微调这几个亿参数只会让模型把训练集的油污纹理背下来。只训练最后一层时可训练参数量约150万和整个ResNet18的1100万参数相比过拟合风险低得多。lr1e-3对全连接层是合适的起点AdamW比SGD在这个规模下更容易收敛T_max30表示30个epoch走完一个余弦周期。训练主循环不需要花哨每5个epoch在验证集上评估一次避免频繁跑验证集造成的判断抖动for epoch in range(30): model.train() total_loss 0 for imgs, labels in train_loader: optimizer.zero_grad() out model(imgs) loss criterion(out, labels) loss.backward() optimizer.step() total_loss loss.item() scheduler.step() if epoch % 5 0: cm eval_model(model, val_loader) print(fepoch{epoch}, loss{total_loss / len(train_loader):.4f})如果训练15个epoch后验证集准确率没有明显上升说明最后一层的学习率偏大把lr降到1e-4解冻最后两个残差块再训。如果损失在训练集上降到0.05以下、验证集不涨立刻停止走第4章的排查流程。3.3 训练过程中重点看什么混淆矩阵的每一行分类任务不能只看验证集准确率80%的准确率可能全部来自正常图。我用一个小的评估函数输出每类召回率from sklearn.metrics import confusion_matrix def eval_model(model, loader, class_names(normal, hair, dot)): model.eval() y_true, y_pred [], [] with torch.no_grad(): for imgs, labels in loader: prob torch.softmax(model(imgs), dim1) y_pred.extend(prob.argmax(dim1).tolist()) y_true.extend(labels.tolist()) cm confusion_matrix(y_true, y_pred) recall cm.diagonal() / (cm.sum(axis1) 1e-6) for i, name in enumerate(class_names): print(f{name} recall: {recall[i]:.3f}) return cm这段代码的逻辑是把所有验证集样本的预测类别和真实类别对齐生成混淆矩阵然后用cm.diagonal() / cm.sum(axis1)计算每个类别的召回率。1e-6防止某类样本数为0时除零。重点看两个值normal被误判成hair或dot的比例以及hair被漏成normal的比例。前者决定产线误杀成本后者决定漏检风险。这两个比例比总准确率重要得多。4. 避坑660张工业图最常见的5个翻车点4.1 准确率99%和82%的落差过拟合来得比想象快现象训练20个epoch后训练集准确率到99%验证集只有82%损失在训练集降到0.03验证集不降反升。原因660张图去掉正常图后训练样本可能只有400张左右模型把训练图里的特定油污纹理、特定光照分布背了下来而不是学会“细长暗线”和“紧凑暗斑”这两个抽象概念。解决冻结更多卷积层只保留最后一个残差块和全连接层可训练学习率降到1e-4增强管线里把Cutout的概率从0.5提到0.7。如果仍然过拟合直接在20个epoch处早停不要迷信“再训几个epoch会变好”。4.2 小黑点总是被漏掉类别不均衡的两种解法现象头发丝有450张小黑点只有80张验证集上头发丝召回率0.93小黑点召回率只有0.4大量小黑点被预测成正常。原因模型优先拟合多数类少数类样本只占训练集的15%左右对损失的贡献被稀释。解决第3.2节里的WeightedRandomSampler就是第一种解法让每个epoch里小黑点的样本出现频率和其他类持平。第二种更直接的做法是给少数类加大损失权重把CrossEntropyLoss改成按类别设置权重class_weight torch.tensor([1.0, 1.0, 4.0])对应normal/hair/dot的权重。如果两种方法一起用注意学习率要再降一点否则少数类权重过大会让normal的误检率上升。4.3 同一根头发丝有人标有人不标标注噪音怎么查现象两个标注员标同一批图A把完整头发丝标成一个框B因为线条中间有一段对比度过低而标成两个框导致模型把断开的短框当成正常形态整根头发丝反而漏检。原因标注规范没有定义“断裂处怎么处理”。油污表面的头发丝常有低对比度段肉眼能分辨连续性标注员各自理解不同。解决在标注规范里明确一条规则——只要两个片段在几何上近似共线且间距小于10个像素就合并成一个框。然后把所有标注文件跑2.2节的校验脚本统计每张图的平均框数框数明显偏离均值的那几张图提出来人工复查。4.4 训练和推理预处理不一致精度凭空掉了5个点现象训练时按RGB读图、全图resize到224×224部署脚本里用了灰度模式读图或者resize尺寸写成256模型推理精度下降5个百分点。原因训练管线和部署管线各写了一套预处理时间一长就对不上了这个问题在现场验收时最容易爆发。解决把预处理封装成一个函数训练和推理共用import cv2 def preprocess(img, size224): # 如果是单通道灰度图先转成三通道保持和训练一致 if img.ndim 2: img cv2.cvtColor(img, cv2.COLOR_GRAY2BGR) img cv2.resize(img, (size, size), interpolationcv2.INTER_LINEAR) img img.astype(float32) / 255.0 img (img - [0.485, 0.456, 0.406]) / [0.229, 0.224, 0.225] return img.transpose(2, 0, 1)这段代码把颜色空间转换、resize、归一化、通道顺序调整全部固定下来。注意transpose(2, 0, 1)是把HWC变成CHW这是PyTorch的默认输入格式。上线的第一件事不是看准确率而是拿10张训练图分别走训练预处理和这个函数对比输出像素值是否完全一致。4.5 油污背景变化换一个批次就崩的域偏移现象训练图里的油污是浅黄色产线换了一个批次的来料后油污发黑小黑点和油污背景的灰度重叠误检率直接翻倍。原因纯图像分类模型不感知“这是背景油污还是缺陷”它学到的是灰度纹理模式。当油污背景本身出现类似小黑点的暗斑时特征分布就偏移了。解决这是小数据集最无解的一个坑只能从采集端和数据端两头堵。采集端用偏振光或调整光源角度把油污的反光压下去让缺陷对比度拉开数据端把现场新批次的图像收集回来哪怕只有50张混入训练集做一次增量训练。最忌讳的做法是只在验证集上反复调阈值因为油污背景变化是分布漂移不是阈值问题。5. 评估与产线落地漏检率优先阈值按现场数据标定5.1 工业场景看漏检率不看mAP分类模型上线后产线最关心的指标只有两个有缺陷的产品流出会带来客诉正常产品被误判会带来浪费。对应的指标就是漏检率和误检率。指标计算方式产线含义优先级漏检率FN / (TP FN)缺陷品被放过的比例最高直接决定客诉误检率FP / (TN FP)正常品被打成缺陷的比例高决定人工复判成本F1值2PR / (P R)综合衡量精确率和召回率辅助单张推理耗时毫秒决定能否在线检测达到节拍即可很多算法工程师习惯看mAP但产线验收表里不会有这个指标。漏检率要做到低于1%误检率根据产线人工复判能力定一般是2%到5%。如果这两条满足不了不管mAP多高都不会被验收。5.2 置信度阈值调参脚本把分类输出变成产线决策分类网络输出的是三个类别的概率实际决策时要合成一个“缺陷置信度”。常见做法是取max(hair_prob, dot_prob)作为缺陷置信度然后遍历阈值选工作点import numpy as np from sklearn.metrics import confusion_matrix def find_best_threshold(y_true, y_prob_defect, min_recall0.99): y_true: 1表示缺陷0表示正常 y_prob_defect: 缺陷置信度即max(hair_prob, dot_prob) best_thr 0.5 best_f1 0.0 for thr in np.arange(0.30, 0.96, 0.05): y_pred (y_prob_defect thr).astype(int) tn, fp, fn, tp confusion_matrix(y_true, y_pred).ravel() recall tp / (tp fn 1e-6) precision tp / (tp fp 1e-6) f1 2 * precision * recall / (precision recall 1e-6) # 优先保证漏检率低于 1%在此前提下选F1最高 if recall min_recall and f1 best_f1: best_f1 f1 best_thr thr return best_thr这个脚本的逻辑很直接在0.30到0.95之间以0.05为步长扫描阈值计算每个阈值下的召回率、精确率和F1值。min_recall0.99是硬约束只有满足漏检率低于1%的阈值才会进入选择范围。最终选出的阈值就是产线决策点的初始值。注意这里confusion_matrix的参数顺序先真实值后预测值。5.3 部署到产线的两个关键推理速度和重训闭环660张图训练出的ResNet18产线根本不需要GPU。用ONNX导出后CPU上单张推理大约5到15毫秒完全满足常规节拍。导出时的常见做法是用动态batch方便一次处理多张import torch torch.onnx.export( model, # 训练好的模型 torch.randn(1, 3, 224, 224), # 固定尺寸的dummy输入 defect.onnx, input_names[images], output_names[logits], dynamic_axes{images: {0: batch}, logits: {0: batch}}, opset_version11 )说明dynamic_axes允许batch维可变产线一次传1张或4张都行opset_version11的兼容性比较好常见的CPU推理框架都支持。比推理速度更重要的是设计重训闭环。推理程序要把每张被判定为缺陷的图保存到一个按日期分目录的文件夹里同时保存原始图和预测类别。每周人工抽检一次把明显的误判图挑出来回灌到训练集重新微调模型。小数据集项目上线后前两周的反馈数据价值最大能补上的都是训练集里没有的真实分布。6. 进阶把缺陷检测当异常检测小数据集的另一种解法6.1 用预训练特征做KNN异常检测如果缺陷样本实在太少连分类网络都扛不住过拟合可以考虑不训分类器而是用预训练网络提取特征做异常检测。思路是用正常图的特征建一个“正常库”推理时比较待测图和正常库的距离距离远就是缺陷。这个方法对头发丝尤其有效因为细长线条在ImageNet预训练特征里会激活特定的纹理通道和小黑点的激活模式明显不同。import torch import numpy as np from torchvision import models model models.resnet18(pretrainedTrue) model.fc torch.nn.Identity() # 把最后一层替换成identity输出embedding model.eval() normal_features [] for img in normal_loader: with torch.no_grad(): normal_features.append(model(img).numpy()) normal_center np.stack(normal_features).mean(axis0) def is_defect(img, threshold12.0): with torch.no_grad(): feat model(img).numpy() dist np.linalg.norm(feat - normal_center) return dist threshold这里的normal_center是正常图特征的均值向量距离度量用欧氏距离。阈值12.0是示例值真正使用时要在验证集上扫描确定方法和5.2节一样。这个做法的局限在于小黑点如果只有几个像素在整图embedding里贡献会被淹没需要配合图像分块或金字塔特征才能检出。6.2 缺陷数量不够时用合成注入补充训练另一个我常用的补充手段是合成注入从真实缺陷样本里crop出头发丝片段和小黑点随机贴到正常图上。位置的随机范围要避开图像中心区域alpha透明度固定为0.9到1.0不要调成半透明否则模型学到的不是表面遮挡而是图像叠加。合成样本只能作为辅助正则占比不超过训练集的30%否则模型会抓住合成的痕迹比如边缘过渡太整齐。6.3 我的习惯每个模型配一张部署卡我现在接手这种小数据集项目都会在交付时做一张部署卡模型版本、训练集构成、有效阈值、典型误判类型、现场批次影响。这张卡是给下一个接手的人看的因为小数据模型的决策边界经常因为新增数据而改变。这张卡上写得最多的往往是“什么情况下会漏检”而不是“准确率有多高”。一开始我也觉得这些记录可有可无后来才明白这类项目最值钱的部分不是那个模型而是这些关于边界情况的记忆。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑