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.
|
After Width: | Height: | Size: 404 KiB |
|
After Width: | Height: | Size: 367 KiB |
|
After Width: | Height: | Size: 367 KiB |
|
After Width: | Height: | Size: 367 KiB |
|
After Width: | Height: | Size: 218 KiB |
|
After Width: | Height: | Size: 252 KiB |
|
After Width: | Height: | Size: 221 KiB |
|
After Width: | Height: | Size: 180 KiB |
|
Before Width: | Height: | Size: 239 KiB After Width: | Height: | Size: 268 KiB |
|
After Width: | Height: | Size: 268 KiB |
|
After Width: | Height: | Size: 226 KiB |
|
After Width: | Height: | Size: 156 KiB |
|
After Width: | Height: | Size: 180 KiB |
|
After Width: | Height: | Size: 113 KiB |
|
After Width: | Height: | Size: 53 KiB |
|
Before Width: | Height: | Size: 165 KiB After Width: | Height: | Size: 447 KiB |
@@ -176,10 +176,10 @@ ground-truth gap, not a model error.
|
|||||||
Archie Yates, t=2521s, 78% confidence. A real detected face, a genuine
|
Archie Yates, t=2521s, 78% confidence. A real detected face, a genuine
|
||||||
lookalike confusion.
|
lookalike confusion.
|
||||||
|
|
||||||

|
Zooey Deschanel, t=2819s, 99% confidence — a high-confidence lookalike
|
||||||
|
confusion in the July pipeline. **The current opencv5 pipeline no longer makes
|
||||||
Zooey Deschanel, t=2819s, 99% confidence. A real detected face at a dinner
|
this identification**; the tighter tracker/registry and re-tuned matching removed
|
||||||
table, high-confidence lookalike confusion.
|
it, so there is no annotated frame for it here.
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,116 @@
|
|||||||
|
#!/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()
|
||||||