☰
工业视觉小目标缺陷检测实战:RetinaNet+ResNeXt+FPN+PISA
2026/10/21 3:28:00 网站建设 项目流程

去年有一段时间我天天泡在工厂的视觉房里,被手机玻璃盖板缺陷检测这个项目折腾得不轻。手机正面那块玻璃盖板,看着平平整整、光洁透亮,产线上想自动找出它上面的划伤、崩边、气泡和脏污,远没有想象中容易。我们最终跑通的方案是RetinaNet + X101-32x4d骨干网络 + FPN多尺度特征融合 + PISA样本注意力训练策略,整套流程从数据清洗、标注规范、config配置到TensorRT推理加速都走了一遍。这篇文章把选型逻辑、实现细节和踩过的坑完整记录下来,给正在做工业视觉检测、尤其是小目标缺陷识别的朋友一个可复现的参考。

项目背景不复杂:某手机玻璃盖板代工厂的质检工位,原来靠人工目检,节拍一快漏检率就上去,工厂想换成基于深度学习的自动光学检测。盖板缺陷的特点是目标小、对比度低、类内差异很大,而且正负样本极度不均衡——一张完好盖板可能只有一两个缺陷框,背景占据绝大多数像素。这类场景用常规检测模型很容易出现两种情况:要么小缺陷漏检,要么误检满天飞。我们对比了好几类方案,最终落地了RetinaNet这条技术路线。

1. 项目背景与模型选型:为什么是RetinaNet + X101-32x4d + FPN + PISA

1.1 玻璃盖板缺陷检测到底难在哪

玻璃盖板的生产要经历切割、精雕、CNC、抛光、强化、丝印等好几道工序,每一道都可能引入不同类型的缺陷。划伤主要来自加工过程中磨粒和硬物接触,崩边常出现在边缘倒角位置,气泡多是在丝印油墨层里,脏污则可能来自环境粉尘或清洗残留。这些缺陷在成像系统里的表现差异极大:划伤是长条状、低对比度,在特定光照下才隐约可见;崩边是弧线形缺口,位置贴在盖板边缘;气泡小而圆,半透明,容易和灰尘混淆。

用传统机器视觉方案做,核心逻辑是阈值分割加形态学滤波,但产线光照稍微波动、产品批次不同,规则就不稳定。深度学习检测模型天然具备特征表达和自适应能力,问题核心变成了"如何让模型在复杂纹理背景下,把又小又淡的目标检出来"。这决定了我们在模型选型时不能只看公开数据集精度,还得综合速度、显存、部署成熟度来选。

1.2 从候选模型到RetinaNet:先定框架,再谈精度

我当时把主流检测模型拉了个表,挨个过了一遍。

方案优点缺点本项目适配度
Faster R-CNN + FPN两阶段精度高,候选框细化好推理速度偏慢,训练复杂中
YOLOv5 / YOLOv8部署方便,推理快小目标、密集目标表现偏弱中低
RetinaNet + FPN单阶段速度和双阶段精度平衡,Focal Loss硬扛正负样本失衡大目标定位精度略逊于级联结构高
Cascade R-CNN精度上限最高显存和算力消耗大,工业节拍紧张低

RetinaNet最大的价值在于Focal Loss。玻璃盖板缺陷检测的数据分布里,一个图上真正的目标可能就一两个框,负样本(背景框)动辄几十万。Focal Loss通过调制因子压低易分类负样本的损失贡献,把训练注意力引向难的、稀少的前景样本,这个特性和工业小目标场景完美匹配。相比之下YOLO系列虽然快,但它的anchor匹配和loss设计在极端小目标下明显吃力,我们拿着初版实测数据对比过,RetinaNet在小缺陷召回上高出好几个点。

选框架还有一个现实因素:可复现性。工业项目不像发论文,要能稳定复现、好量化、好调试。mmdetection对RetinaNet的支持非常成熟,PISA模块也有标准实现,社区用户多、踩坑记录丰富,出了问题好查。这一票投给RetinaNet,稳妥。

