单目3D初始代码
This commit is contained in:
207
eval_tools/model_comparison/generate_eval_report.py
Executable file
207
eval_tools/model_comparison/generate_eval_report.py
Executable file
@@ -0,0 +1,207 @@
|
||||
#!/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()
|
||||
Reference in New Issue
Block a user