# `analyze_val_two_roi_badcases.py` 评测设计说明

本文描述当前脚本 `tools/pdcl_inference/analyze_val_two_roi_badcases.py` 的真实输出内容、评测口径和统计方式。相关实现还涉及：

- `tools/pdcl_inference/two_roi_inference.py`
- `ultralytics/utils/plotting_3d.py`
- `ultralytics/utils/metrics.py`
- `ultralytics/utils/metrics_3d.py`

## 1. 脚本定位

这个脚本不是训练期 validator 的简单包装，而是一个离线分析工具。它的目标是：

- 对 `val/train/test` split 做逐样本推理
- 生成 per-ROI 的 2D/3D 指标汇总
- 输出按类别、距离、框尺寸、可见面 bucket 分解后的统计
- 导出坏例 CSV
- 生成坏例可视化与 HTML 报告
- 可选生成数据画像 `portrait`

默认会分析两个 ROI：

- `roi0`
- `roi1`

## 2. 主流程

每个 ROI 的主流程如下：

1. 读取 split 列表，得到每个样本的 `label_path`。
2. 根据 label 推导 image/calib 路径。
3. 构造 ROI 图像和 ROI 标定。
4. 跑模型，拿到：
   - 2D 检测结果
   - `preds_3d`
   - `preds_edge`
   - `anchors`
   - `strides`
5. 同一张图会走两套预测用途：
   - 一套低阈值预测，用于 2D AP/PR/F1 曲线搜索
   - 一套正式阈值预测，用于 3D 匹配、坏例导出和报告
6. 对 GT 与 Pred 做贪心匹配。
7. 基于匹配结果生成 object-level record。
8. 把 record 聚合进 overall / class / breakdown / interval 等 bucket。
9. 结束后统一写出 CSV、JSON、Markdown、HTML 和可视化。

## 3. 匹配规则

## 3.1 主匹配规则

用于 3D 指标、坏例和 2D thresholded 指标的主匹配规则是：

- 条件：`IoU >= 0.5`
- 要求：类别一致
- 策略：按 IoU 从高到低贪心匹配，保证一对一

对应函数：

- `greedy_match_indices(...)`

## 3.2 2D AP 的 TP 计算

用于 `mAP50` 和 `mAP50-95` 的 TP 计算是标准 COCO 风格：

- IoU 阈值：`0.50:0.95`，步长 `0.05`
- 每个阈值单独做一次贪心匹配
- 然后调用 `ap_per_class(...)`

对应函数：

- `compute_2d_tp_matrix(...)`
- `summarize_2d_ap_store(...)`

## 3.3 focused confusion 的匹配

focused confusion matrix 用的是“任意类别 IoU 匹配”：

- 先只按 `IoU >= 0.5` 做 greedy match
- 再把“预测类别 vs GT 类别”记到 confusion matrix

这么做的目的不是算检测 AP，而是专门看“已经局部对上目标之后的分类混淆”。

对应函数：

- `update_subset_confusion_matrix(...)`

## 4. 2D 指标设计

## 4.1 AP / mAP

脚本会先以很低阈值收集候选框：

- `threshold_search_conf = min(bundle.spec.conf, 0.001)`

然后计算：

- `map50_2d`
- `map50_95_2d`
- 每类的 `map50/map50_95`
- PR/F1 曲线

同时还会从 mean F1 curve 里挑一个推荐阈值：

- `recommended_confidence_2d`

其定义是：

- 对所有类别的 F1 曲线取均值
- 先做 `smooth(..., 0.1)`
- 取最大值对应的 confidence

## 4.2 thresholded 2D 指标

在 `recommended_confidence_2d` 下，脚本会再算一套 thresholded 2D 指标：

- `gt_total`
- `pred_total`
- `matched_2d`
- `precision_2d = matched_2d / pred_total`
- `recall_2d = matched_2d / gt_total`
- `f1_2d`
- `false_negatives_2d = gt_total - matched_2d`
- `false_positives_2d = pred_total - matched_2d`

这些会出现在：

- overall summary
- `class_metrics.csv`
- `summary.json`

## 4.3 2D 分类准确率

脚本还会统计“已经匹配上的检测对里，类别判对了多少”：

- `classification_summary_2d`
- `focused_classification_summary_2d`

核心口径是 confusion matrix 的主对角占比：

