首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >一种大图小目标检测的思路:DINOv3预训练特征 + YOLO26检测头

一种大图小目标检测的思路:DINOv3预训练特征 + YOLO26检测头

作者头像
javpower
发布2026-04-28 13:17:55
发布2026-04-28 13:17:55
1.7K0
举报

一种大图小目标检测的思路:DINOv3预训练特征 + YOLO26检测头

最近在梳理项目,顺手把 DINOv3 和 YOLO26 结合这套方案的做法整理一下。不是"最好"的选择,只是其中一种可行的路径,给需要的朋友一个参考。


问题的起点

工业检测里有种常见场景:图片很大,要找的目标很小。

举个例子,半导体封装里的焊点缺陷检测。PCB整板拍照可能是 8000×6000 的分辨率,但一个焊点缺陷可能只占 3×3 像素。直接用常规检测模型去跑,要么特征图上目标已经缩成了一两个像素根本看不清,要么下采样过程中信息早就丢了。

切片是常见的解决思路,但切分边界可能切断目标。另一种思路是直接用更大分辨率的输入,让目标在特征图上占据更多像素。

这里说的是其中一种:用预训练的视觉大模型提供特征,用成熟的检测头做任务输出。如果输入图片是 1280×1280,一个 3×3 像素的目标在40×40的特征图上大概占 0.6 个像素;但如果输入是 2560×2560,同样目标在特征图上就占 1.2 个像素,信息量翻倍。DINOv3 的特征质量给这种方案提供了新的选择。


整体框架

核心思路就三步:提特征 → 多尺度化 → 检测输出


关于DINOv3的特征

DINOv3是Meta在2025年放出来的自监督模型,用的是ViT架构。它在LVD-1689M这个大规模数据集上训练过,这个规模大概是之前一些方案的几十倍。

我们实际用下来,有几点值得说:

1. 底层特征确实细

自监督学习的损失函数让模型学会了捕捉底层的纹理、边缘信息。这些特征对工业检测来说是有意义的——划痕、凹坑、焊点边缘这些细节,正是缺陷和正常区域的区别所在。

2. 多中间层输出

DINOv3的 get_intermediate_layers 可以取多层输出。不是所有层都同等重要——浅层保留更多底层信息,深层偏语义。在我们的方案里主要用的是第1、2、3层做多尺度,第4层是cls token通常不用。

3. 一个实现细节

代码语言:javascript
复制
def load_dinov3_vit(vit_type="vitb16", pretrained=False, weights=None):
    # 优先从本地加载权重,避免重复下载
    local_weight_path = "dinov3_vitb16_pretrain_lvd1689m-73cec8be.pth"
    if os.path.exists(local_weight_path):
        weights = local_weight_path

    model = torch.hub.load(
        REPO_DIR,
        f'dinov3_{vit_type}',
        source='local',
        weights=weights,
        pretrained=pretrained
    )
    return model

权重文件大概330MB,需要从Meta AI官网申请下载。如果网络不通,可以让同事下好传过来。文件名必须一致,代码里会检查本地路径。


多尺度Neck怎么做的

DINOv3输出的是单尺度 40×40 特征图(640输入的情况下),但YOLO检测需要 P3/P4/P5 三个尺度。

代码语言:javascript
复制
def forward(self, x8, x16, x32):
    f8 = self.proj8(x8)   # 投影到256通道
    f16 = self.proj16(x16)
    f32 = self.proj32(x32)

    # 跨级融合:把高分辨率特征下采样后与低分辨率融合
    down_8to16 = avg_pool2d(f8, kernel_size=2, stride=2)
    n16 = self.fuse16(cat[f16, down_8to16])  # 512ch

    down_16to32 = avg_pool2d(f16, kernel_size=2, stride=2)
    n32 = self.fuse32(cat[f32, down_16to32])  # 512ch

    return [f8, n16, n32]

这里有个细节:下采样用的是 avg_pool2d 而不是 stride=2 的卷积。原因是平均池化能保留更多空间信息,对小目标检测更友好。

另外当特征图尺寸不完全对齐时(有时候输入图片尺寸不是640的整数倍),需要做个插值:

代码语言:javascript
复制
if down_8to16.shape[-2:] != f16.shape[-2:]:
    down_8to16 = F.interpolate(down_8to16, size=f16.shape[-2:], mode='nearest')

检测头的问题

这部分是整个方案里最费时间的。

YOLO26的Detect head内部逻辑比较复杂,训练阶段和推理阶段的输出格式完全不同。如果要替换成自己的检测头,需要处理几个兼容问题:

1. 训练时的loss计算

Ultralytics内部计算loss时,期望检测头返回的是 dict 格式:

