Klaus Clawd
Release v0.2.1: recover attack strength, cross-arch judges, uncapped frontier eval
255b4a8 | """Rebuild the README figures from a completed run (clean vs protected). | |
| python scripts/make_figures.py --run runs/v021_base | |
| Writes: | |
| assets/compare_clean_vs_adv.png a few clean|protected pairs, high-res | |
| assets/grid_before_after_16.png 16 images, clean over protected, 4x4 | |
| """ | |
| from __future__ import annotations | |
| import argparse | |
| from pathlib import Path | |
| from PIL import Image, ImageDraw, ImageFont | |
| IMAGES = Path("examples/testset60") | |
| ASSETS = Path("assets") | |
| CELL = 320 | |
| PAD = 14 | |
| LABEL_H = 26 | |
| def load_manifest() -> list[tuple[str, str]]: | |
| rows = [] | |
| for line in Path("examples/testset60.csv").read_text().splitlines(): | |
| line = line.strip() | |
| if line and not line.startswith("#"): | |
| p, t = line.split(",", 1) | |
| rows.append((Path(p).name, t.strip())) | |
| return rows | |
| def font(size: int) -> ImageFont.FreeTypeFont: | |
| for p in ("/System/Library/Fonts/Supplemental/Arial.ttf", | |
| "/System/Library/Fonts/Helvetica.ttc", | |
| "/Library/Fonts/Arial.ttf"): | |
| if Path(p).exists(): | |
| return ImageFont.truetype(p, size) | |
| return ImageFont.load_default() | |
| def sq(img: Image.Image, n: int = CELL) -> Image.Image: | |
| return img.convert("RGB").resize((n, n), Image.LANCZOS) | |
| def labeled(img: Image.Image, text: str, f) -> Image.Image: | |
| canvas = Image.new("RGB", (img.width, img.height + LABEL_H), "white") | |
| canvas.paste(img, (0, LABEL_H)) | |
| d = ImageDraw.Draw(canvas) | |
| d.text((4, 4), text, fill="black", font=f) | |
| return canvas | |
| def compare(run: Path, rows: list[tuple[str, str]], picks: list[str]) -> None: | |
| f = font(18) | |
| fh = font(20) | |
| items = [(fn, t) for fn, t in rows if fn in picks] | |
| w = 2 * CELL + 3 * PAD | |
| h = len(items) * (CELL + LABEL_H + PAD) + PAD + 30 | |
| canvas = Image.new("RGB", (w, h), "white") | |
| d = ImageDraw.Draw(canvas) | |
| d.text((PAD, 6), "Clean", fill="black", font=fh) | |
| d.text((2 * PAD + CELL, 6), "Protected (VEIL-PGD)", fill="black", font=fh) | |
| y = 34 | |
| for fn, truth in items: | |
| stem = Path(fn).stem | |
| clean = sq(Image.open(IMAGES / fn)) | |
| adv = sq(Image.open(run / "adv" / f"{stem}.png")) | |
| canvas.paste(labeled(clean, truth, f), (PAD, y)) | |
| canvas.paste(labeled(adv, truth, f), (2 * PAD + CELL, y)) | |
| y += CELL + LABEL_H + PAD | |
| ASSETS.mkdir(exist_ok=True) | |
| canvas.save(ASSETS / "compare_clean_vs_adv.png") | |
| print("wrote assets/compare_clean_vs_adv.png", canvas.size) | |
| def grid(run: Path, rows: list[tuple[str, str]], picks: list[str]) -> None: | |
| f = font(16) | |
| items = [(fn, t) for fn, t in rows if fn in picks][:16] | |
| cols = 4 | |
| cellw = CELL | |
| cellh = 2 * CELL + LABEL_H | |
| n = len(items) | |
| grows = (n + cols - 1) // cols | |
| w = cols * cellw + (cols + 1) * PAD | |
| h = grows * (cellh + PAD) + PAD | |
| canvas = Image.new("RGB", (w, h), "white") | |
| for i, (fn, truth) in enumerate(items): | |
| r, c = divmod(i, cols) | |
| x = PAD + c * (cellw + PAD) | |
| y = PAD + r * (cellh + PAD) | |
| stem = Path(fn).stem | |
| clean = sq(Image.open(IMAGES / fn)) | |
| adv = sq(Image.open(run / "adv" / f"{stem}.png")) | |
| block = Image.new("RGB", (cellw, cellh), "white") | |
| d = ImageDraw.Draw(block) | |
| d.text((4, 4), truth, fill="black", font=f) | |
| block.paste(clean, (0, LABEL_H)) | |
| block.paste(adv, (0, LABEL_H + CELL)) | |
| canvas.paste(block, (x, y)) | |
| ASSETS.mkdir(exist_ok=True) | |
| canvas.save(ASSETS / "grid_before_after_16.png") | |
| print("wrote assets/grid_before_after_16.png", canvas.size) | |
| def main() -> None: | |
| ap = argparse.ArgumentParser() | |
| ap.add_argument("--run", default="runs/v021_base") | |
| args = ap.parse_args() | |
| run = Path(args.run) | |
| rows = load_manifest() | |
| have = [fn for fn, _ in rows if (run / "adv" / f"{Path(fn).stem}.png").exists()] | |
| # one image per class for the small comparison | |
| seen, comp_picks = set(), [] | |
| for fn, t in rows: | |
| if fn in have and t not in seen: | |
| seen.add(t) | |
| comp_picks.append(fn) | |
| compare(run, rows, comp_picks[:4]) | |
| # 16 images spread across classes for the grid | |
| grid_picks = [] | |
| by_class: dict[str, list[str]] = {} | |
| for fn, t in rows: | |
| if fn in have: | |
| by_class.setdefault(t, []).append(fn) | |
| rr = 0 | |
| while len(grid_picks) < 16 and any(len(v) > rr for v in by_class.values()): | |
| for t in by_class: | |
| if len(by_class[t]) > rr and len(grid_picks) < 16: | |
| grid_picks.append(by_class[t][rr]) | |
| rr += 1 | |
| grid(run, rows, grid_picks) | |
| if __name__ == "__main__": | |
| main() | |