#!/usr/bin/env python3 """Landmark voting: average SCRFD's overlapping detections instead of discarding them. SCRFD predicts a face from many anchors; NMS keeps the single highest-scoring box and throws the rest away. Each discarded box carries its own 5-landmark estimate of the SAME face, so the survivors are one sample from a distribution we could be averaging over. baseline conf 0.50, nms 0.40 — the shipped settings, one box per face voted conf 0.30, nms 0.90 — duplicates survive, then grouped by IoU and the 5 landmarks averaged, weighted by detection confidence Why this is worth trying when the mesh failed: the mesh moved the landmarks off the definition ArcFace was trained on (a lip-ring centroid is not an annotated mouth corner), and the embedder punished it. A confidence-weighted mean of SCRFD's OWN landmark predictions is the same kind of point, just with less variance — it should stay on-distribution while being steadier. Scored on cross-clip identification through the production sigmoid, which is the thing that actually broke. Raw similarity shown only to locate the threshold; it decides nothing. LD_PRELOAD=/usr/lib/libcudnn_cnn.so.9 python3 landmark_voting.py """ import sys, glob, json, os sys.path.insert(0, "/home/dtourolle/Development/Jray-project/scene-actor-extraction/build-ort") import sae_embed # before cv2 — see alignment_compare.py import numpy as np import cv2 import argparse ROOT = "/home/dtourolle/Development/Jray-project/scene-actor-extraction/" M = ROOT + "models/" CLIPS = ["5157344", "5157339"] PROB_THRESHOLD = 0.754 GROUP_IOU = 0.55 # detections overlapping this much are the same face MATCH_IOU = 0.35 # tie a detection to the hand-labelled face _ap = argparse.ArgumentParser() _ap.add_argument("--detector", default="scrfd_500m_bnkps.onnx", help="detector under models/. SCRFD sizes 500m / 2.5g / 10g come " "from InsightFace's buffalo_sc / buffalo_m / buffalo_l packs") _ap.add_argument("--vote-conf", type=float, default=None, help="confidence floor for the voting pass. Omit to auto-tune " "it to --target-votes") _ap.add_argument("--target-votes", type=int, default=3, help="votes per face to tune --vote-conf towards, so detectors " "are compared at equal redundancy rather than equal settings") _args = _ap.parse_args() base_eng = sae_embed.FaceEmbedder(detector_model=M + _args.detector, arcface_model=M + "LVFace-B_Glint360K.onnx", conf=0.5, nms=0.4, max_side=0) def _make_vote_engine(conf): # Same models, looser suppression: keep the duplicates NMS would have removed. return sae_embed.FaceEmbedder(detector_model=M + _args.detector, arcface_model=M + "LVFace-B_Glint360K.onnx", conf=conf, nms=0.9, max_side=0) cal = sae_embed.gallery_calibration(ROOT + "gallery_lvface.h5") def iou(a, b): ax, ay, aw, ah = a; bx, by, bw, bh = b x0, y0 = max(ax, bx), max(ay, by) x1, y1 = min(ax + aw, bx + bw), min(ay + ah, by + bh) if x1 <= x0 or y1 <= y0: return 0.0 i = (x1 - x0) * (y1 - y0) return i / (aw * ah + bw * bh - i) def vote(dets): """Group overlapping detections, return (bbox, landmarks, conf, n_votes).""" items = sorted(dets, key=lambda d: -d.confidence) used, out = [False] * len(items), [] for i, d in enumerate(items): if used[i]: continue grp = [d] used[i] = True for j in range(i + 1, len(items)): if not used[j] and iou(list(d.bbox), list(items[j].bbox)) >= GROUP_IOU: used[j] = True grp.append(items[j]) w = np.array([g.confidence for g in grp], dtype=np.float32) w = w / w.sum() lms = np.stack([np.array(g.landmarks, dtype=np.float32).reshape(5, 2) for g in grp]) bxs = np.stack([np.array(list(g.bbox), dtype=np.float32) for g in grp]) out.append((( w[:, None] * bxs).sum(0), (w[:, None, None] * lms).sum(0), float(grp[0].confidence), len(grp))) return out def tune_vote_conf(target, sample=6): """Pick the confidence floor giving ~target detections per face to average. A larger SCRFD is more confident and suppresses harder, so at a fixed floor it emits fewer overlapping anchors — median 2 against 500m's 3. Comparing detectors at equal SETTINGS therefore also compares them at unequal redundancy, and the voting arm is handicapped for the bigger models. Tuning each to the same votes-per-face isolates landmark quality from how much there was to average. """ frames = sorted(glob.glob(f"frames/d{CLIPS[0]}_*.png"))[:sample] imgs = [cv2.imread(f) for f in frames] best = (None, None, 1e9) for conf in (0.30, 0.20, 0.12, 0.07, 0.04, 0.02, 0.01): eng = _make_vote_engine(conf) sizes = [n for img in imgs for _, _, _, n in vote(eng.detect(img))] if not sizes: continue med = float(np.median(sizes)) if abs(med - target) < best[2]: best = (conf, eng, abs(med - target)) print(f"[tune] conf={conf:.2f} -> median {med:.0f} votes/face", file=sys.stderr) if med >= target: break if best[1] is None: print(f"[tune] no confidence floor reached {target} votes/face; " f"falling back to 0.30", file=sys.stderr) return 0.30, _make_vote_engine(0.30) print(f"[tune] chose conf={best[0]:.2f} for ~{target} votes/face", file=sys.stderr) return best[0], best[1] if _args.vote_conf is not None: VOTE_CONF, vote_eng = _args.vote_conf, _make_vote_engine(_args.vote_conf) else: VOTE_CONF, vote_eng = tune_vote_conf(_args.target_votes) cal = sae_embed.gallery_calibration(ROOT + "gallery_lvface.h5") def collect(clip): lab = {os.path.basename(p): os.path.basename(os.path.dirname(p)) for p in glob.glob(f"labelling/{clip}/*/*.jpg") if os.path.basename(os.path.dirname(p)) not in ("discard", "unsorted")} man = {m["file"]: m for m in json.load(open(f"labelling/{clip}/manifest.json"))} rows, votes = [], [] for frame in sorted({man[f]["frame"] for f in lab}): img = cv2.imread(f"frames/d{clip}_{frame}.png") base = base_eng.detect(img) voted = vote(vote_eng.detect(img)) for fname, person in lab.items(): m = man[fname] if m["frame"] != frame or m["idx"] >= len(base): continue d = base[m["idx"]] lm5 = np.array(d.landmarks, dtype=np.float32).reshape(5, 2) c_b = sae_embed.align_face(img, lm5) # the voted group covering the same face best, best_v = None, 0.0 for bbox, lms, conf, n in voted: v = iou(list(bbox), list(d.bbox)) if v > best_v: best_v, best = v, (lms, n) c_v = None if best and best_v >= MATCH_IOU: c_v = sae_embed.align_face(img, best[0].astype(np.float32)) votes.append(best[1]) rec = {"person": person} rec["base"] = np.asarray(base_eng.embed_crop(c_b), np.float32) if c_b is not None else None rec["voted"] = np.asarray(base_eng.embed_crop(c_v), np.float32) if c_v is not None else None rows.append(rec) return rows, votes data, allv = {}, [] for c in CLIPS: data[c], v = collect(c) allv += v print(f"[{c}] {len(data[c])} crops", file=sys.stderr) print(f"[voting] group size: median {np.median(allv):.0f}, " f"mean {np.mean(allv):.1f}, max {max(allv)} detections averaged per face", file=sys.stderr) GAL, PRB = "5157344", "5157339" print(f"\ndetector={_args.detector} vote_conf={VOTE_CONF:.2f} " f"gallery {GAL} -> probe {PRB}, P>{PROB_THRESHOLD}\n") print(f"{'align':>8}{'person':>8}{'n_gal':>7}{'n_prb':>7}" f"{'within-clip':>13}{'cross-clip':>12}{'hit rate':>10}") summary = {} for key in ("base", "voted"): gal, prb = {}, {} for r in data[GAL]: if r[key] is not None: gal.setdefault(r["person"], []).append(r[key]) for r in data[PRB]: if r[key] is not None: prb.setdefault(r["person"], []).append(r[key]) gal = {p: np.stack(v) for p, v in gal.items()} prb = {p: np.stack(v) for p, v in prb.items()} hits = tot = 0 for p in sorted(set(gal) & set(prb)): pp = prb[p] @ prb[p].T np.fill_diagonal(pp, -1) within = float(np.median(pp.max(axis=1))) if len(pp) > 1 else float("nan") cross = float(np.median((gal[p] @ prb[p].T).max(axis=0))) h = 0 for e in prb[p]: bp, bn = 0.0, None for q in gal: v = cal.probability(float((gal[q] @ e).max())) if v > bp: bp, bn = v, q if bp > PROB_THRESHOLD and bn == p: h += 1 hits += h; tot += len(prb[p]) print(f"{key:>8}{p:>8}{len(gal[p]):>7}{len(prb[p]):>7}" f"{cal.probability(within):>6.3f}/{within:<6.3f}" f"{cal.probability(cross):>6.3f}/{cross:<5.3f}{100*h/len(prb[p]):>9.0f}%") summary[key] = (hits, tot) print(f"{key:>8}{'ALL':>8}{'':>14}{'':>25}{100*hits/max(tot,1):>9.0f}%\n") hb, tb = summary["base"]; hv, tv = summary["voted"] print(f"voting vs baseline: {100*hv/max(tv,1) - 100*hb/max(tb,1):+.1f} points " f"of cross-clip TPI ({hb}/{tb} -> {hv}/{tv})")