# 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