Files
yolov26_3d/tools/model_inference/docs/two_roi_model_design.md
2026-06-24 09:35:46 +08:00

18 KiB
Executable File
Raw Blame History

双ROI导出模型设计说明新版

若当前使用的是带 difficulty 分支或 fake 3D 分支的新版本双 ROI 合并模型,请优先参考: tools/model_inference/docs/two_roi_model_design_with_diff_and_fake3d.md

1. 文档目标

本文档以 tools/model_inference/run_two_roi_exported_onnx_infer.pytools/model_inference/core/run_two_roi_exported_onnx_infer.py 的当前实现为准,说明新版双 ROI 导出模型在部署侧的输入约定、前处理、输出张量、后处理、结果序列化方式,以及与旧版说明文档之间的差异。

本文档关注的是“部署可执行设计”而不是训练侧的完整网络细节。凡与脚本实现冲突之处,以当前推理脚本和其依赖的本地工具模块为准。

如需查看旧版文档,可参考 tools/model_inference/docs/two_roi_model_design_bak.md


2. 方案概览

新版方案仍然采用双 ROI 单目 3D 检测思路,但部署形态已经收敛为一个“自包含的合并导出模型 + Python 侧解码”的运行时:

  1. 同一帧原图按两套 ROI 规则分别裁剪,得到 ROI0ROI1
  2. 两个 ROI 图像分别送入一个合并后的导出模型,模型输出每个 ROI 的原始检测头张量。
  3. 2D 框解码、Top-K 选择、3D 反归一化、深度恢复、3D 框重建、edge yaw 精化、可视化和 JSON 序列化全部在 Python 侧完成。

当前实现支持:

  • ONNX 推理:通过 onnxruntime
  • TorchScript 推理:通过 torch.jit.load
  • 单 case 推理:--case-dir
  • mined eval 数据集批量推理:--eval-dir

当前运行时不依赖 ultralytics


3. 模型与运行时契约

3.1 导出模型形态

当前脚本只接受双 ROI 合并后的导出模型:

  • 后缀为 .onnx 时走 ONNX Runtime
  • 后缀为 .torchscript / .ts / .jit 时走 TorchScript

脚本会读取导出清单:

  • ONNX 优先读取同名 sidecarmerged_model.export.json
  • TorchScript 优先读 sidecarsidecar 不存在时再读模型内嵌的 config.txt

当前实现会显式校验:

export_mode == "raw_head_outputs"

也就是说,当前部署脚本只支持“原始检测头输出”模式,不接受 hybrid_outputspostprocessed_outputs 等其他导出模式。

3.2 输入输出名称

若导出清单可用,则直接使用清单中的:

  • input_names
  • output_names
  • input_sizes_wh

若清单缺失,则回退到默认约定:

  • 输入名:roi0_inputroi1_input
  • 输出名:
    • roi0_boxes_head_raw
    • roi0_scores_head_raw
    • roi0_preds_3d_head_raw
    • roi0_preds_edge_head_raw
    • roi1_boxes_head_raw
    • roi1_scores_head_raw
    • roi1_preds_3d_head_raw
    • roi1_preds_edge_head_raw

3.3 默认 ROI 配置

默认 ROI 配置来自 tools/model_inference/core/two_roi_infer_utils.py 中的 DEFAULT_DATASET_CONFIG

ROI 裁剪尺寸 (w, h) 裁剪中心模式 virtual_fx 默认输入尺寸 (w, h)
ROI0 1920 x 880 cxvy 537.0 768 x 352
ROI1 768 x 352 vxvy 537.0 768 x 352

其中:

  • cxvy:裁剪中心 x 取原图水平中心,y 取灭点 vp_y
  • vxvy:裁剪中心 x 取灭点 vp_xy 取灭点 vp_y

这些默认值可以被 --data-config--roi0-data--roi1-data 中的 roi_configs 覆盖;若命令行显式传参,也可以进一步覆盖。

3.4 运行时元数据

推理脚本会从数据配置中加载以下元数据:

  • class_map
  • face_3d_classes
  • complete_3d_classes
  • norm_scales_3d

若没有额外 YAML使用内置默认值。


4. 前处理设计

