| """FID + CLIP Score for PixelModel v3 - identical protocol to v1. |
| |
| Real set: MS-COCO val2014 pairs (coco_eval.npz from fetch_coco_subset.py), |
| 256x256 center-crop, n=5000. Metrics via torchmetrics: |
| FID -> torchmetrics.image.fid.FrechetInceptionDistance |
| CLIP -> torchmetrics.multimodal.CLIPScore, openai/clip-vit-base-patch32 |
| |
| Two modes, scored by the *same* code so numbers are comparable: |
| |
| # score v3 directly from model.png |
| python eval/run_eval.py --arch v3 --work ../pm-work \ |
| --png model.png --config config.json --vocab vocab.json --n 5000 |
| |
| # regression check: score any OTHER model (v1, v2) from pre-rendered PNGs |
| # (files named 00000.png, 00001.png, ... aligned to eval order) |
| python eval/run_eval.py --arch precomputed --work ../pm-work \ |
| --images-dir v1_render/ --n 5000 |
| |
| This is what makes the README's v1-vs-v3 comparison apples-to-apples: both go |
| through this script, on the same reals, with the same torchmetrics versions. |
| """ |
|
|
| from __future__ import annotations |
|
|
| import argparse |
| import glob |
| import json |
| import os |
| import sys |
|
|
| import numpy as np |
| import torch |
| from PIL import Image |
|
|
| sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) |
| from model import load_config, load_model_png, load_vocab, encode_caption, make_coord_grid |
|
|
|
|
| def resize_u8(arr_u8, size): |
| return np.asarray(Image.fromarray(arr_u8, "RGB").resize((size, size), Image.BICUBIC), |
| dtype=np.uint8) |
|
|
|
|
| def render_v3(args, captions, device): |
| cfg = load_config(args.config) |
| model = load_model_png(args.png, cfg, map_location=device) |
| vocab = load_vocab(args.vocab) |
| coords = make_coord_grid(args.render_res, args.render_res, device=device, |
| dtype=torch.float32).unsqueeze(0) |
| outs = [] |
| for i in range(0, len(captions), args.batch): |
| batch_caps = captions[i:i + args.batch] |
| toks = np.stack([encode_caption(c, vocab, cfg.max_tokens) for c in batch_caps]) |
| toks = torch.from_numpy(toks).long().to(device) |
| c = coords.expand(len(batch_caps), -1, -1) |
| with torch.no_grad(): |
| rgb = model(toks, c) |
| rgb = (rgb.clamp(0, 1).reshape(len(batch_caps), args.render_res, args.render_res, 3) |
| .cpu().numpy() * 255.0).round().astype(np.uint8) |
| for im in rgb: |
| outs.append(resize_u8(im, args.fid_size)) |
| if i % (args.batch * 20) == 0: |
| print(f"[eval] rendered {i+len(batch_caps)}/{len(captions)}") |
| return np.stack(outs) |
|
|
|
|
| def load_precomputed(images_dir, n, fid_size): |
| files = sorted(glob.glob(os.path.join(images_dir, "*.png")))[:n] |
| if not files: |
| raise FileNotFoundError(f"no PNGs in {images_dir}") |
| outs = [resize_u8(np.asarray(Image.open(f).convert("RGB"), dtype=np.uint8), fid_size) |
| for f in files] |
| return np.stack(outs) |
|
|
|
|
| def to_nchw_u8(arr): |
| return torch.from_numpy(arr).permute(0, 3, 1, 2).contiguous() |
|
|
|
|
| def main(): |
| ap = argparse.ArgumentParser() |
| ap.add_argument("--arch", choices=["v3", "precomputed"], default="v3") |
| ap.add_argument("--work", default="../pm-work", help="dir with coco_eval.npz") |
| ap.add_argument("--png", default="model.png") |
| ap.add_argument("--config", default="config.json") |
| ap.add_argument("--vocab", default="vocab.json") |
| ap.add_argument("--images-dir", default=None, help="precomputed renders") |
| ap.add_argument("--n", type=int, default=5000) |
| ap.add_argument("--render-res", type=int, default=128) |
| ap.add_argument("--fid-size", type=int, default=256, help="match v1 (256 crop reals)") |
| ap.add_argument("--batch", type=int, default=32) |
| ap.add_argument("--clip-model", default="openai/clip-vit-base-patch32") |
| ap.add_argument("--device", default="cuda") |
| ap.add_argument("--out", default="eval_results.json") |
| args = ap.parse_args() |
|
|
| device = args.device if torch.cuda.is_available() else "cpu" |
| from torchmetrics.image.fid import FrechetInceptionDistance |
| from torchmetrics.multimodal.clip_score import CLIPScore |
|
|
| ev = np.load(os.path.join(args.work, "coco_eval.npz"), allow_pickle=True) |
| reals = ev["images"][:args.n] |
| captions = [str(c) for c in ev["captions"][:args.n]] |
| n = min(len(reals), len(captions), args.n) |
| reals, captions = reals[:n], captions[:n] |
| reals = np.stack([resize_u8(im, args.fid_size) for im in reals]) |
| print(f"[eval] arch={args.arch} n={n} fid_size={args.fid_size} device={device}") |
|
|
| if args.arch == "v3": |
| fakes = render_v3(args, captions, device) |
| else: |
| fakes = load_precomputed(args.images_dir, n, args.fid_size) |
| if len(fakes) != n: |
| raise ValueError(f"precomputed count {len(fakes)} != {n}") |
|
|
| fid = FrechetInceptionDistance(feature=2048, normalize=False).to(device) |
| for i in range(0, n, args.batch): |
| fid.update(to_nchw_u8(reals[i:i + args.batch]).to(device), real=True) |
| fid.update(to_nchw_u8(fakes[i:i + args.batch]).to(device), real=False) |
| fid_val = float(fid.compute()) |
|
|
| clip = CLIPScore(model_name_or_path=args.clip_model).to(device) |
| for i in range(0, n, args.batch): |
| imgs = to_nchw_u8(fakes[i:i + args.batch]).to(device) |
| clip.update(imgs, captions[i:i + args.batch]) |
| clip_val = float(clip.compute()) |
|
|
| results = { |
| "arch": args.arch, "n": n, "fid": round(fid_val, 2), |
| "clip_score": round(clip_val, 2), "render_res": args.render_res, |
| "fid_size": args.fid_size, "clip_model": args.clip_model, |
| } |
| with open(args.out, "w") as f: |
| json.dump(results, f, indent=2) |
| print(f"[eval] FID = {fid_val:.2f} CLIP Score = {clip_val:.2f} (n={n})") |
| print(f"[eval] wrote {args.out}") |
|
|
|
|
| if __name__ == "__main__": |
| main() |
|
|