1.3 X101-32x4d与FPN:网络结构的选择逻辑

骨架网络我们对比了ResNet-50、ResNet-101、ResNeXt-101-32x4d和ResNeXt-101-64x4d。X101-32x4d本质上是ResNeXt-101,它把残差块里的3x3卷积替换成了32组、每组宽度为4的分组卷积,相当于在保持FLOPs和ResNet-101相近的前提下,让网络拥有了更丰富的特征表达能力。手工视觉检测里,浅层特征管边界和纹理,深层次特征管语义,玻璃盖板缺陷的"可辨识性"非常依赖细节纹理,比如细微划痕和玻璃边缘的反光干扰,所以需要骨干网络有足够强的特征提取能力。

FPN在这套系统里的角色是搭桥。它把Backbone从C2到C5的不同分辨率特征横向连接,逐层融合,输出P3到P7五个尺度。浅层P3保留高分辨率位置信息,适合找小划痕、小气泡,深层P6、P7语义更强,适合捕捉崩边这类跨度较大的缺陷。没有FPN,单层特征做多尺度预测会严重顾此失彼。

有个取舍细节值得说:X101-32x4d比ResNet-50推理大概慢30%,但精度收益直接体现在小缺陷召回上。我们初期想着产线节拍紧,想用ResNet-50算了,跑了一周效果不理想,换上X101后mAP直接提升1.5个点。最后从推理侧做TensorRT优化把速度补回来。工业项目里算力成本是一次性的,漏检返工的成本是持续性的,这笔账要算清楚。

1.4 PISA:为什么训练策略比模型结构更值得投入

PISA(Prime Sample Attention)是这套方案里最容易被忽略、却对我们结果影响最大的模块。它解决的不是"模型容量够不够"的问题,而是"训练时该重点学哪些样本"的问题。检测任务里,每个GT框周围有大量候选anchor,有的和GT高度重叠、定位很准,有的只是在附近蹭了个边。传统采样方法几乎平等对待它们,Focal Loss虽然按"难易"调整权重,但没有按"重要性"来分配。

打个比方,学生刷题如果平均用力,简单的题做一百遍还是简单题,只有把时间精力花在那些差一点就能做对的题上,考试提分才快。PISA干的就是这件事。它先对每个GT框,把所有匹配候选框按IoU从高到低排序,确定所谓层级局部排名(IoU-HLR),排在前面的框训练时被赋予更高权重;同时那些置信度不低但定位不准的困难负样本,也要拿出来狠练。回归分支再用IoU作为损失加权,让定位误差大、IoU低的样本也有学头,而不是直接忽略。

直观感受:加了PISA之后,模型对微小缺陷的定位稳定了很多,训练前期掉点少,后期收敛更快。后面我会详细说配置参数。

2. 数据准备与标注规范:工业质检的老大难

2.1 缺陷定义了才能标得准

深度学习模型的上限,很大程度由数据质量决定,这句老话在工业项目里尤其扎心。玻璃盖板缺陷标注最大的问题不是数量,而是"标准不一"。不同标注人员对什么是崩边、什么是脏污、边界在哪里,理解差异很大。我们前期砍掉重来的第一版标注就是因为这个原因:同一种缺陷在两个标注员手里框出来尺寸差一半,模型学着学着就歪了。

要避免这种情况,首先要在标注前把缺陷类型定义清楚。我们最终确定了六个类别:划伤、崩边、凹坑、气泡、脏污、油墨异常。每类都写了明确的形态描述和边界判定规则:

  • 划伤:长条状连续亮线或暗线,长度超过2mm才算,头发丝直径大小的细线要标;
  • 崩边:盖板边缘的弧形或直边缺口,必须贴近轮廓线,虚标的不要;
  • 凹坑:点状凹陷,多为抛光工序造成的低对比度浅坑;
  • 气泡:丝印层内圆形或椭圆形透明气泡,直径小于0.3mm不标;
  • 脏污:表面不规则块状污染痕迹,块状边缘用外接矩形尽量贴合;
  • 油墨异常:丝印区域内的油墨缺失、渗入或溢边,通常和logo、听筒网区域有关。

