Files
scene-actor-extraction/scripts/scene_detector/rematch_frames.py
T
dtourolle ef99951360 docs: remake all named TP/FP/FN frames against the opencv5 pipeline
Auto-match each named July frame by film+actor+class and re-extract it from
the current learned-boundary flood replay, drawing GT-aware boxes: green TP,
red FP, orange unknown, plus the blue off-screen/missed (X-Ray cast, no face)
FN panel. Adds rematch_frames.py, the tool that does the matching.

Zooey Deschanel is dropped: the current pipeline no longer makes that
false identification, so the frame is removed and the July deep-dive notes
the fix rather than showing a stale error.
2026-08-10 08:32:30 +02:00

117 lines
5.5 KiB
Python

#!/usr/bin/env python3
"""
rematch_frames.py — remake each named July frame example against the CURRENT
pipeline. For a file named <film>_<...>_<actor>.jpg, find a second in this film's
replay where that actor is drawn in the matching class (FP for *_fpi_*, TP for
*_tp/perfect*), extract + annotate it, and write it over the doc asset. Reports
which July examples no longer reproduce (honest — the config/model changed).
Needs the per-film raw replay (experiments/dumps + replay --raw-out already run by
regen_frame_examples.sh into the scratch predictions). Reads those.
"""
from __future__ import annotations
import json, sys, subprocess, re
from pathlib import Path
sys.path.insert(0, "scripts/optimizer"); sys.path.insert(0, "scripts/validation")
import dump_error_frames as D
from second_score import load_second_timeline, _match
SP = Path("/tmp/claude-1000/-home-dtourolle-Development-scene-actor-extraction/"
"c579f8cf-2974-4cbd-be88-afec68dbbf58/scratchpad")
ASSETS = Path("docs/assets/images")
LUT = json.load(open("experiments/file-lut.json"))
FILMS = json.load(open("experiments/manifests/films_LVFace_opencv5.json"))
XR = {f["slug"]: f["xray"] for f in FILMS}
# filename → (film slug, actor substring, class). class: "fp" | "tp".
# actor substring is matched case-insensitively against drawn names.
JOBS = {
"lord_of_war_fpi_reddick.jpg": ("Lord_of_War", "reddick", "fp"),
"lord_of_war_fpi_shumbris.jpg": ("Lord_of_War", "shumbris", "fp"),
"lord_of_war_fpi_reagan_photo.jpg": ("Lord_of_War", "reagan", "fp"),
"lovelace_fpi_sevigny.jpg": ("Lovelace", "sevigny", "fp"),
"lovelace_robert_patrick_fpi.jpg": ("Lovelace", "patrick", "fp"),
"lovelace_perfect_second.jpg": ("Lovelace", None, "tp"),
"lovelace_polygraph_bridged.jpg": ("Lovelace", None, "tp"),
"many_saints_fpi_deschanel.jpg": ("The_Many_Saints_of_Newark", "deschanel", "fp"),
"many_saints_fpi_gardner.jpg": ("The_Many_Saints_of_Newark", "gardner", "fp"),
"many_saints_fpi_yates.jpg": ("The_Many_Saints_of_Newark", "yates", "fp"),
"many_saints_outofcast_fpi.jpg": ("The_Many_Saints_of_Newark", None, "fp"),
"scarface_fpi_alley.jpg": ("Scarface", "alley", "fp"),
"downton_crew_fn.jpg": ("Downton_Abbey__A_New_Era", None, "tp"),
"downton_wedding_couple.jpg": ("Downton_Abbey__A_New_Era", None, "tp"),
"downton_tp_example.jpg": ("Downton_Abbey__A_New_Era", None, "tp"),
"valerian_screen_call.jpg": ("Valerian_and_the_City_of_a_Thousand_Plan", None, "tp"),
"cafe_society_rapid_cut.jpg": ("Café_Society", None, "tp"),
# germar_beats_xray / downton_funeral_19of20 are July-narrative-specific; skip.
}
def gt_keysets(slug):
tl, _, _ = load_second_timeline(XR[slug])
return tl
def main():
made, missing = [], []
for fname, (slug, actor, cls) in JOBS.items():
raw = SP / f"{slug}_raw.jsonl"
if not raw.exists():
missing.append((fname, "no raw replay")); continue
tl = gt_keysets(slug)
best = None # (t, actor_dict, fp_keys)
for line in open(raw):
d = json.loads(line)
t = int(d["timestamp_sec"])
drawn = [a for a in d.get("visible_actors", []) if a.get("actor_idx", -1) >= 0]
if not drawn:
continue
gt = tl.get(t, [])
fp_keys = {D._name_key(a["name"]) for a in drawn
if not any(D._name_key(a["name"]) in g for g in gt)}
for a in drawn:
nk = D._name_key(a["name"]); is_fp = nk in fp_keys
if actor and actor not in a["name"].lower():
continue
match = (is_fp if cls == "fp" else not is_fp)
if not match:
continue
# prefer high similarity + a clean single-subject frame
score = a["similarity"] - 0.05*len(drawn)
if best is None or score > best[3]:
best = (t, d, fp_keys, score)
if best is None:
missing.append((fname, f"no current {cls} for {actor or 'any'}")); continue
t, d, fp_keys, _ = best
# FN names at t: X-Ray scene cast whose keyset matches no drawn face.
gt = tl.get(t, [])
drawn_keys = [set(D._name_key(a["name"]).replace("name:", "") for _ in [0])
for a in d.get("visible_actors", []) if a.get("actor_idx", -1) >= 0]
drawn_ks = [D._name_key(a["name"]) for a in d.get("visible_actors", [])
if a.get("actor_idx", -1) >= 0]
fn_names = []
for ga in gt:
if not any(dk in ga for dk in drawn_ks):
readable = sorted(x for x in ga
if not x.startswith("imdb:") and not x.startswith("tmdb:")
and not x.startswith("jf:"))
if readable:
fn_names.append(readable[0])
out = ASSETS / fname
try:
D.extract_frame(LUT[slug], t, out)
D.draw_annotations(out, d["visible_actors"], fp_keys=fp_keys,
fn_names=fn_names)
made.append((fname, slug, t))
except subprocess.CalledProcessError:
missing.append((fname, "ffmpeg failed"))
print("=== remade ===")
for f, s, t in made: print(f" {f} ({s} t={t}s)")
print("=== no current equivalent (left as-is / flag in doc) ===")
for f, why in missing: print(f" {f}{why}")
if __name__ == "__main__":
main()