Files
yolov26_3d/评测设计.md

556 lines
13 KiB
Markdown
Raw Normal View History

2026-06-24 09:35:46 +08:00
# `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 样本