- `overall_accuracy = trace(matched_matrix) / sum(matched_matrix)`

每类还会有：

- `cls_eval_pairs_2d`
- `cls_correct_2d`
- `cls_acc_2d`

## 4.4 focused confusion 子集

脚本还会单独做一个“近距离、无遮挡、低难度”的 focused confusion：

- GT 条件：
  - `difficulty == 0`
  - `|lateral| < 5m`
  - `|longitudinal| < 30m` 对 `roi0`
  - `|longitudinal| < 80m` 对 `roi1`
- Pred 条件：
  - 用预测 3D 中心估计空间位置
  - 满足相同的空间范围约束

这一部分主要用于看容易样本的分类混淆情况。

## 5. 3D object-level record 设计

每个 GT-Pred 匹配对，脚本都会尝试构造一个 record。record 是后续所有 3D 聚合统计的基础。

### 5.1 基本字段

包含但不限于：

- 样本标识：`sample_index`, `roi`, `frame_name`
- GT/Pred 索引：`gt_index`, `pred_index`
- 类别：`cls_id`, `cls_name`
- 2D 匹配：`confidence`, `match_iou`
- 3D 属性：
  - `gt_x_m / pred_x_m`
  - `gt_y_m / pred_y_m`
  - `gt_z_m / pred_z_m`
  - `gt_depth_m / pred_depth_m`
  - `gt_yaw_deg / pred_yaw_deg`
  - `x_abs_m / y_abs_m / z_abs_m`
  - `center_error_m`
  - `yaw_abs_deg`

### 5.2 yaw 误差定义

当前脚本的 yaw 误差不是普通 `|pred - gt|`，而是 **pi 周期误差**：

- 先做 `[-pi, pi)` wrap
- 再取 `min(diff, |pi - diff|)`

含义是：

- 180 度前后翻转不被当成大错
- 更接近“朝向轴”误差，而不是“严格方向”误差

对应函数：

- `angle_abs_deg(...)`

这点非常重要，读报告时必须按这个口径理解所有 yaw 指标。

### 5.3 position_eligible

不是所有 matched_3d 都会进入位置误差统计。当前实现里：

- `position_eligible = (cls in complete_3d_classes) or (not cut_object)`

也就是说：

- 完整 3D 类，总是参与位置误差
- face-3D 类如果是 cut object，则不参与位置误差

因此：

- `matched_3d` 是形状/yaw层面的 3D 成功样本数
- `matched_pos` 才是位置误差真正参与统计的样本数

### 5.4 position_error_basis

位置误差并不总是用 box center 计算。

当前实现优先级是：

1. 对 face_3d 类，如果 GT 和 Pred 都能解析到同一个可见 face，则使用该 `face_center`
2. 否则退回 `box_center`

因此 record 中会有：

- `position_error_basis = face_center` 或 `box_center`

这会影响：

- `x_abs_m`
- `y_abs_m`
- `z_abs_m`
- `center_error_m`

### 5.5 yaw compare 相关字段

脚本会同时保存三类 yaw：

1. `pred_yaw`
   - 来自 41 维 whole 头的直接 yaw 回归
2. `pred_edge_visible_yaw`
   - 来自 edge 几何重建
   - 只有 `edge_confident=True` 时才记为有效
3. `pred_edge_bucket_visible_yaw`
   - 记录当前 edge 选择器给出的 yaw
   - 即使它不满足“正式 edge 有效条件”，也会保留，用于 bucket 诊断

同时会记录：

- `direct_visible_yaw_abs_deg`
- `edge_visible_yaw_abs_deg`
- `direct_minus_edge_visible_yaw_abs_deg`

### 5.6 yaw_compare_eligible

只有满足下列条件，样本才被认为“允许进入 yaw compare 范围”：

- `|gt_x_m| < yaw_compare_max_lateral_dist_m`

默认 lateral limit 很大，来自：

- `DEFAULT_YAW_COMPARE_MAX_LATERAL_DIST_M = 30m`

但真正进入 `yaw_compare_count` 的条件更严格，还需要：

- direct yaw 有效
- edge yaw 有效

也就是：

- `yaw_compare_eligible` 只是“允许参与”
- `yaw_compare_count` 才是“真的成对比较了 direct vs edge”

## 6. bucket 聚合指标