4.1 相机标定读取

当前脚本兼容两种 camera4.json 结构:

  1. 扁平格式:
{
  "focal_u": ...,
  "focal_v": ...,
  "cu": ...,
  "cv": ...,
  "pitch": ...,
  "yaw": ...,
  "distort_coeffs": [...]
}
  1. 合并格式:
{
  "intrinsics": { "camera4.json": { ... } },
  "extrinsics": { "camera4.json": { "rpy": [...] } }
}

脚本会规范化出:

  • focal_u, focal_v
  • cu, cv
  • pitch, yaw
  • distort_coeffs
  • angle_unit

4.2 灭点计算

灭点计算逻辑保持不变:

vp_x = cu + focal_u * tan(yaw)
vp_y = cv - focal_v * tan(pitch)

其中角度会先根据 angle_unit 统一到弧度制。

4.3 ROI 裁剪

对每一帧原图:

  1. 读取原始宽高 ori_w, ori_h
  2. 根据 ROI 配置确定目标裁剪尺寸 roi_w, roi_h
  3. 根据 crop_center_mode 计算裁剪中心
  4. 通过 compute_centered_roi_bounds() 做边界裁剪

裁剪边界为:

crop_x1 = clamp(center_x - roi_w / 2, 0, ori_w - roi_w)
crop_y1 = clamp(center_y - roi_h / 2, 0, ori_h - roi_h)
crop_x2 = crop_x1 + roi_w
crop_y2 = crop_y1 + roi_h

4.4 缩放与标定更新

这是新版实现中一个关键变化点。

旧版文档默认把 ROI 裁剪结果直接一次性 resize 到模型输入尺寸;而当前实现使用 _resize_ground3d_image_in_steps(),先重复做若干次 0.5x 下采样,再做最后一次 resize以匹配 Ground3D 训练侧的图像缩放行为。

缩放完成后,相机标定会更新到 ROI-resized 坐标系:

scale_x = target_w / crop_w
scale_y = target_h / crop_h

fx = focal_u * scale_x
fy = focal_v * scale_y
cx = (cu - crop_x1) * scale_x
cy = (cv - crop_y1) * scale_y

depth_scale = fx / virtual_fx

其中:

  • fx, fy, cx, cy 是 resized ROI 空间下的标定
  • depth_scale 用于后处理阶段把网络预测深度恢复到真实焦距尺度

4.5 图像张量化

当前脚本的输入张量构造为:

image_rgb = cv2.cvtColor(image_bgr, cv2.COLOR_BGR2RGB)
array = image_rgb.transpose(2, 0, 1).astype(np.float32) / 256.0
input = array[None, ...]

注意这里是:

/ 256.0

而不是旧文档中的 / 255.0

TorchScript 路径与 ONNX 路径共用同一份前处理逻辑。


5. 输出张量设计

5.1 每个 ROI 的输出分支

当前部署脚本假设每个 ROI 仍然输出四个分支:

  1. boxes_head_raw
  2. scores_head_raw
  3. preds_3d_head_raw
  4. preds_edge_head_raw

即合并模型总共有 8 个输出张量。

5.2 3D 分支定义41 维)

3D 分支通道布局沿用旧版设计:

通道范围 含义
0-5 front facez3d, u_offset, v_offset, h, w, visible_score
6-11 rear facez3d, u_offset, v_offset, h, w, visible_score
12-17 left facez3d, u_offset, v_offset, l, h, visible_score
18-23 right facez3d, u_offset, v_offset, l, h, visible_score
24 whole-box z3d
25-26 whole-box u_offset, v_offset
27-29 whole-box l, h, w
30-33 yaw 4-bin 分类 logits
34-37 yaw 残差 sin(delta)
38-40 cut state logits

5.3 Edge 分支定义60 维)

Edge 分支依然是四个面的底边缘采样点,每个采样点 3 维:

[du, dv, z]

每个面 5 个点,共 5 x 3 = 15 维,四个面共 60 维:

通道范围
front 0-14
rear 15-29
left 30-44
right 45-59

5.4 默认输出形状

对默认输入尺寸 768 x 352

A = (352/8 * 768/8) + (352/16 * 768/16) + (352/32 * 768/32)
  = 4224 + 1056 + 264
  = 5544

