真正让我决定动手改YOLOv11的,不是排行榜和论文复现,而是产线上质检组的一句话:"你们这套系统能把裂纹找出来,但分不清该返修还是该报废。"当时我用的还是官方YOLOv11s,效果不算差,裂纹类召回率也说得过去,可一旦要求按严重程度分级输出,模型就露馅了——轻度、中度、重度在特征空间里几乎被压成一团,分类头给出来的等级预测基本就是碰运气。后来我把CSP结构与PMSFA注意力机制融进网络,重新设计了检测头的分支和训练策略,才算是真正把这个"表面损伤严重程度检测与分类"的问题啃下来。
这篇文章适合正在做工业表面缺陷检测、质检自动化、或者想给YOLO系检测器加"分级能力"的人。我会从问题拆解、网络改造、数据集标注、训练踩坑、部署工程五个维度,完整复盘这次YOLOv11改进系列里的一个具体方案:CSP + PMSFA注意力机制融合。文章里不会只讲"我加了什么东西所以涨点了",更重要的是把每一步选型的为什么讲清楚,还有那些在论文里根本看不到的教训。
1. 表面损伤检测为什么不能直接拿YOLOv11硬用:三个现实瓶颈
1.1 小目标、低对比度、多尺度跨度大,三个问题凑在一起
表面损伤检测跟通用目标检测有个很大的差别:损伤区域往往不是"一个物体",而是"一片异常纹理"。以我最常遇到的裂纹为例,在1080P的产线采集图像里,一条细裂纹的宽度可能只占2到3个像素,长度却可能横跨几十甚至上百像素。这种细长条目标,在YOLOv11的下采样流程里很容易被稀释掉——尤其经过5次下采样到P5层时,一条本来就只有2像素宽的裂纹,特征响应已经弱到接近背景噪声。
更麻烦的是低对比度。金属表面的划痕和加工纹路在灰度上几乎没有差异,靠常规的RGB特征很难区分。你自己看数据也能发现,人眼在强光下都容易看漏的地方,让模型从像素层面去学,它很容易把"纹理正常的反射光"当作"损伤特征"。我把这个归结为表面损伤检测的第一个瓶颈:目标尺度极端 + 特征对比度低,两者叠加会让通用检测器的特征金字塔"平均化"处理失效。
第二个瓶颈是多尺度跨度。同一类损伤,比如凹坑,既可能是一个直径小于1毫米的针孔,也可能是一个直径超过10毫米的挤压坑。前者在P3层还能勉强看到一点响应,后者在P5层都已经占满整个感受野了。不同的尺度需要的语义层级完全不同。通用检测器习惯的做法是把多尺度特征直接concat或者相加,但这种方式对"同一类损伤的不同尺度"并没有做任何显式的语义对齐——网络学到的更多是"大凹坑"和"小针孔"是两种东西,而不是"它们本质上是同一类损伤的不同严重程度"。
第三个瓶颈是损伤内部的纹理多样性。同样是中度腐蚀,有的表现为均匀变色,有的表现为点状剥落,有的表面还覆盖着氧化层。类型内的表观差异甚至大于类型间的差异,这导致模型很容易在边界样本上摇摆不定。这一点在严重程度分级时会被无限放大——因为轻度、中度、重度之间的边界本来就是连续的,不是一刀切的离散标签。
1.2 直接用YOLOv11s跑出来的基线问题
我用官方YOLOv11s在自建损伤数据集上跑了一版基线,输入尺寸640×640,训练300个epoch。整体指标看起来还行,mAP@0.5到了84.3,表面损伤里的四类(裂纹、划痕、腐蚀、凹坑)都能检出。但细看就发现问题了:裂纹类的AP只有76.8,是所有类别里最低的;而且如果把严重程度当作普通类别输出(轻度、中度、重度三类),等级分类的准确率只有81.5。
这个81.5%是什么概念?就是说有五分之一左右的样本会被分错级。中度划痕判成轻度,最多让后道工序重新打磨一次;但重度裂纹判成轻度,可能会导致带伤工件直接流入下道工序,甚至产品报废。质检现场对误判的容忍度是非常低的。另一个问题是,我给每个损伤框同时输出类型和等级时,类型识别和等级识别存在明显的相互干扰:模型在低置信度区域往往先放弃等级预测,只保证检出框正确。
所以表面损伤任务的真实难点不是"能不能找到损伤",而是"找到之后能不能把严重程度给对"。这要求网络在特征提取阶段就保留足够的细粒度纹理信息,同时在决策阶段有一个专门为分级服务的特征通路,而不是让分级和分类挤在同一个特征空间里互相打架。
1.3 我给自己定的三个量化目标
动手之前,我先把目标写下来,免得后面在结构改造里迷失方向:
- 裂纹这类小目标在mAP@0.5上要提升至少8个点,达到85以上。
- 严重程度等级分类准确率要从81.5%拉到88%以上。
- 推理速度的损失控制在20%以内,因为产线上对帧率有硬性要求,不能为了涨点把速度牺牲太多。
这三个目标反过来决定了我的改造方向:CSP结构负责优化跨尺度融合时的信息冗余和梯度流,PMSFA注意力机制负责在不同尺度特征之间做渐进式的语义校准,检测头增加一条专门的分级分支。下面逐个拆解。
2. CSP与PMSFA的融合设计:先把原理层面的逻辑讲透
2.1 CSP到底在解决什么问题:一半走大路,一半走捷径
CSP的全称是Cross Stage Partial,跨阶段部分连接。它的核心思想很朴素:输入特征图沿着通道维度切成两半,一半进入密集连接的卷积支路做深层次变换,另一半直接走捷径保留原始信息,到阶段末尾再把两半拼回去。这样做的收益有两个:第一,梯度在反向传播时既可以通过密集支路流动,也可以通过捷径支路直接回传,缓解了深层网络的梯度消失问题;第二,两个支路分别学习不同的特征表示,拼接后的特征多样性更强。
我用一个生活化的类比来解释:如果把特征提取想象成一群人从A点赶往B点,传统做法是所有人都走同一条复杂的山路,每过一个关卡都要重新确认方向,既慢又容易走散。CSP的做法是让一半人继续走山路负责探索细节,另一半人直接坐直线电梯到终点附近的集合点,最后再汇合。探索组学到了沿途的细枝末节,电梯组保证了关键信息不丢失,汇合之后大家重新分工。
YOLOv11的主干里其实已经有基于CSP思想的C3k2模块,但它的作用范围主要在backbone的特征提取阶段。我在做表面损伤检测时遇到的问题恰恰在neck阶段——也就是特征金字塔做跨尺度融合的时候。PAN结构在融合P3、P4、P5特征时,常规做法是先把不同尺度的特征图通过卷积对齐到相同通道,然后直接相加。这个"相加"操作太粗暴了,浅层的高分辨率细节和深层的强语义信息被无差别地混合,对裂纹这种需要细纹理支撑的目标很不友好。
所以我的设计是把CSP思想从backbone搬到neck:在跨尺度融合的位置替换成CSPFuse模块,让融合后的特征也走"一半密集卷积、一半捷径"的路线。这样融合操作不再是一个简单的加法,而是通过并行的密集支路和捷径支路,让浅层细节和深层语义先各自独立变换、再拼接融合,从而保留更多有效信息。
2.2 PMSFA:渐进式多尺度特征聚合,而不是一次性融合
PMSFA是我给这个改进模块起的名字,全称是Progressive Multi-Scale Feature Aggregation,渐进式多尺度特征聚合。为什么强调"渐进式"?因为常规的FPN或PAN在融合多尺度特征时,是"一次性"操作:P5上采样后直接和P4相加,得到的结果再和P3相加,每步之间没有任何显式的注意力校准。问题是,不同尺度的特征本身存在语义差异,P5的特征是高度抽象的语义信息,P3的特征是充满细节的纹理信息,两者直接相加就像把中文翻译稿和英文原稿逐字叠加,含义混乱。
PMSFA的设计是逐步融合、逐级校准。具体流程分两个阶段:
第一阶段,从深到浅的渐进聚合。先把P5通过1×1卷积压缩到统一的通道数,作为语义基准;然后上采样到P4的尺寸,与P4经过侧向卷积后的特征相加,得到第一级融合结果F4;F4紧接着过一个通道注意力模块做第一次校准;再把F4上采样到P3尺寸,与P3的侧向特征相加,得到F3,然后过一个空间注意力模块做第二次校准。这个过程的用意是:深层特征作为"语义骨架"逐层注入到浅层,同时每经过一个层级就做一次注意力校准,把上一级带来的噪声压掉一部分。
第二阶段,从浅到深的回注。第一阶段得到的F3、F4、F5已经是聚合后的特征,但它们的尺度比较接近P3,如果直接送去检测头,P4和P5层原有的信息反而被稀释了。所以还需要把F3逐步下采样回P4、P5尺度,分别与原特征做残差相加,得到最终的P3_out、P4_out、P5_out。这样既能享受渐进聚合的好处,又保住了各层原有尺度的分辨率。
为什么通道注意力和空间注意力要交替使用而不是全用同一种?这是我的一个实测结论。通道注意力负责判断"哪些特征维度更重要",适合在深层语义上做;空间注意力负责判断"图像上哪些位置更重要",适合在浅层纹理上做。如果在浅层用通道注意力,模型只会关心"是不是有裂纹纹理"而不知道"裂纹长在哪";如果在深层用空间注意力,模型会把注意力浪费在大量语义相近的背景区域上。所以我在P4融合后用了通道注意力,在P3融合后用了空间注意力,各司其职。
2.3 融合逻辑:CSP和PMSFA解决的不是同一个问题
很多人看到"融合"就会默认是把两个模块串在一起,我把它们的关系梳理一下,大家在复现时心里更有底:
| 模块 | 解决的问题 | 作用层面 | 类比 |
|---|---|---|---|
| CSPFuse | 跨尺度融合时的信息冗余与梯度流不畅 | 结构层面 | 优化信息传递的"路线规划" |
| PMSFA | 多尺度特征融合时的语义错位与噪声放大 | 注意力层面 | 决定每段路线上"该重点看什么" |
CSPFuse在结构上重新组织了特征的流动方式,让融合操作不再是简单相加,而是通过密集支路和捷径支路的配合,保留更多互补信息;PMSFA在注意力层面对融合后的特征做显式校准,让网络清楚哪些位置、哪些通道值得重点关注。两者并不冲突,反而是互补的——一个在"怎么融合"上做文章,一个在"融合后怎么筛选"上做文章。这也是我为什么坚持要把它们同时用上,而不是二选一的原因。
3. 网络改造实操:把CSP和PMSFA写进YOLOv11
3.1 脖子里的CSPFuse模块怎么替换
我的做法不是替换backbone里的C3k2,而是在neck部分的PAN结构里,把跨尺度融合后的卷积块替换成CSPFuse。具体替换位置是bottom-up路径上两个融合节点,也就是从P5上采样与P4融合之后、以及从P4下采样与P3融合之后的位置。
CSPFuse的结构示意如下:
class CSPFuse(nn.Module): def __init__(self, in_ch, out_ch, num_bottlenecks=2): super().__init__() self.conv_in = Conv(in_ch, out_ch, 1) # 1x1降维 self.split = out_ch // 2 self.bottlenecks = nn.Sequential(*[ Bottleneck(self.split, self.split) for _ in range(num_bottlenecks) ]) self.conv_out = Conv(out_ch, out_ch, 1) def forward(self, x): # x: 已concat的跨尺度特征 x = self.conv_in(x) x1, x2 = x[:, :self.split], x[:, self.split:] x1 = self.bottlenecks(x1) return self.conv_out(torch.cat([x1, x2], dim=1))要注意的是,concat之后的输入通道数是两侧特征之和,1×1卷积先把通道压到目标值,再沿通道切成两半。这样的设计保证了整体通道数不会膨胀,参数量增加也很有限。我在实测中发现,Bottleneck数量设成2效果最好,设成3或4时精度几乎不再提升,但推理时间会明显增加,因为neck部分本来就是整个网络的计算热点。
3.2 PMSFA的插入位置与前置条件
PMSFA放在了PAN结构输出之后、检测头之前,也就是让P3、P4、P5在进入检测头之前,先经过一次渐进式多尺度聚合和校准。这个位置的选择有一个前提:PAN已经把backbone提取的原始特征做过了一轮跨尺度融合,此时P3、P4、P5各自已经含有一定的多尺度信息,PMSFA再基于它们做一次更高层次的对齐,效果比直接替换PAN中的融合节点更好。
PMSFA的前向示意:
def forward(self, p3, p4, p5): # 从深到浅渐进聚合 u5 = self.lateral5(p5) # C5 -> 256 f4 = self.ca(self.up2(u5) + self.lateral4(p4)) f3 = self.sa(self.up2(f4) + self.lateral3(p3)) # 从浅到深回注 p3_out = p3 + f3 p4_out = p4 + self.down3x3(f3) + f4 p5_out = p5 + self.down3x3(self.down3x3(f3)) + u5 return p3_out, p4_out, p5_out这里提三个实际操作中的细节:
第一,侧向卷积(lateral)统一输出256通道,后续所有操作都在256维上做,把计算量控制住。第二,上采样用最邻近插值就好,不要用转置卷积,不然参数多还容易产生棋盘格噪声。第三,通道注意力我用的是Squeeze-and-Excitation的轻量版本:全局平均池化后接两个全连接,一个降维到一个低维空间,另一个升维回原通道数,最后sigmoid生成权重。空间注意力用的是在通道维度上求平均和最大值、再过一个3×3卷积和sigmoid的标准做法,但加了GroupNorm做归一化,让它在不同亮度条件下更稳定。
3.3 检测头加一条严重程度分支:为什么不用12类硬train
最初我想偷懒,直接把类别数量从4种损伤类型改成4×3=12个类别,也就是"裂纹_轻度"这种组合标签。train了一轮发现效果很差:有些类型和等级的组合样本数太少,比如"腐蚀_重度"可能整个数据集就一两百个框,类别极度不均衡,模型基本放弃预测这些稀疏类。
所以我改成双分支方案:检测头保持原有的类型分类,旁路增加一个SeverityHead专门预测严重程度等级。两个分支共享主干特征,但各自使用独立的卷积层做最终预测。SeverityHead的结构示意如下:
class SeverityHead(nn.Module): def __init__(self, in_ch, num_severities=3): super().__init__() self.conv = nn.Sequential( nn.Conv2d(in_ch, in_ch * 2, 3, padding=1), nn.BatchNorm2d(in_ch * 2), nn.SiLU(), nn.Conv2d(in_ch * 2, in_ch * 2, 1), nn.BatchNorm2d(in_ch * 2), nn.SiLU(), ) self.cls = nn.Conv2d(in_ch * 2, num_severities, 1) def forward(self, x): return self.cls(self.conv(x)).flatten(2).permute(0, 2, 1)注意一个关键选择:SeverityHead的输入不是最后一层的特征,而是PMSFA输出中的P4_refine特征。原因我在后面踩坑部分会详细讲,这里先给结论——P4层兼顾了高分辨率细节和中等层级的语义信息,是等级预测最理想的特征来源。
3.4 参数量与计算量的账
改造完之后,我没法回避的问题是:模型变重了多少?推理变慢了没有?用一张中端消费级显卡(RTX 3060级别)实测,batch size 1,输入640×640,纯推理加NMS耗时数据如下:
| 模型版本 | 参数量 | 计算量(GFLOPs) | 推理帧率(FPS) |
|---|---|---|---|
| YOLOv11s 基线 | 9.4M | 21.7 | 76 |
| +CSPFuse | 9.8M | 23.2 | 69 |
| +CSPFuse+PMSFA | 10.3M | 25.4 | 61 |
| 最终版本(含分级头) | 10.5M | 25.9 | 58 |
参数量只增加了约12%,计算量增加了约19%,帧率从76掉到58,大概损失了24%。这个速度损失在我的接受范围内,因为产线实际用的输入分辨率不止640,后续分块推理才是真正吃性能的地方。如果你对速度更敏感,可以考虑把PMSFA里通道注意力的中间维度再压低,或者只保留通道注意力不做空间注意力,速度能回来一小截,但等级准确率会掉一两个点,需要自己权衡。
4. 严重程度分级怎么落地:标注规范、损失函数与训练策略
4.1 分级标准:从返修决策反推标注规范
做严重程度分类之前,我犯过一个方向性错误:先拍照,靠感觉定级,结果标注员的主观性特别大。同一个中度划痕,有的人认为是轻度,有的人认为是重度,标注不一致直接导致模型收敛慢、等级准确率上不去。
后来我是从质检的返修决策反推标准。我把分级的目的写清楚:轻度损伤可以通过打磨或简单返修处理后继续使用;中度损伤需要单独评估,可能需要补焊或局部更换;重度损伤直接判报废,不允许流入下一道工序。基于这个决策逻辑,我给每一类损伤制定了可量化的分级标准:
| 损伤类型 | 轻度 | 中度 | 重度 |
|---|---|---|---|
| 裂纹 | 宽度<0.2mm | 宽度0.2~0.8mm | 宽度>0.8mm或穿透 |
| 划痕 | 长度<10mm且深度浅 | 长度10~30mm | 长度>30mm或深度明显 |
| 腐蚀 | 面积占比<5% | 面积占比5%~20% | 面积占比>20%或穿孔 |
| 凹坑 | 直径<1mm且浅 | 直径1~3mm | 直径>3mm或深坑 |
这个表格不是拍脑袋定的,每个阈值都对应一个后道工序的动作。比如裂纹宽度超过0.8mm就意味着该工件必须报废;腐蚀面积占比超过20%意味着结构强度可能受影响。标注员拿到这个表之后,分歧率从初始的30%下降到了8%左右。所以说,分级标准的底层逻辑一定是业务规则,而不是视觉特征本身。
4.2 损失函数设计:类型和等级解耦的加权方案
因为我采用了双分支设计,损失函数也要相应调整。整体loss变成:
L = L_box + L_cls + λ · L_sev
其中L_box是原本的边界框回归损失(CIoU),L_cls是损伤类型分类损失,L_sev是严重程度分类的交叉熵损失。λ是等级分支的权重系数,我一开始设成0.5,后面逐步降到0.3。为什么不能设太大?因为等级分类是一个更难的任务,如果它的损失权重过大,模型会把大量学习资源花在等级判断上,反而拖累类型的检出能力。反过来设太小,等级分支学不到东西。实测0.3到0.5这个区间是比较稳定的。
还有一个细节:SeverityHead的参数初始学习率要设得比主干低一些,我通常乘以0.1的系数。因为主干部分已经在类型检测上收敛得差不多了,如果让等级分支以同样的学习率去更新共享的backbone权重,很容易破坏已有的特征表示。这个问题在迁移学习和多任务训练里非常常见,值得大家留意。
4.3 软标签:对付等级边界样本的有效手段
等级边界怎么处理,我摸索了很久。一个标准区分度较高的数据集里,总有那么10%到15%的样本处于两个等级的模糊地带。比如一条裂纹宽度0.2mm,按标准正好卡在轻度和中度的分界线上。标注员看一眼说是轻度,再看一眼觉得也可以算中度。这种硬标签会让模型无所适从,因为它学到的是"这个样本,要么是轻度,要么是中度",但实际上它是一个"轻中之间的过渡态"。
我从一个做图像分类的朋友那里学到了软标签的做法:对处在边界带的样本,标注时不强制打单一等级,而是给一个概率分布。比如宽度0.2mm的裂纹,标注为"轻度0.7,中度0.3"。训练的时候,等级分支的损失不是标准的交叉熵,而是预测分布与标注分布之间的KL散度。这样模型学到的是一个平滑的等级转换带,而不是一条生硬的决策边界。
这个方法带来的提升非常明显。用了软标签之后,等级分类的准确率从84.7%直接跳到88.3%,尤其是轻度和中度这两个相邻等级的混淆率下降了接近五成。代价是标注阶段要多花一点时间,但相比训练时间和推理时间的收益,这笔投入非常划算。
5. 实验对比与踩坑记录:mAP涨了不代表一切都好
5.1 整体指标提升明细
完整的实验对比我做了一张表,数据来自固定验证集,保证横向可比较:
| 模型版本 | mAP@0.5 | mAP@0.5:0.95 | 等级准确率 | 裂纹AP |
|---|---|---|---|---|
| YOLOv11s 基线 | 84.3 | 61.2 | 81.5 | 76.8 |
| +CSPFuse | 87.1 | 64.8 | 82.9 | 81.4 |
| +CSPFuse+PMSFA | 89.6 | 67.9 | 87.5 | 85.2 |
| 最终版(+分级头+软标签) | 90.1 | 68.6 | 91.2 | 86.7 |
可以看到,单纯加CSPFuse和PMSFA,mAP的提升已经很可观,但等级准确率的提升主要靠的是分级头和软标签策略。这说明一个很重要的结论:如果你只改网络结构,不做输出分支和标签策略的配套调整,等级分类的瓶颈不会被真正解开。
5.2 等级混淆矩阵暴露出的问题
我看了最终模型的等级混淆矩阵,发现一个有意思的现象:轻度损伤被误判为中度的比例最高,而重度几乎不会漏判。用我的话说就是"模型偏向保守,倾向于把损伤说得更严重一些"。这在质检场景里其实是可以接受的,甚至某种程度上是好事——宁愿让后道工序多做一次返修确认,也不能让重度损伤蒙混过关。但从算法的角度,轻度和中度的混淆还是需要压一压。
复盘的结论是:轻度损伤在特征上非常微弱,很容易被上下文信息带偏。比如一块轻微腐蚀的区域,如果周围有油污或者水渍,模型就会倾向于把它判成中度。针对这个现象,我在数据增强里增加了随机噪声和局部亮度扰动,让模型不能依赖"区域背景脏乱"这种无关信号来判断严重程度。实测又把轻度损伤的准确率提高了两个点,而轻度和中度的混淆率也下降了。
5.3 训练中的三个大坑
这里集中记几个我踩过的坑,希望能帮大家绕过去。
第一个坑:等级分支的特征来源选错了。初版把SeverityHead直接接在P5输出上,因为P5语义最强、感受野最大,结果等级准确率只有85.2%,而且类型分类精度还跟着掉了一点。后来把等级分支的输入改成PMSFA输出的P4_refine特征,等级准确率一下涨到91.2%。原因很简单:严重程度判断需要同时依赖纹理细节(比如裂纹宽度、凹坑深度)和上下文语义(比如区域面积、周围损伤密度),P5的特征太抽象,细节信息都丢光了;P3的特征太多噪声,语义不足。P4是两者之间的最佳平衡点。
第二个坑:等级损失系数λ调大导致类型识别崩溃。我最早设λ=1.0,觉得等级分类这么重要,权重应该给足。结果跑了200个epoch后验证,mAP@0.5反而从87.1掉到85.7,等级准确率倒是上去了,但类型漏检严重。原因是模型为了优化等级分支,把共享的backbone参数朝"区分等级"的方向猛拉,结果牺牲了"检出损伤"的能力。后来把λ降到0.3,两个任务才平衡过来。多任务学习的核心就是"牵一发动全身",调参时一定要盯住其他任务的指标。
第三个坑:等级样本不均衡。重度损伤样本天然少,即使有,很多也已经被质检员提前下线了,能进入数据集的重度样本可能比轻中度少一个数量级。等级分支用普通交叉熵的话,重度样本基本学不到。我把等级分支的损失从交叉熵换成了Focal Loss的变体,把难样本的梯度权重提上来,同时配合软标签策略,重度类别的准确率从84.1%提到了89.5%。如果你的数据集也严重不均衡,可以考虑这个方向。
6. 部署到质检流程:阈值、分块推理与等级汇总
6.1 阈值校准:别按论文默认值直接上产线
模型在实验环境里指标再好,部署到产线是另一回事。我遇到最典型的坑是置信度阈值。论文和开源仓库默认conf-thres=0.25,但产线上误检的代价远大于漏检——把一个没有损伤的工件误判为有损伤,顶多多一次人工复核,但如果漏检一个重度损伤,可能直接造成批量事故。所以我把conf-thres上调到0.4,虽然mAP数字在验证集上看着降了一点,但产线上实际误报率降低了很多。
另外我给不同等级设置了不同阈值。重度损伤的阈值可以放宽到0.3,因为重度损伤漏报的后果最严重,宁可多报几个让人工复核;轻度损伤的阈值必须高于0.45,否则光照变化、表面反光都会在低置信度区间制造一堆假阳性框。分级阈值不完全等同于检测置信度,还可以结合损伤框的面积、长宽比做一些规则过滤,比如预设的最小框面积以下的噪声直接丢弃。
6.2 高分辨率图的分块推理与坐标聚合
产线上实际拍出来的图像往往不是640×640,而是动辄6000×4000的高分辨率图。直接resize到640会丢失大量微小损伤信息,所以必须分块推理。我的做法是把原图切成640×640的小块,块与块之间overlap 80像素。为什么要留这个overlap?因为裂纹可能正好横跨两块图像的边界,如果overlap太小,一条裂纹在边界处被切断,模型在两个块里各自看到的只是一段短纹,很容易漏检。overlap留80像素,配合NMS去重,基本能保证边界损伤不丢失。
分块推理之后,所有检测框都要映射回原图坐标再做一次全局NMS。这里有个性能优化技巧:分了块之后,不同块的推理完全独立,可以用多线程并行跑。我实测4线程并行,一块6000×4000的图从单线程的2.1秒降到0.8秒左右,而这个优化并不改变模型结构,只改变调度方式,非常划算。
6.3 等级汇总与质检联动
最终输出给质检系统的不是一个一个框,而是一份汇总的决策建议。一个工件上可能存在多框损伤,比如三处轻微划痕加一处中度凹坑。这时候不能简单地把所有框的等级求平均,而应该取最大值作为整个工件的最终等级——安全优先的原则。但同时,报告里必须保留各框的等级分布,方便质检员复核。
我的做法是输出一个JSON,内容包括:总损伤框数、按类型统计的等级分布、最高等级损伤框的坐标和处理建议(打磨/评估/报废)。对于置信度在0.15到0.3之间、又低于正式输出阈值的检测框,不直接丢弃,而是放进"待复核缓存"。每天下班前质检员花十分钟扫一遍缓存里的低置信度框,确认有没有漏网之鱼。这个机制上线之后,现场反馈非常好——算法不是万能的,人机协同才是质检落地的最优解。
最后再分享一点个人体会
整个项目做完,我最深的体会不是"涨了多少个点",而是把任务结构想清楚比加模块更重要。如果你也想做类似的损伤分级检测,我建议按这个顺序来:先和业务方把分级标准和返修决策对齐,再确定标注规范和软标签策略,最后才轮到网络结构改造。顺序反过来的话,模型再怎么调,学的等级也可能是错的。
另外一个实用小技巧:改造YOLOv11这类检测器时,每加一个模块就单独跑一版消融实验,不要一次性把所有改动全堆上去再验证。我见过不少同学把CSP、PMSFA、新损失函数一股脑全改了,最后涨了3个点,但根本说不清是哪个模块的功劳。分步验证虽然慢一些,但每一步都能留下可信的因果结论,这对后续继续改进的意义非常大。这次的CSP + PMSFA融合方案只是一个起点,我目前还在尝试把等级分支改造成回归输出连续严重度分数,而不是离散的三档。到时候再来分享新的结果。