Files
yolov26_3d/模型设计.md
2026-06-24 09:35:46 +08:00

11 KiB
Raw Blame History

YOLO26-2D3D 检测任务模型设计

本文基于当前仓库实现整理,主要对应以下代码:

  • ultralytics/cfg/models/26/yolo26-3d.yaml
  • ultralytics/nn/modules/head.py
  • ultralytics/utils/plotting_3d.py
  • ultralytics/data/ground3d_augment.py
  • ultralytics/data/dataset.py
  • ultralytics/cfg/datasets/mono3d_ground.yaml

1. 任务概览

当前任务是在 YOLO26 检测框架上扩展出的联合 2D+3D 检测任务,核心特征是:

  • 2D 检测仍然走 YOLO26 的 P3/P4/P5 三层检测头。
  • 在 2D 框与分类之外,额外输出 3D 属性分支 preds_3d 和可见边采样分支 preds_edge
  • 模型既支持完整 3D 类,也支持四面可见面建模类,还兼容纯 2D 类。

按当前 mono3d_ground.yaml

  • face_3d_classes = [0..8],主要是车辆类,带四个面的 3D 标注。
  • complete_3d_classes = [9..12],主要是行人/两轮/三轮类,只有整框 3D。
  • 13..16 这些类当前只参与 2D 检测,不参与 3D 有效监督。

因此,分类头覆盖的是全量类别,而 3D 头是否真正参与监督,取决于类别分组。

2. 模型输入设计

2.1 图像输入

模型最终输入是标准张量:

  • 形状:[B, 3, H, W]
  • 通道RGB/BGR 三通道图像
  • 当前训练脚本默认 --imgsz 704,352,即宽高目标尺寸通常为 704 x 352

但输入尺寸不是写死的,代码允许矩形输入,只要满足 stride 对齐即可。

2.2 ROI 与相机预处理

当前数据集是鱼眼地面 3D 数据,输入前会做 ROI/虚拟相机处理:

  • 原图尺寸:ori_img_size = [1920, 1080]
  • roi0
    • ROI 大小 1920 x 880
    • 以图像中心 x 和消失点 y 为裁剪中心
  • roi1
    • ROI 大小 768 x 352
    • 以消失点 x、y 为裁剪中心

预处理流程大致为:

  1. 读取原图和标定。
  2. 按 ROI 裁剪,或走 virtual camera 变换。
  3. resize 到目标 imgsz
  4. 同步修正 calib
  5. 将 3D 深度按 virtual_fx 做归一化。

因此模型实际看到的是“ROI/虚拟相机坐标系”下的图像与标签,而不是原图直接输入。

2.3 标签输入

原始标签支持三种格式:

  • 6 列:纯 2D
  • 19 列:完整 3D
  • 51 列:带四面信息的 face-3D

加载后统一映射为内部 48 维格式,再拆成:

  • 2D 标签:cls + xywh
  • 3D 标签:labels_3d,形状 (N, 42)

labels_3d 的 42 维布局为:

  • 0-2 x3d, y3d, z3d

  • 3-5 l, h, w

  • 6 rot_y

  • 7-8 xc, yc,整框 3D 中心投影

  • 9 alpha

  • 10-17 front face

  • 18-25 rear face

  • 26-33 left face

  • 34-41 right face

每个 face 的 8 维 GT 信息是 face 级 3D/投影/可见性描述,供 face 分支与 edge 分支监督使用。

2.4 训练时额外输入

除了图像和标签batch 中还会携带:

  • calib:当前样本经过 ROI crop / virtual-camera 变换后的最终相机内参
    • 把 3D 点投影到 2D 图像上;
    • 把 2D 点 + depth 反投影回 3D
    • 训练/验证/可视化时恢复 depth 真实尺度;
    • 给 edge yaw、3D box decode、TensorBoard 可视化用
  • edge_faces_points_2d(n, 4, 5, 2)对于一个车的前后左右如果某个面可见就在这个面的底边上均匀采5个点然后投影到图片上作为edge分支的监督目标。
    • n当前图片里的目标数
    • 4四个face通常是front/rear/left/right
    • 5每个 face 的底边采样 5 个点。
    • 2每个点的 2D 坐标 (u, v)。
  • edge_faces_depths(n, 4, 5) 和 edge_faces_points_2d 一一对应,表示那 5 个 2D 采样点各自的 3D depth。
  • edge_faces_valid(n, 4):标记每个目标的每个 face 是否有有效 edge GT
  • edge_partial_points_2d(n, 5, 2),目标被图像边缘或 ROI 裁掉了一部分,比如车只露出半个头或半个尾。
  • edge_partial_depths
  • edge_partial_face_type:表示这个 partial edge 属于哪个 face。 loss 里需要知道该拿模型 preds_edge 里的哪一个 face 分支来对齐这个 partial GT。

