# 合并模型评测指南

## 概述

`val_merged_model.py` 是专门用于评测合并双ROI模型的脚本。它基于 `val.py` 的评测逻辑，但针对双ROI模型进行了适配。

## 功能特性

1. **双ROI数据加载**：为ROI0和ROI1分别创建数据加载器
2. **同步推理**：同时对两个ROI进行推理
3. **跨ROI结果融合**：使用NmsForScale算法合并两个ROI的检测结果，避免重复检测
4. **完整指标计算**：计算2D mAP和3D指标（深度误差、方向误差、中心点误差）

## 工作流程

```
输入数据
   ↓
ROI0 Dataloader  →  推理 →  NMS  →  ↘
                                     跨ROI融合  →  2D/3D指标计算
ROI1 Dataloader  →  推理 →  NMS  →  ↗
```

### 详细步骤

1. **加载合并模型**：加载包含 `model_roi0` 和 `model_roi1` 的合并模型
2. **创建数据加载器**：
   - ROI0: 裁剪区域 [0, 120, 1920, 1080]
   - ROI1: 中心区域 [704, 352]，原始图像 [1920, 1080]
3. **推理**：对两个ROI同时进行前向传播
4. **NMS**：对每个ROI的检测结果独立进行NMS
5. **跨ROI融合**：使用NmsForScale算法合并检测结果：
   - 移除边缘伪影（IoU > 0.9且超出ROI边界）
   - 抑制重复检测（IoU > 0.3，保留更靠近ROI中心的检测）
   - 移除高度包含的检测（IoU_in > 0.8）
6. **指标计算**：对融合后的结果计算评测指标

## 使用方法

### 基本用法

```bash
python eval_tools/val_merged_model.py \
    --data data/mono3d.yaml \
    --weights release/yolov5s-30w/merged_model.pt \
    --imgsz 704 352 \
    --batch-size 1
```

### 参数说明

| 参数 | 默认值 | 说明 |
|------|--------|------|
| `--data` | `data/mono3d.yaml` | 数据集配置文件路径 |
| `--weights` | 必需 | 合并模型权重文件路径（.pt格式）|
| `--batch-size` | `1` | 批大小（仅支持1）|
| `--imgsz` | `[704, 352]` | 推理图像尺寸 [宽度, 高度] |
| `--conf-thres` | `0.001` | NMS置信度阈值（低值用于AP计算）|
| `--conf-thres-plot` | `0.25` | 可视化和3D指标的置信度阈值 |
| `--iou-thres` | `0.6` | NMS的IoU阈值 |
| `--max-det` | `300` | 每张图片最大检测数 |
| `--device` | `''` | 设备（如'0'或'cpu'）|
| `--workers` | `8` | 数据加载器工作线程数 |
| `--project` | `runs/val_merged` | 结果保存目录 |
| `--name` | `exp` | 实验名称 |
| `--verbose` | - | 按类别报告mAP |
| `--half` | - | 使用FP16半精度推理 |

### 完整示例

```bash
# 基本评测
python eval_tools/val_merged_model.py \
    --data data/mono3d.yaml \
    --weights release/yolov5s-30w/merged_model.pt \
    --imgsz 704 352

# 使用GPU加速和半精度
python eval_tools/val_merged_model.py \
    --data data/mono3d.yaml \
    --weights release/yolov5s-30w/merged_model.pt \
    --imgsz 704 352 \
    --device 0 \
    --half

# 详细输出（按类别显示mAP）
python eval_tools/val_merged_model.py \
    --data data/mono3d.yaml \
    --weights release/yolov5s-30w/merged_model.pt \
    --imgsz 704 352 \
    --verbose

# 自定义输出目录
python eval_tools/val_merged_model.py \
    --data data/mono3d.yaml \
    --weights release/yolov5s-30w/merged_model.pt \
    --imgsz 704 352 \
    --project runs/my_eval \
    --name merged_exp1
```

## 输出结果

### 控制台输出

评测过程中会实时显示：

```
                 Class     Images  Instances          P          R      mAP50   mAP50-95    Matched   Depth(m)  Orient(°)  Center(m)
                   all         50        150      0.856      0.823      0.891      0.672         89      2.345     12.678      0.234
```

- **Class**: 类别名称
- **Images**: 处理的图片数量
- **Instances**: 真实标注数量
- **P**: 精确率
- **R**: 召回率
- **mAP50**: mAP@0.5
- **mAP50-95**: mAP@0.5:0.95
- **Matched**: 3D匹配的检测数
- **Depth(m)**: 平均深度误差（米）
- **Orient(°)**: 平均方向误差（度）
- **Center(m)**: 平均中心点误差（米）

### 最终统计

```
3D Metrics (Fused ROI0+ROI1):
  Total matched: 89
  Depth error (abs): 2.345m
  Orientation error: 12.678°
  Center error: 0.234m
```

### 保存的文件

结果保存在 `--project/--name` 目录下：

- `confusion_matrix.png`: 混淆矩阵图
- `F1_curve.png`: F1曲线
- `P_curve.png`: 精确率曲线
- `R_curve.png`: 召回率曲线
- `PR_curve.png`: PR曲线

## ROI配置

脚本内置了ROI配置，与 `test_val_merged_model.py` 保持一致：

