Spaces:
Running on Zero
Running on Zero
Download scripts/build_report.py from RISHIVEL/RAP_DocLayout_DetectionD: direct link, hf CLI and curl.
- Browser
- Download file 9.54 kB
-
https://huggingface.co/spaces/RISHIVEL/RAP_DocLayout_DetectionD/resolve/main/scripts/build_report.py
- Command line
-
hf download hf://spaces/RISHIVEL/RAP_DocLayout_DetectionD/scripts/build_report.py
-
curl -L -o build_report.py https://huggingface.co/spaces/RISHIVEL/RAP_DocLayout_DetectionD/resolve/main/scripts/build_report.py
9.54 kB
| # Builds a visual PDF report from reports/metrics.json and | |
| # reports/run_metadata.json - same numbers as the memo, laid out so | |
| # someone can understand the results without reading raw JSON. | |
| from __future__ import annotations | |
| import json | |
| from pathlib import Path | |
| import matplotlib | |
| import matplotlib.pyplot as plt | |
| from matplotlib.backends.backend_pdf import PdfPages | |
| matplotlib.rcParams.update({ | |
| "font.family": "sans-serif", | |
| "font.size": 11, | |
| "text.color": "#0b0b0b", | |
| "axes.edgecolor": "#0b0b0b", | |
| "axes.labelcolor": "#0b0b0b", | |
| "xtick.color": "#0b0b0b", | |
| "ytick.color": "#0b0b0b", | |
| }) | |
| BLUE = "#2a78d6" # validated single hue, see dataviz skill palette | |
| GRID = "#d9d9d6" # recessive gridline | |
| MUTED = "#52514e" # secondary text | |
| ROOT = Path(__file__).resolve().parent.parent | |
| metrics = json.loads((ROOT / "reports/metrics.json").read_text()) | |
| run = json.loads((ROOT / "reports/run_metadata.json").read_text()) | |
| CLASS_INSTANCES = { # from the evaluate.py console output for this run | |
| "Caption": 149, "Footnote": 47, "Formula": 150, "List-item": 962, | |
| "Page-footer": 408, "Page-header": 311, "Picture": 143, | |
| "Section-header": 873, "Table": 252, "Text": 3002, "Title": 52, | |
| } | |
| def new_page(figsize=(8.5, 11)): | |
| fig = plt.figure(figsize=figsize, facecolor="white") | |
| fig.patch.set_facecolor("white") | |
| return fig | |
| def bare_axes(fig, rect): | |
| ax = fig.add_axes(rect) | |
| ax.set_facecolor("white") | |
| for spine in ax.spines.values(): | |
| spine.set_visible(False) | |
| ax.set_xticks([]) | |
| ax.set_yticks([]) | |
| return ax | |
| def page_overview(): | |
| fig = new_page() | |
| ax = bare_axes(fig, [0, 0, 1, 1]) | |
| ax.text(0.07, 0.94, "Document Layout Detection — Evaluation Report", | |
| fontsize=20, fontweight="bold", ha="left") | |
| ax.text(0.07, 0.905, "RT-DETR-L fine-tuned on DocLayNet, evaluated on the held-out test split", | |
| fontsize=12, color=MUTED, ha="left") | |
| ax.axhline(0.885, xmin=0.07, xmax=0.93, color="#0b0b0b", linewidth=1) | |
| overall = metrics["overall"] | |
| stats = [ | |
| ("mAP50", f"{overall['mAP50']:.3f}"), | |
| ("mAP50-95", f"{overall['mAP50_95']:.3f}"), | |
| ("Precision", f"{overall['precision']:.3f}"), | |
| ("Recall", f"{overall['recall']:.3f}"), | |
| ] | |
| x0 = 0.07 | |
| box_w = 0.20 | |
| for i, (label, value) in enumerate(stats): | |
| x = x0 + i * (box_w + 0.013) | |
| ax.add_patch(plt.Rectangle((x, 0.74), box_w, 0.12, fill=False, | |
| edgecolor="#0b0b0b", linewidth=1)) | |
| ax.text(x + box_w / 2, 0.815, value, fontsize=22, fontweight="bold", | |
| ha="center", va="center", color=BLUE) | |
| ax.text(x + box_w / 2, 0.755, label, fontsize=10.5, ha="center", | |
| va="center", color=MUTED) | |
| ax.text(0.07, 0.68, "Test set: 499 pages, 6,349 annotated regions, 11 classes (none in COCO)", | |
| fontsize=10.5, color=MUTED) | |
| # metric bar chart - 4 distinct measures of the same model, so a small | |
| # categorical set (not a repeated single-series magnitude chart) | |
| cats = ["Precision", "Recall", "mAP50", "mAP50-95"] | |
| vals = [overall["precision"], overall["recall"], overall["mAP50"], overall["mAP50_95"]] | |
| colors = ["#2a78d6", "#eb6834", "#1baf7a", "#eda100"] # fixed categorical order, slots 1-4 | |
| ax2 = fig.add_axes([0.10, 0.48, 0.80, 0.16]) | |
| bars = ax2.bar(cats, vals, color=colors, width=0.55) | |
| ax2.set_ylim(0, 1.0) | |
| ax2.grid(axis="y", color=GRID, linewidth=0.8, zorder=0) | |
| ax2.set_axisbelow(True) | |
| for spine in ("top", "right", "left"): | |
| ax2.spines[spine].set_visible(False) | |
| ax2.spines["bottom"].set_color("#0b0b0b") | |
| ax2.tick_params(left=False) | |
| ax2.set_yticklabels([]) | |
| for bar, v in zip(bars, vals): | |
| ax2.text(bar.get_x() + bar.get_width() / 2, v + 0.02, f"{v:.3f}", | |
| ha="center", fontsize=10, fontweight="bold") | |
| # training config, factual, no names/dates | |
| hp = run["hyperparameters"] | |
| hw = run["hardware"] | |
| config_lines = [ | |
| f"Epochs: {hp['epochs']} Batch: {hp['batch']} Image size: {hp['imgsz']}px", | |
| f"Optimizer: {hp['optimizer']} LR: {hp['lr0']} Seed: {hp['seed']}", | |
| f"Hardware: {hw['gpu']} ({hw['gpu_memory_gb']} GB) Wall clock: {run['wall_clock_human']}", | |
| f"Query-budget saturation: {metrics['query_saturation']['percent_over_budget']}% of test pages " | |
| f"(max {metrics['query_saturation']['max_regions_on_any_page']} regions, budget " | |
| f"{metrics['query_saturation']['query_budget']})", | |
| f"Train/test source-PDF overlap: {metrics['split_leakage']['percent_affected']}% " | |
| f"({metrics['split_leakage']['test_pages_total']} test pages checked)", | |
| ] | |
| ax.text(0.07, 0.40, "Training configuration", fontsize=13, fontweight="bold") | |
| for i, line in enumerate(config_lines): | |
| ax.text(0.07, 0.365 - i * 0.032, line, fontsize=10.5, color="#0b0b0b") | |
| return fig | |
| def page_per_class(): | |
| fig = new_page() | |
| ax_title = bare_axes(fig, [0, 0, 1, 1]) | |
| ax_title.text(0.07, 0.955, "Per-class performance (mAP50)", fontsize=16, fontweight="bold") | |
| ax_title.text(0.07, 0.928, "Sorted by score. Instance count shown per class — rarity, not size alone, " | |
| "drives the weakest results.", fontsize=10, color=MUTED) | |
| per_class = metrics["overall"] # placeholder, real data below | |
| items = sorted( | |
| [(name, CLASS_INSTANCES[name]) for name in CLASS_INSTANCES], | |
| key=lambda kv: PER_CLASS_MAP50[kv[0]], | |
| ) | |
| names = [n for n, _ in items] | |
| values = [PER_CLASS_MAP50[n] for n, _ in items] | |
| counts = [c for _, c in items] | |
| ax = fig.add_axes([0.28, 0.08, 0.62, 0.80]) | |
| y = range(len(names)) | |
| ax.barh(y, values, color=BLUE, height=0.6, zorder=3) | |
| ax.set_yticks(list(y)) | |
| ax.set_yticklabels(names, fontsize=11) | |
| ax.set_xlim(0, 1.0) | |
| ax.grid(axis="x", color=GRID, linewidth=0.8, zorder=0) | |
| ax.set_axisbelow(True) | |
| for spine in ("top", "right"): | |
| ax.spines[spine].set_visible(False) | |
| ax.spines["left"].set_color("#0b0b0b") | |
| ax.spines["bottom"].set_color("#0b0b0b") | |
| ax.set_xlabel("mAP50", fontsize=10.5, color=MUTED) | |
| for yi, (v, c) in enumerate(zip(values, counts)): | |
| ax.text(v + 0.015, yi, f"{v:.3f}", va="center", fontsize=9.5, fontweight="bold") | |
| ax.text(1.02, yi, f"n={c}", va="center", fontsize=8.5, color=MUTED, transform=ax.get_yaxis_transform()) | |
| return fig | |
| def page_per_category(): | |
| fig = new_page() | |
| ax_title = bare_axes(fig, [0, 0, 1, 1]) | |
| ax_title.text(0.07, 0.955, "Per-document-category performance (mAP50)", fontsize=16, fontweight="bold") | |
| ax_title.text(0.07, 0.928, "Tests generalization across document styles, not just aggregate accuracy.", | |
| fontsize=10, color=MUTED) | |
| cat = metrics["per_doc_category"] | |
| items = sorted(cat.items(), key=lambda kv: kv[1]["mAP50"]) | |
| names = [k.replace("_", " ") for k, _ in items] | |
| values = [v["mAP50"] for _, v in items] | |
| pages = [v["pages"] for _, v in items] | |
| ax = fig.add_axes([0.30, 0.55, 0.60, 0.34]) | |
| y = range(len(names)) | |
| ax.barh(y, values, color=BLUE, height=0.55, zorder=3) | |
| ax.set_yticks(list(y)) | |
| ax.set_yticklabels(names, fontsize=11) | |
| ax.set_xlim(0, 1.0) | |
| ax.grid(axis="x", color=GRID, linewidth=0.8, zorder=0) | |
| ax.set_axisbelow(True) | |
| for spine in ("top", "right"): | |
| ax.spines[spine].set_visible(False) | |
| ax.spines["left"].set_color("#0b0b0b") | |
| ax.spines["bottom"].set_color("#0b0b0b") | |
| ax.set_xlabel("mAP50", fontsize=10.5, color=MUTED) | |
| for yi, (v, p) in enumerate(zip(values, pages)): | |
| ax.text(v + 0.015, yi, f"{v:.3f} ({p} pages)", va="center", fontsize=9.5, fontweight="bold") | |
| # failure notes, factual only | |
| ax_title.text(0.07, 0.42, "Observed failure patterns", fontsize=13, fontweight="bold") | |
| notes = [ | |
| "Footnote (47 instances): recall 0.048 — rare class, near-total miss rate.", | |
| "Page-footer (408 instances): mAP50 0.843 — same thin shape as Footnote, but", | |
| " 9x more training instances and a consistent position. Rarity, not size,", | |
| " is the dominant factor.", | |
| "Picture: precision 0.416 despite recall 0.622 — composite regions (diagrams", | |
| " with embedded text) get split into multiple overlapping boxes instead", | |
| " of one region.", | |
| "Dense repeated-entry layouts (directories, org charts) produce duplicate,", | |
| " overlapping box proposals rather than one box per entry.", | |
| ] | |
| for i, line in enumerate(notes): | |
| ax_title.text(0.07, 0.385 - i * 0.028, line, fontsize=10, color="#0b0b0b") | |
| return fig | |
| PER_CLASS_MAP50 = { | |
| "Caption": 0.622, "Footnote": 0.182, "Formula": 0.788, "List-item": 0.671, | |
| "Page-footer": 0.843, "Page-header": 0.658, "Picture": 0.495, | |
| "Section-header": 0.762, "Table": 0.761, "Text": 0.832, "Title": 0.224, | |
| } | |
| if __name__ == "__main__": | |
| import sys | |
| preview = "--preview" in sys.argv | |
| out_path = ROOT / "reports" / "evaluation_report.pdf" | |
| pages = [page_overview(), page_per_class(), page_per_category()] | |
| with PdfPages(out_path, metadata={ | |
| "Title": "", "Author": "", "Subject": "", "Creator": "", "Producer": "", "CreationDate": None, | |
| }) as pdf: | |
| for i, fig in enumerate(pages, start=1): | |
| pdf.savefig(fig) | |
| if preview: | |
| fig.savefig(ROOT / f"reports/_preview_page{i}.png", dpi=110) | |
| plt.close(fig) | |
| print(f"wrote {out_path}") | |