这些主要服务于 edge 辅助监督和可见面 yaw 解码。

3. 模型结构设计

3.1 主干与颈部

模型配置在 yolo26-3d.yaml 中,整体结构仍是 YOLO26 风格:

  • BackboneConv + C3k2 + SPPF + C2PSA
  • Neck上采样 + 拼接 + C3k2
  • 输出层P3/8、P4/16、P5/32 三层检测特征

即:

  • P3/8 小目标层
  • P4/16 中目标层
  • P5/32 大目标层

3.2 检测头

最终 head 不是普通 Detect,而是 Detect3D

  • 继承自 Detect
  • 在 2D boxcls 之外,新增:
    • cv441 通道 3D 属性头
    • cv560 通道 edge 辅助头

3.3 End-to-End 模式

当前模型配置 end2end: True,因此会同时维护两套 head

  • one2many
  • one2one

训练时两套都会输出;推理时主要使用 one2one

推理阶段不是传统 IoU-NMS而是

  1. 对所有 anchor 的类别分数做 sigmoid
  2. 直接选 top-k
  3. 输出 [box, score, class]
  4. 再把同一批 top-k 对应的 preds_3d / preds_edge / anchors / strides 一起选出来

所以它是“top-k 选择 + 端到端检测”的风格,不是经典 YOLO 的后置 NMS 路线。

4. 输出设计

4.1 每层 feature map 的逻辑输出

对每个检测层P3/P4/P5Detect3D 都会产生 4 路输出:

分支 通道数 当前任务通道数 含义
2D box head 4 * reg_max 4 2D 框回归
2D cls head nc 17 类别分类
3D head 固定 41 41 3D 属性
edge head 固定 60 60 4 个面的边采样点

说明:

  • 当前 reg_max = 1,所以 2D box head 只有 4 个通道DFL 实际上关闭。
  • 当前数据集唯一类别 ID 为 0..16,因此 nc = 17
  • yolo26-3d.yaml 里的 nc: 80 只是默认占位值,真正建模时会被 trainer 用数据集类别数覆盖。
  • 所以当前任务单个 anchor 的原始输出总通道数为:
    • 4 + 17 + 41 + 60 = 122

如果只看最终推理对外最直接的输出:

  • 2D 检测结果:[x1, y1, x2, y2, conf, cls]
  • 以及与 top-k 对齐的:
    • preds_3d_selected: (k, 41)
    • preds_edge_selected: (k, 60)
    • anchors_selected: (2, k)
    • strides_selected: (k,)

4.2 2D box head 输出

通道定义

  • 通道数:4
  • 语义anchor 到框四边的距离回归

激活与后处理

  • 训练前向:不加激活,保持线性输出
  • 推理前向:
    • reg_max=1,因此 DFL = Identity
    • 直接通过 dist2bbox(...) 解码
    • 再乘以 stride 还原到像素坐标

备注

  • 这一分支没有显式 sigmoid/tanh/softmax
  • 2D box 的可用性主要依赖训练约束和 dist2bbox 解码

4.3 2D cls head 输出

通道定义

  • 通道数:nc
  • 当前任务:17

激活与后处理

  • 训练时:输出 raw logits
  • 推理时:对分类分数做 sigmoid
  • 然后与 box 一起进入 top-k 选择

4.4 3D head 输出

preds_3d 每个 anchor 固定 41 通道,布局如下。

4 个 face 分支,共 24 通道

通道范围 face 含义
0-5 front z3d, du, dv, size0, size1, visible_score
6-11 rear z3d, du, dv, size0, size1, visible_score
12-17 left z3d, du, dv, size0, size1, visible_score
18-23 right z3d, du, dv, size0, size1, visible_score

其中:

  • front/rear 的 size0,size1 对应 h,w
  • left/right 的 size0,size1 对应 l,h

也就是说face 分支并不直接回归完整 l,h,w,而是回归当前面最相关的两个尺寸。

