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

393 lines
11 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 风格:
- Backbone`Conv + C3k2 + SPPF + C2PSA`
- Neck上采样 + 拼接 + `C3k2`
- 输出层P3/8、P4/16、P5/32 三层检测特征
即:
- `P3/8` 小目标层
- `P4/16` 中目标层
- `P5/32` 大目标层
### 3.2 检测头
最终 head 不是普通 `Detect`,而是 `Detect3D`
- 继承自 `Detect`
- 在 2D `box``cls` 之外,新增:
- `cv4`41 通道 3D 属性头
- `cv5`60 通道 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/P5`Detect3D` 都会产生 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 为例:
- 点 1`du1, dv1, z1`
- 点 2`du2, dv2, z2`
- ...
- 点 5`du5, 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