简介:面向工业视觉表面缺陷检测场景,这份数据集聚焦三星油污缺陷中的头发丝与小黑点两类目标,共660张原始图像,并配有对应XML标注文件,可直接用于目标检测、实例分割或分类模型的训练与验证;其中TFS、XZW两类标签已按规范整理,适合算法工程师、竞赛选手及质检方案研究者快速上手。资源共1324个文件,主要为660个jpg原图与660个xml标注文件,另有4个txt辅助说明,压缩包整体约314.54MB,rar格式;jpg与xml按文件名一一对应,省去手动标注环节,便于直接划分训练集和验证集。已有1412人学习下载,适合在工业质检、深度学习缺陷识别项目中作为基准数据集,可结合数据增强、超参调整等手段提升检测精度,也适合对照表面缺陷场景做模型迁移与效果对比,为后续算法迭代提供清晰的数据基础。
1. 头发丝和小黑点:为什么660张缺陷图才是三星油污缺陷检测的真实战场
三星OLED面板的油污缺陷检测里,最不起眼的两个缺陷类别——头发丝、小黑点,往往最能看出一个视觉项目能不能真正落地。头发丝是产线上飘落的纤维异物,落在面板上拖出一条细线;小黑点是油污、胶材残留或碳化颗粒聚成的暗斑。两者共同点非常扎心:面积小、对比度低、边界模糊,跟面板正常纹理极度相似,靠传统阈值分割根本分不开。
我第一次拿到这个项目时,数据集只有660张图,心里是打鼓的。但在产线上蹲过之后你会明白,660张不是少,反而是一种常态——面板良率控制得越好,你能拍到真实缺陷的机会就越少。这660张,可能要车间攒两三个月。文章就围绕这个真实的小样本场景展开:数据怎么整理、模型怎么选、参数怎么调、部署前有哪些坑,按一条能落地的路径讲清楚。适合正在做面板外观检测、或者准备用分类网络处理小样本缺陷数据的算法和视觉工程师。
2. 从660张原图到一套可用的训练集:目录划分、标注口径与增强策略
2.1 先定好类别口径:正常、头发丝、小黑点怎么分才不容易扯皮
这类项目第一步不是写代码,而是跟品质部门把“什么算头发丝、什么算小黑点”对清楚。AOI自动光学检测设备在OLED基板上扫描后,会以64×64到512×512不等的窗口把可疑区域抠出来,这个窗口业内叫ROI。我们拿到的660张图,绝大多数已经是抠好的ROI,少数是整张大图需要自己切。
我的习惯是先把类别口径用文档写死,避免返工:
- 头发丝:细长条状异物,长宽比明显大于3比1,形态可以有弯曲,灰度上比背景偏暗或偏亮。产线常见的来源是无尘服纤维、擦拭布掉絮。
- 小黑点:近似圆形的暗斑或亮斑,直径一般只有几个到几十个像素,边缘模糊,常成片出现,来源是油污吸附颗粒、胶材固化残留。
- 正常:没有上述两类缺陷的ROI,但可能包含轻微色差、淡化Mura等不影响判级的纹理。
分类别归到三个文件夹,这是后续所有脚本的基础结构。
defect_660/ ├── hair/ # 头发丝样本 ├── dot/ # 小黑点样本 └── normal/ # 正常样本注意一点:如果一张ROI里同时出现了头发丝和小黑点,必须在标注规范里写明优先级,否则后面训练时模型会学出一团浆糊。这个我放到第5章专门展开,因为它是真实项目里最容易吵架的地方。
2.2 用脚本按比例切分三分类数据集,别按单张随机切
660张图看起来少,切分方式却直接决定模型真实水平。最常见的错误是直接把所有图打乱,按7:2:1随机分成训练、验证、测试。如果这些图是从不同批次、不同时间段采集的,随机切分会让同一批次的相似背景图同时出现在训练和验证里,造成“验证集准确率95%,一到新批次就掉到60%”的翻车现场。
我一般会分两步切分:先按批次分桶,再在桶内抽文件。因为同一个批次往往来自同一时段、同一台AOI设备、同样的光照条件,把它们拆开,模型会靠“背背景”来刷分,而不是真正学到缺陷形态。
import os import random import shutil from collections import defaultdict random.seed(42) data_root = "defect_660" target_root = "defect_splitted" categories = ["hair", "dot", "normal"] split_ratio = (0.7, 0.15, 0.15) # 训练、验证、测试 # 建立输出目录 for split in ["train", "val", "test"]: for cat in categories: os.makedirs(os.path.join(target_root, split, cat), exist_ok=True) # 把图片按文件名里的批次号分组 def get_batch_key(filename: str) -> str: # 假设文件名形如 "batch20250110_001_hair.png",批次号取前段 return filename.split("_")[0] for cat in categories: img_dir = os.path.join(data_root, cat) all_files = [f for f in os.listdir(img_dir) if f.lower().endswith((".jpg", ".png", ".bmp"))] batch_dict = defaultdict(list) for f in all_files: batch_dict[get_batch_key(f)].append(f) batch_list = list(batch_dict.keys()) random.shuffle(batch_list) n_train_batch = int(len(batch_list) * split_ratio[0]) n_val_batch = int(len(batch_list) * split_ratio[1]) train_batches = set(batch_list[:n_train_batch]) val_batches = set(batch_list[n_train_batch:n_train_batch + n_val_batch]) test_batches = set(batch_list[n_train_batch + n_val_batch:]) for batch, files in batch_dict.items(): if batch in train_batches: split = "train" elif batch in val_batches: split = "val" else: split = "test" for f in files: shutil.copy(os.path.join(img_dir, f), os.path.join(target_root, split, cat, f))这里说三个关键点。第一,random.seed(42)固定随机种子,保证每次切分结果一致,别人复现你的实验不会因为切分不同而得到完全不同的结论。第二,按批次切分的前提是文件名里有批次信息;如果拿到手的图没有任何批次标记,就只能退而求其次,按采集时间排序后做间隔切分。第三,切分后打印一下每个类别的文件数量,确认没有某个类别在验证集里只有两三张的情况,那会导致评估结果随机性太大。
2.3 按缺陷形态设计增强策略,而不是一把梭
小数据集离不开数据增强,但“一把梭”式增强会把缺陷特征毁掉。我的做法是:头发丝吃旋转和翻转,小黑点吃亮度和对比度。原因是两类缺陷的物理形态不同:头发丝是长条状,方向随机,旋转增强能帮助模型学到“不管什么角度的丝都是缺陷”;小黑点没有方向性,旋转和翻转对它意义不大,真正影响它可见度的是照明不均和相机增益带来的灰度漂移。
import albumentations as A from albumentations.pytorch import ToTensorV2 train_transform = A.Compose([ A.Resize(224, 224), A.RandomRotate90(p=0.5), # 头发丝方向随机,旋转增强收益大 A.Rotate(limit=20, p=0.5), # 小角度旋转,模拟取像平台抖动 A.HorizontalFlip(p=0.5), A.VerticalFlip(p=0.2), A.RandomBrightnessContrast(brightness_limit=0.2, contrast_limit=0.2, p=0.8), A.GaussNoise(var_limit=(10, 40), p=0.3), A.OneOf([ A.MotionBlur(blur_limit=3), A.MedianBlur(blur_limit=3), ], p=0.2), A.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]), ToTensorV2(), ])这里我特别说明两个参数的设定逻辑。Rotate(limit=20)只做20度以内的小角度旋转,因为AOI取像时平台基本是水平的,缺陷不会出现大角度翻转;如果允许旋转90度、180度,等于让模型学了现实中不存在的形态,训练集和真实分布差距被拉大,反而掉点。GaussNoise(var_limit=(10, 40))模拟的是相机传感器噪声,对小黑点这种小目标影响较大,但噪声太强会把本来就模糊的小黑点直接淹没,所以方差上限定在40而不是100。
推理端的验证集和测试集不要加任何随机增强,只用Resize和Normalize。我见过不少项目把增强用在测试集上,结果部署时发现指标虚高,这是数据泄漏的一种变体,必须避免。
3. 模型选型:不做检测、不堆大模型,用分类网络跑通缺陷识别
3.1 为什么这个任务用图像分类而不是目标检测
660张图、三类标签,很多人第一反应是上YOLO。但我的经验是,这个场景不需要目标检测,因为AOI设备已经把“在哪里”这件事干完了。AOI在整张面板图上跑的是像素级的连通域分析,它会把可疑区域以ROI形式抠出来,分类器只需要回答“这个ROI是头发丝、小黑点,还是正常纹理”。
目标检测模型在这里的劣势很明显。第一,660张图上标注检测框,工作量比分类标注多一个量级,人力不够;第二,YOLO这类检测模型对bbox回归的训练样本需求远大于660张,强行训练只会得到一堆置信度低下的预测框;第三,面板产线的实时性要求通常很高,分类网络单张推理只要几毫秒,检测网络要跑到几十毫秒,性价比不划算。
如果未来需要做整面板扫描,常见做法是保留AOI的候选框提取,把分类器嵌入到逐ROI判断的流水线上。这比一步到位的端到端检测可靠得多,也更容易在产线上解释每一个判级结果。
3.2 主流分类主干对比与选择建议
在660张这样的小数据集上,主干网络的选择比损失函数更影响最终结果。我拿自己做过的对比测试列一个参考表,推理耗时是224×224输入在普通工控机上的经验值。
| 主干网络 | 参数量 | GPU推理耗时 | CPU推理耗时 | 小样本数据友好度 | 我的建议 |
|---|---|---|---|---|---|
| ResNet18 | 11.7M | 约2ms | 约30-50ms | 较高,首选 | 默认选项 |
| ResNet50 | 25.6M | 约4ms | 约80-120ms | 中等,易过拟合 | 数据量少于1k时慎用 |
| MobileNetV3-Small | 约2.5M | 约1ms | 约15-25ms | 较高,需更多调节 | CPU部署时选它 |
| EfficientNet-B0 | 约5.3M | 约3ms | 约40-60ms | 中等 | 二次验证时可用 |
我的默认选择是ResNet18。理由是:它比ResNet50轻很多,但在缺陷分类难度上和小黑点这种细粒度特征上,效果已经足够;比起MobileNet,它对学习率变化更不敏感,训练过程不容易翻车。只有当产线最终部署环境是CPU且对帧率要求很高时,我才会换MobileNetV3-Small,并把训练迭代次数拉长一倍。
3.3 迁移学习主干搭建:冻结底层还是全部微调
小数据集必须用ImageNet预训练权重。缺陷图的纹理虽然和自然图像差异很大,但预训练模型在前几层学到的边缘、角点、纹理基元仍然有用。这里给出一个可跑的PyTorch模型构建代码。
import torch import torch.nn as nn from torchvision import models def get_model(num_classes=3, backbone="resnet18", pretrained=True): if backbone == "resnet18": model = models.resnet18( weights=models.ResNet18_Weights.IMAGENET1K_V1 if pretrained else None ) in_features = model.fc.in_features # 替换最后的全连接层为三分类输出 model.fc = nn.Linear(in_features, num_classes) elif backbone == "mobilenet_v3_small": model = models.mobilenet_v3_small( weights=models.MobileNet_V3_Small_Weights.IMAGENET1K_V1 if pretrained else None ) in_features = model.classifier[0].in_features model.classifier = nn.Sequential( nn.Dropout(p=0.2, inplace=True), nn.Linear(in_features, num_classes), ) return model这个代码里有一个容易犯迷糊的地方:ResNet18的最后一层叫fc,MobileNetV3按官方实现则叫classifier,而且MobileNetV3的输出层是一个Sequential容器,替换时要保留前面的Dropout,不能直接覆盖成单个Linear,否则推理时Dropout模块还在,结果会变差。
4. 在小样本上把模型训稳:损失函数、优化器与训练流程
4.1 类别不平衡:直接上Softmax还是换Focal Loss
660张图里,三类数量一般不会均等,常见分布可能是头发丝280张、小黑点200张、正常180张。这种情况下,直接交叉熵训练也能跑,但模型对样本量少的类别容易欠拟合。我做了几次对比后,给的结论是:先用带类别权重的交叉熵,不要一上来就上Focal Loss。
Focal Loss对难样本的聚焦能力在小数据集上是双刃剑。它会把大量学习权重放到模型当前分不清的样本上,而这些难样本在660张的规模里可能本身就是标注争议样本,强行拟合它们反而让决策边界变得扭曲。类别权重更直接:少数类样本的loss乘以一个倍率,相当于告诉模型“这类样本虽然少,但要认真学”。
Focal Loss的PyTorch实现不复杂,如果你确定自己的样本里有大量难分样本,可以按下面代码尝试,但要把gamma从默认的2.0降到1.0。
import torch import torch.nn as nn import torch.nn.functional as F class FocalLoss(nn.Module): def __init__(self, alpha=None, gamma=1.0, reduction="mean"): super().__init__() self.alpha = alpha self.gamma = gamma self.reduction = reduction def forward(self, logits, targets): ce = F.cross_entropy(logits, targets, reduction="none") pt = torch.exp(-ce) loss = (1 - pt) ** self.gamma * ce if self.alpha is not None: alpha_t = self.alpha.to(logits.device)[targets] loss = alpha_t * loss return loss.mean() if self.reduction == "mean" else loss.sum()使用Focal Loss时,gamma=1.0比2.0更适合小数据集。我踩过这个坑:gamma取2.0会把所有注意力都放到少数极端样本上,训练loss降不下去,验证集指标反而更差。alpha直接取类别频率的倒数再归一化就可以了。
4.2 两步训练法:先训分类头,再解冻微调
迁移学习在小数据集上最容易犯的错是直接解冻全部层,用统一学习率从头到尾一起训练。这个做法在660张图上几乎必然过拟合,因为模型底层参数多达1100多万,而样本只有几百张。我固定使用的流程是两步走:
阶段一:冻结主干,只训练新替换的分类头。这一阶段让分类头先学会在预训练特征上区分三类,通常跑10到15个epoch就够了。阶段二:解冻主干最后两到三个Block,用更低的学习率微调。这一步让模型可以稍微调整高层特征,去适应面板纹理和缺陷形态。
需要强调的是,解冻时不要把Stage1和Stage2的卷积层全部解冻,那样等于从头训练。对于工业缺陷分类,低层特征(边缘、颜色、纹理基元)在ImageNet和面板图上都是通用的,只有高层语义需要适配。
4.3 训练脚本与关键参数配置
下面是我在小样本缺陷分类上常用的一套训练循环配置。它包含了冷启动、余弦退火、以及每个epoch后的验证评估。
import torch from torch.optim import AdamW from torch.optim.lr_scheduler import CosineAnnealingLR device = "cuda" if torch.cuda.is_available() else "cpu" model = get_model(num_classes=3, backbone="resnet18").to(device) # 阶段一:冻结主干,只训练分类头 for name, param in model.named_parameters(): if "fc" not in name: param.requires_grad = False optimizer = AdamW( filter(lambda p: p.requires_grad, model.parameters()), lr=1e-3, weight_decay=1e-4 ) scheduler = CosineAnnealingLR(optimizer, T_max=15) criterion = torch.nn.CrossEntropyLoss( label_smoothing=0.1, weight=torch.tensor([1.0, 1.2, 1.5]).to(device) ) for epoch in range(15): model.train() total_loss = 0.0 for imgs, labels in train_loader: imgs, labels = imgs.to(device), labels.to(device) optimizer.zero_grad() logits = model(imgs) loss = criterion(logits, labels) loss.backward() optimizer.step() total_loss += loss.item() scheduler.step() val_acc = evaluate(model, val_loader, device) print(f"epoch={epoch+1:02d} loss={total_loss/len(train_loader):.4f} val_acc={val_acc:.4f}") # 阶段二:解冻最后两个block,降低学习率微调 for name, param in model.named_parameters(): if "layer4" in name or "layer3" in name: param.requires_grad = True optimizer = AdamW( filter(lambda p: p.requires_grad, model.parameters()), lr=2e-5, weight_decay=1e-4 ) scheduler = CosineAnnealingLR(optimizer, T_max=20) for epoch in range(20): model.train() for imgs, labels in train_loader: imgs, labels = imgs.to(device), labels.to(device) optimizer.zero_grad() logits = model(imgs) loss = criterion(logits, labels) loss.backward() optimizer.step() scheduler.step()参数理解上有三个关键。学习率:阶段二设2e-5,绝对不能超过1e-4,否则解冻后的预训练权重会被一步冲毁,验证集准确率直接跳水。weight_decay:1e-4是针对小数据集的经验值,太大会让分类头学不动,太小则模型过拟合。label_smoothing:0.1很关键,它让模型不要对训练样本的标签过于自信,相当于一种隐式正则化。类别权重[1.0, 1.2, 1.5]按样本量反比设置,你按自己的实际数据集分布重新计算。
4.4 训练结果的评估标准:准确率不是唯一指标
缺陷分类项目里,准确率会骗人。如果正常类占比60%,模型全部预测正常就能拿到60%的准确率,但对产线毫无意义。我平时不只盯Acc,而是看混淆矩阵和两个产线指标:
- 漏检率:真实缺陷被预测为正常的比例。这个指标必须压到极低,漏掉一个缺陷意味着不良品流出到下一道工序。
- 误杀率:正常样本被预测为缺陷的比例。误杀率过高会导致复判岗位工作量爆炸,产线不敢用。
利用混淆矩阵把每个类别的表现分开看。如果头发丝类很容易被预测成小黑点,大概率是两类缺陷在某个形态维度上确实相似,需要回头检查标注规范,而不是单纯调模型。
5. 油污缺陷分类的常见坑与排查:我在这类项目上踩过的5个坑
5.1 现象:训练集准确率95%,新批次样本准确率却只有60%
这是我接手每一个面板缺陷项目时几乎都会遇到的第一个坑。训练集和验证集从同一个批次里随机切分,模型靠背背景纹理把准确率刷得很高;一旦换了新的生产批次,背景亮度、设备增益、环境温度都不一样,模型立刻现原形。
原因很简单:数据泄漏。同一批次采集的样本,背景高度相似,随机切分等于把测试答案提前漏给了模型。解决方法是文章第2章写的按批次划分。当时我把660张图按来源批次重切分后,训练集准确率从虚高的95%降到82%,但新批次上的真实准确率反而从60%涨到78%。这才是模型真正的水平。
5.2 现象:一张图里既有头发丝又有小黑点,模型判来判去不稳定
660张图里大概有3%到5%的ROI同时包含两种缺陷。标注阶段如果一个标注员标成头发丝、另一个标成小黑点,训练时模型就会困惑:同一个视觉特征在类别A里是正样本,在类别B里又是负样本。
我的解决路径是定义优先级规则。从产线处理角度看,头发丝导致面板报废的概率远大于小黑点,因此只要ROI中存在长条状纤维,就统一标为头发丝类;小黑点只在单独出现时标注为小黑点。这样虽然损失了一部分“同时存在”的信息,但换来了标注一致性和分类稳定。如果后续有需要,可以把这个问题升级为多标签分类,但660张图撑不起多标签模型,不要硬做。
5.3 现象:增强把小黑点增广成了正常纹理
这是数据增强过头导致的翻车。亮度对比度增强范围开到0.4以上时,小黑点的对比度被大幅削弱,在增强后的图里看起来就像正常面板的轻微色差。模型训练时看到的是大量“假正常”的小黑点,学习目标被污染。
解决办法是把RandomBrightnessContrast的brightness_limit和contrast_limit控制在0.15到0.2区间,同时每次增强后抽样几张图目检。增强不是越狠越好,它要模拟真实生产条件的变化,而不是制造模型识别不了的极端样本。
5.4 现象:阈值设成0.5,误杀率高到产线不敢用
Softmax输出的概率分布在小样本模型上往往校准得很差。验证集里正常样本的预测概率可能是0.55、0.6,离1.0很远;缺陷样本的概率也可能只有0.4、0.5。直接取最大概率类别,等于把大量正常样本误判成缺陷。
这个现象在两类缺陷对比度低的情况下尤其严重。解决方法是部署时用阈值而不是argmax:只有缺陷类概率超过0.6或0.7才判为缺陷,低于阈值的全部丢回人工复检。具体阈值怎么定,第6章会给一个简单可行的校准脚本。
5.5 现象:把不同尺寸的ROI统一缩放到224×224,头发丝直接缩没了
ROI原始尺寸很不一致,有的是64×64小窗口,有的是256×256大图。直接Resize到224×224,大图里的头发丝会被压成几个像素宽的短线,原本连续的特征断裂成零散的边缘点,模型学到的是完全错误的结构信息。
我一般会在预处理里分两步处理:先按最长边等比缩到224,再在短边方向用灰色填充(letterbox),避免非等比缩放扭曲缺陷形态;或者对原始ROI尺寸做分组,小窗口用更短的边长输入,比如直接使用128×128。注意,推理时也必须使用和训练时完全相同的letterbox参数,否则特征的尺度又对不上。
6. 把模型部署到产线前的最后一公里:阈值校准与推理加速
6.1 用验证集做阈值校准,而不是直接采用0.5
模型训练结束后,我习惯先冻结模型,在测试集上把所有样本的softmax概率导出来,然后手动搜索一组阈值,让漏检率尽量降低的同时,误杀率不要超过产线复判岗位的承载力。
做法很简单:对每个样本,只有当头发丝概率大于th_hair且大于正常类概率时才判为头发丝;只有当小黑点概率大于th_dot且大于正常类概率时才判为小黑点;否则判为正常或进入人工复检。
import numpy as np from sklearn.metrics import confusion_matrix def calibrate_thresholds(probs, y_true, th_hair=0.5, th_dot=0.5): y_pred = [] for p, t in zip(probs, y_true): cls = int(np.argmax(p)) # 缺陷类概率不够高时,宁可直接判为正常/复检 if cls == 1 and p[1] < th_hair: cls = 0 if cls == 2 and p[2] < th_dot: cls = 0 y_pred.append(cls) return confusion_matrix(y_true, y_pred) # 在测试集上遍历不同的th_hair和th_dot th_hair_values = [0.5, 0.6, 0.7, 0.8, 0.9] th_dot_values = [0.5, 0.6, 0.7, 0.8, 0.9] for th_h in th_hair_values: for th_d in th_dot_values: cm = calibrate_thresholds(probs, y_true, th_hair=th_h, th_dot=th_d) fnr = (cm[1][0] + cm[2][0]) / cm[1:].sum() # 漏检率 fpr = (cm[0][1] + cm[0][2]) / cm[0].sum() # 误杀率 print(f"th_hair={th_h}, th_dot={th_d}, 漏检率={fnr:.3f}, 误杀率={fpr:.3f}")我实际测试的经验结果是:th_hair取0.7、th_dot取0.65左右,漏检率和误杀率能达到一个可接受的平衡点。但那是在我的数据分布下,你的数据需要自己扫一遍——不要直接抄别人的阈值。
6.2 一个可以直接改用的推理脚本
部署时的预处理必须和训练时严格一致。下面是能直接跑在工控机CPU上的推理代码,注意它使用了和训练相同的均值方差归一化。
import torch import torch.nn.functional as F from PIL import Image from torchvision import transforms device = "cuda" if torch.cuda.is_available() else "cpu" model = get_model(num_classes=3, backbone="resnet18", pretrained=False) model.load_state_dict(torch.load("best_model.pth", map_location=device)) model.eval().to(device) infer_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]), ]) def predict_roi(image_path, th_hair=0.7, th_dot=0.65): img = Image.open(image_path).convert("RGB") img_tensor = infer_transform(img).unsqueeze(0).to(device) with torch.no_grad(): logits = model(img_tensor) prob = F.softmax(logits, dim=1)[0].cpu().numpy() # 顺序:0=正常, 1=头发丝, 2=小黑点 pred_idx = int(np.argmax(prob)) if pred_idx == 1 and prob[1] < th_hair: pred_idx = 0 if pred_idx == 2 and prob[2] < th_dot: pred_idx = 0 label_map = {0: "normal", 1: "hair", 2: "dot"} return label_map[pred_idx], prob这段代码里,convert("RGB")是为了统一通道数。AOI相机如果是黑白图,读出来后是单通道,必须转成RGB,否则预训练模型的第一层卷积会报维度不匹配。另外注意,best_model.pth保存的应该是state_dict,而不是整个模型对象,这样换backbone时不需要重新训练数据。
6.3 产线部署的最后一个技巧:给分类器加一道前置候选框过滤
在实际产线上,AOI送过来的ROI里可能有很大一部分是边缘噪声、走线纹理、或者拾取工具留下的轻擦痕。这些样本既不完全是正常,也不完全是缺陷,直接送进分类器会让置信度变得很低。我的习惯是在分类器前面加一个轻量的前置过滤规则,把明显正常的ROI直接放行,只把可能缺陷的ROI送进分类网络。前置规则可以很简单,比如计算ROI的灰度标准差、对比度、梯度幅值均值,超过某个下限才送入分类器,否则直接判为正常。
这个技巧对推理耗时的收益很明显。比如你在一张面板图上提取出了500个ROI,真正值得分类器看的可能只有50个,省下来的时间相当可观。而前置过滤的计算量几乎可以忽略,几十行OpenCV算子就够了。更重要的是,它把分类器的任务聚焦到真正的难题上,让置信度阈值更有意义。
这个660张样本的项目教会我一件事:小数据集不代表不能做深度学习,但它逼迫你把每个环节都做扎实。数据划分要慎重,阈值必须校准,增强不能过头。把这一步一步踩稳,模型在产线上能顶住的时间,远比盲目堆数据量和模型参数量要长。希望这份梳理能帮到正在和头发丝、小黑点较劲的你。
本文还有配套的精品资源,点击获取