whole-box 分支,共 17 通道

通道范围 含义
24 whole z3d
25-26 whole du, dv
27-29 whole l, h, w
30-33 yaw bin logits4 个方向 bin
34-37 yaw residual表示 sin(delta)
38-40 cut class logits3 类:normal / cut_in / cut_out

3D head 的激活/反归一化

这一分支在 Detect3D.forward_head() 中会立刻做一次“反归一化”,变成更接近物理量的形式:

  1. z3d

    • 通道:0, 6, 12, 18, 24
    • 变换:raw * z3d_scale + z3d_offset
    • 单位:米
  2. du, dv

    • 通道:1-2, 7-8, 13-14, 19-20, 25-26
    • 变换:sigmoid(raw) * 16 - 8
    • 含义:相对 anchor 的网格偏移,范围约 [-8, 8]
  3. face 尺寸与 whole 尺寸

    • face 通道:3-4, 9-10, 15-16, 21-22
    • whole 通道:27-29
    • 变换:raw * size_scale + size_offset
    • 单位:米
  4. yaw residual

    • 通道:34-37
    • 变换:tanh(raw)
    • 含义:限制到 [-1, 1],表示 sin(delta)
  5. yaw bin logits

    • 通道:30-33
    • 不加激活,保持 raw logits
  6. cut logits

    • 通道:38-40
    • 不加激活,保持 raw logits
  7. face visible score

    • 通道:每个 face 的第 6 维,即 5, 11, 17, 23
    • 不加激活,保持原值

3D 语义解码

  1. whole yaw 解码:

    • 先取 yaw_cls_logits[30:34]argmax 得到最佳 bin
    • 再取同一 bin 的 yaw_residual_sin
    • 最终 yaw = asin(residual_sin) + yaw_bin_offset
  2. cut 状态解码:

    • cut_logits[38:41]argmax
    • 得到 normal / cut_in / cut_out
  3. visible face 选择:

    • 当前实现直接对 face 的 visible_score 与阈值比较
    • 这里不是 sigmoid 概率,而是“反归一化后直接比较”的分数

当前实现需要特别注意的一点

visible_score 当前没有走 sigmoid,而是直接作为一个原始连续值参与阈值判断与监督。这意味着:

  • 它更接近“回归分数”而不是标准概率
  • 文档或报告里如果把它理解成概率,会和实现不完全一致

4.5 edge head 输出

preds_edge 每个 anchor 固定 60 通道:

  • 4 个 face
  • 每个 face 5 个采样点
  • 每个采样点 3 个值:du, dv, z

即:

  • 60 = 4 * 5 * 3

每个 face 的 15 通道布局

以某个 face 为例:

  • 点 1du1, dv1, z1
  • 点 2du2, dv2, z2
  • ...
  • 点 5du5, dv5, z5

激活与反归一化

  1. du, dv

    • 变换:sigmoid(raw) * 16 - 8
    • 含义:相对 anchor 的网格偏移
  2. z

    • 变换:raw * z3d_scale + z3d_offset
    • 单位:米

作用

这一分支不直接参与最终 2D 检测输出,而主要用于:

  • 可见底边几何恢复
  • edge-based yaw 解码
  • cut object 的 partial side edge 重建
  • edge 辅助监督

5. 输出在不同类别上的使用方式

5.1 face_3d_classes

这类目标会同时使用:

  • 2D box/class
  • face 级 3D 分支
  • whole 3D 分支
  • cut 分类
  • edge 分支

5.2 complete_3d_classes

这类目标主要使用:

  • 2D box/class
  • whole 3D 分支

face 分支与 edge 分支虽然网络上也会输出,但在监督/评测上通常不作为有效主信号。

5.3 2D-only classes

这类目标只关心:

  • 2D box
  • 2D class

3D 分支对它们没有有效语义约束。

6. 一句话总结

当前 YOLO26-2D3D 模型本质上是一个三层 YOLO26 检测器,每个 anchor 同时输出:

  • 4 维 2D 框
  • nc=17 维分类
  • 41 维 3D 属性
  • 60 维 edge 几何

其中最关键的设计点是:

  • whole 分支负责整框 3D 位置/尺寸/yaw/cut
  • face 分支负责面中心 3D 属性与可见性
  • edge 分支负责基于几何的 yaw 与可见边重建
  • 推理阶段使用 end-to-end top-k 选择,而不是传统 NMS