Files
yolov26_3d/eval_tools/model_comparison/generate_eval_report.py

208 lines
8.1 KiB
Python
Raw Normal View History

2026-06-24 09:35:46 +08:00
#!/usr/bin/env python3
"""
自动将单模型 evaluation_report.json 转换为中文 Markdown 评测报告
用法:
python generate_eval_report.py <evaluation_report.json 路径>
python generate_eval_report.py <evaluation_report.json 路径> --output <输出路径>
python generate_eval_report.py <evaluation_report.json 路径> --model "模型名称" --date 2026-03-01
示例:
python eval_tools/model_comparison/generate_eval_report.py \
evaluation_results/.../evaluation_report.json \
--model yolov5s-300w-newdata-cncap
"""
import json
import argparse
import sys
from datetime import date
from pathlib import Path
# Allow importing class_config from the eval_tools root
sys.path.insert(0, str(Path(__file__).parent.parent))
from class_config import REPORT_3D_CLASS_LABELS
def fmt(v: float, decimals: int = 4) -> str:
return f"{v:.{decimals}f}"
def fmt2(v: float) -> str:
return f"{v:.2f}"
def pct(v: float) -> str:
return f"{v * 100:.1f}%"
def build_report(data: dict, model_name: str, report_date: str) -> str:
lines = []
# ── 标题 ─────────────────────────────────────────────────────────────────
lines.append(f"# 模型评测报告: {model_name}")
lines.append("")
lines.append(f"**模型**: {model_name} ")
lines.append(f"**评测日期**: {report_date} ")
eval_cfg = data.get("evaluation_config", {})
if eval_cfg:
lines.append(f"**置信度阈值 (P/R/F1)**: {eval_cfg.get('conf_threshold', '-')} ")
lines.append(f"**IoU 阈值**: {eval_cfg.get('iou_threshold', '-')} ")
lines.append(f"**AP 计算方法**: {eval_cfg.get('ap_method', '-')} ")
lines.append("")
lines.append("---")
lines.append("")
# ── 2D Overall ────────────────────────────────────────────────────────────
ov2d = data["2d_evaluation"]["overall"]
lines.append("## 📊 2D检测指标 (Overall)")
lines.append("")
lines.append("| 指标 | 数值 |")
lines.append("|------|------|")
lines.append(f"| **Precision** | {fmt(ov2d['precision'])} |")
lines.append(f"| **Recall** | {fmt(ov2d['recall'])} |")
lines.append(f"| **F1-Score** | {fmt(ov2d['f1_score'])} |")
lines.append(f"| **mAP** | {fmt(ov2d['map'])} |")
lines.append(f"| **TP** | {ov2d['tp']:,} |")
lines.append(f"| **FP** | {ov2d['fp']:,} |")
lines.append(f"| **FN** | {ov2d['fn']:,} |")
lines.append("")
# ── 2D Per-Class ──────────────────────────────────────────────────────────
lines.append("## 📋 2D检测指标 (Per Class)")
lines.append("")
lines.append("| 类别 | Precision | Recall | F1 | AP | GT | TP | FP | FN |")
lines.append("|------|-----------|--------|----|----|-----|-----|-----|-----|")
pc2d = data["2d_evaluation"]["per_class"]
for cls, cd in pc2d.items():
lines.append(
f"| **{cls}** | {fmt(cd['precision'])} | {fmt(cd['recall'])} "
f"| {fmt(cd['f1_score'])} | {fmt(cd['ap'])} "
f"| {cd['num_gt']:,} | {cd['tp']:,} | {cd['fp']:,} | {cd['fn']:,} |"
)
lines.append("")
lines.append("---")
lines.append("")
# ── 3D Overall per class ──────────────────────────────────────────────────
m3d = data.get("3d_evaluation", {})
if not m3d:
return "\n".join(lines)
lines.append("## 🎯 3D检测指标")
lines.append("")
CLS_LABELS = REPORT_3D_CLASS_LABELS
LONG_RANGES = ["long_0-10m", "long_10-20m", "long_20-30m", "long_30-40m",
"long_40-50m", "long_50-60m", "long_60-70m", "long_70-80m",
"long_80-90m", "long_90-100m", "long_100-999m"]
for cls_key, cls_label in CLS_LABELS.items():
if cls_key not in m3d:
continue
cd = m3d[cls_key]
ov = cd["overall"]
n = ov["num_samples"]
lines.append(f"### {cls_label} ({n:,} 样本)")
lines.append("")
# Overall stats
lines.append("#### 总体指标")
lines.append("")
lines.append("| 指标 | Mean | Median | Std | P90 |")
lines.append("|------|------|--------|-----|-----|")
def stat_row(label, key):
s = ov[key]
return (f"| **{label}** | {fmt2(s['mean'])} | {fmt2(s['median'])} "
f"| {fmt2(s['std'])} | {fmt2(s['percentile_90'])} |")
lines.append(stat_row("Lateral Error (m)", "lateral_error"))
lines.append(stat_row("Longitudinal Error (m)", "longitudinal_error"))
lines.append(stat_row("Long. Relative Error", "longitudinal_relative_error"))
lines.append(stat_row("Heading Error (rad)", "heading_error"))
lines.append(stat_row("Heading Error Relaxed", "heading_error_relaxed"))
rev_pct = ov['reversal_percentage']
lines.append(f"| **Reversal** | {ov['reversal_count']:,} ({rev_pct:.2f}%) | - | - | - |")
lines.append("")
# Distance range breakdown
avail_ranges = [r for r in LONG_RANGES if r in cd]
if avail_ranges:
lines.append("#### 按距离分段 (纵向误差 Mean / Lateral Mean / Samples)")
lines.append("")
lines.append("| 距离段 | 样本数 | Lateral (m) | Longitudinal (m) | Long.Rel | Heading | Reversal% |")
lines.append("|--------|--------|-------------|------------------|----------|---------|-----------|")
for rng in avail_ranges:
r = cd[rng]
rov = r
rn = rov["num_samples"]
if rn == 0:
continue
lat = rov["lateral_error"]["mean"]
lon = rov["longitudinal_error"]["mean"]
lrel = rov["longitudinal_relative_error"]["mean"]
hd = rov["heading_error"]["mean"]
rev = rov["reversal_percentage"]
lines.append(
f"| {rng.replace('long_', '')} | {rn:,} | {fmt2(lat)} | {fmt2(lon)} "
f"| {lrel:.3f} | {fmt2(hd)} | {rev:.1f}% |"
)
lines.append("")
lines.append("---")
lines.append("")
return "\n".join(lines)
def main():
parser = argparse.ArgumentParser(
description="将单模型 evaluation_report.json 转换为中文 Markdown 评测报告"
)
parser.add_argument("json_path", help="evaluation_report.json 的路径")
parser.add_argument("--output", "-o", default=None,
help="输出 Markdown 文件路径(默认与 JSON 同目录,文件名 EVALUATION_REPORT.md")
parser.add_argument("--model", default=None,
help="模型名称(默认从目录名推断)")
parser.add_argument("--date", default=str(date.today()),
help="评测日期 (默认今天,格式 YYYY-MM-DD)")
args = parser.parse_args()
json_path = Path(args.json_path).resolve()
if not json_path.exists():
print(f"错误: 文件不存在: {json_path}", file=sys.stderr)
sys.exit(1)
with open(json_path, "r", encoding="utf-8") as f:
data = json.load(f)
# 从路径推断模型名称:取 json 所在目录的上级目录名
if args.model:
model_name = args.model
else:
# e.g. .../yolov5s-300w-newdata-cncap/20260228_102849/evaluation_report.json
model_name = json_path.parent.parent.name
if not model_name or model_name == ".":
model_name = json_path.parent.name
print(f"模型: {model_name}")
report = build_report(data, model_name, args.date)
if args.output:
out_path = Path(args.output)
else:
out_path = json_path.parent / "EVALUATION_REPORT.md"
out_path.write_text(report, encoding="utf-8")
print(f"报告已生成: {out_path}")
if __name__ == "__main__":
main()