标注规范定完之后,还要有审核机制。我们的做法是标注团队先产出,再由一位有质检经验的工程师按10%比例抽检,抽检不一致的标注框全部退回重标。这个环节看起来费人力,实际上省掉了后面无数调参的时间。教训是:宁可多投入1天洗数据,不要少花1小时调模型。

还有一个细节:原始图像分辨率高(2448x2048),有些缺陷只占几十个像素。标注框太小的时候模型基本学不到信息,所以我们写脚本把面积小于9平方像素的GT框直接过滤掉,同时记录每一类缺陷的尺寸分布,后面配置anchor时有用。

2.2 增强策略:模拟真实产线光照环境

工业质检的数据增强不能照着公开数据集那种路线随便来。玻璃盖板有明确的方向性特征,例如划伤走向、边缘形态,翻转和旋转都要克制。我们实际用的是下面这套组合:

几何增强只做小幅度:水平翻转、垂直翻转、±15度以内的随机旋转、0.8到1.2倍的随机缩放。这里要注意,垂直翻转其实有争议,因为玻璃盖板的纹理一般是各向同性的,但如果我们定制了丝印图案,翻转会带来方向错乱。后来我们保留翻转是因为缺陷样本太少,收益大于风险。

像素级增强是重点。产线光照波动会导致图像整体亮度、对比度变化,甚至局部出现高光反射。我们对原始图像增加亮度扰动(±30%)、对比度扰动(±20%)和高斯噪声。效果最明显的是模拟局部阴影和镜头脏污——在图像上随机叠加小范围调暗区域,这样模型不会对某个固定位置的明暗变化过拟合。

少样本类别的增强我们用了copy-paste策略:把气泡、崩边这类稀有缺陷的真实裁剪块,透明叠加到干净盖板上,粘贴时做一点随机旋转和尺度变化,再手动合成对应标注框。这个方法简单但非常有效,气泡类别的AP从原来的40%出头拉到了70%以上。相比之下mosaic和mixup我们试过,效果并不明显,因为工业图像背景高度相似,合成过度反而模糊了缺陷边界,属于典型的"为了增强而增强"。

2.3 数据集划分与验证方案

数据集划分也有讲究。产线上不同批次的光照、产品型号可能不同,如果随机划分训练集和验证集,验证集里可能会混进和训练集来自同一批次的图片,造成虚假高分。我们的做法是按拍摄时间和产品批次划分:前10天采集的批次作为训练集,最后两天独立批次作为验证集和测试集。这样评估出的指标更接近线上真实水平。

最终数据集规模是训练集12000张、验证集1500张、测试集500张,其中约30%是带缺陷的正样本,其余是干净的负样本。负样本不是拿来训练的,而是在测试阶段用来评估误检率(FPI,即单张图误检框数)。工业场景下,我们更关心漏检率和误检率,而不是纯粹的mAP指标。

3. 基于mmdetection的核心实现:配置详解

3.1 环境搭建与基础配置

我们用的是mmdetection 2.x版本,PyTorch 1.10 + CUDA 11.1,两张3090(24GB显存)做训练。环境搭建的坑主要在版本对齐:mmdetection 2.x要求MMCV和PyTorch版本严格匹配,装错了会直接在import时报一堆算子错误。建议直接用官方镜像或按mmdetection的install文档走,不要自己发挥。

数据集组织按COCO格式,目录结构如下:

data/glass_defect/ ├── annotations/ │ ├── train.json │ ├── val.json │ └── test.json ├── train/ ├── val/ └── test/

COCO标注格式的好处是mmdetection直接支持,转成voc或其他格式反而麻烦。注意在json里把categories按我们六类缺陷重新编号,背景类不用写进去。

3.2 RetinaNet + X101-32x4d + FPN的配置细节

下面是一份核心config骨架,基本就是我们跑生产训练用的配置简化版:

model = dict( type='RetinaNet', pretrained='open-mmlab://resnext101_32x4d', backbone=dict( type='ResNeXt', depth=101, groups=32, base_width=4, num_stages=4, out_indices=(0, 1, 2, 3), frozen_stages=1, norm_cfg=dict(type='BN', requires_grad=True), norm_eval=True, style='pytorch'), neck=dict( type='FPN', in_channels=[256, 512, 1024, 2048], out_channels=256, start_level=0, add_extra_convs=True, num_outs=5), bbox_head=dict( type='PISARetinaNetHead', num_classes=6, in_channels=256, stacked_convs=4, feat_channels=256, anchor_generator=dict( type='AnchorGenerator', octave_base_scale=4, scales_per_octave=3, ratios=[0.5, 1.0, 2.0, 5.0], strides=[8, 16, 32, 64, 128]), bbox_coder=dict( type='DeltaXYWHBBoxCoder', target_means=[.0, .0, .0, .0], target_stds=[1.0, 1.0, 1.0, 1.0]), loss_cls=dict( type='FocalLoss', use_sigmoid=True, gamma=2.0, alpha=0.25, loss_weight=1.0), loss_bbox=dict(type='SmoothL1Loss', beta=0.11, loss_weight=1.0)), train_cfg=dict( assigner=dict( type='PISAAssigner', topk=10, iou_calculator=dict(type='BboxOverlaps2D')), allowed_border=-1, pos_weight=-1, debug=False), test_cfg=dict( nms_pre=1000, min_bbox_size=0, score_thr=0.05, nms=dict(type='nms', iou_threshold=0.5), max_per_img=100))

有几个关键点要单独说。

anchor的ratios我比默认多加了一个5.0。默认[0.5, 1.0, 2.0]是按自然图像通用物体设计的,但划伤这种细长条缺陷的长宽比经常在4:1甚至更高。如果anchor没有覆盖这个比例,匹配阶段就找不到合适的候选框,小划伤直接成了漏检目标。加了5.0之后,划伤召回率显著提升。这是纯经验值,建议做类似长条缺陷检测的朋友都检查一下自己数据的长宽比分布,再决定要不要调ratios。

octave_base_scale=4意味着基础anchor面积是4个像素平方,配合strides=[8, 16, 32, 64, 128],对应在原始图像上的anchor尺寸跨度从32像素到1024像素。我们的缺陷尺寸多在几十到几百像素,覆盖得比较合理。如果你的目标都是10像素以内的极小点,需要把octave_base_scale调小、甚至手动启用P2层。

注意:anchor匹配策略以及PISA的样本排序都依赖GT框质量,如果标注框普遍偏大或偏小,anchor匹配率会掉很多,调anchor之前先确认标注。

3.3 PISA模块的加入与参数解读

PISA在mmdetection里并不是额外加一个loss,而是替换掉RetinaNet的assigner和head类型。上面config里已经体现:assigner用PISAAssigner,bbox_head用PISARetinaNetHead。核心参数有三个:

  • topk:每个GT参与IoU排序的候选框数量,默认10。这个值太小会损失有效样本,太大会把低质量框也拉进来干扰排序,我们试过8和16,反馈不明显,默认10就好;
  • pisa_ratio:当前批次里有多少比例的检测样本会被视为"prime sample"参与重点加权,默认0.7。我试过0.3、0.5、0.9,0.7整体最优,0.9反而掉点,因为高置信度的简单正样本太多,梯度被稀释;
  • score_threshold:负样本中置信度高于这个值的一律视为困难负样本,训练时加大惩罚力度。

PISA只在训练阶段生效,推理时完全不影响速度,这一点对工业部署非常友好。另外注意,PISA和Focal Loss是配合关系:Focal Loss管"难易样本的权重衰减",PISA管"重要性样本的局部排名",两者叠加后训练曲线前期会更稳,后期mAP能多一两个点。

3.4 优化器、学习率与batch策略

检测框架里优化器几乎都是SGD,不要为了贪图省事换成AdamW。SGD+momentum的泛化效果在目标检测上依然最好,AdamW的优势主要体现在Transformer类结构上。我们的配置是SGD、momentum=0.9、weight_decay=0.0001,batch size=8(两张卡各4张),初始学习率0.005。