所有 overall/class/breakdown/lateral-bin 等汇总，最终都通过 `summarize_metric_bucket(...)` 生成。

## 6.1 计数项

- `gt_total`
- `pred_total`
- `matched_2d`
- `matched_3d`
- `matched_pos`
- `yaw_compare_eligible_count`
- `yaw_compare_count`
- `length_compare_count`

## 6.2 2D 派生项

- `precision_2d = matched_2d / pred_total`
- `recall_2d = matched_2d / gt_total`
- `f1_2d`

## 6.3 3D 主指标

- `mean_confidence`
- `mean_match_iou`
- `yaw_mae_deg`
- `yaw_p90_deg`
- `x_abs_mae_m`
- `y_abs_mae_m`
- `z_abs_mae_m`
- `center_error_mae_m`

## 6.4 direct vs edge 对比指标

- `direct_regression_yaw_mae_deg`
- `direct_regression_yaw_p90_deg`
- `edge_based_yaw_mae_deg`
- `edge_based_yaw_p90_deg`
- `mean_direct_minus_edge_yaw_deg`
- `median_direct_minus_edge_yaw_deg`
- `yaw_compare_edge_better_count/rate`
- `yaw_compare_direct_better_count/rate`
- `yaw_compare_tie_count/rate`

脚本的判优规则是：

- `edge_err + eps < direct_err` 记为 edge better
- `direct_err + eps < edge_err` 记为 direct better
- 否则 tie

## 6.5 长度对比指标

当前还统计了 direct box 与 edge box 的车长误差对比：

- `direct_regression_length_mae_m`
- `side_edge_length_mae_m`
- `mean_direct_minus_side_edge_length_m`
- `length_compare_edge_better_count/rate`
- `length_compare_direct_better_count/rate`
- `length_compare_tie_count/rate`

## 6.6 超阈值坏例占比

- `yaw_bad_rate = yaw_bad_count / matched_3d`
- `horizontal_bad_rate = x_bad_count / matched_pos`
- `vertical_bad_rate = z_bad_count / matched_pos`

默认阈值：

- yaw：`5 deg`
- horizontal：`0.5 m`
- vertical：`0.5 m`

## 6.7 当前实现的命名注意事项

这里有一个很容易误解的点：

- `vertical_*` 相关统计，当前实现实际上是基于 `z_abs_m` 做的，也就是纵向/深度误差
- 不是基于 `y_abs_m`

具体来说：

- `bad_cases_vertical.csv` 的导出条件是 `z_abs_m > vertical_bad_threshold_m`
- `vertical_abs_mae_m` 实际对应 `mean(z_abs_m)`
- `class_vertical_depth_5m.csv` 也是按 `position_gt_z_m` 分桶

另外：

- `y_abs_m` 会被记录
- 但 `y_bad_count` 当前实现没有真正累加，因此 `y_bad_rate` 基本恒为 `0` 或无实际含义

所以看报告时，建议把：

- `horizontal` 理解为 `x`
- `vertical` 理解为 `z/depth`

## 7. breakdown 与分桶统计

## 7.1 breakdowns.json

脚本会输出 `breakdowns.json`，当前有 5 类 breakdown：

1. `cut_status`
   - `cut`
   - `non_cut`
   - `n/a`
2. `face_visibility`
   - `front_rear_only`
   - `side only`
   - `two-face`
3. `distance_bin`
   - `<20m`
   - `20-40m`
   - `40-60m`
   - `>=60m`
4. `bbox_diag_bin`
   - `<32px`
   - `32-64px`
   - `64-128px`
   - `>=128px`
5. `class_group`
   - `face_3d`
   - `complete_3d`
   - `2d_only`

## 7.2 interval CSV

脚本还会输出几类按区间统计的 CSV：

### `class_horizontal_lateral_5m.csv`

- 横轴：GT 的 `position_gt_x_m`
- 分桶：`[-30m, 30m)` 内 5m 一桶
- 指标：`x_abs_m`
- 额外输出：
  - mean
  - median
  - p90
  - max
  - `rate_gt_0p5`

### `class_vertical_depth_5m.csv`

- 横轴：GT 的 `position_gt_z_m`
- 分桶：5m 一桶
- 指标：`z_abs_m`

### `class_yaw_depth_10m.csv`

