18 KiB
Executable File
双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.py 和 tools/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 侧解码”的运行时:
- 同一帧原图按两套 ROI 规则分别裁剪,得到
ROI0和ROI1。 - 两个 ROI 图像分别送入一个合并后的导出模型,模型输出每个 ROI 的原始检测头张量。
- 2D 框解码、Top-K 选择、3D 反归一化、深度恢复、3D 框重建、edge yaw 精化、可视化和 JSON 序列化全部在 Python 侧完成。
当前实现支持:
ONNX推理:通过onnxruntimeTorchScript推理:通过torch.jit.load- 单 case 推理:
--case-dir - mined eval 数据集批量推理:
--eval-dir
当前运行时不依赖 ultralytics。
3. 模型与运行时契约
3.1 导出模型形态
当前脚本只接受双 ROI 合并后的导出模型:
- 后缀为
.onnx时走 ONNX Runtime - 后缀为
.torchscript/.ts/.jit时走 TorchScript
脚本会读取导出清单:
- ONNX 优先读取同名 sidecar:
merged_model.export.json - TorchScript 优先读 sidecar,sidecar 不存在时再读模型内嵌的
config.txt
当前实现会显式校验:
export_mode == "raw_head_outputs"
也就是说,当前部署脚本只支持“原始检测头输出”模式,不接受 hybrid_outputs、postprocessed_outputs 等其他导出模式。
3.2 输入输出名称
若导出清单可用,则直接使用清单中的:
input_namesoutput_namesinput_sizes_wh
若清单缺失,则回退到默认约定:
- 输入名:
roi0_input、roi1_input - 输出名:
roi0_boxes_head_rawroi0_scores_head_rawroi0_preds_3d_head_rawroi0_preds_edge_head_rawroi1_boxes_head_rawroi1_scores_head_rawroi1_preds_3d_head_rawroi1_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_yvxvy:裁剪中心x取灭点vp_x,y取灭点vp_y
这些默认值可以被 --data-config、--roi0-data、--roi1-data 中的 roi_configs 覆盖;若命令行显式传参,也可以进一步覆盖。
3.4 运行时元数据
推理脚本会从数据配置中加载以下元数据:
class_mapface_3d_classescomplete_3d_classesnorm_scales_3d
若没有额外 YAML,使用内置默认值。
4. 前处理设计
4.1 相机标定读取
当前脚本兼容两种 camera4.json 结构:
- 扁平格式:
{
"focal_u": ...,
"focal_v": ...,
"cu": ...,
"cv": ...,
"pitch": ...,
"yaw": ...,
"distort_coeffs": [...]
}
- 合并格式:
{
"intrinsics": { "camera4.json": { ... } },
"extrinsics": { "camera4.json": { "rpy": [...] } }
}
脚本会规范化出:
focal_u,focal_vcu,cvpitch,yawdistort_coeffsangle_unit
4.2 灭点计算
灭点计算逻辑保持不变:
vp_x = cu + focal_u * tan(yaw)
vp_y = cv - focal_v * tan(pitch)
其中角度会先根据 angle_unit 统一到弧度制。
4.3 ROI 裁剪
对每一帧原图:
- 读取原始宽高
ori_w,ori_h - 根据 ROI 配置确定目标裁剪尺寸
roi_w,roi_h - 根据
crop_center_mode计算裁剪中心 - 通过
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 仍然输出四个分支:
boxes_head_rawscores_head_rawpreds_3d_head_rawpreds_edge_head_raw
即合并模型总共有 8 个输出张量。
5.2 3D 分支定义(41 维)
3D 分支通道布局沿用旧版设计:
| 通道范围 | 含义 |
|---|---|
0-5 |
front face:z3d, u_offset, v_offset, h, w, visible_score |
6-11 |
rear face:z3d, u_offset, v_offset, h, w, visible_score |
12-17 |
left face:z3d, u_offset, v_offset, l, h, visible_score |
18-23 |
right face:z3d, 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() 完成:
- 将
boxes_head_raw还原为l, t, r, b - 与预先缓存的 anchor 网格中心做几何组合
- 再乘以 stride 还原到 ROI-resized 图像像素坐标
anchor 由 build_anchor_cache() 预生成,默认 stride 为:
(8, 16, 32)
6.2 Top-K 选择
当前脚本没有做 NMS,而是严格遵循导出 raw head 的 one-to-one Top-K 路径:
- 对分类 logits 做 sigmoid
- 每个 anchor 取最大类别分数
- 先按 anchor 最大分类分数做一轮 Top-K
- 再对 gather 后的
(anchor, class)展平分数做第二轮 Top-K - 生成
detections = [x1, y1, x2, y2, conf, cls_id]
6.3 3D / Edge 反归一化
preds_3d_head_raw 和 preds_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 解码:
- 根据
pred_41中的 face visibility 选择可见面 - 根据最佳可见面读取
z_face + uv_face_offset - 利用整体尺寸
dims_whole和回归的yaw重建 3D 框 - 若
pred_edge_60可用,则同时解码可见底边缘点 - 若目标处于 cut 状态,还会尝试解码 partial side edge,并把它补充进可见面集合
这部分比旧版文档中的描述更具体,已经显式纳入了:
- cut state 感知
- partial edge 补边
- 多可见面 edge 采样
6.6.2 完整体 3D 类别
对 complete_3d_classes 中的类别,直接使用 whole-box 分支:
z_wholeuv_wholedims_wholeyaw
重建完整 3D 包围盒。
6.6.3 其他类别
对不属于上述两类集合的类别,脚本通常不会生成可视化 3D 框,但仍会保留 whole-box 分支解码出的:
center_3ddimsyaw_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 几何诊断链:
- 从
pred_edge_60解码选中的可见底边缘点 - 反投影得到 3D edge points
- 由
decode_edge_yaw_selection_from_prediction()选择最可信的单面或双面组合 - 计算
edge_yaw_rad - 判断横向距离是否满足
max_lateral_dist_m - 基于选中的 edge 几何重建
edge_box - 若重建可信,则生成
decoded_edge_heading
默认横向距离阈值为:
edge_yaw_max_lateral_dist_m = 5.0
当前实现额外产出以下诊断信息:
edge_yaw_confidentedge_yaw_lateral_distance_medge_yaw_lateral_okedge_yaw_two_face_eligibleedge_yaw_selected_face_typesedge_yaw_selected_face_is_partialedge_vs_reg_yaw_radedge_box_center_3dedge_box_dimsedge_box_modeedge_box_length_sourceedge_box_width_sourceselected_edge_direct_box_fit_*selected_edge_edgeyaw_box_fit_*selected_edge_fit_gain_px
6.9 可视化
每个 ROI 会输出三张 panel:
2D:绘制 ROI-resized 坐标系中的检测框3D:绘制常规回归 yaw 的 3D 框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 坐标系约定
当前实现涉及四种常见坐标系:
-
bbox_xyxyROI 裁剪并缩放后的图像坐标系 -
original_bbox_xyxy原始整图坐标系 -
center_uvROI-resized 图像坐标系下的中心点 -
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_outputs、hybrid_outputs、postprocessed_outputs、denorm_branch_outputs |
当前脚本只接受 raw_head_outputs,并在启动时校验 |
| 后端支持 | 主要按 ONNX 方案描述 | 当前同时支持 ONNX 和 TorchScript,并自动解析 manifest |
| 运行时依赖 | 更偏训练/导出视角 | 当前是完全自包含的部署侧实现,不依赖 ultralytics |
| ROI 缩放 | 单次 resize 描述 | 当前实现先多次 0.5x 下采样,再最终 resize,以贴合训练前处理 |
| 图像归一化 | /255.0 |
当前脚本实际使用 /256.0 |
| 输入尺寸来源 | 假定固定输入尺寸 | 当前优先读 manifest 中的 input_sizes_wh,也支持命令行覆盖 |
| 标定输入格式 | 主要描述平铺内参格式 | 当前同时兼容 flat camera4.json 和 combined calibration |
| 类别定义 | 类别表较粗,face_3d_classes 和 complete_3d_classes 范围较小 |
默认类表扩展到 17 个 id,face_3d_classes=0-8,complete_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_xyxy 和 original_bbox_xyxy,两种坐标系并存 |
| 可视化 | 只描述 2D/3D 结果 | 当前每个 ROI 输出 2D、3D、3D EdgeRecon 三个 panel |
| 运行模式 | 以单 case 为主 | 当前新增 --eval-dir,支持整个 mined eval 数据集复用同一个模型批量跑 |
9.1 对齐建议
如果后续继续维护部署文档,建议把以下三项视为“新版真值来源”:
tools/model_inference/run_two_roi_exported_onnx_infer.pytools/model_inference/core/two_roi_infer_utils.pytools/model_inference/core/two_roi_3d_utils.py
特别是以下内容最容易因实现更新而与文档产生偏差:
- 输入归一化常数
- ROI resize 策略
- 类别映射与 3D 类别分组
- edge yaw 的筛选和诊断字段
- 简化 JSON 中
box2d的坐标系定义