#!/usr/bin/env python3 """Build labelling/review.html — a local page for correcting the labels. One row per crop, ordered most-suspicious first: left the person it is currently filed under (medoid of that person's hand-sorted crops, so the reference is one you trust) centre the crop under review — context with the detection boxed, and beneath it the 112x112 the embedder actually receives right the person it matches better, if any, with both probabilities Pick a destination per row, then Export to download corrections.json and apply it with apply_corrections.py. Nothing is moved by this script. Self-contained: images are inlined as data URIs and the page is opened from disk, so no server runs and no face crop leaves the machine. Ordering is by P(other) - P(self), both from the global gallery sigmoid, so rows where the evidence disagrees with the label float to the top and the agreement cases sink. It is a review order, not a verdict — you are the arbiter, which is the whole point of labelling by hand. """ import sys, glob, json, os, base64 import numpy as np import cv2 sys.path.insert(0, "/home/dtourolle/Development/Jray-project/scene-actor-extraction/build-ort") import sae_embed ROOT = "/home/dtourolle/Development/Jray-project/scene-actor-extraction/" M = ROOT + "models/" EMBEDDER = M + "LVFace-B_Glint360K.onnx" GALLERY = ROOT + "gallery_lvface.h5" REF_CLIP = "5157344" # the clip sorted by hand — reference faces come from here CLIPS = ["5157344", "5157339"] eng = sae_embed.FaceEmbedder(detector_model=M + "scrfd_500m_bnkps.onnx", arcface_model=EMBEDDER, conf=0.5, nms=0.4, max_side=0) cal = sae_embed.gallery_calibration(GALLERY) def b64(img, size, q=72): img = cv2.resize(img, (size, size)) ok, buf = cv2.imencode(".jpg", img, [cv2.IMWRITE_JPEG_QUALITY, q]) return "data:image/jpeg;base64," + base64.b64encode(buf).decode() if ok else "" rows = [] for clip in CLIPS: man = {m["file"]: m for m in json.load(open(f"labelling/{clip}/manifest.json"))} placed = {os.path.basename(p): (os.path.basename(os.path.dirname(p)), p) for p in glob.glob(f"labelling/{clip}/*/*.jpg")} by_frame = {} for fname, (person, path) in placed.items(): if fname in man and person != "unsorted": by_frame.setdefault(man[fname]["frame"], []).append((fname, person, path)) for frame, items in sorted(by_frame.items()): img = cv2.imread(f"frames/d{clip}_{frame}.png") if img is None: continue dets = eng.detect(img) for fname, person, path in items: i = man[fname]["idx"] if i >= len(dets): continue lm = np.array(dets[i].landmarks, dtype=np.float32).reshape(5, 2) crop = sae_embed.align_face(img, lm) if crop is None: continue rows.append({"clip": clip, "person": person, "file": fname, "path": path, "px": man[fname]["px"], "aligned": np.asarray(crop), "emb": np.asarray(eng.embed_crop(crop), dtype=np.float32)}) people = sorted({r["person"] for r in rows}) E = np.stack([r["emb"] for r in rows]) lab = np.array([people.index(r["person"]) for r in rows]) S = E @ E.T np.fill_diagonal(S, -1.0) # reference face per person: medoid of their REF_CLIP crops ref_img = {} for k, p in enumerate(people): idx = [i for i in np.where(lab == k)[0] if rows[i]["clip"] == REF_CLIP] if not idx: idx = list(np.where(lab == k)[0]) if not idx: continue sub = S[np.ix_(idx, idx)].copy() medoid = idx[int(np.argmax(sub.mean(axis=1)))] ref_img[p] = b64(rows[medoid]["aligned"], 112) items = [] for i, r in enumerate(rows): k = lab[i] same = [j for j in np.where(lab == k)[0] if j != i] p_self = cal.probability(float(S[i, same].max())) if same else 0.0 best_other, p_other = None, 0.0 for k2, p2 in enumerate(people): if k2 == k: continue other = np.where(lab == k2)[0] if not len(other): continue pv = cal.probability(float(S[i, other].max())) if pv > p_other: p_other, best_other = pv, p2 ctx = cv2.imread(r["path"]) items.append({ "file": r["file"], "clip": r["clip"], "person": r["person"], "px": int(r["px"]), "p_self": round(p_self, 3), "p_other": round(p_other, 3), "other": best_other, "delta": round(p_other - p_self, 3), "ctx": b64(ctx, 150) if ctx is not None else "", "ali": b64(r["aligned"], 112), }) items.sort(key=lambda x: -x["delta"]) payload = json.dumps({"people": people, "refs": ref_img, "items": items}) HTML = """