#!/usr/bin/env python3 """ 自动将单模型 evaluation_report.json 转换为中文 Markdown 评测报告。 用法: python generate_eval_report.py python generate_eval_report.py --output <输出路径> python generate_eval_report.py --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()