学习率策略用了500 iter的warmup,然后CosineAnnealing衰减到0,总共训练50个epoch。warmup的意义在于模型刚开始时head是随机初始化的,直接用0.005的大学习率会让反向传播产生剧烈震荡,前几百个iter先用0.1倍到1倍的小学习率起步,等梯度方向稳定后再放开。这个细节非常重要,我第一次跳过warmup直接训,loss直接崩到不收敛。

混合精度amp在训练稳定阶段再开,不要一上来就开。我们前期不开,训到第30个epoch把loss降到0.8左右之后,再切换到amp继续训练,这样既省显存又保持稳定。如果你一开始就用amp,很可能出现loss下降缓慢、小样本类别学不动的现象,排查起来很头疼。

4. 训练过程、调参与效果提升

4.1 训练曲线复盘:从震荡到收敛

第一版训练用的朴素RetinaNet + X101-32x4d + FPN,没有PISA。在验证集上mAP@0.5大约78.4,看起来还行,但漏检率集中在气泡和凹坑这两类小目标上。而且看训练曲线,loss在前期略有震荡,到了后期虽然降下来,但验证集AP在几个类别间波动很大,尤其气泡类一会高一会低,明显是训练注意力不集中。

第二版加入PISA之后,同样epoch下loss曲线前期平稳了很多,最终mAP@0.5到82.7。这个涨幅看着不算夸张,但分布很健康——所有类别的AP都有提升,尤其气泡从41.2涨到52.8。PISA的"关注重要样本"确实缓解了少样本类别被多数类淹没的问题。

训练效率方面,X101-32x4d的backbone比较重,输入图像resize到短边800、长边1333之后,单卡只能放4张图,两卡batch=8,一个epoch大约1400个iter,50个epoch大概需要一天半到两天,业界可接受。

4.2 调参的三个关键操作

整个调参过程里,对最终指标影响最大的三个操作按顺序排:扩展anchor ratios、放大输入分辨率配合crop训练、PISA参数微调。

扩展anchor ratios这项前面已讲,mAP提升约2.3个点。它的本质是让网络在匹配阶段"想得到"瘦长目标。类似地,如果你做的是圆形或方形点状缺陷,可能会觉得默认ratios够用,不用改。

放大输入分辨率配合crop训练是第二个关键。原图2448x2048,直接resize到1333长边,小划痕在图上只剩几个像素,基本等于消失。我们做了两段式方案:训练时先用1280x1280的窗口在原始图上随机裁剪若干块,再把这些块resize到合理尺度输入网络;推理时整图输入,但把最大长边放宽到1600、最小短边到1200。这个改动让小目标召回率从88%提升到95%,代价是训练时间增加30%左右。工业项目里显存够的话,都建议优先从输入分辨率下手,而不是迷信更复杂的网络。

PISA参数微调放在最后。我做了pisa_ratio=0.3、0.5、0.7、0.9的对比实验,0.7最稳;score_threshold默认0.7,没有太多调优空间。有一个经验:如果训练集里某一类缺陷特别少,可以把对应的分类损失权重稍微调大,或者直接在copy-paste增强里多贴几块这类缺陷,比调PISA参数更有效。

4.3 测试评估与推理部署

最终验证结果:mAP@0.5达到87.2,recall@0.5=97.4%,precision@0.5=92.1%,单张图误检框数FPI约0.6。工业标准下,漏检率控制在2%以内,过杀率(误检后被人工复判拒绝的比例)低于3%就算合格,我们达到了要求。

推理侧,我们先用mmdetection的pytorch模型测试,X101在GPU上单张图推理约80ms,明显赶不上产线节拍。随后转成TensorRT FP16,推理时间压到20ms左右,产线看板和多视角检测都能应对。TensorRT转换时有两个必须留意的点:一是FPN里面的上采样和concat算子,有些低版本TensorRT不支持,需要升级或者手写plugin;二是NMS的后处理最好放在TensorRT外面做,否则动态shape配置会非常痛苦。

