单目3D初始代码

This commit is contained in:
zhao.zhu
2026-06-24 09:35:46 +08:00
commit 04a5895b6b
1153 changed files with 340700 additions and 0 deletions

View File

@@ -0,0 +1,48 @@
{
"0": {
"type": "0",
"type_name": "truck",
"score": "0.9664586186408997",
"roi_id": "0",
"box2d": [
"1357.25390625",
"542.76123046875",
"1743.7847290039062",
"803.9830627441406"
],
"xyzlhwyaw": [
"3.5862553000515422",
"0.6910880269131991",
"5.21181583404541",
"4.622976303100586",
"1.9204307794570923",
"1.481898546218872",
"-1.5685003995895386"
],
"face_cls": "tail",
"cut_cls": "0"
},
"1": {
"type": "0",
"type_name": "truck",
"score": "0.9642692804336548",
"roi_id": "0",
"box2d": [
"791.8895263671875",
"498.2795104980469",
"1162.1598205566406",
"816.8395385742188"
],
"xyzlhwyaw": [
"-0.04261477524786694",
"0.5091491993856199",
"6.116147041320801",
"4.321102619171143",
"2.0373644828796387",
"1.6876970529556274",
"-1.5730922222137451"
],
"face_cls": "tail",
"cut_cls": "0"
}
}

View File

@@ -0,0 +1,9 @@
tools/model_inference/examples/cncap/G1M3_AFS1616_CNCAP-202411.json文件中记录了一批场地评测数据的路径。
一组数据路径的示例为:"/mnt/hfs/project-G1M3/gt_org_data/G1M3_AFS1616/CNCAP2024数采/20251115/CVE/CPLA_RL_AEB_20_5_1_20251115140740/sigmastar.1",查找本地挂载路径时,需替换'hfs/project-G1M3'为'G1M3'同时查找路径下的camera4.bin。
具体的视频路径为"/mnt/hfs/project-G1M3/gt_org_data/G1M3_AFS1616/CNCAP2024数采/20251115/CVE/CPLA_RL_AEB_20_5_1_20251115140740/sigmastar.1/camera4.bin"
另外,其对应的标定参数路径为:"/mnt/hfs/project-G1M3/gt_org_data/G1M3_AFS1616/CNCAP2024数采/20251115/CVE/test_data/calibs/camera4.json"
现在期望tools/model_inference/run_two_roi_exported_onnx_infer.py脚本能够接收tools/model_inference/examples/cncap/G1M3_AFS1616_CNCAP-202411.json的输入从而对相关的视频数据进行模型推理。

View File

@@ -0,0 +1,5 @@
tools/model_inference/examples/events/G1Q3_场地评测数据集.json文件中记录了不同工况的数据每个工况下有多组数据每组数据中的data_path为eventid信息。
tools/model_inference/adapters/get_clip_by_eventid.py脚本可以用过event_id信息获取clip数据
现在期望tools/model_inference/run_two_roi_exported_onnx_infer.py脚本能够接收tools/model_inference/examples/events/G1Q3_场地评测数据集.json的输入从而对相关的clip数据进行模型推理。

View File

@@ -0,0 +1,8 @@
/data1/dongying/Mono3d/G1M3/test_data/cncap_20260408
上述路径中存放了多组测试数据,每组测试数据中有视频数据和标定参数,具体示例路径如下:
/data1/dongying/Mono3d/G1M3/test_data/cncap_20260408/20260407203248/sigmastar.1/camera4.bin
/data1/dongying/Mono3d/G1M3/test_data/cncap_20260408/20260407203248/test_data/calibs/camera4.json
请对脚本tools/model_inference/run_two_roi_exported_onnx_infer.py的输入接口进行扩展支持这种视频数据路径的模型推理。

View File

