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
11 KiB
Embedding-dump HDF5 schema (v2)
One file per analysed title. Captures the pipeline state at the EmbeddedSceneFrame
channel — i.e. after decode → detect → align → embed, but before tracking and
identity matching. Everything downstream (face tracker, identity matcher, scene
tracker/anneal) is cheap CPU math, so replaying from this file lets a parameter
sweep re-run the whole downstream tail thousands of times with no GPU and no video.
Written by the C++ dump sink (--dump-embeddings out.h5); read by
scripts/optimizer/replay.py.
Layout
The dump is flat/ragged: all faces across all frames are concatenated into per-face arrays, with a per-frame index table pointing into them. This avoids variable-length HDF5 types and reads straight into numpy.
/ (root)
attrs:
schema_version : int = 2
embed_dim : int = 512
# ── what produced the vectors (GR-004) ──────────────────────────────────
embedder_model : str basename of the embedding model
embedder_sha256: str SHA-256 of that model file
# ── what produced the faces (VR-010) ────────────────────────────────────
detector_model : str basename of the detector .onnx
detector_conf : float score floor a detection had to clear to be dumped
detector_nms : float NMS IoU threshold
min_face_px : float minimum box side, ORIGINAL-resolution px (AR-002)
max_faces : int per-frame cap; 0 = uncapped, the default (AR-003)
# ── what produced the frames (VR-010) ───────────────────────────────────
movie : str source video path
sample_fps : float frames analysed per second of movie
start_sec : float seek point
end_sec : float stop point; -1 = end of file
cut_threshold : float histogram correlation below which is_cut fires
dense_scale : float decoded-frame downscale in dense mode; 1 = off
bbox_upscale : float multiply faces/bbox and faces/landmarks by this to
reach original video pixels; 1 when dense_scale is 1
scene_detect : uint8 0/1 — was TransNetV2 running at all (see below)
# ── downstream setting recorded for comparability (VR-010) ──────────────
track_assoc_min_prob : float the run's tracker admission probability
frames/ group — one row per sampled frame
timestamp_sec : float64 [F]
frame_idx : int64 [F]
is_cut : uint8 [F] (histogram intra-scene cut)
is_scene_boundary : uint8 [F] (TransNetV2 boundary; 0 if scene_detect off)
face_offset : int64 [F] start index into faces/* for this frame
face_count : int32 [F] number of faces in this frame
faces/ group — one row per detected face, concatenated
embedding : float32 [N, 512] L2-normalised ArcFace embedding
bbox : float32 [N, 4] x, y, w, h in DECODED-frame pixels
landmarks : float32 [N, 10] 5 (x,y) pairs, SCRFD/ArcFace order,
same space as bbox
confidence : float32 [N] detector confidence
# ── embedding input quality (AR-028), v2 onward ─────────────────────────
sharpness : float32 [N] normalised Laplacian variance on the
112x112 aligned crop (AR-029)
alignment_residual : float32 [N] RMS landmark misfit in canonical px,
after the AR-005 similarity fit (AR-030)
F = number of sampled frames, N = total faces (= sum of face_count).
Frame i's faces are faces/*[ face_offset[i] : face_offset[i]+face_count[i] ].
Provenance (VR-010)
The attributes above are not documentation; they are the only thing that makes a
dump interpretable. Two dumps of the same film at detector_conf 0.5 and 0.7, or
at dense_scale 1.0 and 0.5, or with scene detection on and off, are different
measurements of different things — and they are byte-shaped identically. Without
provenance a consumer that mixes them gets a plausible number from an incoherent
input, and nothing anywhere reports a problem.
scene_detect is the one that cannot be inferred. is_scene_boundary is
all-zero both when TransNetV2 found no boundaries in the clip and when it was
never enabled, and those mean opposite things: the first says this footage has
no shot changes, the second says nobody looked. A consumer that reads the
array alone must guess. The flag is what removes the guess. (dump_embeddings
has no --scene-detect, so every dump it writes records false — which is
exactly the fact the committed fixtures needed to state.)
bbox_upscale is recorded, not applied. See the coordinate-space note below.
Reading is by name with a default or an existence check on both sides —
replay.py (f.attrs.get(...)) and read_dump_provenance() in
src/nodes/embedding_dump_node.hpp (attrExists). So the attributes are
additive and did not themselves move schema_version off 1: a pre-VR-010 dump
still loads, and a post-VR-010 dump still reads on old code. (AR-028 later took
it to 2 by adding datasets — see below.)
A missing attribute means unknown, never a default value. Substituting
detector_conf = 0.5 for a dump that does not say so manufactures the provenance
the requirement exists to prevent — per docs/requirements.md, "a fixture whose
provenance is unknown is worse than no fixture, because it will be trusted."
The committed tests/fixtures/dumps/*.h5 predate VR-010 and carry none of these
attributes; re-dump to bind them, as with GR-004.
Embedding input quality (AR-028) — and why this one bumps the version
sharpness and alignment_residual are two of the three AR-028 quality axes,
written beside the embedding they describe. The third axis, size, is already
here: it is bbox, scaled by bbox_upscale to reach the original resolution
AR-002 thresholds in. It is not duplicated into a third column, because that
would put the same quantity in two coordinate spaces inside one file — the trap
the bbox_upscale note below records — and the copy is the one that drifts.
The vector is carried, not consumed. Nothing in the pipeline thresholds or discounts on it yet; VR-012 locates the knees from these columns, which is only possible if they were recorded at inference. A study cannot recover how sharp a face was from an embedding, any more than it can recover which model produced it.
This is the change that bumps schema_version to 2, where VR-010's
attributes did not. The rule is unchanged — a bump is for the datasets — and
so is the reason behind it. Readers are fine either way: replay.py and
test_replay_fixtures.cpp take these datasets by name with an existence check,
so a v1 dump still replays and loses only what it never had. The version exists
for a consumer of the quality vector, which otherwise cannot tell "this
film's faces were never scored" from "this film's faces scored zero" —
sharpness 0 is a real reading, meaning a featureless crop. That is the same
distinction scene_detect exists to make, and it is equally unrecoverable from
the arrays.
A v1 dump reports the vector as unknown, never as a default — load_frames
omits the keys rather than filling zeros, and the C++ side leaves the
DetectedFace fields at their -1 "unscored" sentinel. Re-dump to acquire it;
there is no migration, for the same reason GR-004 has none.
The committed
tests/fixtures/dumps/*.h5are v1 and carry no quality vector. Re-dumping needs a GPU host (scripts/make_fixtures.sh), so until that runs, anything driven from the fixtures sees the sentinel.
Model binding (GR-004)
embedder_model / embedder_sha256 record which embedder produced every vector
in faces/embedding. A replay has no live embedder, so the dump is the embedder
as far as the gallery is concerned: replay.py checks these two attributes
against the gallery's own /embedder stamp and refuses to run on a mismatch,
naming both sides. Cross-model cosines are meaningless but look plausible.
The attributes are additive, not a format break — schema_version stays 1. Dumps
written before GR-004 simply lack them, which reports as unverifiable (a loud
warning, or a hard error under SAE_REQUIRE_GALLERY_STAMP=1) rather than as a
pass. Re-dump to bind an old dump; there is no in-place migration, because unlike
a gallery nobody can assert after the fact which model produced a vector.
Coordinate space — bbox, landmarks, bbox_upscale
bbox and landmarks are in decoded-frame pixels: exactly the numbers SCRFD
produced, untransformed. To reach original video pixels, multiply by
bbox_upscale. With dense_scale == 1 (the default, and every committed
fixture) bbox_upscale == 1 and the two spaces coincide.
Earlier revisions of this document claimed the upscale was applied at dump time. It never was.
embedding_dump_node.hppwritesf.bboxraw; the upscale lives inidentity_matcher_node.hpp, which is downstream of the dump tap. The claim was harmless only becausedense_scalewas 1 in practice.
The fix is to record the factor rather than to apply it, because the dump's whole
contract is to be a faithful tap at the EmbeddedSceneFrame channel — VR-002
requires replay to drive the real nodes, and a replay is only equivalent to the
live run if the tracker is fed the geometry the live tracker saw. Rescaling at
the tap would break that: the replayed tracker would associate on boxes the live
one never received. Two further reasons:
- The matcher's upscale is applied to
bboxonly, not tolandmarks. Pre-multiplying at the tap would leave the two arrays in different coordinate spaces inside one file — a worse trap than the one being fixed. - Pre-multiplying is lossy in the sense that matters: a dump that had been
upscaled would be indistinguishable from one taken at
dense_scale == 1, so you would have to recordbbox_upscaleanyway to know which you were holding.
Invariants
embeddingrows are unit-norm (cosine == dot product against the gallery).face_offset[0] == 0;face_offset[i+1] == face_offset[i] + face_count[i].bboxandlandmarksshare one coordinate space;bbox_upscalemaps both to original resolution (see above).- A frame with no faces has
face_count == 0(still gets a row, so timestamps stay dense). - EOF sentinel frames are NOT written.
- v2 onward:
sharpnessandalignment_residualare[N], parallel toconfidence, so face i's quality indexes with the same slice as its embedding. Both are>= 0for any face the aligner admitted; a negative value means unscored and must never be read as a quality.