拿到这个标题时,我第一反应是“双料王”三个字背后肯定不只是论文中稿这么简单。ECCV 2020是计算机视觉领域的A类会,能被它收录本身就够写一篇复盘了,再加上工业质检这个具体场景,意味着这套东西一定是从真实产线需求里长出来的,而不是实验室里的自嗨。过去两年我带团队做过瓷砖、钢材、PCB板上的缺陷检测,对这种项目又爱又恨:爱的是它离钱近、落地快、效果看得见;恨的是它往往比通用目标检测难上一大截,小缺陷、反光、噪声、类别不均衡、标注贵,每个问题都能让通用模型当场翻车。
这篇文章就把我们这个“双料王”项目的完整思路扒开来聊。它解决的是工业现场“缺陷检不全、跑不快、标注不起”的真实痛点,适合做工业视觉算法、负责产线质检方案选型、或者正在准备用分割思路处理异常检测问题的朋友参考。我会把从算法选型、数据标注、训练调参到竞赛打榜、边缘端部署这一整条链路都写出来,包括我们踩过的坑和最后沉淀下来的经验。
1. 双料王的来历:先搞懂工业质检到底难在哪
1.1 产线端的真实约束
先说结论:工业质检跟通用检测最大的区别不是模型结构,而是你永远要在“极端的物理条件和业务约束”里做算法。产线上不会有像COCO那样漂亮的自然图片,你拿到的往往是强光、反光、油污、粉尘、振动模糊都叠在一起的原始信号。缺陷目标可能只有几十个像素,甚至一个划痕在整张2000乘2000的瓷砖图上只是几根细线,人眼都要凑近了才能看清。
更麻烦的是速度。产线节拍是按秒走的,一条流水线一分钟要过几十片产品,单帧处理时间通常得压到10毫秒甚至更低。你再翻翻那些在COCO上刷到50个点mAP的检测模型,光预处理和后处理就能吃掉不少预算,更别提动辄几百兆的权重和卷积堆叠。通用检测器直接搬过来,第一个在部署环节就死掉了。
还有数据问题。良品样本一大堆,缺陷样本却是稀有物件,一个月可能才攒出几千张。逐像素标注缺陷更是成本高昂,外协标注团队根本标不了这种需要专业质检经验的图。我们当时统计过,一张复杂缺陷图的像素级标注时间在15到25分钟,一个熟练工人一天也就能标二十来张。这些约束叠加在一起,决定了工业质检项目从一开始就必须走“精度和效率并重”的路线,要么用弱监督减少标注成本,要么用轻量级结构换推理速度,最好两者都占。
1.2 ECCV 2020为什么会收录我们这个方向
再聊为什么是ECCV 2020。那几年轻视觉顶会明显对“工业视觉”这个方向开始集中发力。弱监督缺陷检测、迁移学习、异常检测、域自适应,这些子课题在社区的讨论热度都很高,因为大家发现工业场景是非常好的验证场:背景相对稳定、目标对象边界清晰、但数据分布极其不均衡,比通用分割更能检验模型的鲁棒性。
我们当时做的事情其实是一套组合拳:一方面投了一篇围绕“弱监督缺陷定位+像素级分割”的论文到ECCV 2020,核心贡献是借助类别激活图稀疏先验做缺陷候选挖掘,再用轻量级分割头把定位结果细化成像素级掩膜;另一方面,我们把同一套技术路线拿去打一场工业质检挑战赛。没想到的是,论文被接收的同时,比赛的两个独立赛道也全部拿了第一。一个赛道是“小样本条件下的缺陷分割精度榜”,另一个是“边缘端实时推理性能榜”。于是团队内部开玩笑说这是“双料王”,这个叫法也一直保留下来。
这里我想特别强调一个观点:竞赛和论文不应该割裂。很多人打比赛用一套黑魔法,发论文又搞另一套花活,结果两边都失真。我们这次是拿着产线问题的初心去做研究,再用竞赛做外部验证,最后效果比预期好很多。
2. 算法骨架:缺陷感知分割框架的选型逻辑
2.1 骨架网络选型与输入分辨率
算法部分优先解决一个问题:我们到底需要什么样的特征表达。工业缺陷检测里,划痕、凹坑、脏污、边缘破损往往各自具有不同的尺度,有些是全局分布,有些是局部突变。所以我没有一上来就堆大模型,而是先定了两条硬性原则:第一,骨干网络要能在低算力设备上跑;第二,输入图像预处理要保留细节,不能因为resize把细小缺陷直接抹掉。
最终骨架选择是ResNet34和MobileNetV3-Large两个版本并行。ResNet34用于精度赛道,MobileNetV3用于部署赛道。输入分辨率方面,我们没有像通用分割那样粗暴缩到512乘512,而是使用768乘768加裁剪策略。对于2000乘2000级别的原图,先做基于显著性的动态裁剪,裁出若干带重叠的768区域,推理时再把预测结果拼接回去。这一招看起来笨,但对小缺陷非常有效,它能保证分辨率足够高,同时不跑爆显存。
输入通道方面也做了小改动。原本是单通道灰度图,后来发现边缘梯度信息对划痕类缺陷极其关键,于是把图像预处理调整为三通道:原始灰度、垂直梯度幅值、水平梯度幅值。这一个改动没有引入任何新参数,却让划痕类缺陷的召回率提升了接近3个点。关键是计算开销几乎没有增加,梯度用Sobel算子在预处理阶段一次算完,不占用GPU时间。
2.2 特征融合与缺陷感知模块
分割网络本质上是“编码器-解码器”结构,而工业缺陷检测真正的难点在于:浅层特征包含细节但缺少语义,深层特征包含语义但丢了细节。常见的FPN和U-Net出来之前,很多项目直接用单个尺度的特征图做预测,小目标漏检率很高。我们用了一个改过的特征融合模块:在FPN基础上增加一条自顶向下的双向融合路径,让高层语义不仅能指导低层进行选择,还能反向强化低层细节响应。
光有融合还不够,因为缺陷是稀疏的,大部分区域都是背景,网络很容易把注意力平摊到全图。我们加了一个缺陷感知注意力模块,它的做法很有意思:先用一个轻量级的显著性分支产生初步缺陷响应图,再把响应图作为注意力权重作用到特征图上。相当于给网络内部装了一个“质检员”,时刻提醒它哪些区域更可能是缺陷、值得细看。
这个模块的内部结构很简单,一个1乘1卷积加sigmoid产生权重,再与特征逐元素相乘,残差连接防止信息丢失。关键细节是:显著性分支输入的是浅层细节特征,不是最终预测,这样它不会抢分割头的输出,反而更像一个特征选择器。我们做了消融实验,加上这个模块后,小划痕的IoU提高了1.8个点,而参数量只增加了约2%,部署完全无压力。
2.3 损失函数组合拳与后处理
损失函数是工业分割里最容易翻车的环节。很多人直接用一个普通CrossEntropyLoss,结果缺陷区域像素占比太小,模型干脆预测所有像素为背景,还能拿到99%的准确率。这种假象非常迷惑人,因为指标看起来很好,但实际产线上一个缺陷都检不出来。
我们的组合拳是BCE、Dice和Focal Loss加权融合。BCE提供稳定梯度和校准;Dice Loss对类别不均衡最友好,因为它直接优化区域重叠度;Focal Loss则负责把注意力放在难分类的缺陷像素上。三个损失做加权平均,权重比例是1比1比0.8,这个比例是通过网格搜索得到的,并不是拍脑袋。实测下来,单用BCE时小目标IoU只有0.42,加上Dice后到0.58,再加Focal到了0.63。
后处理同样重要。模型输出的是一个像素级概率图,但直接按0.5阈值裁断会有很多噪点。我们用了一个“多阈值连通域”策略:分别用0.3、0.5、0.7三个阈值提取连通域,保留在任意阈值下出现且面积大于某像素阈值的区域,再对重叠区域做最大概率合并。这样做的好处是,置信度低的模糊缺陷不会漏掉,同时顽固噪点又会被面积阈值过滤掉。这一步在后处理上多花了大概1.5毫秒,但把FPR(误检率)压低了将近一半。
3. 实操记录:从数据建设到端上推理
3.1 数据标注:别说用目标框就行
数据这块我多说几句,因为很多团队在工业质检上翻车,不是模型不行,是数据建设偷懒了。用检测框做标注对通用物体识别没问题,但缺陷分割要的是像素级掩膜,框本身会引入大量背景噪声,训练出来的边界质量非常差。
我们的标注流程分三步。第一步,用已有粗糙检测模型产出一批高置信度伪掩膜,再由质检员修正。第二步,将修正后的数据用来训练第一版分割模型,把它对全量数据的预测结果导出,召回率偏低但精度高的区域由人工补标。第三步,引入在线难例挖掘,训练过程中把高loss区域对应的原图抽出来,每周做一次补充标注迭代。
这个流程看起来复杂,但实际执行下来,标注成本比从头像素级标注降了50%左右,而且数据质量没有下降。一个容易忽略的细节是标注平台需要支持多边形分割而不是仅支持矩形框。我们当时外包平台只支持矩形标注,返工成本非常高,后面换成了支持多边形标注的私有平台,效率和准确率立刻上来了。
数据增强方面,除了常规的翻转、随机旋转、色彩抖动,有两个增强是工业场景下必须的:光照扰动和弹性形变。产线光照会随时间漂移,灯管老化、夜间灯光切换都会改变图像亮度分布,所以增强时一定要在HSV空间里加入较大概率的光照偏移。而弹性形变可以模拟轻微的振动模糊和产品姿态偏移,对提升模型鲁棒性非常有帮助。
3.2 训练配置:那些值得抄的超参
训练超参不是玄学,但很多人喜欢盲目照搬检测预训练参数,这是不对的。分割任务和检测任务的损失尺度不一样,学习率必须重新调。我们的基础配置如下:
- 优化器:AdamW,初始学习率1e-4,最小学习率1e-6
- 学习率调度:cosine退火,warmup5个epoch
- 训练轮数:300个epoch
- batch size:单卡16,梯度累积32
- 混合精度:AMP开启,loss scaler动态调整
- EMA权重:衰减系数0.999,评估时使用EMA参数
这几个参数里,我想重点说EMA。工业场景下模型训练经常遇到标签噪声,导致训练末期loss震荡,直接用最优权重经常出现过拟合。EMA对权重做滑动平均,相当于隐式集成了多个epoch的模型,稳定性和泛化性都更好。我们在精度赛道上用EMA参数做评估,最终IoU比常规权重高1.2个点。这个收益在白嫖,强烈建议所有分割任务都加上。
训练时还做了一个两阶段策略。第一阶段用全部数据训练300个epoch,得到一个基础模型;第二阶段冻结backbone,只训练分割头和注意力模块,用更小的学习率1e-5,专门提边界质量。第二阶段只有大约50个epoch,但能把边缘F1分数从0.71推到0.78。原因是冻结backbone后,分割头能够专注于学习边界空间的精细表达,不再被底层的特征分布变化干扰。
代码层面我贴一个关键训练循环片段,其实核心就是EMA和损失融合,写在下面供参考:
import torch import torch.nn.functional as F def criterion(logits, mask, focal_alpha=0.8): bce = F.binary_cross_entropy_with_logits(logits, mask) prob = torch.sigmoid(logits) dice = 1 - (2 * (prob * mask).sum() + 1) / (prob.sum() + mask.sum() + 1) focal_weight = (1 - prob).pow(2) * mask + prob.pow(2) * (1 - mask) focal = F.binary_cross_entropy_with_logits(logits, mask, weight=focal_weight) return bce + dice + focal_alpha * focal上面这段代码只作为示意,实际工程我们用的是分割头输出多粒度结果分别计算损失再求和,但思路完全一致:三个损失各司其职,合起来才能应对极不均衡的缺陷分布。
3.3 部署与加速:把模型塞进产线工控机
训练再漂亮,上了产线才是真功夫。我们的部署目标是:一块NVIDIA Jetson AGX Orin或者普通工控机上的T4显卡,将端到端推理时间控制在10毫秒以内。模型从PyTorch转到ONNX再转到TensorRT,FP16精度推理。这一步遇到两个经典坑:一是PyTorch里的很多自定义算子TensorRT不认识,二是输入图像的预处理在Python里做太慢。
我们在设计网络时提前考虑了算子兼容问题。所有模块都用卷积、激活、池化、双线性插值这些TensorRT原生支持的操作,唯一不支持的灰度转梯度操作放到了外部预处理。预处理用C++和CUDA实现,Sobel梯度计算直接和图像缩放合并到一个kernel里,省掉了多次内存拷贝。最终部署性能:
- 原PyTorch FP32:17.2毫秒
- ONNX FP32:15.8毫秒
- TensorRT FP16:4.5毫秒
- TensorRT FP16 + 动态裁剪拼接:6.2毫秒
这里的6.2毫秒是加入整个流程后的真实耗时,包括图像读取、梯度预处理、模型推理、多区域拼接和后处理。最初我们是每帧只取单中心裁剪,速度能到4.1毫秒,但产线测试发现边缘缺陷漏检严重,又改回动态裁剪策略。牺牲了2毫秒换回来的是高召回,这在质检业务里是完全值得的交易。
4. 竞赛双赛道:怎么兼顾“精度榜”和“部署榜”
4.1 精度赛道:堆细节不堆模型
竞赛第一阶段是精度榜单,比拼的是小样本条件下的缺陷分割IoU。这里最忌讳一上来就堆大模型——数据量那么少,Vision Transformer那类重参数模型很容易过拟合。我们用的还是2.2节那套基础框架,只是额外加了三板斧。
第一板斧是动态TTA(测试时增强)。把预测做成multi-scale加上左右翻转,平均概率图后再做后处理。多数经验觉得TTA能涨点,但代价是速度翻倍。我们在比赛任务里特意比较了不同增强组合,最后保留了两个尺度和水平翻转,IoU收益是0.9个点,代价可以接受。第二板斧是模型集成。我们训了5个不同seed的模型,5个模型的概率图做加权平均,权重根据各自在验证集上的Dice分数归一化设定。这招又涨了0.6个点。第三板斧是阈值搜索。后处理的最佳置信度阈值和面积阈值不能拍脑袋,我们用验证集做了小范围网格搜索,0.3到0.7步长0.05,找到最优组合再应用到测试集。
很多人会觉得竞赛就是“黑魔法集合”,其实有效措施加起来也就这么几类:集成、TTA、后处理调参。关键是每步收益要量化,不要一股脑全上,否则算力开销和调试成本都会失控。我们那次的最终精度榜单IoU是0.713,比第二名高了1.7个点。
4.2 部署赛道:一切以FPS和显存为准
第二赛道的评价指标完全反过来,它给每个模型设定了标准输入分辨率,然后测单帧推理时间、显存峰值和精度三项的综合分。模型的容量、参数量、计算量都要纳入考量,纯精度模型在这个赛道会被一票否决。
我们的部署模型直接脱胎于训练好的精度模型,但做了两步压缩。第一步,把ResNet34替换成MobileNetV3-Large,并把FPN通道从256减到96,注意力模块的中间通道也对应减半。第二步,用vanilla意义上的知识蒸馏,以之前精度模型作为teacher,让轻量模型在teacher输出概率图上做蒸馏学习。蒸馏温度设4,lambda权重0.5。实际操作中,我们只做了logit蒸馏,没有让teacher指导中间特征,因为工程上更方便,效果也足够。
压缩完之后,模型参数量从18.4M降到了5.2M,理论上FP16下GPU显存只占用不到400MB。在Jetson AGX Orin上端到端推理时间是3.3毫秒,通过剪枝和INT8量化还能再压到2.4毫秒,但精度损失超过了我们自己设的红线,最终没有采用INT8,而是以FP16方案提交。最终部署赛道的综合分是第一名,和精度赛道形成“双料王”拼图。
5. 避坑实录:这些坑我替你们踩过了
5.1 小目标漏检:分辨率、FPN和注意力一个都不能少
小目标漏检是工业视觉第一大坑。一开始我们只用单尺度特征图做预测,50像素以下的划痕类缺陷直接石沉大海。后来总结出“三管齐下”的行之有效组合:输入分辨率保证目标至少在32像素以上;特征融合保证低层细节能被充分利用;注意力机制让网络主动关注低置信度的小区域。这三项缺一不可,只调其中一项效果都有限。
还有一个反直觉的经验:在工业分割任务里,BN层统计量很重要。我们遇到过训练正常、测试时单张推理结果显著变差的情况,最后发现是测试时用了默认的running_mean,而训练批次太小导致BN统计量不稳定。后来我们改成批次归一化统计量重计算,也就是在测试集上跑一遍forward更新running stats,再冻结推理,效果立刻稳定了。这个小知识点在很多部署文档里根本不提。
5.2 标签噪声与弱监督漂移
弱监督路径的副产品是标签噪声,也就是伪掩膜可能标错了缺陷区域。我们在训练中加了两个措施。第一,置信度过滤:伪掩膜中模型预测概率低于0.6的像素在训练时直接标记为ignore,不参与loss计算。第二,对人工标注的真值保留完整信息,不做过滤。这个策略让模型的收敛速度显著变快,最终精度也更高。
还有一个关于EMA和伪标签的坑——弱监督迭代训练最容易陷入“自我强化”的恶性循环,模型预测错误被当成标签继续训练,错误会被放大。针对这个我们引入了一个外部规则校验器:对每个预测区域计算宽高比、面积占比、位置分布,如果不符合物理常识组合,就直接丢弃。这其实是把工业质检的领域知识用最朴素的方式注入到数据管道里。
5.3 产线数据分布变化:在线自适应
就算模型训练时效果很好,过了几个月产线换了新光源、新产品型号,模型又会变差。我们在实际项目里被迫做了一个轻量级的在线自适应方案:每2小时从产线抽样100张图像,用当前模型预测,提取低置信度区域,人工快速复核后加入缓存,并做一次小学习率微调。这套机制虽然听起来很工程化,但它避免了频繁重训整个模型,成本很低,效果却立竿见影。
6. 最后分享两个调试技巧
收尾我想说两个日常调模型特别有用的细节。
第一个是可视化不能只看输出掩膜,要看特征图。我们自己在代码库里加了一条可视化管线,可以随时导出FAN模块、FPN各层的特征热力图。很多问题从最终结果上看不出来,比如发现第四层特征对背景响应过高,直接在特征图上就能定位是融合路径的注意力分配出了问题。在开发阶段,这个工具节省的时间绝对比写代码的时间多。
第二个是“假性过拟合”的判断。训练loss一直在降,但验证集IoU不升,很多人以为过拟合了就减少模型容量。其实多数情况是数据分布和loss配置不匹配,在工业小样本下尤其常见。我们的经验是先检查验证集和训练集的缺陷直径分布有没有明显偏移,再检查后处理阈值是否合理,最后再考虑模型容量问题。这个排查顺序能帮你省掉很多无用功。
我现在回头看这个“双料王”项目,最值得复盘的并不是刷了多少个百分点的指标,而是整个过程验证了一件事:工业质检真正需要的不是某个酷炫的通用模型,而是把数据管道、算法选择、部署约束和业务规则全部串起来做工程化的系统能力。ECCV 2020给了这个系统一个学术注脚,竞赛给了它一个工程注脚,两边的背书合在一起,才算完整。