AIP-AD / scripts /make_video.py
Nabidnur's picture
AIP-AD full artifact: code, results, figures, video, Kaggle notebook, research report
acc1556 verified
Raw History Blame Contribute Delete
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))