做工业缺陷检测最怕什么?不是缺陷种类多,而是缺陷明明就在图上,算法却视而不见。最近我在做轴承端面瑕疵检测,被一个非常典型的问题卡了近两周:极小划痕、针孔、油斑混在金属纹理和光照反光里,普通YOLO系列检测器反复漏检,误检更是压不下去。我试过调anchor、加小目标检测头、换数据增强,效果都不理想。最后把DINOv3接到YOLO26上,用“小缺陷+复杂背景”这条主线做了一轮完整改造,漏检率明显下降,误检也可控了。这篇文章就是我这次改造的完整记录,包含方案选型背后的原因、模型怎么改、训练参数怎么调,以及踩过的几个大坑。如果你也在做轴承缺陷检测、机油盖缺陷检测、试管缺陷检测这类场景,应该能直接参考。
小缺陷检测不是单一技术点能解决的,它牵涉到数据标注、特征表达、训练策略、部署取舍四个层面。YOLO26本身的检测能力并不弱,真正弱的,是它在复杂背景下对微小目标的特征表达。而DINOv3这类视觉基础模型擅长恰恰是细粒度特征和局部纹理理解,两者结合,正好互补。下面我按改造顺序把整个过程拆开讲。
1. 问题的根源:为什么YOLO26在复杂背景下会漏掉小缺陷
1.1 小缺陷的视觉特征为什么“不显眼”
先说一个很直观的事实:小缺陷在图像里占的像素量极少。一个针孔缺陷,在500万像素的工业相机图像里可能只有几十个像素;一条微裂纹,宽度往往只有2到3个像素。这种目标在经过检测网络的多次下采样之后,到高层特征图时可能只剩下一个点,甚至直接被池化抹掉了。
更麻烦的是对比度低。金属表面的拉丝纹、机油盖上的注塑纹、玻璃试管内壁的细微气泡,灰度值和真正的缺陷非常接近,缺陷与背景之间的灰度差往往只有几个像素级。人眼能看出来,是因为人的视觉系统对局部纹理连续性异常敏感;但卷积神经网络的感受野是有限的,边缘响应在低对比度下非常弱,模型很难从梯度信息里判断这里到底是不是缺陷。
还有标注层面的问题。小目标的真实框在图上只有很小的面积,标注时框的边界偏移几个像素,IoU就会发生剧烈变化,导致正样本分配不稳定。同一个缺陷,标注严一点就落在正样本里,松一点就变成负样本,训练出来的模型自然不稳定。
1.2 YOLO26自身的检测能力边界
YOLO26是YOLO系列的一个新版本,延续了单阶段检测器一贯的设计思路:网络结构简单直接,推理速度快。但这类检测器有一个通病——速度和精度存在固有取舍,而在小缺陷检测上,牺牲的往往是细节。
下采样倍数是最直接的问题。为了获得更大的感受野和更好的语义信息,检测网络通常会把特征图缩小到输入尺寸的1/32甚至更低。小缺陷在这个尺度下的信息量所剩无几,只能依靠浅层的低阶特征来回忆细节。但浅层特征本身噪声大,背景纹理稍微复杂一点,缺陷信号就淹没在纹理噪声里了。
另一个问题是注意力分配。检测头的训练目标是大目标和小目标一起拟合,但在损失函数层面,大目标天然占据更多损失贡献。哪怕是focal loss对小目标做了一定加权,大目标附近的高响应区域还是会压制小缺陷的学习。这不是YOLO26独有的问题,而是所有单阶段检测器面对微小目标时的通病,只是YOLO26在追求极致速度时把这个问题放大了一些。
1.3 复杂背景的“复杂”到底指什么
我们在讨论缺陷检测时,经常会说“背景复杂”。但“复杂”这个词太笼统了,落到具体场景里,其实是三种情况的叠加。
第一种是背景纹理与缺陷局部同构。金属拉丝纹和划痕在灰度纹理上高度相似,机油盖的注塑流纹和裂纹在走向上难以区分,试管内壁的气泡和玻璃划痕在边缘特征上表现接近。这种情况下,模型看到的不是“缺陷与背景的区别”,而是“两组非常相似的局部纹理之间的细微偏差”,这对特征分辨力要求极高。
第二种是光照与反光带来的灰度分布漂移。同一个轴承端面,不同角度打光下,同一缺陷的灰度、对比度、甚至形状都会变化。工业现场没法保证每次成像条件完全一致,训练集里的光照分布又往往比较集中,模型很容易把特定光照下的背景纹理误学成缺陷特征。
第三种是训练数据过少。工业缺陷样本获取难度大,良品很多、缺陷很少,标注成本又高,很多项目实际能用的缺陷图只有一两百张。数据少的时候,模型会倾向于记住背景的伪相关特征,而不是缺陷本身的本质特征。比如开机油盖的注塑纹和裂纹经常同时出现,模型可能把“有注塑纹的区域”当成缺陷判断依据,而不是真的学会了识别裂纹。
2. 方案选型:DINOv3在检测流程里到底扮演什么角色
2.1 DINOv3的核心能力:通用视觉特征
要解决上面的问题,我需要的不只是一个更强的分类器,而是一个真正理解“局部纹理是否连续、是否符合预期”的特征提取器。DINOv3就是朝这个方向走的视觉基础模型。
DINOv3这类模型的核心是用自监督方式在海量自然图像上预训练,不依赖具体的检测标签,学到的是通用的视觉特征。它输出的patch级特征向量,本质上是对图像局部内容做高维语义编码。两个patch的特征向量越接近,意味着它们在视觉语义上越相似。这种能力放到缺陷检测里特别有用:一个划痕patch和一个拉丝纹patch,在小像素空间里可能灰度分布接近,但在DINOv3的高维特征空间里,它们会被拉开距离,因为模型学到了两者在结构连续性、局部方向性上的差异。
还有一个工科同学容易忽略的点:DINOv3没有见过我们的缺陷标注,因此不会把训练集里的背景伪相关学进去。它是拿着一套“通用视觉先验”来参与的,这对工业小样本场景极其宝贵。它的缺点也很明显:模型体量大,前向计算慢,直接拿来推理不现实。所以关键问题不是“它强不强”,而是“怎么用”。
2.2 三种可落地的结合思路
我调研了社区里这类模型的常见用法,结合自己的理解,总结出三种可以落地的结合方式。
方案A是前融合。把DINOv3当作一个额外的特征提取分支,和YOLO26的backbone特征拼在一起,再送到检测头。DINOv3的高维特征直接参与检测推理,效果最直接,但有两个代价:一是训练和推理时都要跑DINOv3,速度显著下降;二是显存占用很大,工业场景的1280分辨率输入会让显存瞬间爆炸。
方案B是训练时蒸馏,推理时不带DINOv3。做法是用DINOv3的特征图当“语义老师”,训练时让YOLO26的中间层特征向DINOv3的特征对齐,目标是让YOLO26在没有DINOv3的情况下也能学到类似的高维语义分辨能力。推理阶段完全移除DINOv3,速度不受影响,部署链路也干净。
方案C是把DINOv3当作难例挖掘工具。因为DINOv3对patch相似度敏感,可以自动找出图像里“最不像背景”的局部区域,把这些区域标为候选困难样本,交给YOLO训练时重点学习,或者直接推荐给标注人员优先标注。这个方案不改变检测网络结构,纯粹靠数据侧提升,胜在改动最小。
| 方案 | 训练改动 | 推理速度 | 显存占用 | 精度提升潜力 | 落地难度 |
|---|---|---|---|---|---|
| A 前融合 | 中 | 明显变慢 | 很高 | 最高 | 中 |
| B 蒸馏 | 低 | 不影响 | 较高 | 高 | 低 |
| C 难例挖掘 | 很低 | 不影响 | 低 | 中 | 较低 |
2.3 我的选型:B为主,A辅助
综合下来,我最后采用的是“B为主,A辅助”的组合。训练阶段在YOLO26旁边挂一个DINOv3分支,一方面把DINOv3特征融合进浅层P3、P4特征,让检测头在训练中能拿到更细的纹理信息;另一方面通过蒸馏损失,把这种分辨能力压到YOLO26自己的backbone上。
这样选的原因是:我手上的轴承缺陷检测项目有节拍要求,推理不能比纯YOLO26慢太多。方案A如果带着DINOv3上线,单帧耗时从十几毫秒涨到几百毫秒,产线根本接受不了。而方案B在推理时把DINOv3摘掉,只保留已经融合了蒸馏知识的YOLO26结构,速度完全回到原始水平。
方案A也没有完全浪费,我把它留作离线分析工具:对YOLO26检测出的低置信度缺陷候选框,用DINOv3提取对应区域的patch特征,和标注库里的真实缺陷特征做相似度比对,作为二次确认手段。这一步只在抽检或回放分析时用,不影响在线节拍。
3. 实操落地:把DINOv3接入YOLO26的完整改造
3.1 环境准备与权重文件管理
先说环境。我使用的基础环境是Python 3.10、PyTorch 2.x、CUDA 11.8,YOLO26用pip安装的官方包管理方式,DINOv3通过HuggingFace的transformers库加载。如果你的环境是PyTorch 1.x,建议先升级再跑,因为DINOv3的官方权重实现里用到了较新的算子,老版本很容易在forward时报错。
加载DINOv3的代码范式大致是这样的:
import torch from transformers import AutoModel, AutoImageProcessor # 这里的checkpoint按你实际能下载到的权重路径填 model = AutoModel.from_pretrained("your/dinov3-checkpoint") processor = AutoImageProcessor.from_pretrained("your/dinov3-checkpoint") model.eval() model.cuda()权重下载这件事我单独说一句。某些网络环境下载HuggingFace或官方仓库的权重会超时,建议优先用国内镜像源,或者把权重包下载后放到本地目录手动加载。下载中断会产生不完整的缓存文件,再次下载前务必将残留文件清干净,否则hash校验会一直失败,反复卡在同一个地方。权重文件比较大,不要随手扔在代码目录里,建议单独建一个weights目录集中管理,并记录每个权重的来源和版本号。工业项目经常要回溯复现,这个习惯能省很多事。
3.2 模型结构改造:在YOLO26中插入特征融合分支
改造的思路很直接。YOLO26的backbone会输出多个不同尺度的特征图,我用P3、P4、P5来指代从浅到深的三个层次。DINOv3输出的是patch-level特征网格,reshape后可以变成一张低分辨率的高维特征图。我们要做的,就是把这张特征图上采样到与P3、P4、P5匹配的尺寸,做通道投影后与原有特征融合。
通道投影这一步非常关键。DINOv3输出的特征通道数通常很高,直接拼接会让后续卷积的参数量暴涨,也容易干扰原本稳定的特征分布。我用1x1卷积把DINOv3特征投影到与YOLO26各层特征相同的通道数,再做融合。融合方式我用了可学习的加权求和,而不是简单的把两组特征直接相加,这样做的好处是让模型自己决定DINOv3特征在哪个尺度、哪个训练阶段贡献更大。
下面是一段概念性的伪代码,说明改造后的前向路径:
class DINOEnhancedYOLO(nn.Module): def __init__(self, yolo_model, dinov3_model): self.yolo_backbone = yolo_model.backbone self.yolo_neck = yolo_model.neck self.yolo_head = yolo_model.head self.dinov3 = dinov3_model self.proj_p3 = nn.Conv2d(dinov3_dim, 256, 1) self.proj_p4 = nn.Conv2d(dinov3_dim, 512, 1) self.proj_p5 = nn.Conv2d(dinov3_dim, 1024, 1) self.fuse_w = nn.Parameter(torch.tensor(0.5)) def forward(self, x): p3, p4, p5 = self.yolo_backbone(x) with torch.no_grad(): dino_feat = self.dinov3(x) # [B, C, H/14, W/14] dino_p3 = F.interpolate(self.proj_p3(dino_feat), size=p3.shape[-2:], mode='bilinear') dino_p4 = F.interpolate(self.proj_p4(dino_feat), size=p4.shape[-2:], mode='bilinear') dino_p5 = F.interpolate(self.proj_p5(dino_feat), size=p5.shape[-2:], mode='bilinear') fused_p3 = p3 + self.fuse_w * dino_p3 fused_p4 = p4 + self.fuse_w * dino_p4 fused_p5 = p5 + self.fuse_w * dino_p5 return self.yolo_head(self.yolo_neck([fused_p3, fused_p4, fused_p5]))注意我在前向里给DINOv3加了torch.no_grad(),这是因为训练阶段我始终冻结它。DINOv3预训练特征已经足够通用,即使微调它对缺陷检测的提升也非常有限,反而会大幅增加显存和训练时间。把这个冻结策略写死在forward里,可以有效防止工程同学在实验时误开了DINOv3的梯度。
3.3 数据准备与标注规范
接入DINOv3之后,数据侧的准备工作反而变得更重要。小缺陷检测的标注,直接影响正样本分配和后面的特征蒸馏质量。
小缺陷的标注框一定要贴边。框大了,模型看到的正样本区域里背景占了一大半,注意力被背景带走;框太小,又可能切掉缺陷的真实边缘,导致特征不完整。轴承划痕这类细长缺陷,我建议用旋转框标注,虽然标注成本高一些,但对训练效果帮助明显。
如果做方案B的特征对齐,还需要额外准备缺陷区域的mask。做法是:读取标注框,在特征对齐损失计算时,只对标注框周围扩展若干像素的局部区域计算蒸馏损失。这样做的原因是,DINOv3的特征在背景区域同样很丰富,如果整图都做特征对齐,模型会把大量学习容量花在模仿背景纹理上,而不是缺陷本身。我最初做整图对齐时训练loss降得很慢,换成区域mask对齐后,收敛速度立刻上来了。
数据增强方面,有一个容易被忽略的坑:随机擦除和过强的旋转会直接毁掉小缺陷样本。随机擦除可能把缺陷本身抹掉,强旋转则会让细长划痕发生不可逆的形变,模型学到的特征变得不稳定。我实际保留的增强包括轻度翻转、小角度旋转、亮度和对比度微调、轻微的色彩抖动。Mosaic增强在小缺陷上要慎用,四张图拼接后缺陷会被缩得太小,信息量不足。
分辨率是另一个关键因素。小缺陷检测任务里,训练分辨率从640提升到1280,效果提升往往比任何模型结构改动都明显。工业检测相机给的原始图一般都在2000万像素以上,我采用的方法是限制长边到1280,短边等比缩放,训练阶段保持输入分布的一致性。
3.4 训练配置与辅助损失设计
训练配置我分两步走。第一步,只训练投影层和融合权重,YOLO26主体和DINOv3全部冻结。这一步让模型先适应“YOLO特征+DINO特征”的融合形态,把基础loss降到平稳。第二步,解开YOLO26的backbone和neck,继续训练,此时融合层已经稳定,不会出现一上来就互相冲突的情况。
完整训练阶段的损失由两部分组成:YOLO26原有的检测损失,加上特征蒸馏损失。蒸馏损失我用的不是简单L2,而是余弦相似度损失。
def distill_loss(yolo_feat, dino_feat, mask): yolo_feat = F.normalize(yolo_feat, dim=1) dino_feat = F.normalize(dino_feat, dim=1) sim = (yolo_feat * dino_feat).sum(dim=1) masked_sim = sim[mask > 0] return (1 - masked_sim.mean())这里把两组特征分别做L2归一化,再计算逐位置的余弦相似度,最后只取mask区域的均值。相比直接回归特征向量,余弦相似度对特征幅值不敏感,关注的是方向一致性,更符合蒸馏的语义。蒸馏损失的权重我设置在0.1左右,太大会干扰检测主任务,太小则学不到DINOv3的特征分辨能力。
优化器和调度方面,我用的是AdamW,初始学习率1e-3,warmup设置300步。检测头部分的学习率会单独乘一个0.1的系数,避免检测头剧烈波动。EMA权重衰减系数设0.999,小缺陷检测对训练后期的不稳定性很敏感,EMA能明显改善最终精度。
4. 一次完整的踩坑实录:从爆显存到不收敛
4.1 显存直接爆掉的四个对策
我把DINOv3挂上训练的第一天,就遇到了显存爆炸。1280分辨率的输入,batch size设为8,一上来就OOM。当时卡是RTX 4090 24GB,照样扛不住。后来按优先级依次做了四件事。
第一,彻底冻结DINOv3,确保它不计算梯度。这一步很关键,很多同学以为只要不更新参数就行,但PyTorch里如果不设置requires_grad=False,前向时依然会为反向传播保存中间激活,显存占用并不比训练少。第二,给DINOv3外包一层torch.no_grad(),进一步释放中间变量。第三,对YOLO26的backbone启用梯度检查点,用一点点计算量换显存。第四,batch size降到4,用梯度累积8步模拟batch size 32的效果。
如果做了以上四步还爆显存,还有一个近乎作弊的终极大法:离线预计算DINOv3特征。因为训练集是固定的,DINOv3又完全冻结,整个数据集跑一遍DINOv3得到的特征图完全一样。我在训练开始前把所有训练图片的DINOv3特征一次性算好,存成内存映射文件,训练时直接从磁盘读取,不再实时前向DINOv3。这样一来,显存占用直接回到纯YOLO26的水平,训练速度还快了不少。这个方法我只建议在数据集规模不大时使用,如果你的训练集有几万张图,磁盘空间和读取IO会成为新的瓶颈。
4.2 训练不收敛与掉点排查
改造后第一次完整训练,我遇到了一个很诡异的现象:检测loss能降,但验证集的mAP50反而比纯YOLO26还低。顺着特征对齐的思路排查,问题出在三个地方。
第一是通道对齐方式。最初我图省事把DINOv3特征直接concat到YOLO特征后面,结果检测头在前几层要面对的通道数暴涨,训练初期严重不稳定,收敛速度也被拖慢。改成1x1卷积投影后,这个现象立刻消失。第二是蒸馏损失权重。我一开始把蒸馏损失设到0.5,模型把大量精力放在模仿DINOv3特征上,检测头反而没学好。把权重降到0.1,并且只在训练中期以后开启,效果才正常。第三是mask没有生效。我发现蒸馏损失虽然包了mask,但mask区域太小,实际参与计算的像素极少,导致蒸馏信号几乎没有。后来我把mask扩展成以标注框中心为基准的高斯热力图,缺陷中心权重高、周围递减,既保留了局部聚焦,又给了模型足够的正反馈区域。
| 问题表现 | 可能原因 | 我用的解决办法 |
|---|---|---|
| 显存OOM | DINOv3计算了梯度或保存了中间激活 | 冻结+no_grad+离线预计算特征 |
| 验证集mAP反而下降 | concat通道过多/蒸馏权重过大 | 1x1投影降维,蒸馏权重设0.1 |
| 蒸馏没有效果 | mask区域过小或无mask | 高斯热力图扩展区域 |
| 训练后期震荡 | 学习率过大、EMA缺失 | 检测头lr乘0.1,EMA=0.999 |
| 小缺陷完全检不出 | 分辨率低/下采样丢失 | 训练尺寸提升到1280,切片推理 |
4.3 推理速度与部署取舍
方案B最大的优势是推理时不带DINOv3。但这里有一个坑:训练时模型前向里挂了一整个DINOv3分支,代码结构里融合层和投影层的参数都在,导出部署模型时,需要重新组装一个纯YOLO26的模型,把训练好的backbone、neck、head权重加载进去,而不是直接导出整个训练模型的ONNX。
我第一次导出时直接对完整模型做ONNX转换,导出的文件里有DINOv3的整棵计算图,体积从几十MB涨到了几百MB,推理速度也慢得离谱。后来改成“手动组装纯YOLO结构+加载训练权重”的方式导出,ONNX文件就是标准YOLO26的大小,速度和纯模型一致。
如果确实需要在推理时保留DINOv3参与判断,比如方案A做离线二次确认,我建议单独对DINOv3做一次ONNX导出,再把两个模型串联部署。不要把DINOv3塞进YOLO的模型文件里,否则每次算力升级、模型升级都会牵连到基础模型,维护成本很高。
4.4 小缺陷特有的问题库
小缺陷检测还有一些单靠模型结构解决不了的问题,需要靠推理策略和数据策略配合。
漏检通常集中在极小的目标上。即使训练时分辨率已经提到1280,缺陷若是只有几十像素,推理时依然可能被漏掉。我用的方法是滑窗切片推理:把原图按重叠窗口切成若干块,每块单独送进模型检测,再把结果映射回原图坐标。推理耗时翻了几倍,但对极小缺陷的召回提升非常明显。如果产线节拍允许,这个方案比单纯堆模型结构可靠得多。
测试时增强也有用。多尺度推理在小缺陷检测上带来的收益比其他任务都明显,我一般用0.8倍、1.0倍、1.2倍三个尺度做集成,最后用NMS合并结果。这个操作纯属推理期方案,不需要重新训练,适合模型已经训练好之后做快速效果冲刺。
难负样本的问题是漏检之外的另一个大头。金属水渍、拉丝纹、光照反光这些“看着像缺陷但实际不是”的区域,会把误检率顶得很高。我处理的办法是持续收集误检样本,把它们作为负样本加进训练集,同时用DINOv3对负样本区域做特征聚类,把聚类中心附近的难负样本反复采样,让模型看到更多“容易被骗”的情况。
5. 这个方案能用在哪、不该用在哪
5.1 能用的场景:小缺陷+复杂背景的典型应用
这套改造方案最适用的场景,就是我开头提到的“缺陷小、背景杂、样本少”三座大山同时存在的项目。
轴承缺陷检测是典型场景。轴承端面有拉丝纹、研磨纹,缺陷是针孔、细微划痕、麻点,光照一变,缺陷和背景的区分度就急剧下降。DINOv3引入后,模型对局部纹理连续性的判断能力明显增强,针孔和麻点的召回率提升最显著。
机油盖缺陷检测也很合适。机油盖表面有注塑流纹、缩水纹,与裂纹同时出现时视觉上极其相似。团队里另一个同学复现了我的做法,把DINOv3的蒸馏分支加进他训练的机油盖检测模型里,裂纹漏检率下降了近三成。试管缺陷检测同理,玻璃反光和气泡干扰让划痕几乎隐身,单靠YOLO确实难,DINOv3对边缘连续性的判断刚好派上用场。
连垃圾分类这类目标不算小的任务,如果场景里背景极其杂乱,比如垃圾袋扯碎后的各种碎片混在一起,这套思路也有参考价值。它不是非要用在“缺陷”上,本质上是给YOLO赋予更细的纹理分辨能力,任何对细粒度特征敏感的任务都可以类比。
5.2 不建议用的场景与我的判断标准
反过来,我也要说清楚这套方案的适用边界,避免大家无脑套用。
如果缺陷本身的纹理差异已经足够明显,比如大块缺色、明显断差、大面积崩边,纯YOLO26就能做得很好,强行引入DINOv3只会增加训练时间、占用显存,让工程复杂度上升,收益却很有限。
如果项目完全没有GPU,或者只有极低算力的嵌入式平台,这个方案也不适合。方案B虽然推理时不带DINOv3,但训练阶段仍然需要一块显存足够的GPU来完成DINOv3前向和蒸馏损失计算。没有这个前提,整套改造无从谈起。
如果项目交付周期只有两三天,也建议放弃。这套改造从环境搭建、模型改结构、调试蒸馏损失到导出部署,我自己走完花了两周。时间紧的项目里,用最保守的“更高分辨率+切片推理+多尺度TTA”组合,往往效果更快见效,没必要为了技术新意承担交付风险。
5.3 数据量与效果预期的经验参考
很多人关心这套方案到底需要多少数据才有效。我的经验是,越小的数据量下,DINOv3带来的相对提升反而越明显。训练集只有200张缺陷图时,模型极度依赖预训练先验来弥补数据不足,DINOv3蒸馏带来的特征分辨能力几乎是雪中送炭。而当训练集扩到2000张以上,模型自身已经能从大量数据里学到足够判别性特征,DINOv3的增量收益就会递减,最终可能只比纯YOLO高出几个点的mAP。
所以我的建议是,如果你手头缺陷样本很少、背景又复杂,优先尝试这套方案;如果数据量已经很充足,先花点时间检查已有的漏检样本,分析到底是标注问题还是分辨率问题,不要急着改网络结构。小缺陷检测是一个系统性工程,模型结构只是其中一环,数据、标注、推理策略每一环都可能带来比换模型更大的收益。
最后再分享一个小技巧。做特征对齐蒸馏时,很多人会把整张图的特征都拉向DINOv3,实际效果其实不好。工业场景里我们希望模型关注的是缺陷区域,背景特征学到再多也没用,反而会把背景的噪声也当成模式学进去。我最后在损失函数里加了一个空间mask,只对标注框周围的高斯扩展区域做蒸馏约束,这个小改动带来的收益非常明显。如果你也正在被小缺陷复杂背景搞到头秃,不妨先从这个mask开始试起。