```python
# ROI0: 裁剪区域
roi0 = (0, 120, 1920, 1080)
ori_img_size_roi0 = None

# ROI1: 中心区域
roi1 = (704, 352)
ori_img_size_roi1 = (1920, 1080)

# 跨ROI NMS使用的ROI坐标（原始图像坐标系）
roi_list = [
    (0, 120, 1920, 1080),  # ROI0
    (608, 364, 1312, 716)   # ROI1在原始图像中的坐标
]
```

## 跨ROI NMS算法

`NmsForScale` 函数实现了跨ROI的检测融合：

1. **边缘伪影过滤**：
   - 计算检测框与ROI的IoU
   - 如果IoU > 0.9且检测框超出ROI边界，则移除

2. **重复检测抑制**：
   - 对于来自不同ROI的检测框
   - 如果IoU > 0.3（高度重叠）
   - 保留距离ROI中心更近的检测

3. **包含关系处理**：
   - 如果一个检测框被另一个检测框高度包含（IoU_in > 0.8）
   - 移除被包含的检测框

## 注意事项

1. **批大小限制**：合并模型评测仅支持 `batch_size=1`
2. **模型格式**：仅支持PyTorch .pt格式的合并模型（包含Model_Merged类）
3. **数据集要求**：数据集必须包含3D标注（48列标签格式）
4. **内存使用**：同时加载两个ROI的数据会增加内存使用
5. **速度**：由于需要推理两个ROI和跨ROI融合，速度会比单ROI评测慢

## 与单ROI评测的对比

| 特性 | val.py (单ROI) | val_merged_model.py (双ROI) |
|------|----------------|------------------------------|
| 批大小 | 支持任意 | 仅支持1 |
| 数据加载器 | 单个 | 两个（ROI0+ROI1）|
| 推理次数 | 1次/图像 | 2次/图像 |
| NMS | 单次 | 2次独立+跨ROI融合 |
| 结果 | 单ROI检测 | 融合后的全图检测 |
| 速度 | 快 | 较慢（约2倍推理时间）|
| 覆盖范围 | 单个ROI | 全图覆盖 |

## 故障排除

### 问题1：模型加载失败

```
ValueError: Model at xxx is not a merged dual-ROI model
```

**解决方法**：确保模型是通过 `merge_models_of_2roi.py` 创建的合并模型，包含 `model_roi0` 和 `model_roi1` 属性。

### 问题2：数据加载器长度不匹配

```
WARNING Dataloader lengths mismatch: ROI0=100, ROI1=98
```

**解决方法**：检查数据集配置，确保两个ROI访问相同的图像文件。

### 问题3：路径不匹配

```
WARNING Path mismatch: xxx vs yyy
```

**解决方法**：这通常表示两个数据加载器返回的图像顺序不一致，检查数据集配置和路径。

### 问题4：内存不足

**解决方法**：
- 减少 `--workers` 数量
- 使用 `--half` 启用FP16推理
- 在CPU上运行：`--device cpu`

## 相关文件

- `val.py`: 单ROI模型评测脚本
- `merge_models_of_2roi.py`: 合并两个ROI模型的脚本
- `test_val_merged_model.py`: 合并模型推理测试脚本（仅推理，不计算指标）
- `val_merged_model.py`: 本脚本，合并模型完整评测

## 工作流程示例

完整的训练+合并+评测流程：

```bash
# 1. 训练ROI0模型
python train_mono3d.py --data data/mono3d.yaml --roi 0 120 1920 1080 --weights yolov5s.pt

# 2. 训练ROI1模型  
python train_mono3d.py --data data/mono3d.yaml --roi 704 352 --ori-img-size 1920 1080 --weights yolov5s.pt

# 3. 评测单个模型（可选）
python val.py --weights runs/train/roi0_exp/weights/last.pt --data data/mono3d.yaml --roi 0 120 1920 1080
python val.py --weights runs/train/roi1_exp/weights/last.pt --data data/mono3d.yaml --roi 704 352 --ori-img-size 1920 1080

# 4. 合并模型
python eval_tools/merge_models_of_2roi.py \
    --roi0_model_path runs/train/roi0_exp/weights/last.pt \
    --roi1_model_path runs/train/roi1_exp/weights/last.pt \
    --save_dir release/merged \
    --skip-export  # 仅保存合并模型，不导出ONNX

# 5. 评测合并模型
python eval_tools/val_merged_model.py \
    --data data/mono3d.yaml \
    --weights release/merged/merged_model.pt \
    --imgsz 704 352

# 6. 推理测试（可选）
python eval_tools/test_val_merged_model.py \
    --source /path/to/test/images \
    --weights release/merged/merged_model.pt \
    --model-type torchscript
```

## 扩展说明

如果需要修改ROI配置，编辑脚本中的以下部分：

```python
# 在 run() 函数中，约第375行
roi0 = (0, 120, 1920, 1080)  # 修改ROI0坐标
roi1 = (704, 352)             # 修改ROI1尺寸
ori_img_size_roi1 = (1920, 1080)  # 修改原始图像尺寸

# 在跨ROI NMS部分，约第431行
roi_list = [
    (0, 120, 1920, 1080),    # ROI0坐标
    (608, 364, 1312, 716)    # ROI1在原始图像中的坐标（需要计算）
]
```

计算ROI1在原始图像中的坐标：
```python
# 如果ROI1是中心裁剪，计算方式：
ori_w, ori_h = 1920, 1080
roi1_w, roi1_h = 704, 352
x1 = (ori_w - roi1_w) // 2  # 608
y1 = (ori_h - roi1_h) // 2  # 364
x2 = x1 + roi1_w            # 1312
y2 = y1 + roi1_h            # 716
```