因此典型输出为:

输出名 形状 含义
roi*_boxes_head_raw [1, 4 * reg_max, 5544] 2D 框原始回归输出
roi*_scores_head_raw [1, nc, 5544] 分类原始 logits
roi*_preds_3d_head_raw [1, 41, 5544] 3D 原始预测
roi*_preds_edge_head_raw [1, 60, 5544] edge 原始预测

脚本支持 reg_max > 1 的通用 DFL 解码;当前导出模型若 reg_max == 1,会退化为直接距离回归。


6. 后处理设计

6.1 2D 框解码

2D 框解码由 decode_boxes_xyxy() 完成:

  1. boxes_head_raw 还原为 l, t, r, b
  2. 与预先缓存的 anchor 网格中心做几何组合
  3. 再乘以 stride 还原到 ROI-resized 图像像素坐标

anchor 由 build_anchor_cache() 预生成,默认 stride 为:

(8, 16, 32)

6.2 Top-K 选择

当前脚本没有做 NMS而是严格遵循导出 raw head 的 one-to-one Top-K 路径:

  1. 对分类 logits 做 sigmoid
  2. 每个 anchor 取最大类别分数
  3. 先按 anchor 最大分类分数做一轮 Top-K
  4. 再对 gather 后的 (anchor, class) 展平分数做第二轮 Top-K
  5. 生成 detections = [x1, y1, x2, y2, conf, cls_id]

6.3 3D / Edge 反归一化

preds_3d_head_rawpreds_edge_head_raw 会用 norm_scales_3d 做反归一化:

项目 反归一化方式
z3d raw * z3d_scale + z3d_offset
u/v offset sigmoid(raw) * 16 - 8
size raw * size_scale + size_offset
yaw residual tanh(raw)
edge z raw * z3d_scale + z3d_offset
edge du/dv sigmoid(raw) * 16 - 8

默认值来自内置配置:

z3d_scale  = 24.415
z3d_offset = 39.937
size_scale = 1.945
size_offset = 3.780

6.4 深度恢复

由于训练时使用 virtual_fx,部署时必须恢复真实焦距尺度:

preds_3d[:, (0, 6, 12, 18, 24)] *= depth_scale
preds_edge[:, 2::3] *= depth_scale

这里的 depth_scale = fx / virtual_fx 是按每个 ROI、每一帧动态计算的。

6.5 置信度与类别过滤

当前脚本在完成 Top-K 后,再通过 filter_prediction_rows() 做最终过滤:

  • conf >= roi.spec.conf
  • 若给了 --classes,则再做类别白名单过滤
  • 最终数量再截断到 max_det

6.6 3D 框解码与重建

6.6.1 面型 3D 类别

face_3d_classes 中的类别,当前实现使用“可见面驱动”的 3D 解码:

  1. 根据 pred_41 中的 face visibility 选择可见面
  2. 根据最佳可见面读取 z_face + uv_face_offset
  3. 利用整体尺寸 dims_whole 和回归的 yaw 重建 3D 框
  4. pred_edge_60 可用,则同时解码可见底边缘点
  5. 若目标处于 cut 状态,还会尝试解码 partial side edge并把它补充进可见面集合

这部分比旧版文档中的描述更具体,已经显式纳入了:

  • cut state 感知
  • partial edge 补边
  • 多可见面 edge 采样

6.6.2 完整体 3D 类别

complete_3d_classes 中的类别,直接使用 whole-box 分支:

  • z_whole
  • uv_whole
  • dims_whole
  • yaw

重建完整 3D 包围盒。

6.6.3 其他类别

对不属于上述两类集合的类别,脚本通常不会生成可视化 3D 框,但仍会保留 whole-box 分支解码出的:

  • center_3d
  • dims
  • yaw_rad

用于结果序列化。

6.7 Yaw 解码

Yaw 解码仍是 4-bin 分类 + 残差方式:

best_bin = argmax(pred[30:34])
yaw = arcsin(clamp(pred[34 + best_bin], -1, 1)) + yaw_bin_offset[best_bin]

6.8 Edge Yaw 与 Edge Box 重建

