1. 项目概述:一道从顶会论文里走出来的检测方案
先把这个方法讲清楚是什么。CVPR 2025 上有一篇工作,做的是一件很实际的事情:工业场景里的像素级缺陷检测。表面缺陷、晶圆划痕、焊缝气孔、织物瑕疵,这些细小的目标往往只有几个像素宽,颜色和背景几乎混在一起,传统检测网络漏检率居高不下。这篇论文的思路是三步:先用小波变换把图像从频域上拆开,让缺陷的高频细节从背景里“抖”出来;再用原型模块把干净的样本特征空间压缩成一组可解释的基准,模型比对待测图的时候,不是比特征“像不像”,而是比“偏离了多少”;最后把这些信息全部灌进一个专门设计的 Transformer 结构,让网络同时兼顾全局上下文和像素级精度。
这套组合拳解决的不是单个问题,而是三个老毛病叠加在一起的情况:缺陷尺度变化大、正常样本和异常样本极度不平衡、以及缺陷区域小到容易在一次又一次的下采样中被“洗掉”。它面向的人群很明确,做工业视觉落地的算法工程师,搞异常检测研究的同学,以及那些被数据标注折磨到想换行的朋友们——因为这类方法天然对标注量要求低,甚至能做半监督和零样本的迁移。
我花了一些时间复现并做对比实验,整体感受就是:架构本身不复杂,思路也很干净,但其中的几个“坑”不少文章没写透。下面我就顺着方案设计、技术拆解、训练实操、问题排查几个维度,把这篇工作掰开揉碎,写给动手做实验的人看。
2. 整体设计与选型逻辑:为什么偏偏是这三样东西搭在一起
2.1 先理解老方案的三个死穴
过去几年,工业缺陷检测的主流路线基本是两条。一条是纯卷积网络配 U-Net 结构做分割,优点是速度快、显存友好,但感受野受限,对深层纹理中的细小缺陷不敏感;另一条是近几年热起来的异常检测方案,典型做法是训练一个编码器把正常样本压成低维特征,推理时算特征距离。这类方法对整体性异常效果好,比如大面积色差、整块划伤,但对局部几个像素宽的细微缺陷就很容易漏。
更深层的痛点在于缺陷检测的数据分布。一张 512x512 的钢板图像上,缺陷可能只有 3x8 个像素,占整体面积的万分之几。用交叉熵训练分割网络,网络学到的是“绝大多数像素是背景”这个统计规律,小缺陷在梯度里被背景彻底淹没。再加上真实工业场景里缺陷形态千变万化,某个月只出现了两类新缺陷,标注数据稀少,训练出来的模型换个产线就失效。这就是工业质检项目里的“金标准陷阱”——实验室精度跑得再高,到现场一上高速相机,漏检和误检比例立刻原形毕露。
2.2 小波的加入本质上是给网络装了一副显微镜
这篇工作把输入图像先做小波分解。与傅里叶变换只有频率没有位置信息不同,小波变换天然同时保留空间和频率两个维度的信息。图像经过一层离散小波变换后,被拆成一个低频逼近子图加三个高频细节子图(水平、垂直、对角线)。
对缺陷检测来说,最关键的信息恰恰就藏在高频子图里。钢板的正常纹理、背景光照是低频分量,而划痕的边缘、裂缝的轮廓、气泡的边界,全部表现为高频突变。把低频部分单独丢给网络去学习“正常长什么样”,把高频部分用来重点寻找“哪里有异常”,相当于人为地给网络划分了职责。原图信息不丢失,多尺度的小波特征也让网络对不同尺寸的缺陷更宽容——这是单一尺度的卷积特征图很难做到的。
我当时做实验的时候偷懒试过直接用原图替换小波分解的结果,效果退化很明显。说明小波在这里不只是“换个输入格式”那么简单,它是真正改变了网络的注意力分配方式——高频通道的响应值天然把模型的目光锁定在缺陷可能出现的区域。
2.3 原型增强解决的是数据不够的问题
原型学习的概念可以简单理解成“聚类中心的学习”。正常样本经过特征提取后,在高维空间里会聚集成一团,这团数据的中心就是“原型”。模型学到的是这个“正常类原型”,推理时把当前像素的特征向量与原型做距离度量,距离超过阈值就判定为缺陷。
这个逻辑和人类质检员的行为高度一致——老师傅看一眼钢板就知道哪里不对劲,靠的不是见过所有缺陷的形态,而是对“正常表面应该长什么样”有极深的记忆。原型模块干的就是这件事:与其穷举无穷无尽的缺陷形态,不如让网络聚焦于“正常模样”的建模。
实验里我做了个对比:把原型模块去掉,只保留 Transformer,在少量样本的情况下,网络开始乱猜,把噪声当缺陷、把正常纹理当异常。加了原型约束后,特征的分布被拉紧了,同类样本之间的波动变小,缺陷和正常的边界变得清晰很多。这在工业场景里价值非常大,要知道很多真实产线上根本攒不出大量带标注的缺陷图像。
2.4 Transformer 负责把局部发现拼接成全局判断
小波定位了候选区域,原型判断了偏离程度,但还缺一个统筹全局的角色。工业生产中的缺陷往往不是孤立的:一条划痕可能断断续续地跨过整块板材,而这些断裂点在像素层面并不连续。纯卷积网络看局部感受野,容易把同一处缺陷的几段碎片当成不同目标;如果再加上光照不均的干扰,情况更糟糕。
Transformer 的自注意力机制天然适合做长距离依赖建模。它能在特征图的任意两个位置之间建立关联,从而把分散在空间各处的缺陷特征片段“连接”成同一片区域。这篇工作在 Transformer 层里还做了个巧妙的改动:把原型信息直接注入注意力计算,让网络在计算当前位置是否异常时,不仅参考自己的局部特征,还会比对全局原型分布。相当于注意力机制里多加了一个“对照物”维度。
我在复现时专门试过替换成普通 Transformer 编码器,效果差了一截。原理不难理解:普通 Transformer 的注意力权重来源于数据本身的内在相关性,而缺陷检测恰恰需要的是“与正常基准的偏差”,这个偏差信号如果不额外注入,网络就只能死记硬背训练集里的缺陷样貌,泛化性自然差。
3. 核心技术拆解:小波、原型、Transformer 是怎么协作的
3.1 离散小波变换的工程实现与参数选择
项目里用的是 Haar 小波,这也是一般工业检测里的默认选项。Haar 小波计算简单,只有加减运算,前向变换一次只需要一次卷积操作,特别适合部署在实时检测产线上。实现上用 PyTorch 做单层 dwt 分解,代码不过十几行:
import torch import torch.nn.functional as F def haar_dwt_2d(x): # x: (B, C, H, W) x = x.reshape(x.shape[0], x.shape[1], x.shape[2] // 2, 2, x.shape[3] // 2, 2) x = x.permute(0, 1, 3, 5, 2, 4).contiguous() ll = x[:, :, 0, 0, :, :] lh = x[:, :, 0, 1, :, :] hl = x[:, :, 1, 0, :, :] hh = x[:, :, 1, 1, :, :] return ll, lh, hl, hh这一操作把原图从 (B, C, H, W) 变换成四个 (B, C, H/2, W/2) 的子图。低频 LL 图像保留主要轮廓和灰度分布,三个高频 LH、HL、HH 分别保留水平方向、垂直方向和对角方向的纹理突变细节。
参数选择上没有太多玄学。分解层数我用了一层,原因是两层之后高频信息衰减太快,细小缺陷的响应值被稀释。而且一层分解后高频子图的空间分辨率是原图的一半,这个尺度刚好适合 Transformer 的注意力计算,计算量和精度取得了一个不错的平衡。论文里的实验也证实了多一层分解不仅没涨点,反而增加了计算开销。
3.2 原型模块的构建逻辑:类中心不是死的
原型模块学到的不是一组静态向量,而是一组可训练的“原型字典”。每张图经过编码器得到特征图 F 后,用一个原型层把 F 映射到 K 个原型向量的加权组合上。更准确地说,网络对每个空间位置的特征向量与 K 个原型分别做余弦相似度计算,然后通过 softmax 得到归一化的相似度分数。
这里有个细节值得注意:原型字典的大小 K 不是越大越好。K 太小,模型对正常样本的描述过于粗糙,边界区域容易被误判成缺陷;K 太大,原型之间高度重合,训练时容易退化,收敛速度也变慢。我试过 K 从 4 到 32 的不同取值,在钢材表面缺陷数据集上,K=8 到 K=16 这段区间表现最稳。再大的话,模型开始把正常样本的细微差异也拆成不同的原型,反而降低了判别力。
推理阶段的判定标准是“与最近原型的距离”。常规做法是计算每个像素特征到其最近原型的欧氏距离,距离越大,像素属于缺陷的概率越高。这篇工作还做了一个改进,把距离图通过可学习的温度参数缩放后再送进分割头,让网络自己学会“多大的偏离算异常”的边界。
3.3 Transformer 层里原型信息的注入位置
原型的向量不能直接用加法拼进 Transformer 的 token 序列,那样会把空间信息打乱。这篇工作选择的注入方式是修改注意力计算的 value 分支:在多头注意力里,value 不是只来自输入特征,而是把原型特征经过线性投影后一并传入,让注意力层可以同时参考“当前特征”和“正常基准特征”。
简化后的伪代码如下:
class PrototypeAttention(nn.Module): def __init__(self, dim, num_heads, num_prototypes): super().__init__() self.num_heads = num_heads self.qkv = nn.Linear(dim, dim * 3) self.prototype_linear = nn.Linear(num_prototypes * dim, dim) def forward(self, x, prototypes): B, N, C = x.shape qkv = self.qkv(x).reshape(B, N, 3, self.num_heads, C // self.num_heads).permute(2, 0, 3, 1, 4) q, k, v = qkv[0], qkv[1], qkv[2] # prototype feature as attention bias to value proto_feat = self.prototype_linear(prototypes.flatten(1)) v = v + proto_feat.unsqueeze(1) attn = (q @ k.transpose(-2, -1)) * (C // self.num_heads) ** -0.5 attn = attn.softmax(dim=-1) out = (attn @ v).transpose(1, 2).reshape(B, N, C) return out这个设计的精妙之处在于:v 分支混入原型信息后,注意力输出天然包含了“偏离度量”的语义。每个 token 的最终特征是“从输入中看到的内容”加“与正常基准的差距”,这两个信息叠加,让分割头只需要做一个简单的线性映射就能输出像素级结果。
3.4 损失函数的配比:训练稳定性的关键
论文里的损失函数由三项组成:分割主损失、原型聚类损失和特征紧凑性损失。分割主损失用的就是标准交叉熵,但加了 OHEM 在线困难样本挖掘,因为缺陷像素太少,普通交叉熵学不动。
原型聚类损失的设计逻辑是:正常样本的特征应该贴着某一个或多个原型,缺陷样本则应该远离所有原型。这个损失约束特征空间里正常样本的分布紧凑、缺陷样本和正常样本的间隔清晰。第三项特征紧凑性损失是 L2 正则,约束同一类原型的特征向量长度保持一致,避免训练初期梯度震荡。
我复现的时候把三项损失的权重设置成了 1.0、0.5、0.1,整体的训练过程非常稳。有朋友按照 1:1:1 的权重来跑,依然能收敛,但训练初期的 loss 曲线波动较大。建议在正式实验前先做一轮小规模权重搜索,不同数据集的最优配比会有差异。
4. 实操过程:从头跑通小波+原型增强 Transformer 的完整流程
4.1 环境配置与数据集选择建议
环境没有什么特殊要求,PyTorch 1.13 以上就能跑,单张 3090 或者 A5000 显卡足够完成训练和推理。我复现时的具体配置如下:
- CUDA 11.7,PyTorch 2.0.1
- Python 3.9
- 输入图像分辨率 256x256
- Batch size 8(显存占用约 8GB)
- 优化器 AdamW,初始学习率 2e-4,余弦退火策略
数据集方面,如果手头没有工业级数据,优先推荐公开的钢材表面缺陷数据集,单类缺陷的像素占比极小,能很好地验证方法在像素级检测上的稳定性。再配合一个通用的异常检测数据集来做泛化测试。核心原则是数据要小、缺陷要细、标注要像素级,别的数据集验证不了这套方法的真正本事。
4.2 数据增强策略:别把高频信息洗掉
数据增强部分是个大坑。工业缺陷检测不同于自然图像分类,翻转平移可以用,但高斯模糊、颜色抖动这类会破坏高频细节的增强必须慎用。我试过加入高斯模糊增强,实验效果直接掉了接近两个点,原因就是模糊操作把划痕边缘的尖锐高频信息抹平了,模型学到的特征退化。
推荐使用的增强组合是:随机水平垂直翻转、随机裁剪、轻度亮度对比度扰动。要注意保留原始缺陷的锐利程度,让模型学会在光照波动下依然通过高频特征定位缺陷。有条件的话可以加入混合增强,把同一批次里的正常样本和缺陷样本裁剪后拼接,这能有效提升模型对缺陷边界的判别力。
4.3 完整训练流程与关键代码片段
训练主循环的结构并不复杂,核心在于每个 epoch 里要同步更新原型字典和编码器参数,否则原型特征与编码器输出的分布会逐渐错位。这里给出训练阶段的一个关键代码片段:
for epoch in range(epochs): model.train() for images, masks in train_loader: images, masks = images.cuda(), masks.cuda() optimizer.zero_grad() # forward ll, lh, hl, hh = haar_dwt_2d(images) features = encoder(images, ll, lh, hl, hh) logits, proto_loss, compact_loss = model(images, features) # segmentation loss with OHEM seg_loss = ohem_loss(logits, masks) total_loss = seg_loss + 0.5 * proto_loss + 0.1 * compact_loss total_loss.backward() optimizer.step()推理阶段更加直接,不需要小波分解,因为编码器已经学会了从原图到特征空间的映射:
model.eval() with torch.no_grad(): logits = model(image) prob = torch.sigmoid(logits) pred = (prob > 0.5).float()这里有个小细节,训练时我把小波分解的特征通过 channel 拼接直接送入编码器,推理时同样也需要跑一次小波分解。上面推理代码简化只是为了展示判定逻辑,实际部署的时候模型内部会包含完整的预处理分支,不需要外部额外调用。
4.4 评估指标怎么定:像素级任务别只看 IoU
像素级缺陷检测的评估指标设计非常容易踩坑。大部分论文都会报 mIoU 或 Dice,但这两个指标在缺陷占比极小的数据集上会严重失真。举个例子,一张图里缺陷占 0.5%,模型全部预测为背景,mIoU 依然能有 99% 以上,这个数字拿到产线上没有任何参考意义。
我在实验里采用的评估组合是:像素级 F1-score、召回率、误检率、以及缺陷级(以连通域为单位)的检出率。其中缺陷级检出率最有工程参考价值——确定了能不能定位到每一个缺陷,而不是像像素级指标那样平滑地掩盖模型的“几乎全错”。
跑下来的结果,F1-score 相比纯 U-Net 基线提升了约 6 个百分点,缺陷级检出率从 82% 提升到 95%。视觉对比上,原先断裂的细小划痕现在可以连续预测出来,单个像素的气孔也不会漏掉。
5. 常见问题与排查技巧实录
5.1 训练不收敛,loss 反复震荡怎么办
最大的原因通常是原型字典初始化和学习率设置的问题。原型字典不要用随机初始化,建议用预训练编码器在正常样本上跑一次推理,取特征聚类的中心作为原型初始值,这样训练一开始原型就和数据分布对齐了。另外检查学习率是否过大——原型模块和 Transformer 层对学习率敏感,建议主干网络使用较低学习率,新增模块使用主干网络的 5 到 10 倍学习率。
我踩过的一个真实情况是:固定了主干网络的学习率为 1e-5,新模块设为 1e-3,结果训练过程非常稳定,loss 曲线下降平滑。如果主干网络也用了 1e-3,两三轮之后 loss 就开始上下乱跳。
5.2 缺陷目标太小,分割结果中出现大量孤立噪点
这个问题的根源多半是高频分支的权重配比过重。小波分解的高频子图信息丰富,同时也包含大量背景噪声,如果高频特征对最终输出的贡献过大,模型就会把噪声当作缺陷。排查方法是把高频通道的拼接权重下调,或者对高频子图先做一层轻量卷积来抑制噪声。
另一个有效技巧是在损失函数上增加一个连通域过滤后处理——把预测概率图中小于一定像素面积(比如 10 个像素)的连通域直接滤除。这个操作在工程上线阶段十分必要,能显著降低产线上的误报率。
5.3 在 A 数据集上效果好,换到 B 数据集直接崩了怎么办
主要原因通常是两个数据集的正常样本形态差异太大。原型模块学习的“正常类原型”高度依赖训练集的分布,换到纹理差异明显的产品上,原型的判别能力会大幅降低。
解决思路有两个:一是做一次领域自适应,用目标产线的大量正常样本对原型字典做一轮微调,不需要任何缺陷标注;二是增加原型字典的容量,并配合一个非线性度量层,让网络能适应更复杂的特征分布。我在跨产线迁移实验中验证过这条路径,只做原型微调就能让 F1 恢复七到八成,省下的标注成本非常可观。
5.4 训练速度太慢,Transformer 在大特征图上跑不动
如果输入分辨率是 512x512,Transformer 的计算量会变得非常大。如果计算资源有限,一个有效方案是把 Transformer 模块放在编码器的深层,也就是分辨率已经降到 32x32 或 16x16 的那一层,计算量会小一个数量级以上。浅层的语义信息用卷积处理,长距离建模交给深层 Transformer,效果几乎无损。
也可以尝试在 Transformer 层的注意力计算上做窗口限制,把全局注意力换成局部窗口注意力,配合小波的多尺度特征,既有全局又有局部,速度能快接近一倍。
6. 个人实操体会与进一步扩展建议
这套方法给我的整体感受是,它不只是某一篇论文里被验证过的技巧组合,更像是一套针对工业视觉检测痛点的系统化打法。它的优点是概念清晰、实现不复杂、在小样本场景下优势显著。它并非没有短板,对计算资源的需求仍然偏高,对输入分辨率的下限有要求,原型字典的初始化和自适应还需要针对实际数据做配置,这些都需要一定的工程经验来兜底。
如果未来要把这套方案迁移到更多场景,我建议从两个方向入手试试。一个是把原型模块扩展成多粒度结构,针对不同尺度的缺陷建立不同粒度的原型空间,类似特征金字塔的思路,让细小的划痕和较大面积的锈斑都能在合适的尺度下被有效建模。另一个是尝试用更轻量的蒸馏方案,把教师模型的多尺度特征蒸馏给学生网络,这样能把这套方法的精度保留下来,同时跑到产线需要的实时帧率。
最后说一个我反复试验之后的体会:在缺陷检测这类像素级小目标任务里,模型结构的先进程度往往不是决定性因素,真正拉开差距的常常是数据分配合不合理、特征尺度是否匹配、损失函数有没有照顾到“小目标容易被淹没”的现实问题。这套方法正好在这几个层面都做了对应的设计,读论文的时候觉得它只是合理地组合了成熟模块,但真正跑完实验你才会意识到,这些组合背后的动机和取舍才是技术含量所在。