# 双ROI合并模型方案文档

## 1. 概述

本文档描述基于 YOLO26-3D 的双 ROI（Region 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）基础上新增：

- **cv4（3D预测分支）**：每个 anchor 输出 41 维张量；
- **cv5（可见面边缘分支）**：每个 anchor 输出 60 维张量。

#### 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 | 整体 z3d（米） |
| 25–26 | 整体 u_offset, v_offset（网格坐标偏移） |
| 27–29 | 整体 l, h, w（米） |
| 30–33 | 4个朝向 bin 的分类 logits |
| 34–37 | 4个朝向 bin 的残差 sin 值 |
| 38–40 | cut 状态分类 logits（normal/cut-in/cut-out） |

#### 可见面边缘张量（60维）格式

每张可见面对应 5 个采样点 × 3 维（du, dv, z），共 4 个面 × 15 维 = 60 维：

| 面索引 | 通道范围 |
|--------|----------|
| 前面 | 0–14 |
| 后面 | 15–29 |
| 左面 | 30–44 |
| 右面 | 45–59 |

每个采样点格式：`[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 张量在网络内经过归一化压缩，推理侧需还原为物理量：

| 通道 | 操作 | 含义 |
|------|------|------|
| z3d（ch 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/w（ch 3-4, 9-10, 15-16, 21-22, 27-29） | `raw × size_scale + size_offset` | 尺寸（米），默认 scale=1.945, offset=3.780 |
| 朝向 bin logits（ch 30-33） | 保持原始 logits | 4-bin softmax 分类 |
| 朝向残差（ch 34-37） | `tanh(raw)` | sin(Δ) in [−1, 1] |
| face visibility（ch 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）：行人、骑手等

直接使用整体预测（通道 24–29）重建包围盒，无需面选择。

### 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 |

类别 0–4 属于 `face_3d_classes`（面型3D重建），类别 5–8 属于 `complete_3d_classes`（整体3D重建）。

---

## 8. 鱼眼镜头支持

若相机标定文件中包含有效的 `distort_coeffs`（4个系数 [k1, k2, k3, k4]），则：
- 2D 坐标系下 3D 点投影时使用鱼眼畸变正向模型（`apply_fisheye_distortion`）；
- 2D → 3D 反投影时使用 Newton 迭代法求解畸变逆映射（`remove_fisheye_distortion`）；
- 3D 包围盒可视化时对各条棱进行密集采样后逐点投影，确保曲线正确。
