Spaces:
Running on CPU Upgrade
Running on CPU Upgrade
File size: 7,963 Bytes
cd24b4a 463a482 cd24b4a | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 | """Evaluate an SynthScope detector on a benchmark track and write a schema-valid submission JSON.
python benchmark/evaluate.py --model fat_former --track forensynths \
--data /path/to/forensynths_test --out benchmark/submissions/fat_former.json
Reuses the verified ``synthscope`` inference path (model build + checkpoint load + per-model
preprocessing + fake-probability), then computes per-generator + mean metrics with torchmetrics —
the same metrics the trainer reports. Image tracks only in v1 (video harness is future work; the
video seed entries come from their reproduction eval).
Expected data layout (SynthScope common layout):
<data>/<generator>/real/*.png <data>/<generator>/fake/*.png
Use ``--reals DIR`` for benchmarks that share one real set across per-generator fake folders.
"""
from __future__ import annotations
import argparse
import datetime
import json
from pathlib import Path
import torch
from PIL import Image, ImageFile
import synthscope
ImageFile.LOAD_TRUNCATED_IMAGES = True
IMAGE_EXTS = {".jpg", ".jpeg", ".jfif", ".png", ".webp", ".bmp", ".tif", ".tiff"}
REAL_NAMES = {"real", "0_real", "reals"}
FAKE_NAMES = {"fake", "1_fake", "fakes", "ai"}
def _images(d: Path) -> list[Path]:
return sorted(p for p in d.rglob("*") if p.suffix.lower() in IMAGE_EXTS) if d and d.is_dir() else []
def _find_split(gen_dir: Path, names: set[str]) -> Path | None:
for n in names:
if (gen_dir / n).is_dir():
return gen_dir / n
return None
def discover_generators(root: Path, shared_reals: Path | None):
"""Return [(generator_name, real_paths, fake_paths)] from the benchmark root."""
shared = _images(shared_reals) if shared_reals else []
out = []
# Single pooled set: <root>/real, <root>/fake
if _find_split(root, REAL_NAMES) or _find_split(root, FAKE_NAMES):
reals = _images(_find_split(root, REAL_NAMES)) or shared
fakes = _images(_find_split(root, FAKE_NAMES))
if reals and fakes:
return [("all", reals, fakes)]
for gen_dir in sorted(p for p in root.iterdir() if p.is_dir()):
reals = _images(_find_split(gen_dir, REAL_NAMES)) or shared
fakes = _images(_find_split(gen_dir, FAKE_NAMES))
if reals and fakes:
out.append((gen_dir.name, reals, fakes))
else:
print(f" skip generator '{gen_dir.name}': need both real and fake images")
return out
@torch.no_grad()
def score(det: "synthscope.Detector", paths: list[Path], batch_size: int) -> torch.Tensor:
"""Return P(fake) for a list of image paths, using the detector's own preprocessing."""
det._ensure_weights()
model = det._engine._get_model(det.spec) # build + load (verified path)
spec, device = det.spec, det._engine.device
probs = []
for i in range(0, len(paths), batch_size):
batch = []
for p in paths[i:i + batch_size]:
batch.append(spec.transform(Image.open(p).convert("RGB")))
x = torch.stack(batch).to(device)
probs.append(det._engine._fake_prob(model, spec, x).detach().float().cpu())
return torch.cat(probs) if probs else torch.empty(0)
def _per_generator_metrics(fake_probs: torch.Tensor, labels: torch.Tensor) -> dict | None:
from torchmetrics.functional import auroc, average_precision
if labels.unique().numel() < 2:
return None # AUROC/AP need both classes
ap = float(average_precision(fake_probs, labels, task="binary")) * 100
au = float(auroc(fake_probs, labels, task="binary")) * 100
preds = (fake_probs >= 0.5).int()
real_acc = float((preds[labels == 0] == 0).float().mean()) * 100
fake_acc = float((preds[labels == 1] == 1).float().mean()) * 100
return {"ap": round(ap, 2), "auroc": round(au, 2),
"real_acc": round(real_acc, 2), "fake_acc": round(fake_acc, 2),
"balanced_acc": round((real_acc + fake_acc) / 2, 2)}
def _infer_eval_type(model_type: str) -> str:
from interface.backend.registry import get_spec
spec = get_spec(model_type)
if str(spec.ckpt_kind).startswith("official"):
return "official-weights"
if spec.ckpt_kind == "none":
return "training-free"
return "from-scratch"
def main(argv=None) -> int:
ap = argparse.ArgumentParser(description="Evaluate a detector and emit a benchmark submission.")
ap.add_argument("-m", "--model", required=True, help="SynthScope detector key (see `synthscope list`)")
ap.add_argument("--track", required=True, choices=["forensynths", "genimage-sdv1.4"],
help="benchmark track (image tracks only in v1)")
ap.add_argument("--data", required=True, help="benchmark root (per-generator real/fake dirs)")
ap.add_argument("--reals", default=None, help="shared real-image dir (for shared-real layouts)")
ap.add_argument("--out", required=True, help="output submission JSON path")
ap.add_argument("--batch-size", type=int, default=32)
ap.add_argument("--limit", type=int, default=None, help="cap images per class per generator")
ap.add_argument("--device", default=None)
ap.add_argument("--method", default=None, help="display name (default: registry name)")
ap.add_argument("--eval-type", default=None,
help="override provenance (official-weights/from-scratch/frozen-probe/...)")
ap.add_argument("--submitted-by", default="SynthScope")
args = ap.parse_args(argv)
det = synthscope.load(args.model, device=args.device)
root = Path(args.data).expanduser()
gens = discover_generators(root, Path(args.reals).expanduser() if args.reals else None)
if not gens:
print(f"No generators with real+fake images under {root}")
return 1
per_gen, means = {}, {"ap": [], "auroc": [], "balanced_acc": []}
for name, reals, fakes in gens:
if args.limit:
reals, fakes = reals[:args.limit], fakes[:args.limit]
paths = reals + fakes
labels = torch.tensor([0] * len(reals) + [1] * len(fakes))
print(f" {name}: {len(reals)} real + {len(fakes)} fake ...", flush=True)
probs = score(det, paths, args.batch_size)
m = _per_generator_metrics(probs, labels)
if m is None:
print(f" (skipped metrics: only one class)")
continue
per_gen[name] = m
for k in means:
means[k].append(m[k])
if not per_gen:
print("No generator produced valid metrics.")
return 1
def mean(xs):
return round(sum(xs) / len(xs), 2)
from interface.backend.registry import get_spec
spec = get_spec(args.model)
submission = {
"method": args.method or spec.name,
"track": args.track,
"modality": "image",
"model_type": args.model,
"paper": spec.paper,
"url": "https://github.com/SMSD75/SynthScope",
"evaluation": args.eval_type or _infer_eval_type(args.model),
"metrics": {
"acc": mean(means["balanced_acc"]),
"map": mean(means["ap"]),
"ap": None,
"auroc": mean(means["auroc"]),
},
"per_generator": per_gen,
"submitted_by": args.submitted_by,
"date": datetime.date.today().isoformat(),
"verified": True,
"source": f"synthscope evaluate.py on {root} ({len(per_gen)} generators)",
"protocol_notes": None,
"synthscope_version": synthscope.__version__,
}
out = Path(args.out).expanduser()
out.parent.mkdir(parents=True, exist_ok=True)
out.write_text(json.dumps(submission, indent=2) + "\n")
print(f"Wrote {out}")
print(f" mAP={submission['metrics']['map']} AUROC={submission['metrics']['auroc']} "
f"Acc={submission['metrics']['acc']} over {len(per_gen)} generators")
print("Run `python benchmark/leaderboard.py` to refresh LEADERBOARD.md.")
return 0
if __name__ == "__main__":
raise SystemExit(main())
|