代码语言:javascript
复制
def forward_head(self, x):
    bs = x[0].shape[0]
    boxes = cat([self.cv2[i](x[i]).view(bs, 4 * self.reg_max, -1) for i in range(self.nl)], dim=-1)
    scores = cat([self.cv3[i](x[i]).view(bs, self.nc, -1) for i in range(self.nl)], dim=-1)
    return dict(boxes=boxes, scores=scores, feats=x)

boxes 是 DFL 输出的原始分布,[B, channels, total_anchors]。scores 是每个类别的预测置信度,[B, nc, total_anchors]。feats 是原始特征图,用于生成锚框。

2. 推理时的NMS输入

推理阶段Ultralytics内部会做 NMS,期望检测头输出的是 [B, anchors, 5+nc] 格式的解码后坐标。DINOv3的特征没有先验框信息,需要在检测头里做解码:

代码语言:javascript
复制
def forward(self, x):
    if self.training:
        return self.forward_head(x)

    # 推理时:先 DFL 解码 box,再拼接 class scores
    decoded_boxes = []
    for i in range(self.nl):
        box_raw = x[i][:, :4 * self.reg_max, :, :]
        box_decoded = self.dfl(box_raw.view(bs, 4 * self.reg_max, -1))
        decoded_boxes.append(box_decoded)

    all_boxes = cat([b.contiguous().view(bs, -1, 4) for b in decoded_boxes], dim=1)
    all_scores = cat([x[i][:, 4 * self.reg_max:4 * self.reg_max + self.nc, :, :]
                      for i in range(self.nl)], dim=-1)

    return cat([all_boxes, all_scores], dim=-1)

3. 锚框stride的问题

我们的特征图是 40×40、20×20、10×10,对应的stride是 16、32、64,不是标准的 8、16、32。这个要在检测头初始化时指定:

代码语言:javascript
复制
self.stride = torch.tensor([16., 32., 64.])

关于DFL

DFL(Distribution Focal Loss)这个设计在2019年的论文里提的,用来解决边界框回归的问题。核心是把坐标建模成离散的分布,而不是直接回归数值。

在我们这个场景里,设置的是 dfl=1.5(reg_max=16)。

这东西对小目标有效的原因在于:模糊的边界会得到更宽的概率分布,而不是硬给一个确定坐标。比如一个边界不清晰的划痕,如果直接回归坐标,模型会困惑,loss容易震荡;但用DFL的话,模型可以表达"边界大概在这个范围内"的不确定性。

这个参数不一定是最优的,实际项目中可以尝试 1.0 到 2.0 之间的值。


训练配置的一点经验

不是所有场景都适用,说几个通用原则:

关于冻结策略

如果数据量少(几百张),可以冻结DINO的大部分层,只训练Neck和检测头。这样能避免破坏预训练特征。

如果数据量够(几千张以上),可以考虑解冻DINO的前两层,让它稍微适应一下工业图像的分布。学习率要小,weight_decay可以设大一些防止破坏预训练权重。

关于学习率

用预训练模型时,学习率一般要比从头训练小一个量级。我们一般用 lr0=0.00010.0005 这个范围。配合余弦衰减效果更稳。

关于数据增强

工业检测场景如果标注数据不多,建议把 mosaic 和 mixup 关掉或者调小。这些增强对自然图像效果不错,但对工业图像有时候会把缺陷形态破坏掉,反而引入噪声。


部署相关

产线上不可能跑Python推理,最后导出 ONNX 是常见选择:

代码语言:javascript
复制
model.export(format='onnx', imgsz=[640, 640], dynamic=False, opset=12)

几个注意点:

  • 导出时输入尺寸要固定,不能动态。因为我们切片推理时每块是固定640×640。
  • int8量化后精度会有1-2%的损失,如果精度要求高可以用fp16。
  • 推理引擎用 ONNX Runtime,跨平台支持好。

什么时候适合用这套方案

坦白说,不是所有场景都适合:

适合的场景:

  • 数据量不多,但又想利用大规模预训练特征
  • 特征提取和检测任务相对分离,不想从零训backbone
  • 对精度有一定要求,但更看重方案稳定性

不太适合的场景:

  • 数据量很大,从头训效果可能更好
  • 对延迟要求极高,需要极致轻量化
  • 目标形态和自然图像差异极大,预训练特征迁移效果差

具体还是要看实际场景。可以在小数据集上先做个验证性实验,再决定要不要上。


以上是这套方案的一些整理。有具体问题可以讨论,但不一定能及时回复。如果有更好的思路也欢迎交流。

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2026-04-25,如有侵权请联系 cloudcommunity@tencent.com 删除
目录
  • 一种大图小目标检测的思路:DINOv3预训练特征 + YOLO26检测头
    • 问题的起点
    • 整体框架
    • 关于DINOv3的特征
    • 多尺度Neck怎么做的
    • 检测头的问题
    • 关于DFL
    • 训练配置的一点经验
    • 部署相关
    • 什么时候适合用这套方案
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档