Files
scene-actor-extraction/scripts/movienet_eval.py
dtourolle f891e579c5 chore(traces): put TRACES tags on their own line; regenerate the report
The parser reads a tag up to end of line, so `# TRACES: GR-004 | SR-001 —
prose` swallowed the prose into the tag and the row went unmatched. Splitting
the comment leaves the tag greppable by the same pattern as the code tags and
the commit trailers, which is the point of the house format.

Mechanical throughout; no logic touched. The regenerated report reflects this
session's new tags: 137 -> 148 found, and one more tagged-but-unexecuted, which
is the SuperHero accuracy assertion that is documented but not yet a test.
2026-08-04 14:04:21 +02:00

133 lines
4.6 KiB
Python

#!/usr/bin/env python3
"""
movienet_eval.py — embed probe crops and match against a gallery.
Usage:
python scripts/movienet_eval.py \
--gallery gallery_r50.h5 \
--arcface models/arcface_w600k_r50.onnx \
--gt eval/gt.json \
--output eval/predictions_r50.json \
[--build-dir build]
Input (--gt): list of {"crop": <path>, "imdb_id": <str>, "actor_name": <str>}
Output: list of {"crop", "gt", "pred", "similarity", "detection_failed", "all_scores"}
"""
import argparse
import json
import sys
from pathlib import Path
import numpy as np
sys.path.insert(0, str(Path(__file__).resolve().parent))
from sae_embed_loader import load_embedder, resolve_arcface
from sae_gallery import load_gallery_hdf5, verify_gallery_stamp
def load_gallery(path: str) -> dict[str, dict]:
"""Return {imdb_id: {"name": str, "refs": np.ndarray[n_refs, dim]}}."""
data = load_gallery_hdf5(Path(path))
return {a["imdb_id"]: {"name": a["name"],
"refs": np.asarray(a["embeddings"], dtype=np.float32)}
for a in data["actors"]}
def match(embedding: list[float], gallery: dict[str, dict]) -> tuple[str, float, dict[str, float]]:
"""Return (best_imdb_id, best_similarity, {imdb_id: similarity}).
Similarity to an actor is the max dot product over that actor's reference
embeddings; vectorised with numpy so a whole-library gallery stays fast.
"""
vec = np.asarray(embedding, dtype=np.float32)
scores: dict[str, float] = {
imdb_id: float((actor["refs"] @ vec).max())
for imdb_id, actor in gallery.items()
}
best_id = max(scores, key=lambda k: scores[k])
return best_id, scores[best_id], scores
def main():
p = argparse.ArgumentParser()
p.add_argument("--gallery", required=True)
p.add_argument("--arcface", required=True)
p.add_argument("--gt", required=True)
p.add_argument("--output", required=True)
p.add_argument("--build-dir", default="build",
help="Build directory containing the sae_embed module (default: build)")
p.add_argument("--models-dir", default="models",
help="Directory containing ONNX models (default: models/)")
args = p.parse_args()
embedder = load_embedder(args.build_dir, args.models_dir, args.arcface)
# TRACES: GR-004 | SR-001
# match() below is a bare dot product against the
# gallery's vectors; if the gallery came from another model those numbers are
# noise wearing a similarity's clothes.
verify_gallery_stamp(args.gallery,
resolve_arcface(args.models_dir, args.arcface))
gallery = load_gallery(args.gallery)
print(f"[eval] gallery: {len(gallery)} actors", file=sys.stderr)
with open(args.gt) as f:
gt_entries = json.load(f)
print(f"[eval] probe crops: {len(gt_entries)}", file=sys.stderr)
crop_paths = [Path(e["crop"]) for e in gt_entries]
missing = [p for p in crop_paths if not p.exists()]
if missing:
print(f"[warn] {len(missing)} crop(s) not found on disk, skipping", file=sys.stderr)
embed_results = [embedder.embed(str(p)) if p.exists() else None for p in crop_paths]
predictions = []
n_det_fail = 0
n_correct = 0
for entry, result in zip(gt_entries, embed_results):
detection_failed = result is None or not result.ok
if detection_failed:
n_det_fail += 1
predictions.append({
"crop": entry["crop"],
"gt": entry["imdb_id"],
"pred": None,
"similarity": None,
"detection_failed": True,
"all_scores": {},
})
continue
pred_id, sim, all_scores = match(result.embedding, gallery)
correct = pred_id == entry["imdb_id"]
if correct:
n_correct += 1
predictions.append({
"crop": entry["crop"],
"gt": entry["imdb_id"],
"pred": pred_id,
"similarity": sim,
"detection_failed": False,
"all_scores": all_scores,
})
n_total = len(gt_entries)
n_evaluated = n_total - n_det_fail
rank1 = n_correct / n_evaluated * 100 if n_evaluated else 0
print(f"[eval] detection failures: {n_det_fail}/{n_total}", file=sys.stderr)
print(f"[eval] rank-1 accuracy: {rank1:.1f}% ({n_correct}/{n_evaluated})", file=sys.stderr)
out_path = Path(args.output)
out_path.parent.mkdir(parents=True, exist_ok=True)
with open(out_path, "w") as f:
json.dump(predictions, f, indent=2)
print(f"[eval] written → {out_path}", file=sys.stderr)
if __name__ == "__main__":
main()