393 lines
11 KiB
Markdown
393 lines
11 KiB
Markdown
# 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
|