# 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 logits，4 个方向 bin |
| `34-37` | yaw residual，表示 `sin(delta)` |
| `38-40` | cut class logits，3 类：`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