- 横轴：GT 的 `gt_depth_m`
- 分桶：10m 一桶
- 指标：`yaw_abs_deg`

### `class_yaw_horizontal_5m.csv`

- 横轴：GT 的 `gt_x_m`
- 分桶：`[-30m, 30m)` 内 5m 一桶
- 指标：`yaw_abs_deg`

### `yaw_compare_signed_lateral_5m.csv`

这是专门给 direct vs edge yaw 对比用的：

- 横轴：GT 的 `gt_x_m`
- 范围：`[-yaw_compare_max_lateral_dist_m, yaw_compare_max_lateral_dist_m)`
- 分桶：5m
- 额外还有一个纵向过滤条件：
  - `0 <= gt_z_m < 50m`

桶内不是简单均值列表，而是整套 `summarize_metric_bucket(...)` 输出。

### `yaw_compare_signed_lateral_5m_by_face_visibility.csv`

与上面类似，但再按 `face_visibility_bucket` 细分：

- `front_rear_only`
- `side only`
- `two-face`

## 7.3 large vehicle 专项统计

脚本对大车类单独做了一个 two-face yaw 对比视角：

- 类别集合：`5, 6, 7`
- 即：
  - bus
  - truck/tanker/large_truck/construction_vehicle
  - special_vehicle

输出在：

- `summary.json` 的 `large_vehicle_compare`

## 8. 坏例导出规则

## 8.1 CSV 全量导出

脚本会把所有超阈值样本写入 CSV：

- `bad_cases_yaw.csv`
  - 条件：`yaw_abs_deg >= yaw_bad_threshold_deg`
- `bad_cases_horizontal.csv`
  - 条件：`position_eligible and x_abs_m > horizontal_bad_threshold_m`
- `bad_cases_vertical.csv`
  - 条件：`position_eligible and z_abs_m > vertical_bad_threshold_m`
- `bad_cases_2d_false_negative.csv`
- `bad_cases_2d_false_positive.csv`

## 8.2 可视化导出

可视化不是把所有坏例都画出来，而是做 reservoir sampling：

- overall top-k
- per-class top-k
- per-error-bin top-k

其中按误差区间导出时：

- yaw：`1 deg` 一桶
- horizontal/vertical：`0.5 m` 一桶
- 每桶先保留最多 `error_bin_badcases`
- 再按 frame 聚合，最终每桶最多导出 `error_bin_samples_per_bin` 张图

## 9. 输出文件组织

## 9.1 每个 ROI 目录

每个 ROI 目录下会有：

- `summary.json`
- `summary.md`
- `summary_zh.md`
- `class_metrics.csv`
- `breakdowns.json`
- `class_horizontal_lateral_5m.csv`
- `class_vertical_depth_5m.csv`
- `class_yaw_depth_10m.csv`
- `class_yaw_horizontal_5m.csv`
- `yaw_compare_signed_lateral_5m.csv`
- `yaw_compare_signed_lateral_5m_by_face_visibility.csv`
- `bad_cases_yaw.csv`
- `bad_cases_horizontal.csv`
- `bad_cases_vertical.csv`
- `bad_cases_2d_false_negative.csv`
- `bad_cases_2d_false_positive.csv`
- `confusion_matrix_normalized.png`
- `2d_confidence_curves/*.png`
- `visuals/...`

## 9.2 总输出目录

总输出目录会汇总所有 ROI，写出：

- `summary.json`
- `summary.md`
- `summary_zh.md`
- `report.html`

如果启用了 data portrait，还会额外有：

- `<split>_portrait/summary.json`
- portrait 相关 CSV

## 10. 一句话总结

`analyze_val_two_roi_badcases.py` 当前实现的核心是：

- 用 `IoU>=0.5 + 类别一致` 的贪心匹配构造 object-level 记录
- 以 record 为中心，统一聚合 2D、3D、direct-vs-edge、分桶和坏例统计
- 2D AP 与 thresholded 2D 指标分开算
- yaw 误差采用 pi 周期口径
- 报告里名为 `vertical` 的主统计，当前实际上对应 `z/depth` 误差

如果后续要和训练期 validator 对齐，最值得先确认的就是：

- yaw 误差口径是否仍然保持 pi 周期
- `vertical` 是否继续表示 `z`
- `position_eligible` 是否继续排除 cut 的 face-3D 样本
