Download scripts/make_video.py from Nabidnur/AIP-AD: direct link, hf CLI and curl.
- Browser
- Download file 7.87 kB
-
https://huggingface.co/Nabidnur/AIP-AD/resolve/main/scripts/make_video.py
- Command line
-
hf download hf://Nabidnur/AIP-AD/scripts/make_video.py
-
curl -L -o make_video.py https://huggingface.co/Nabidnur/AIP-AD/resolve/main/scripts/make_video.py
7.87 kB
| #!/usr/bin/env python3 | |
| """Animated overview video (mp4): AIP-AD swarm decision process + RL training. | |
| Scenes: (1) title, (2) per-view scores of a sample & poolers competing, | |
| (3) GRPO policy evolution over epochs, (4) continual learning curve, | |
| (5) summary board. Rendered with matplotlib FuncAnimation -> ffmpeg. | |
| """ | |
| from __future__ import annotations | |
| import json | |
| import os | |
| import subprocess | |
| import sys | |
| import matplotlib | |
| matplotlib.use("Agg") | |
| import matplotlib.pyplot as plt | |
| import numpy as np | |
| from matplotlib.animation import FuncAnimation, FFMpegWriter | |
| from matplotlib.patches import FancyArrowPatch, FancyBboxPatch | |
| HERE = os.path.dirname(os.path.abspath(__file__)) | |
| ROOT = os.path.dirname(HERE) | |
| sys.path.insert(0, ROOT) | |
| RES = os.path.join(ROOT, "results") | |
| FIG = os.path.join(RES, "figures") | |
| OUT = os.path.join(RES, "aipad_overview.mp4") | |
| with open(os.path.join(RES, "results.json")) as f: | |
| R = json.load(f) | |
| table = R["fixed_strategies"] | |
| arb = R["grpo_arbiter"] | |
| cats = [c for c in table["max"] if c != "macro"] | |
| snames = ["max", "mean", "median", "top2", "trimmed", "aip_d05", "aip_c03", "aip_soft2", "aip_mt01", "aip_mt025", "aip_mt05", "aip_mt075", "aip_wt05", "aip_wt075"] | |
| # pick the first anomalous sample of the first category for the demo panel | |
| demo_cat = cats[0] | |
| ev = np.load(os.path.join(RES, "scores", f"{demo_cat}.npz.eval.npz")) | |
| vs_all = ev["view_scores"].astype(np.float64) | |
| y_all = ev["view_labels"].max(1) | |
| demo_i = int(np.where(y_all == 1)[0][0]) | |
| demo_scores = vs_all[demo_i] | |
| from src.fusion import STRATEGIES # noqa (repo root on sys.path via ROOT) | |
| fused_demo = {n: fn(demo_scores) for n, fn in STRATEGIES.items()} | |
| T = 40 # frames per scene | |
| def setup_axis(ax): | |
| ax.set_axis_off() | |
| fig = plt.figure(figsize=(12.8, 7.2), dpi=100) | |
| fig.patch.set_facecolor("#0b1220") | |
| # ---------- scene frames ---------- | |
| ax_title = fig.add_axes([0, 0.86, 1, 0.14]) | |
| ax_sc = fig.add_axes([0.06, 0.42, 0.38, 0.38]) | |
| ax_pool = fig.add_axes([0.52, 0.42, 0.42, 0.38]) | |
| ax_policy = fig.add_axes([0.52, 0.08, 0.42, 0.28]) | |
| ax_curve = fig.add_axes([0.06, 0.08, 0.38, 0.28]) | |
| for a in (ax_title, ax_sc, ax_pool, ax_policy, ax_curve): | |
| a.set_facecolor("#0b1220") | |
| for s in a.spines.values(): | |
| s.set_color("#334155") | |
| a.tick_params(colors="#cbd5e1", labelsize=8) | |
| [t.set_color("#cbd5e1") for t in a.get_xticklabels() + a.get_yticklabels()] | |
| for a in (ax_title,): | |
| a.axis("off") | |
| def draw_static(): | |
| ax_title.clear(); ax_title.axis("off") | |
| ax_title.text(0.5, 0.62, "AIP-AD · Liars Are Information for Multi-View Industrial Anomaly Detection", | |
| ha="center", va="center", color="#f8fafc", fontsize=15, weight="bold") | |
| ax_title.text(0.5, 0.22, "5 camera-agents report per-view anomalies → Byzantine-robust + AIP pooling → GRPO arbiter decides", | |
| ha="center", va="center", color="#94a3b8", fontsize=10) | |
| draw_static() | |
| ax_sc.set_title(f"Per-view agent reports — '{demo_cat}' sample (anomalous)", | |
| color="#e2e8f0", fontsize=10, weight="bold") | |
| ax_pool.set_title("Fusion strategies compete for the decision", color="#e2e8f0", fontsize=10, weight="bold") | |
| ax_policy.set_title("GRPO policy over strategies (training epochs)", color="#e2e8f0", fontsize=10, weight="bold") | |
| ax_curve.set_title("Continual learning: macro S-AUROC on all categories", color="#e2e8f0", fontsize=10, weight="bold") | |
| def scene_bars(fr): | |
| ax_sc.clear() | |
| ax_sc.set_facecolor("#0b1220") | |
| for s in ax_sc.spines.values(): | |
| s.set_color("#334155") | |
| ax_sc.tick_params(colors="#cbd5e1", labelsize=8) | |
| views = np.arange(5) | |
| shown = np.clip(demo_scores, None, None) | |
| cols = ["#ef4444" if v == shown.max() else "#38bdf8" for v in shown] | |
| ax_sc.bar(views, shown * (fr / T), color=cols, width=0.62) | |
| med = np.median(shown) * (fr / T) | |
| ax_sc.axhline(med, color="#facc15", ls="--", lw=1.2) | |
| ax_sc.text(4.45, med, "swarm consensus (median)", color="#facc15", fontsize=7, va="bottom", ha="right") | |
| ax_sc.set_xticks(views) | |
| ax_sc.set_xticklabels([f"cam {i+1}" for i in range(5)]) | |
| ax_sc.set_ylim(0, max(shown.max() * 1.25, 0.1)) | |
| ax_sc.set_ylabel("anomaly score", color="#cbd5e1") | |
| ax_sc.set_title(f"Per-view agent reports — '{demo_cat}' sample (anomalous)", | |
| color="#e2e8f0", fontsize=10, weight="bold") | |
| def scene_pool(fr): | |
| ax_pool.clear() | |
| ax_pool.set_facecolor("#0b1220") | |
| for s in ax_pool.spines.values(): | |
| s.set_color("#334155") | |
| ax_pool.tick_params(colors="#cbd5e1", labelsize=7) | |
| keys = ["max", "mean", "median", "top2", "aip_mt01", "aip_wt05", "aip_mt025"] | |
| vals = np.array([fused_demo[k] for k in keys]) | |
| grow = np.clip(fr / (T * 0.5), 0.02, 1) | |
| cols = ["#94a3b8", "#94a3b8", "#94a3b8", "#94a3b8", "#f59e0b", "#f59e0b", "#f59e0b"] | |
| ax_pool.barh(np.arange(len(keys))[::-1], vals * grow, color=cols, height=0.6) | |
| ax_pool.set_yticks(np.arange(len(keys))[::-1]) | |
| ax_pool.set_yticklabels(keys) | |
| ax_pool.set_xlim(0, max(vals.max() * 1.3, 0.1)) | |
| ax_pool.set_xlabel("fused anomaly score", color="#cbd5e1") | |
| ax_pool.set_title("Fusion strategies compete for the decision", color="#e2e8f0", fontsize=10, weight="bold") | |
| def scene_policy(fr): | |
| ax_policy.clear() | |
| ax_policy.set_facecolor("#0b1220") | |
| for s in ax_policy.spines.values(): | |
| s.set_color("#334155") | |
| ax_policy.tick_params(colors="#cbd5e1", labelsize=7) | |
| hist = arb["train_history"] | |
| P = np.array(hist[-1]["mean_probs"]) | |
| frac = fr / T | |
| idx = min(int(frac * (len(hist) - 1)), len(hist) - 1) | |
| Pt = np.array(hist[idx]["mean_probs"]) | |
| order = np.argsort(P) | |
| ax_policy.barh(np.array(snames)[order], Pt[order], color="#dc2626", alpha=0.85, height=0.6) | |
| ax_policy.set_yticks(np.arange(len(snames))[::-1] * 0 + np.arange(len(snames))) | |
| ax_policy.set_yticklabels([snames[i] for i in order], fontsize=7) | |
| ax_policy.set_xlim(0, max(P.max() * 1.25, 0.05)) | |
| ent = hist[idx]["policy_entropy"] | |
| ax_policy.set_xlabel(f"epoch {hist[idx]['epoch']} · policy entropy {ent:.2f} nats", color="#cbd5e1") | |
| ax_policy.set_title("GRPO policy over strategies (training epochs)", color="#e2e8f0", fontsize=10, weight="bold") | |
| def scene_curve(fr): | |
| ax_curve.clear() | |
| ax_curve.set_facecolor("#0b1220") | |
| for s in ax_curve.spines.values(): | |
| s.set_color("#334155") | |
| ax_curve.tick_params(colors="#cbd5e1", labelsize=8) | |
| run = R["continual"][0]["curve"] | |
| xs = np.arange(1, len(run) + 1) | |
| ys = np.array([c["macro_sauroc_all6"] for c in run]) | |
| mmax = np.mean([table["max"][c] for c in cats]) | |
| k = max(1, int((fr / T) * len(xs))) | |
| ax_curve.plot(xs[:k], ys[:k], "o-", color="#2563eb", lw=2) | |
| ax_curve.axhline(mmax, color="#94a3b8", ls=":", lw=1.4) | |
| ax_curve.text(len(xs) * 0.55, mmax, "static max-pool (paper protocol)", color="#94a3b8", fontsize=8, va="bottom") | |
| ax_curve.set_xticks(xs) | |
| ax_curve.set_xticklabels([c["after"] for c in run], rotation=25, ha="right", fontsize=7) | |
| ax_curve.set_ylim(0.5, 1.0) | |
| ax_curve.set_ylabel("macro S-AUROC", color="#cbd5e1") | |
| ax_curve.set_title("Continual learning: macro S-AUROC on all categories", color="#e2e8f0", fontsize=10, weight="bold") | |
| SCENES = [(scene_bars, scene_pool), (scene_policy, scene_curve)] | |
| total_frames = T * 2 | |
| def update(fr): | |
| i = fr // T | |
| fr2 = fr % T | |
| scene_bars(fr2) if i == 0 else scene_policy(fr2) | |
| scene_pool(fr2) if i == 0 else scene_curve(fr2) | |
| draw_static() | |
| anim = FuncAnimation(fig, update, frames=total_frames, interval=110, blit=False) | |
| try: | |
| writer = FFMpegWriter(fps=9, bitrate=2200) | |
| anim.save(OUT, writer=writer) | |
| print("video saved:", OUT, os.path.getsize(OUT)) | |
| except Exception as e: | |
| print("ffmpeg failed:", e) | |
| OUT_GIF = OUT.replace(".mp4", ".gif") | |
| anim.save(OUT_GIF, writer="pillow", fps=8) | |
| print("gif saved:", OUT_GIF, os.path.getsize(OUT_GIF)) | |