@@ -0,0 +1,654 @@
# 双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 侧解码”的运行时:
1. 同一帧原图按两套 ROI 规则分别裁剪,得到 `ROI0``ROI1`
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 优先读取同名 sidecar`merged_model.export.json`
- TorchScript 优先读 sidecarsidecar 不存在时再读模型内嵌的 `config.txt`
当前实现会显式校验:
```text
export_mode == "raw_head_outputs"
```
也就是说,当前部署脚本只支持“原始检测头输出”模式,不接受 `hybrid_outputs``postprocessed_outputs` 等其他导出模式。
### 3.2 输入输出名称
若导出清单可用,则直接使用清单中的:
- `input_names`
- `output_names`
- `input_sizes_wh`
若清单缺失,则回退到默认约定:
- 输入名:`roi0_input``roi1_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_x``y` 取灭点 `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. 扁平格式:
```json
{
"focal_u": ...,
"focal_v": ...,
"cu": ...,
"cv": ...,
"pitch": ...,
"yaw": ...,
"distort_coeffs": [...]
}
```
2. 合并格式:
```json
{
"intrinsics": { "camera4.json": { ... } },
"extrinsics": { "camera4.json": { "rpy": [...] } }
}
```
脚本会规范化出:
- `focal_u`, `focal_v`
- `cu`, `cv`
- `pitch`, `yaw`
- `distort_coeffs`
- `angle_unit`
### 4.2 灭点计算
灭点计算逻辑保持不变:
```text
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()` 做边界裁剪
裁剪边界为:
```text
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 坐标系:
```text
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 图像张量化
当前脚本的输入张量构造为:
```python
image_rgb = cv2.cvtColor(image_bgr, cv2.COLOR_BGR2RGB)
array = image_rgb.transpose(2, 0, 1).astype(np.float32) / 256.0
input = array[None, ...]
```
注意这里是:
```text
/ 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 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 维:
```text
[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`
```text
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 为:
```text
(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_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` |
默认值来自内置配置:
```text
z3d_scale = 24.415
z3d_offset = 39.937
size_scale = 1.945
size_offset = 3.780
```
### 6.4 深度恢复
由于训练时使用 `virtual_fx`,部署时必须恢复真实焦距尺度:
```text
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 分类 + 残差方式:
```text
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`
默认横向距离阈值为:
```text
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 推理时,输出目录下会生成:
```text
output_dir/
├── visualizations/
│ ├── *.jpg
├── predictions/
│ ├── *.json
└── predictions.json
```
其中:
- `visualizations/*.jpg`:每帧一个可视化网格图
- `predictions/*.json`:下游消费的简化逐帧结果
- `predictions.json`:完整聚合结果
批量 eval 推理时,会在 `output_dir` 下保留与 `eval_dir` 相同的相对目录层级。
### 7.2 完整聚合结果 `predictions.json`
聚合结果顶层结构为:
```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例如
```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_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 对齐建议
如果后续继续维护部署文档,建议把以下三项视为“新版真值来源”:
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` 的坐标系定义

View File

@@ -0,0 +1,385 @@
# 双ROI合并模型方案文档
## 1. 概述
本文档描述基于 YOLO26-3D 的双 ROIRegion of Interest单目3D目标检测方案。该方案将同一张原始图像按两种不同的裁剪策略分别送入两个独立训练的 Detect3D 模型,并将两模型合并导出为单一 ONNX/TorchScript 文件实现2D检测框与3D属性深度、尺寸、朝向角的联合预测。
---
## 2. 模型架构
### 2.1 骨干网络与检测头
模型基于 `yolo26-3d.yaml` 配置,结构如下:
```
输入 (3 × H × W)
├── Backbone: Conv → C3k2 × 2 → Conv → C3k2 × 2 → Conv → C3k2 → Conv → C3k2 → SPPF → C2PSA
└── Neck (FPN): Upsample + Concat × 2 → C3k2
→ Detect3D(P3/8, P4/16, P5/32)
```
模型使用 `end2end=True`,即训练时启用 one-to-one 分支,推理时直接输出 Top-K 检测结果,无需 NMS。
### 2.2 Detect3D 检测头
`Detect3D` 继承自标准 `Detect`在2D分支cv2 box、cv3 cls基础上新增
- **cv43D预测分支**:每个 anchor 输出 41 维张量;
- **cv5可见面边缘分支**:每个 anchor 输出 60 维张量。
#### 3D预测张量41维格式
| 通道范围 | 含义 |
|----------|------|
| 05 | 前面Front face: z3d, u_offset, v_offset, h, w, visible_score |
| 611 | 后面Rear face: z3d, u_offset, v_offset, h, w, visible_score |
| 1217 | 左面Left face: z3d, u_offset, v_offset, l, h, visible_score |
| 1823 | 右面Right face: z3d, u_offset, v_offset, l, h, visible_score |
| 24 | 整体 z3d |
| 2526 | 整体 u_offset, v_offset网格坐标偏移 |
| 2729 | 整体 l, h, w |
| 3033 | 4个朝向 bin 的分类 logits |
| 3437 | 4个朝向 bin 的残差 sin 值 |
| 3840 | cut 状态分类 logitsnormal/cut-in/cut-out |
#### 可见面边缘张量60维格式
每张可见面对应 5 个采样点 × 3 维du, dv, z共 4 个面 × 15 维 = 60 维:
| 面索引 | 通道范围 |
|--------|----------|
| 前面 | 014 |
| 后面 | 1529 |
| 左面 | 3044 |
| 右面 | 4559 |
每个采样点格式:`[du (网格偏移), dv (网格偏移), z (米)]`
### 2.3 双 ROI 设计
两个独立模型分别对同一帧图像的不同感兴趣区域进行预测:
| 参数 | ROI0 | ROI1 |
|------|------|------|
| 裁剪尺寸(宽×高) | 1920 × 880 | 768 × 352 |
| 裁剪中心模式 | `cxvy`(图像水平中心 + 灭点y | `vxvy`灭点x + 灭点y |
| 虚拟焦距 virtual_fx | 537.0 | 537.0 |
| 模型输入尺寸 | 768 × 352 | 768 × 352 |
- **ROI0**以图像水平中心为裁剪中心x涵盖宽视野适合远距离大视野检测
- **ROI1**:以灭点为裁剪中心,聚焦正前方,适合近中距离精细检测。
### 2.4 合并导出
两模型通过 `merge_models_of_2roi_yolo26.py` 合并为单一模型文件(`merged_model.onnx``.torchscript`),导出时同时写出 `merged_model.export.json` 元数据 sidecar 文件,记录 input_names、output_names、input_sizes_wh 等信息。
支持多种导出模式(`--export-mode`
| 模式 | 描述 |
|------|------|
| `raw_head_outputs`(默认) | 输出各分支的原始未后处理张量,所有解码在推理侧完成 |
| `hybrid_outputs` | 输出后处理2D检测 + 原始 3D/edge 张量 |
| `postprocessed_outputs` | 输出完整后处理结果(含 Top-K 选取) |
| `denorm_branch_outputs` | 输出反归一化后、Top-K 选取前的分支张量 |
---
## 3. 推理前处理
### 3.1 加载相机标定
`camera4.json` 读取原始相机内参和外参:
```
RawCameraCalib:
focal_u, focal_v — 原始像素焦距
cu, cv — 主点 (principal point)
pitch, yaw — 相机安装角度(弧度)
distort_coeffs — 鱼眼畸变系数 [k1, k2, k3, k4](可选)
```
### 3.2 计算灭点Vanishing Point
根据相机安装姿态计算图像灭点坐标:
```
vp_x = cu + focal_u × tan(yaw)
vp_y = cv - focal_v × tan(pitch)
```
### 3.3 ROI 裁剪
根据 ROI 规格(`roi_size``crop_center_mode`)在原图上确定裁剪区域:
```
crop_center_x:
cxvy 模式: ori_w / 2
vxvy 模式: vp_x
crop_center_y = vp_y两种模式均以灭点y为中心
crop_bounds [x1, y1, x2, y2]:
x1 = clamp(crop_center_x - roi_w/2, 0, ori_w - roi_w)
y1 = clamp(crop_center_y - roi_h/2, 0, ori_h - roi_h)
```
裁剪结果为固定尺寸图像块roi_w × roi_h
### 3.4 缩放与标定更新
将裁剪图像缩放到模型输入尺寸768 × 352同时更新相机内参
```
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 # 深度恢复系数
```
`depth_scale` 记录了当前帧缩放后真实焦距与虚拟训练焦距之间的比值,用于后处理阶段的深度恢复。
### 3.5 图像归一化
```
image_rgb = BGR → RGB 转换
tensor = image_rgb.transpose(2,0,1) # HWC → CHW
tensor = tensor / 255.0 # 归一化到 [0, 1]
input = tensor[None, ...] # 增加 batch 维
```
---
## 4. 模型输出格式
以默认 `raw_head_outputs` 模式为例,合并模型共输出 **8 个张量**(每 ROI 4 个):
| 输出名 | 形状 | 含义 |
|--------|------|------|
| `roi0_boxes_head_raw` | `[1, 4×reg_max, A]` | ROI0 DFL box 回归原始 logits |
| `roi0_scores_head_raw` | `[1, nc, A]` | ROI0 类别分数原始 logits |
| `roi0_preds_3d_head_raw` | `[1, 41, A]` | ROI0 3D 预测原始值 |
| `roi0_preds_edge_head_raw` | `[1, 60, A]` | ROI0 可见面边缘原始值 |
| `roi1_boxes_head_raw` | `[1, 4×reg_max, A]` | ROI1 DFL box 回归原始 logits |
| `roi1_scores_head_raw` | `[1, nc, A]` | ROI1 类别分数原始 logits |
| `roi1_preds_3d_head_raw` | `[1, 41, A]` | ROI1 3D 预测原始值 |
| `roi1_preds_edge_head_raw` | `[1, 60, A]` | ROI1 可见面边缘原始值 |
其中 `nc` 为类别数,`A` 为总 anchor 数(三个尺度之和):
```
A = (H/8 × W/8) + (H/16 × W/16) + (H/32 × W/32)
对于 352×768: = (44×96) + (22×48) + (11×24) = 4224 + 1056 + 264 = 5544
```
`reg_max=1`(当前配置),因此 box 分支通道数为 4。
---
## 5. 后处理步骤
### 5.1 2D 检测框解码
**DFL 解码**(分布式焦点损失解码):
```python
# reg_max == 1 时简化为:
dist = raw_boxes.reshape(batch, 4, A) # [l, t, r, b] 形式的距离预测
# 还原为 anchor-relative xyxy:
boxes_xyxy[:, 0:2] = anchors - dist[:, 0:2] # x1, y1
boxes_xyxy[:, 2:4] = anchors + dist[:, 2:4] # x2, y2
# 乘以 stride 还原到像素坐标:
boxes_xyxy = boxes_xyxy * strides
```
Anchor 坐标为各尺度特征图的网格中心点0.5 偏移),由 `build_anchor_cache()` 预计算。
### 5.2 Top-K 选取(无 NMS
模型使用 end-to-end one-to-one 训练,推理时直接按类别最高分取 Top-K默认 K=300无需 NMS
```python
# 1. sigmoid 激活类别分数
scores = sigmoid(raw_scores) # [1, A, nc]
# 2. 每个 anchor 取最大分数,选 Top-K anchor
anchor_max_scores = scores.max(axis=-1) # [1, A]
topk_anchor_indices = argsort(anchor_max_scores)[-K:]
# 3. 再对 gathered scores 扁平化取 Top-K双重 Top-K
# 最终输出形状: detections [1, K, 6] (x1,y1,x2,y2, conf, cls_id)
```
### 5.3 3D 预测反归一化
原始 3D 张量在网络内经过归一化压缩,推理侧需还原为物理量:
| 通道 | 操作 | 含义 |
|------|------|------|
| z3dch 0,6,12,18,24 | `raw × z3d_scale + z3d_offset` | 深度(米),默认 scale=24.415, offset=39.937 |
| UV 偏移ch 1-2, 7-8, 13-14, 19-20, 25-26 | `sigmoid(raw) × 16 8` | 网格坐标偏移(格子数,范围约 [8, 8] |
| 尺寸 l/h/wch 3-4, 9-10, 15-16, 21-22, 27-29 | `raw × size_scale + size_offset` | 尺寸(米),默认 scale=1.945, offset=3.780 |
| 朝向 bin logitsch 30-33 | 保持原始 logits | 4-bin softmax 分类 |
| 朝向残差ch 34-37 | `tanh(raw)` | sin(Δ) in [1, 1] |
| face visibilitych 5,11,17,23 | 保持原始分数 | 各面可见置信度 |
| cut 状态ch 38-40 | 保持原始 logits | normal/cut-in/cut-out |
同理edge 张量60维的 UV 偏移和深度 z 也做相同的 sigmoid/线性反归一化。
### 5.4 深度恢复Depth Scale 还原)
由于训练时使用固定 `virtual_fx` 虚拟焦距,而实际推理焦距因裁剪+缩放而变化,需乘以 `depth_scale` 还原真实深度:
```python
preds_3d[:, [0, 6, 12, 18, 24]] *= depth_scale # z3d 通道
preds_edge[:, 2::3] *= depth_scale # edge 深度通道
```
其中 `depth_scale = fx_actual / virtual_fx`,每帧根据实际裁剪/缩放参数计算。
### 5.5 置信度过滤
按置信度阈值(默认 0.25)和类别白名单过滤低质量检测:
```python
keep = detections[:, 4] >= conf_thres
if classes is not None:
keep &= cls_id in classes
detections = detections[keep][:max_det]
```
### 5.6 朝向角解码
模型采用 **4-bin 分类 + 残差 sin** 联合解码朝向角:
```
4 个 bin 偏置角: [0°, 90°, 90°, 180°]
yaw_bin = argmax(logits[30:34])
bin_offset = {0: 0, 1: π/2, 2: −π/2, 3: π}[yaw_bin]
sin_delta = tanh(raw[34 + yaw_bin])
delta = arcsin(clamp(sin_delta, 1, 1))
yaw_reg = bin_offset + delta
```
### 5.7 3D 包围盒重建
#### 面型类face_3d_classes车辆等大目标
1. 按 face visibility score 选取最高置信可见面front/rear/left/right
2. 用该面的 UV 偏移 + z 在像素坐标系中反投影,得到 3D 中心点;
3. 结合整体尺寸 l/h/w 和朝向角 yaw计算 8 个角点坐标;
4. 若有 edge 预测,用 cv5 中对应面的5个采样点重投影辅助可视化。
```python
# 面中心 UV → 像素坐标
u_face = (anchor_x + u_offset) * stride
v_face = (anchor_y + v_offset) * stride
# 像素坐标 → 相机坐标系 3D 中心
X = (u_face - cx) / fx * z_face
Y = (v_face - cy) / fy * z_face
Z = z_face
# 基于 3D 中心 + 尺寸 + 朝向重建 8 角点
corners = compute_3d_box_corners(center_3d, [l, h, w], yaw, face_type)
```
#### 完整型类complete_3d_classes行人、骑手等
直接使用整体预测(通道 2429重建包围盒无需面选择。
### 5.8 Edge Yaw 精化(可选)
对于横向距离 ≤ `edge_yaw_max_lateral_dist_m`(默认 5.0m)的目标,尝试利用可见面底边缘点拟合更精确的朝向角:
1. 从 cv5 解码可选的1-2个可见面底边缘采样点2D
2. 结合深度,将采样点反投影到 3D
3. 拟合底边缘方向,得到 `edge_yaw_rad`
4. 若两面同时可见two-face eligible优先使用双面拟合结果`edge_yaw_confident=True`
---
## 6. 输出数据格式
每帧的结构化输出以 JSON 格式保存:
```json
{
"frame_index": 0,
"frame_name": "000000.png",
"rois": {
"roi0": {
"crop_bounds": [x1, y1, x2, y2],
"vp_x": 960.0,
"vp_y": 175.0,
"calib": { "fx": ..., "fy": ..., "cx": ..., "cy": ..., "depth_scale": ... },
"predictions": [
{
"bbox_xyxy": [x1, y1, x2, y2],
"confidence": 0.87,
"cls_id": 0,
"cls_name": "car",
"yaw_rad": -0.32,
"edge_yaw_rad": -0.31,
"edge_yaw_confident": true,
"edge_yaw_lateral_distance_m": 3.2,
"edge_yaw_lateral_ok": true,
"edge_yaw_two_face_eligible": true,
"edge_yaw_selected_face_types": [2, 0],
"edge_vs_reg_yaw_rad": 0.01,
"center_uv": [u, v],
"center_3d": [X, Y, Z],
"visible_face_type": 2,
"visible_face_types": [2, 0],
"crop_bounds": [x1, y1, x2, y2]
}
]
},
"roi1": { ... }
},
"visualization": "/path/to/000000.jpg"
}
```
所有二维坐标(`bbox_xyxy``center_uv`)均在对应 ROI 裁剪并缩放后的图像坐标系下;`center_3d` 在相机坐标系下,单位为米。
---
## 7. 类别定义
| cls_id | 类别名称(示例) |
|--------|-----------------|
| 0 | car含 suv, van 等) |
| 1 | bus |
| 2 | truck含 large_truck |
| 3 | tanker / construction_vehicle |
| 4 | unknown |
| 5 | pedestrian |
| 6 | bicycle / motorcycle |
| 7 | motorcyclist / bicyclist |
| 8 | tricycle / tricyclist |
| 9 | traffic_sign |
| 10 | wheel |
| 11 | plate |
| 12 | face |
类别 04 属于 `face_3d_classes`面型3D重建类别 58 属于 `complete_3d_classes`整体3D重建
---
## 8. 鱼眼镜头支持
若相机标定文件中包含有效的 `distort_coeffs`4个系数 [k1, k2, k3, k4]),则:
- 2D 坐标系下 3D 点投影时使用鱼眼畸变正向模型(`apply_fisheye_distortion`
- 2D → 3D 反投影时使用 Newton 迭代法求解畸变逆映射(`remove_fisheye_distortion`
- 3D 包围盒可视化时对各条棱进行密集采样后逐点投影,确保曲线正确。

View File

@@ -0,0 +1,690 @@
# 双ROI导出模型设计说明Diff 与 Fake3D 版本)
## 1. 文档目标
本文档说明当前新版双 ROI 合并导出模型在部署侧的输入约定、输出张量、后处理流程,以及 `difficulty` 分支与 `fake 3D` 分支的运行时语义。
本文档覆盖的对象是:
- `tools/model_merging/merge_models_of_2roi_yolo26.py`
- `tools/model_inference/core/run_two_roi_exported_onnx_infer.py`
- `tools/model_inference/core/two_roi_infer_utils.py`
- `ultralytics/nn/modules/head.py`
凡与旧文档冲突之处,以当前代码实现为准。
如只查看旧版双 ROI 合并模型,请参考:
- `tools/model_inference/docs/two_roi_model_design.md`
---
## 2. 版本背景
当前仓库中的双 ROI 合并导出模型已经从旧版的:
- `2D boxes + class scores + 3D branch (+ 可选 edge branch)`
演进为新版可选扩展的形态:
- `2D boxes + class scores + 3D branch + difficulty branch`
- `2D boxes + class scores + 3D branch + difficulty branch + fake 3D branch`
- `(+ 可选 edge branch)`
其中:
- `difficulty branch` 用于输出目标 difficulty 二分类 logit
- `fake 3D branch` 用于输出 fake class 专用的 3D 预测
当前合并导出脚本支持以下控制项:
- `--edge-head-mode keep|drop`
- `--fake-3d-branch-mode keep|drop`
因此新版模型存在两条主要导出变体:
1. `drop fake_3d`:导出 `3D + diff`
2. `keep fake_3d`:导出 `3D + diff + fake_3d`
---
## 3. 方案概览
当前运行时仍然沿用双 ROI 单目 3D 检测的总体思路,但部署形态已经收敛为:
1. 原图按 `ROI0``ROI1` 两套规则分别裁剪
2. 两个 ROI 图像送入同一个合并导出模型
3. 模型输出每个 ROI 的原始检测头张量
4. 2D 框解码、Top-K、3D 反归一化、深度恢复、3D 框重建、fake-class 路由、可视化与 JSON 序列化全部在 Python 侧完成
当前支持:
- ONNX`onnxruntime`
- TorchScript`torch.jit.load`
- 单 case 推理
- mined / eval 数据集批量推理
当前部署脚本不依赖训练时的 `ultralytics` dataloader只依赖导出张量契约和运行时元数据。
---
## 4. 模型与运行时契约
### 4.1 导出模型形态
当前推理脚本只接受双 ROI 合并后的导出模型:
- `.onnx`
- `.torchscript` / `.ts` / `.jit`
脚本会读取导出清单:
- ONNX 优先读取 sidecar`merged_model.export.json`
- TorchScript 优先读 sidecar若 sidecar 缺失再读取模型内嵌 `config.txt`
当前部署脚本会显式要求:
```text
export_mode == "raw_head_outputs"
```
也就是说,当前文档只讨论原始检测头输出模式,不讨论 `hybrid_outputs``postprocessed_outputs` 等其他导出形态。
### 4.2 输入名称
默认输入名为:
- `roi0_input`
- `roi1_input`
默认输入尺寸为:
- `768 x 352`
若导出清单里提供了 `input_names``input_sizes_wh`,运行时以清单为准。
### 4.3 导出控制开关
当前导出脚本暴露两个关键开关:
```bash
--edge-head-mode keep|drop
--fake-3d-branch-mode keep|drop
```
它们会直接影响导出后的输出张量数量与名称。
### 4.4 运行时元数据
运行时会从数据配置 YAML 中读取以下元数据:
- `class_map`
- `face_3d_classes`
- `complete_3d_classes`
- `fake_3d_classes`
- `norm_scales_3d`
若未提供额外 YAML则使用 `tools/model_inference/core/two_roi_infer_utils.py` 中的 `DEFAULT_DATASET_CONFIG`
当前内置默认值包括:
- `face_3d_classes = [0,1,2,3,4,5,6,7,8,17]`
- `complete_3d_classes = [9,10,11,12,18,19]`
- `fake_3d_classes = [17,18,19]`
对应 fake 类别名称:
- `car_fake`
- `bicyclist_fake`
- `pedestrian_fake`
### 4.5 新旧版本输出类别对比
当前部署侧的 `cls_id` / `cls_name``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` |
新版默认类别定义为:
| `cls_id` | 新版默认类别 |
|---|---|
| `0-16` | 与旧版相同 |
| `17` | `car_fake` |
| `18` | `bicyclist_fake` |
| `19` | `pedestrian_fake` |
因此默认 `scores_head_raw` 的类别数从旧版的:
- `nc = 17`
扩展为新版的:
- `nc = 20`
对应的 3D 类别分组也从旧版:
- `face_3d_classes = [0,1,2,3,4,5,6,7,8]`
- `complete_3d_classes = [9,10,11,12]`
扩展为新版:
- `face_3d_classes = [0,1,2,3,4,5,6,7,8,17]`
- `complete_3d_classes = [9,10,11,12,18,19]`
- `fake_3d_classes = [17,18,19]`
语义上可以理解为:
- `0-16`:延续旧版类别语义
- `17-19`:新增 fake 类别2D 分类仍走统一 `scores_head_raw`
- `17` 会按 face 类路径参与 3D 解码,但在保留 fake 分支时优先读取 `preds_3d_fake_head_raw`
- `18-19` 会按 complete 类路径参与 3D 解码,但在保留 fake 分支时同样优先读取 `preds_3d_fake_head_raw`
---
## 5. ROI 与前处理设计
### 5.1 默认 ROI 配置
默认 ROI 配置来自 `DEFAULT_DATASET_CONFIG["roi_configs"]`
| 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_x``y` 取灭点 `vp_y`
### 5.2 标定与灭点
当前运行时兼容扁平 `camera4.json` 与合并格式 `camera4.json`
灭点计算逻辑:
```text
vp_x = cu + focal_u * tan(yaw)
vp_y = cv - focal_v * tan(pitch)
```
### 5.3 ROI 裁剪
裁剪边界通过 `compute_centered_roi_bounds()` 计算:
```text
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
```
### 5.4 resize 与标定更新
当前脚本会把 ROI 图像 resize 到模型输入尺寸,并同步更新 resized ROI 坐标系下的标定:
```text
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
```
其中 `depth_scale` 会在 3D 后处理阶段恢复真实焦距下的深度尺度。
### 5.5 输入张量化
输入张量构造方式:
```python
image_rgb = cv2.cvtColor(image_bgr, cv2.COLOR_BGR2RGB)
array = image_rgb.transpose(2, 0, 1).astype(np.float32) / 256.0
input = array[None, ...]
```
注意是:
```text
/ 256.0
```
不是 `/ 255.0`
---
## 6. 输出张量设计
### 6.1 总体原则
当前部署脚本只支持每个 ROI 输出原始检测头张量。
输出张量是否包含 `preds_edge``preds_3d_fake` 由导出开关控制。
每个 ROI 至少包含:
1. `boxes_head_raw`
2. `scores_head_raw`
3. `preds_3d_head_raw`
4. `preds_diff_head_raw`
### 6.2 `drop fake_3d` 且 `drop edge`
当:
```bash
--fake-3d-branch-mode drop
--edge-head-mode drop
```
合并模型总输出为 8 个:
1. `roi0_boxes_head_raw`
2. `roi0_scores_head_raw`
3. `roi0_preds_3d_head_raw`
4. `roi0_preds_diff_head_raw`
5. `roi1_boxes_head_raw`
6. `roi1_scores_head_raw`
7. `roi1_preds_3d_head_raw`
8. `roi1_preds_diff_head_raw`
### 6.3 `keep fake_3d` 且 `drop edge`
当:
```bash
--fake-3d-branch-mode keep
--edge-head-mode drop
```
合并模型总输出为 10 个:
1. `roi0_boxes_head_raw`
2. `roi0_scores_head_raw`
3. `roi0_preds_3d_head_raw`
4. `roi0_preds_diff_head_raw`
5. `roi0_preds_3d_fake_head_raw`
6. `roi1_boxes_head_raw`
7. `roi1_scores_head_raw`
8. `roi1_preds_3d_head_raw`
9. `roi1_preds_diff_head_raw`
10. `roi1_preds_3d_fake_head_raw`
### 6.4 `keep edge` 的扩展
若再加:
```bash
--edge-head-mode keep
```
则每个 ROI 还会额外多一个:
- `roi*_preds_edge_head_raw`
### 6.5 典型 shape
对默认输入尺寸 `768 x 352`
```text
A = (352/8 * 768/8) + (352/16 * 768/16) + (352/32 * 768/32)
= 4224 + 1056 + 264
= 5544
```
所以典型输出 shape 为:
| 输出名 | 形状 | 含义 |
|---|---|---|
| `roi*_boxes_head_raw` | `[1, 4, 5544]``[1, 4*reg_max, 5544]` | 2D 框回归头原始输出 |
| `roi*_scores_head_raw` | `[1, nc, 5544]` | 分类原始 logits旧版默认 `nc=17`,新版默认 `nc=20` |
| `roi*_preds_3d_head_raw` | `[1, 41, 5544]` | 常规 3D 分支 |
| `roi*_preds_diff_head_raw` | `[1, 1, 5544]` | difficulty 二分类分支 |
| `roi*_preds_3d_fake_head_raw` | `[1, 41, 5544]` | fake class 专用 3D 分支 |
| `roi*_preds_edge_head_raw` | `[1, 60, 5544]` | edge 分支 |
注意:
- 实际 `boxes_head_raw` 通道数取决于 `reg_max`
- 旧版本文档里常见的 `[1, 4*reg_max, 5544]` 写法仍然成立
- 你当前部分导出模型如果已在导出前把 `reg_max` 相关结构折叠,也可能看到 `[1, 4, 5544]`
### 6.6 各分支语义
#### 6.6.1 `boxes_head_raw`
2D 框回归头的原始输出,逐 anchor 表示边界框回归量。
运行时需要结合 anchor 网格与 stride 做 decode不能直接作为最终 bbox。
#### 6.6.2 `scores_head_raw`
分类头的原始 logits逐 anchor、逐类输出。
运行时会先做 sigmoid再走 one-to-one Top-K 选择。
#### 6.6.3 `preds_3d_head_raw`
常规 3D 分支,适用于常规 3D 类别。
该分支输出 41 维 3D 表达,后续会按 `norm_scales_3d` 做反归一化。
#### 6.6.4 `preds_diff_head_raw`
difficulty 分支输出,每个 anchor 只有 1 维 logit。
当前训练逻辑中difficulty 被视为二分类:
- `0/1 -> easy-like`
- `2/3 -> hard-like`
部署侧目前保留该分支输出,但默认主推理可视化链路还不会直接把它画到结果图上。
#### 6.6.5 `preds_3d_fake_head_raw`
fake class 专用 3D 分支。
该分支与常规 3D 分支一样,也是 41 维,但只用于 fake 类别:
- `car_fake`
- `bicyclist_fake`
- `pedestrian_fake`
当导出模型包含这个张量且当前 detection 的 `cls_id` 落在 `fake_3d_classes` 中时,运行时会优先用它,而不是 `preds_3d_head_raw`
#### 6.6.6 `preds_edge_head_raw`
edge 分支输出 60 维。
每个面的底边缘由 5 个采样点组成,每个采样点 3 维:
```text
[du, dv, z]
```
4 个面一共 60 维。
---
## 7. 3D 分支定义
### 7.1 常规 3D 分支41 维)
通道布局:
| 通道范围 | 含义 |
|---|---|
| `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 |
### 7.2 fake 3D 分支41 维)
`preds_3d_fake_head_raw``preds_3d_head_raw` 在张量结构上完全一致,也是 41 维。
区别只在于用途:
- `preds_3d_head_raw`:常规类别使用
- `preds_3d_fake_head_raw`fake class 使用
### 7.3 edge 分支60 维)
| 面 | 通道范围 |
|---|---|
| front | `0-14` |
| rear | `15-29` |
| left | `30-44` |
| right | `45-59` |
---
## 8. 后处理设计
### 8.1 2D 框解码
运行时用 `decode_boxes_xyxy()``boxes_head_raw` 解码:
1. 还原 `l, t, r, b`
2. 与 anchor 网格中心组合
3. 乘以 stride 还原到 ROI-resized 图像坐标
anchor 由 `build_anchor_cache()` 预生成,默认 stride
```text
(8, 16, 32)
```
### 8.2 Top-K 选择
当前推理脚本沿用 one-to-one Top-K 路径:
1. 对分类 logits 做 sigmoid
2. 每个 anchor 取最大类别分数
3. 做 Top-K
4. 生成 `detections = [x1, y1, x2, y2, conf, cls_id]`
### 8.3 3D / Edge 反归一化
`preds_3d_head_raw``preds_3d_fake_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` |
默认值:
```text
z3d_scale = 24.415
z3d_offset = 39.937
size_scale = 1.945
size_offset = 3.780
yaw_scale = 1.5707963
```
### 8.4 深度恢复
部署时需要按真实焦距恢复深度尺度:
```text
preds_3d[:, (0, 6, 12, 18, 24)] *= depth_scale
preds_3d_fake[:, (0, 6, 12, 18, 24)] *= depth_scale
preds_edge[:, 2::3] *= depth_scale
```
其中:
```text
depth_scale = fx / virtual_fx
```
### 8.5 fake class 的 3D 路由
这是新版运行时与旧版的关键差异之一。
当前 exported inference 在逐检测框解码时,会做如下判断:
1. 读取 detection 的 `cls_id`
2.`cls_id in fake_3d_classes`
3. 且当前导出模型包含 `preds_3d_fake`
4. 则用 `preds_3d_fake` 作为该框的 3D 解码输入
5. 否则继续使用 `preds_3d`
也就是说:
- `keep fake_3d`fake 类别走专用 3D 分支
- `drop fake_3d` 时:所有类别都走主 `preds_3d`
### 8.6 difficulty 分支的使用
当前部署侧已经能把 `preds_diff` 读入运行时结果结构:
- `ROISelectedPredictions.preds_diff`
但默认主推理链路暂未把它用于:
- 过滤
- 合并
- 可视化绘制
- JSON 输出主字段
因此当前它的状态是:
- 已导出
- 已可解析
- 已可供后续扩展
- 默认主链路尚未深度消费
### 8.7 edge yaw 与 edge box
若导出模型包含 edge 分支,则运行时可继续执行 edge-based 诊断链:
1. 反投影可见底边缘点
2. 选择单面或双面组合
3. 计算 `edge_yaw`
4. 约束横向距离阈值
5. 重建 `edge_box`
6. 生成额外诊断信息
若使用:
```bash
--edge-head-mode drop
```
则该能力在部署侧被关闭。
---
## 9. 下游解析逻辑
### 9.1 新旧输出协议兼容
当前 `run_two_roi_exported_onnx_infer.py` 对以下情况都兼容:
1. 旧版仅 `boxes + scores + preds_3d`
2. 新版 `boxes + scores + preds_3d + preds_diff`
3. 新版 `boxes + scores + preds_3d + preds_diff + preds_3d_fake`
4. 上述版本再叠加 `preds_edge`
兼容方式为:
- 必需输出只要求旧版最小集合
- 若发现 `preds_3d_fake` / `preds_diff` / `preds_edge` 则自动启用
- 若缺失则退化为旧行为
### 9.2 建议的外部调用方式
外部调用方不要按固定 output index 写死解析逻辑。
推荐使用:
1. 先读取 `merged_model.export.json`
2.`output_names` 解析输出
原因是:
- `keep/drop fake_3d_branch` 会改变输出总数
- `keep/drop edge_head` 也会改变输出总数
---
## 10. 与旧版文档的主要差异
相对旧版双 ROI 文档,当前新版多出以下变化:
1. 新增 `preds_diff_head_raw`
2. 新增可选 `preds_3d_fake_head_raw`
3. 默认输出类别从 `17` 类扩展到 `20` 类,新增:
- `car_fake`
- `bicyclist_fake`
- `pedestrian_fake`
4. 运行时引入 `fake_3d_classes`
5. fake 类别不再强制走主 `preds_3d`
6. 导出 manifest 中新增:
- `fake_3d_branch_mode`
- `keep_fake_3d_branch`
7. 输出总数不再固定为 8
典型情况:
- 旧版 no-edge8 输出
- 新版 drop fake + drop edge8 输出
- 新版 keep fake + drop edge10 输出
- 新版 keep fake + keep edge12 输出
---
## 11. 导出示例
### 11.1 保留 fake 3D 分支
```bash
python tools/model_merging/merge_models_of_2roi_yolo26.py \
--roi0-model-path runs/detect/mono3d_roi0_20260506_epoch99.pt \
--roi1-model-path runs/detect/mono3d_roi1_20260506_epoch99.pt \
--save-dir runs/export/train_mono3d_two_roi_20260506-keep_fake_3d_branch \
--imgsz 768 352 \
--edge-head-mode drop \
--fake-3d-branch-mode keep
```
输出:
- `boxes`
- `scores`
- `preds_3d`
- `preds_diff`
- `preds_3d_fake`
### 11.2 去掉 fake 3D 分支
```bash
python tools/model_merging/merge_models_of_2roi_yolo26.py \
--roi0-model-path runs/detect/mono3d_roi0_20260506_epoch99.pt \
--roi1-model-path runs/detect/mono3d_roi1_20260506_epoch99.pt \
--save-dir runs/export/train_mono3d_two_roi_20260506-drop_fake_3d_branch \
--imgsz 768 352 \
--edge-head-mode drop \
--fake-3d-branch-mode drop
```
输出:
- `boxes`
- `scores`
- `preds_3d`
- `preds_diff`
---
## 12. 建议与注意事项
1. 如果下游要使用 fake class 的专用 3D 几何,请使用 `--fake-3d-branch-mode keep`
2. 如果只想保持旧版解析复杂度,可使用 `--fake-3d-branch-mode drop`
3. 如果外部部署程序不是本仓库当前的 `run_two_roi_exported_onnx_infer.py`,请务必按 manifest 的 `output_names` 解析。
4. `preds_diff` 当前默认未用于主要可视化与合并策略,但导出时建议保留,便于后续直接扩展。
5. `keep fake_3d``drop fake_3d` 的输出总数不同,不能混用固定 index 的后处理脚本。

View File

@@ -0,0 +1,272 @@
# 单目3D测距周期性波动问题分析与解决方案
## 1. 文档目标
本文档聚焦当前双 ROI 单目 3D 检测部署方案中出现的“目标测距周期性波动”问题,结合现有实现分析可能成因,并给出可落地的改进方案与推荐推进顺序。
本文档不替代 `tools/model_inference/docs/two_roi_model_design.md`。凡涉及当前部署链路的事实性描述,仍以现有推理脚本及其依赖模块为准。
---
## 2. 问题描述
当前模型在部分场景下会出现如下现象:
- 同一目标在连续帧中的测距结果呈现周期性起伏
- 目标实际距离变化不大,但 `z3d``center_3d.z` 存在明显波动
- 波动往往与车辆颠簸、路面不平、减速带、桥头跳车等场景同步出现
初步分析表明,该问题很可能与自车俯仰姿态变化有关。当自车颠簸时,目标在图像中的垂直位置会发生上下起伏,而当前单帧单目 3D 模型会将这类图像变化部分解释为距离变化。
---
## 3. 与当前实现的关系
### 3.1 当前实现中的关键链路
结合现有设计与代码实现,测距波动问题存在以下可能传播链路:
1. 当前灭点计算依赖静态标定中的 `pitch`
```text
vp_y = cv - focal_v * tan(pitch)
```
2. 当前 `camera4.json` 在 case 开始时加载一次,后续帧默认复用同一份标定,不会按帧更新 `pitch / roll`
3. 当前 ROI 裁剪在 `y` 方向上直接依赖 `vp_y`,也就是依赖静态俯仰角计算出的灭点位置。
4. ROI 裁剪和 resize 后,会重新计算 `fx / fy / cx / cy`,后续 3D 回投和深度恢复仍继续使用这套 ROI 坐标系下的标定。
5. 当真实车身姿态随时间变化、但运行时仍使用静态姿态来解释图像时,目标的上下漂移会被系统误解释为深度变化,最终表现为 `z3d` 周期性波动。
### 3.2 问题本质
问题本质可以概括为:
- 当前实现默认相机姿态在时序上稳定
- 实际车辆在颠簸场景下存在动态 `pitch / roll`
- 单帧视觉观测中,目标垂直位移与真实深度变化发生耦合
因此,该问题既是“相机姿态建模不足”的问题,也是“单帧深度估计对图像上下扰动敏感”的问题。
---
## 4. 解决方案分层
从工程与算法角度可以将方案分为五层逐帧姿态补偿、时序平滑、ROI 抗扰动、训练增强、几何一致性融合。
### 4.1 逐帧姿态补偿
这是最接近根因修复的方案。
核心思路:
- 获取逐帧 `pitch / roll`
- 按帧更新灭点位置、ROI 裁剪中心和相机姿态
- 必要时在进入模型前先完成图像稳定,再执行 ROI 裁剪与推理
可选实现路径:
- 接入 `IMU / CAN` 的实时姿态信号
- 无额外传感器时,基于地平线、车道线、路面结构、稳定背景点估计视觉姿态
建议改造点:
- 将静态 `camera4.json` 扩展为逐帧动态姿态输入
- 按帧更新 `vp_y`
- 按帧更新 ROI 的 `crop_center_y`
- 若结果需要映射到 ego 坐标系,则同步更新相机到 ego 的姿态变换
优点:
- 直接处理误差源头
- 同时改善 ROI 对位与 3D 回投
缺点:
- 依赖新增姿态源或新增视觉姿态估计模块
- 集成复杂度最高
### 4.2 Track 级时序平滑
这是最适合快速落地的工程方案。
核心思路:
- 不直接使用单帧 `z3d` 作为最终结果
- 先对目标建立 track
- 再对同一目标的连续帧 3D 结果做时序滤波
推荐方式:
- 基于 2D/3D 关联建立 tracking
-`center_3d.z``center_3d.x``yaw` 等量做 `EMA``Kalman Filter` 或固定窗平滑
- 根据类别、置信度、框尺寸、edge 几何一致性等动态调整测量噪声
优点:
- 不需要修改训练
- 工程改造范围小
- 对周期性抖动通常能快速见效
缺点:
- 只能抑制表现,不能消除根因
- 会引入一定延迟
### 4.3 降低 ROI 对上下抖动的敏感性
当前 ROI 在 `y` 方向上缺少独立的稳定策略,因此可以先做轻量改造。
可选方案:
- 引入独立的 `crop_center_y_mode`
- 支持 `fixed_cy`
- 支持 `smoothed_vpy`
- 支持 `blend(cv, vp_y)` 形式的加权中心
-`crop_center_y` 做低通滤波或逐帧最大步长限制
- 适当增大 ROI 高度,降低轻微颠簸引入的相对位置扰动
优点:
- 代码改动较小
- 便于快速验证问题是否主要由 ROI 跟随静态灭点触发
缺点:
- 只能缓解,不能完整表达真实动态姿态
### 4.4 训练侧抗颠簸增强
若该问题在量产场景中普遍存在,建议在训练侧显式纳入姿态扰动域。
建议方向:
- 增加 `pitch / roll` 扰动增强
- 增加垂直平移、轻微透视变化、动态模糊等增强
- 对连续帧加入时序一致性约束
- 条件允许时,将姿态信息、地平线位置或多帧特征作为额外输入
进一步方案:
- 训练轻量多帧模型
- 引入显式姿态辅助分支
- 增加地平线或路面几何中间量预测
优点:
- 可以从模型层面提升鲁棒性
缺点:
- 需要重新训练和完整回归验证
### 4.5 几何一致性融合
对于车辆类目标,可将网络输出与几何约束融合,以降低单一回归分支对上下抖动的敏感性。
可利用信息包括:
- ground plane 或接地点约束
- 目标底边缘与 edge 分支几何
- 类别尺寸先验
- 连续帧运动学先验
可选策略:
-`z3d` 与 edge / ground 几何明显冲突时,对该帧深度降权
- 将网络回归深度与几何估计深度做加权融合
- 将异常跳变作为诊断信号输出
优点:
- 提高异常帧下的可解释性
- 有助于构建更稳健的诊断链路
缺点:
- 依赖额外几何假设,适用范围需要单独验证
---
## 5. 推荐推进顺序
若目标是“先快速抑制线上波动,再逐步修复根因”,建议按以下顺序推进:
1. 先增加 `track` 级时序平滑,快速压制测距抖动
2. 同时引入 ROI `y` 向稳定策略,验证 ROI 机制的敏感性
3. 若验证表明问题与车身俯仰强相关,再接入逐帧 `pitch / roll` 补偿
4. 在训练侧加入抗颠簸增强,降低模型对垂直扰动的敏感性
5. 视业务需要补充 edge / ground / 运动学等几何一致性融合
该顺序兼顾了:
- 短期可落地性
- 中期定位能力
- 长期根因修复效果
---
## 6. 推荐实施策略
从工程收益比看,建议优先采用“两阶段策略”。
### 6.1 第一阶段:快速止血
目标:
- 尽快降低线上测距周期波动
- 快速判断问题是否主要与姿态扰动相关
建议动作:
- 增加 track 级滤波
- 为 ROI 增加 `y` 向平滑或固定策略
- 输出更多诊断字段例如平滑前后深度、ROI 中心变化量、疑似姿态扰动标记
### 6.2 第二阶段:根因修复
目标:
- 将静态姿态假设升级为动态姿态建模
建议动作:
- 接入逐帧姿态输入
- 按帧更新灭点、裁剪、回投与 ego 变换
- 联合训练增强与几何融合进一步提升鲁棒性
---
## 7. 验证建议
为了判断方案是否有效,建议重点关注以下指标:
- 同一目标连续帧 `center_3d.z` 的标准差
- 目标静稳跟车场景下的深度峰峰值
- 颠簸场景中深度频谱的主频能量
- 引入平滑后的延迟和响应速度
- ego 坐标系下 `xyzlhwyaw` 的稳定性
建议单独构建以下评测子集:
- 路面平稳场景
- 明显颠簸场景
- 远距离车辆场景
- 近距离跟车场景
- 高速桥头或减速带场景
---
## 8. 结论
当前测距周期性波动问题,很大概率与“真实动态俯仰变化”和“运行时静态姿态假设”之间的不一致有关。
从方案优先级上看:
- 短期最有效的是 `track` 级时序平滑与 ROI `y` 向稳定
- 中长期最关键的是逐帧姿态补偿
- 若要进一步提升模型鲁棒性,应结合训练增强与几何一致性融合
其中,逐帧姿态补偿是最接近根因修复的方案;其余方案更适合作为快速缓解、问题定位和整体稳健性增强手段。