新版实现中edge 分支不只是“可选 yaw 精化”,而是形成了一条完整的 edge-based 几何诊断链:

  1. pred_edge_60 解码选中的可见底边缘点
  2. 反投影得到 3D edge points
  3. decode_edge_yaw_selection_from_prediction() 选择最可信的单面或双面组合
  4. 计算 edge_yaw_rad
  5. 判断横向距离是否满足 max_lateral_dist_m
  6. 基于选中的 edge 几何重建 edge_box
  7. 若重建可信,则生成 decoded_edge_heading

默认横向距离阈值为:

edge_yaw_max_lateral_dist_m = 5.0

当前实现额外产出以下诊断信息:

  • edge_yaw_confident
  • edge_yaw_lateral_distance_m
  • edge_yaw_lateral_ok
  • edge_yaw_two_face_eligible
  • edge_yaw_selected_face_types
  • edge_yaw_selected_face_is_partial
  • edge_vs_reg_yaw_rad
  • edge_box_center_3d
  • edge_box_dims
  • edge_box_mode
  • edge_box_length_source
  • edge_box_width_source
  • selected_edge_direct_box_fit_*
  • selected_edge_edgeyaw_box_fit_*
  • selected_edge_fit_gain_px

6.9 可视化

每个 ROI 会输出三张 panel

  1. 2D:绘制 ROI-resized 坐标系中的检测框
  2. 3D:绘制常规回归 yaw 的 3D 框
  3. 3D EdgeRecon (1+ face):只绘制 edge_yaw_confident=True 的 edge 重建结果

最终按 ROI 拼成一个总览网格图。


7. 输出数据设计

7.1 输出目录结构

单 case 推理时,输出目录下会生成:

output_dir/
├── visualizations/
│   ├── *.jpg
├── predictions/
│   ├── *.json
└── predictions.json

