feat(quality): score every face on sharpness and alignment before it is evidence

Every embedding now carries the quality of the input it came from. Both
axes fall out of the AR-005 warp for free: crop_sharpness() is the
normalised Laplacian variance over the aligned 112x112, so contrast and
size cannot leak into it, and the alignment residual is the part of the
landmark deformation a similarity transform cannot explain, so in-plane
roll reads as zero and foreshortening does not.

Carried, not consumed. Nothing discounts or thresholds on either number
yet -- that is AR-030 and VR-012, and the knee has to be located against
recorded data before a gate is chosen. What this change buys is that the
data exists to locate it with.

No face is admitted unscored: the -1 sentinel is preserved rather than
clamped, and a degenerate landmark fit is counted rather than silently
dropped.

Takes the VR-001 dump to schema_version 2. The bump is not for readers,
which check for the datasets by name and replay a v1 dump unchanged; it
is so a consumer can tell "never scored" from "scored zero", which is
not recoverable from the arrays afterwards.

TRACES: AR-028, AR-029, AR-030 | VR-001 | SR-002
This commit is contained in:
2026-08-05 14:37:30 +02:00
parent c1155cb607
commit 777c98cb33
10 changed files with 726 additions and 17 deletions
+12
View File
@@ -63,6 +63,14 @@ def load_frames(dump_path: str, min_conf: float = 0.0):
bbox = f["faces/bbox"][:]
lmk = f["faces/landmarks"][:]
conf = f["faces/confidence"][:]
# TRACES: AR-028 | SR-002
# The quality vector, present from schema v2. A v1 dump predates AR-028
# and simply has no such dataset — read as absent, never as a default,
# so a face from an old dump stays at the C++ -1 "unscored" sentinel
# rather than acquiring a fabricated sharpness of 0 (which is a real
# value on this axis, meaning a featureless crop).
qual = {k: f[f"faces/{k}"][:] for k in ("sharpness", "alignment_residual")
if f"faces/{k}" in f}
movie = f.attrs.get("movie", "")
fps = float(f.attrs.get("sample_fps", 1.0))
@@ -81,6 +89,8 @@ def load_frames(dump_path: str, min_conf: float = 0.0):
"landmarks": np.ascontiguousarray(lmk[keep][sel], dtype=np.float32),
"confidence": np.ascontiguousarray(c[sel], dtype=np.float32),
"embeddings": np.ascontiguousarray(emb[keep][sel], dtype=np.float32),
**{k: np.ascontiguousarray(v[keep][sel], dtype=np.float32)
for k, v in qual.items()},
})
else:
frames.append({
@@ -90,6 +100,8 @@ def load_frames(dump_path: str, min_conf: float = 0.0):
"landmarks": np.ascontiguousarray(lmk[keep], dtype=np.float32),
"confidence": c,
"embeddings": np.ascontiguousarray(emb[keep], dtype=np.float32),
**{k: np.ascontiguousarray(v[keep], dtype=np.float32)
for k, v in qual.items()},
})
last_ts = float(ts[-1]) if len(ts) else 0.0
frames.append({"timestamp_sec": last_ts, "eof": True})