# `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,还会额外有: - `_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 样本