其中:

  • visualizations/*.jpg:每帧一个可视化网格图
  • predictions/*.json:下游消费的简化逐帧结果
  • predictions.json:完整聚合结果

批量 eval 推理时,会在 output_dir 下保留与 eval_dir 相同的相对目录层级。

7.2 完整聚合结果 predictions.json

聚合结果顶层结构为:

{
  "case_name": "...",
  "images_dir": "...",
  "calib_file": "...",
  "exported_model_path": "...",
  "edge_yaw_max_lateral_dist_m": 5.0,
  "frames": [
    {
      "frame_index": 0,
      "frame_name": "000000.png",
      "visualization": ".../visualizations/000000.jpg",
      "rois": {
        "roi0": {
          "crop_bounds": [x1, y1, x2, y2],
          "vp_x": ...,
          "vp_y": ...,
          "crop_center_x": ...,
          "crop_center_y": ...,
          "edge_yaw_max_lateral_dist_m": 5.0,
          "calib": {
            "fx": ...,
            "fy": ...,
            "cx": ...,
            "cy": ...,
            "depth_scale": ...
          },
          "predictions": [
            {
              "bbox_xyxy": [...],
              "original_bbox_xyxy": [...],
              "confidence": 0.93,
              "cls_id": 6,
              "cls_name": "truck",
              "center_uv": [...],
              "center_3d": [...],
              "dims": [...],
              "yaw_rad": ...,
              "edge_yaw_rad": ...,
              "edge_yaw_confident": true,
              "cut_cls": 0,
              "roi_id": 0,
              "visible_face_type": 1,
              "visible_face_types": [1, 2]
            }
          ]
        }
      }
    }
  ]
}

7.3 简化逐帧结果 predictions/*.json

脚本还会额外导出更接近下游消费格式的逐帧 JSON例如

{
  "0": {
    "type": "0",
    "type_name": "truck",
    "score": "0.9664",
    "roi_id": "0",
    "box2d": ["1357.25", "542.76", "1743.78", "803.98"],
    "xyzlhwyaw": ["3.58", "0.69", "5.21", "4.62", "1.92", "1.48", "-1.57"],
    "face_cls": "tail",
    "cut_cls": "0",
    "edge_yaw_rad": "-1.56",
    "edge_yaw_confident": true,
    "edge_vs_reg_yaw_rad": "0.01"
  }
}

这里有一个新版语义变化:

  • box2d 优先使用 original_bbox_xyxy
  • 若原图坐标不可用,才回退到 bbox_xyxy

也就是说,简化逐帧 JSON 里的 2D 框优先是“原图坐标系”。

7.4 坐标系约定

当前实现涉及四种常见坐标系:

  1. bbox_xyxy ROI 裁剪并缩放后的图像坐标系

  2. original_bbox_xyxy 原始整图坐标系

  3. center_uv ROI-resized 图像坐标系下的中心点

  4. center_3d 相机坐标系,单位米

旧版文档中“所有二维坐标均在 ROI 坐标系”这一表述对当前实现已不完全成立,因为现在明确同时输出了 original_bbox_xyxy


8. 默认类别配置

当前运行时默认的 class_map 为:

cls_id 类别
0 car
1 suv
2 pickup
3 medium_car
4 van
5 bus
6 truck / tanker / large_truck / construction_vehicle
7 special_vehicle
8 unknown
9 pedestrian
10 bicyclist / motorcyclist
11 bicycle / motorcycle
12 tricycle / tricyclist
13 traffic_sign
14 wheel
15 plate
16 face

默认 3D 类别分组为:

  • face_3d_classes = {0,1,2,3,4,5,6,7,8}
  • complete_3d_classes = {9,10,11,12}

因此:

  • 0-8 使用“可见面驱动”的 3D 解码
  • 9-12 使用 whole-box 3D 解码
  • 13-16 默认不绘制 3D 框,但仍保留部分序列化属性

9. 新旧版本差异总结

下表总结了新版实现相对旧版说明文档的主要差异。

项目 旧版说明 新版实现
导出模式 文档列出了 raw_head_outputshybrid_outputspostprocessed_outputsdenorm_branch_outputs 当前脚本只接受 raw_head_outputs,并在启动时校验
后端支持 主要按 ONNX 方案描述 当前同时支持 ONNXTorchScript,并自动解析 manifest
运行时依赖 更偏训练/导出视角 当前是完全自包含的部署侧实现,不依赖 ultralytics
ROI 缩放 单次 resize 描述 当前实现先多次 0.5x 下采样,再最终 resize以贴合训练前处理
图像归一化 /255.0 当前脚本实际使用 /256.0
输入尺寸来源 假定固定输入尺寸 当前优先读 manifest 中的 input_sizes_wh,也支持命令行覆盖
标定输入格式 主要描述平铺内参格式 当前同时兼容 flat camera4.json 和 combined calibration
类别定义 类别表较粗,face_3d_classescomplete_3d_classes 范围较小 默认类表扩展到 17 个 idface_3d_classes=0-8complete_3d_classes=9-12
3D 解码 描述了基础 face / whole 两条路径 当前新增 cut-aware partial edge、edge 几何筛选、edge box 重建等更完整逻辑
edge yaw 作为可选精化模块描述 当前 edge yaw 已融入主结果结构,带 lateral gating、双面可见判断和拟合残差诊断
输出 JSON 主要描述聚合 JSON字段较少 当前同时输出完整聚合 JSON 和简化逐帧 JSON并新增大量 edge 诊断字段
2D 坐标语义 默认都在 ROI-resized 坐标系 当前同时输出 bbox_xyxyoriginal_bbox_xyxy,两种坐标系并存
可视化 只描述 2D/3D 结果 当前每个 ROI 输出 2D3D3D EdgeRecon 三个 panel
运行模式 以单 case 为主 当前新增 --eval-dir,支持整个 mined eval 数据集复用同一个模型批量跑

9.1 对齐建议

如果后续继续维护部署文档,建议把以下三项视为“新版真值来源”:

  1. tools/model_inference/run_two_roi_exported_onnx_infer.py
  2. tools/model_inference/core/two_roi_infer_utils.py
  3. tools/model_inference/core/two_roi_3d_utils.py

特别是以下内容最容易因实现更新而与文档产生偏差:

  • 输入归一化常数
  • ROI resize 策略
  • 类别映射与 3D 类别分组
  • edge yaw 的筛选和诊断字段
  • 简化 JSON 中 box2d 的坐标系定义