部署输出做了一个检测结果JSON,包含缺陷类别、框坐标和置信度,再接入产线的MES上报系统。为了方便复判工位高效筛选,默认把score_thr设成0.05,宁可多出框也不漏框。这个策略在工业场景里非常实用,因为误检只是多花几秒人工确认,漏检才是真正的质量事故。

5. 常见问题与排查技巧实录

5.1 小目标缺陷漏检的排查清单

如果你也遇到小目标漏检问题,按下面顺序排查,千万别一上来就换模型:

  • 看看GT标注的尺寸分布:缺陷是不是很多都小于16像素?如果是,考虑增加P2层,或者在anchor设置里调低octave_base_scale;
  • 分析anchor匹配率:训练日志里如果正样本anchor数量极少,说明anchor设计或assigner阈值有问题,需要调ratios或放宽匹配阈值;
  • 检查输入分辨率:resize之后目标在图上还占多少像素?小于10x10基本学不到,建议放大输入或crop训练;
  • 验证增强是否破坏了小目标:旋转、缩放过度会让本来就小的GT变得更小,甚至被裁掉。

5.2 训练不收敛或震荡的排查

训练loss震荡是个让人头秃的问题。我们的经验是分四层排查:第一看学习率,batch小于8时lr不要超过0.005;第二看warmup,有没有开、步数够不够;第三看数据,尤其检查标注里是不是混入了大量错标框,错标对PISA这种基于IoU排序的方案影响巨大;第四看梯度,加grad_clip=35,能防止个别异常样本把梯度和loss带飞。

有一个我们踩过系统的坑:一开始用batch=4、lr=0.01训练,loss卡在1.5不动。后来把lr降到0.005并配上500 iter warmup,恢复正常。工业项目里batch size和lr一定要同步考虑,batch减半、lr也要相应减半,很多人的训练震荡其实都出在这个匹配关系上。

5.3 误检率压不下去的排查

误检多发在三种情况:一是把玻璃纹理、反光当缺陷;二是把丝印边缘、油墨过渡区当缺陷;三是把灰尘弄脏的镜头成像当缺陷。对于纹理反光,需要增加负样本的多样性,专门收集印有纹理但无缺陷的图;对于丝印区域,可以考虑做一块语义分割的后处理,把不在允许区域的检测结果屏蔽;对于镜头脏污,产线端要保持光源和镜头的定期清洁,算法再强也处理不了光学系统的物理干扰。

这里还有很多值得展开的细节:

现象可能原因排查方向解决方案
小缺陷漏检anchor匹配率低、输入分辨率不足看训练日志正样本数、可视化特征响应扩展anchor ratios、放大输入、crop训练
训练loss震荡学习率与batch不匹配、warmup不足检查lr、bath size、warmup iterlr降到0.005以下、加梯度裁剪
mAP不升反降过拟合、数据标注不一致、PISA参数不当对比train/val loss曲线、抽查GT可视化加强增强、清理标注、调pisa_ratio到0.5-0.7
误检率高缺陷纹理相似、负样本不足收集误检样本归因增加负样本、后处理屏蔽丝印区
TensorRT推理出错算子不支持、动态shape问题逐层调试ONNX节点升级TensorRT版本、NMS移出引擎
PISA加了没效果高IoU样本太少、标注框不齐检查GT与anchor的IoU分布清洗标注、调整topk、减少pisa_ratio

在产线共存了两个月之后,我个人最深的体会是:这套方案的难点真不在模型结构,而在于数据和工程细节的闭环。RetinaNet + X101-32x4d + FPN + PISA的组合,在今天看来不算新潮,但它DDL很稳、问题可排查、效果可复现,工业现场需要的就是这种"不花哨但可靠"的算子。如果你也在做类似的缺陷检测,建议一步不落地从数据清洗、anchor匹配、输入分辨率、PISA参数这几层开始推进,不要急着追最新的模型架构,先把这四件事做透,精度一定会给你惊喜。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询