Files
yolov26_3d/eval_tools/model_comparison/generate_eval_report.py
2026-06-24 09:35:46 +08:00

208 lines
8.1 KiB
Python
Executable File
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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()