Merge branch 'feature/dump-provenance' into feature/opencv5

This commit is contained in:
2026-07-31 16:53:58 +02:00
3 changed files with 266 additions and 25 deletions
+173 -14
View File
@@ -1,16 +1,113 @@
#pragma once
/// TRACES: VR-001 | PR-002
/// TRACES: VR-001, VR-010 | PR-002
#include "types.hpp"
#include "config.hpp"
#include "gallery/embedder_stamp.hpp"
#include <H5Cpp.h>
#include <atomic>
#include <cstdint>
#include <iostream>
#include <optional>
#include <string>
#include <type_traits>
#include <vector>
// ── DumpProvenance ────────────────────────────────────────────────────────────
/// TRACES: VR-010 | PR-002
// Everything that determined a dump's *content*, read back tolerantly.
//
// Two dumps of the same film with different detector thresholds, a different
// `dense_scale`, or scene detection on versus off are different measurements of
// different things — but they are byte-shaped identically, so a consumer that
// mixes them gets a plausible number from an incoherent input. GR-004 closed the
// worst case (a cross-model replay, where every cosine is meaningless); this
// closes the rest.
//
// Every field is optional because dumps written before VR-010 lack the
// attributes. A missing field reads as *unknown*, never as a default — a
// silently-defaulted `detector_conf` is exactly the fabricated provenance the
// requirement exists to prevent ("a fixture whose provenance is unknown is worse
// than no fixture, because it will be trusted").
struct DumpProvenance {
// Model identity
std::optional<std::string> embedder_model; // GR-004
std::optional<std::string> embedder_sha256; // GR-004
std::optional<std::string> detector_model;
// Sampling
std::optional<std::string> movie;
std::optional<float> sample_fps;
std::optional<double> start_sec;
std::optional<double> end_sec; // -1 = to end of file
// Detection — what the run admitted into the dump
std::optional<float> detector_conf;
std::optional<float> detector_nms;
std::optional<float> min_face_px;
std::optional<int> max_faces; // 0 = uncapped (AR-003)
// Frame geometry
std::optional<float> dense_scale;
std::optional<float> bbox_upscale; // faces/bbox × this = original-resolution px
std::optional<float> cut_threshold;
// Scene detection. The reason this flag exists: `is_scene_boundary` is
// all-zero both when TransNetV2 found no boundaries and when it never ran,
// and no amount of staring at the array distinguishes them.
std::optional<bool> scene_detect;
// Downstream knob that shaped nothing in the dump but everything a replay is
// compared against — recorded so a sweep can be told apart from the baseline.
std::optional<float> track_assoc_min_prob;
};
// Read whatever provenance a dump carries. Never throws on a missing attribute;
// an old dump simply yields a DumpProvenance full of empty optionals.
inline DumpProvenance read_dump_provenance(const H5::H5File& f) {
DumpProvenance p;
auto str = [&](const char* n, std::optional<std::string>& out) {
if (!f.attrExists(n)) return;
// Written as a variable-length string, so the read must name the same
// type explicitly — the default would truncate to a fixed length.
H5::StrType vlen(H5::PredType::C_S1, H5T_VARIABLE);
std::string v;
f.openAttribute(n).read(vlen, v);
out = v;
};
auto num = [&](const char* n, const H5::PredType& dt, auto& out) {
if (!f.attrExists(n)) return;
typename std::decay_t<decltype(out)>::value_type v{};
f.openAttribute(n).read(dt, &v);
out = v;
};
str("embedder_model", p.embedder_model);
str("embedder_sha256", p.embedder_sha256);
str("detector_model", p.detector_model);
str("movie", p.movie);
num("sample_fps", H5::PredType::NATIVE_FLOAT, p.sample_fps);
num("start_sec", H5::PredType::NATIVE_DOUBLE, p.start_sec);
num("end_sec", H5::PredType::NATIVE_DOUBLE, p.end_sec);
num("detector_conf", H5::PredType::NATIVE_FLOAT, p.detector_conf);
num("detector_nms", H5::PredType::NATIVE_FLOAT, p.detector_nms);
num("min_face_px", H5::PredType::NATIVE_FLOAT, p.min_face_px);
num("max_faces", H5::PredType::NATIVE_INT, p.max_faces);
num("dense_scale", H5::PredType::NATIVE_FLOAT, p.dense_scale);
num("bbox_upscale", H5::PredType::NATIVE_FLOAT, p.bbox_upscale);
num("cut_threshold", H5::PredType::NATIVE_FLOAT, p.cut_threshold);
num("track_assoc_min_prob", H5::PredType::NATIVE_FLOAT, p.track_assoc_min_prob);
if (f.attrExists("scene_detect")) {
uint8_t v = 0;
f.openAttribute("scene_detect").read(H5::PredType::NATIVE_UINT8, &v);
p.scene_detect = (v != 0);
}
return p;
}
// ── EmbeddingDumpFunc ─────────────────────────────────────────────────────────
// KPN sink that taps the EmbeddedSceneFrame channel and writes the per-frame face
// metadata + embeddings to one HDF5 file (schema: scripts/optimizer/SCHEMA.md).
@@ -32,13 +129,39 @@ struct EmbeddingDumpFunc {
// gallery hours or weeks later — the same silent cross-model hazard as the
// gallery itself, so it carries the same stamp.
stamp_ = make_embedder_stamp(cfg.arcface_model);
/// TRACES: VR-010 | PR-002
// The rest of what determined this file's content. Captured from the live
// Config at construction, so it describes the run that is being written
// rather than whatever config happens to be lying around at read time.
prov_.detector_model = basename_of(cfg.detector_model);
prov_.detector_conf = cfg.detector_conf;
prov_.detector_nms = cfg.detector_nms;
prov_.min_face_px = cfg.min_face_px;
prov_.max_faces = cfg.max_faces;
prov_.cut_threshold = cfg.cut_threshold;
prov_.dense_scale = cfg.dense_scale;
prov_.start_sec = cfg.start_sec;
prov_.end_sec = cfg.end_sec;
prov_.scene_detect = cfg.scene_detect;
prov_.track_assoc_min_prob = cfg.track_assoc_min_prob;
std::cerr << "[embedding_dump] writing " << path_
<< " embedder: " << stamp_.describe() << "\n";
<< " embedder: " << stamp_.describe()
<< " detector: " << *prov_.detector_model
<< " @conf " << cfg.detector_conf
<< " scene_detect=" << (cfg.scene_detect ? "on" : "off") << "\n";
}
void operator()(EmbeddedSceneFrame ef) {
if (ef.source.eof) { flush(); return; }
/// TRACES: VR-010 | PR-002
// Taken from the frames themselves, not recomputed from dense_scale — the
// factor the source actually stamped on them is the one that maps
// faces/bbox back to original resolution, whatever rule produced it.
if (!prov_.bbox_upscale) prov_.bbox_upscale = ef.source.bbox_upscale;
const int32_t n = static_cast<int32_t>(ef.faces.size());
ts_.push_back(ef.source.timestamp_sec);
fidx_.push_back(ef.source.frame_idx);
@@ -71,9 +194,18 @@ struct EmbeddingDumpFunc {
}
private:
// Root attributes are additive: schema_version stays 1 across VR-010, because
// every reader takes attributes by name with a default (replay.py) or an
// existence check (read_dump_provenance), so an old dump loses nothing and a
// new dump breaks nothing. A bump is for a change to the *datasets*.
static constexpr int kSchemaVersion = 1;
static constexpr int kEmbedDim = 512;
static std::string basename_of(const std::string& path) {
const auto slash = path.find_last_of("/\\");
return slash == std::string::npos ? path : path.substr(slash + 1);
}
template<typename T>
void write_vec(H5::Group& g, const char* name, const std::vector<T>& v,
const H5::PredType& dtype, hsize_t cols = 0) {
@@ -85,23 +217,49 @@ private:
if (!v.empty()) ds.write(v.data(), dtype);
}
static void attr_str(H5::H5File& f, const char* name, const std::string& v) {
H5::StrType str(H5::PredType::C_S1, H5T_VARIABLE);
f.createAttribute(name, str, H5::DataSpace(H5S_SCALAR)).write(str, v);
}
template<typename T>
static void attr_num(H5::H5File& f, const char* name, const H5::PredType& dt, T v) {
f.createAttribute(name, dt, H5::DataSpace(H5S_SCALAR)).write(dt, &v);
}
void write_hdf5() {
H5::H5File file(path_, H5F_ACC_TRUNC);
// root attrs
auto scalar = H5::DataSpace(H5S_SCALAR);
auto ver = file.createAttribute("schema_version", H5::PredType::NATIVE_INT, scalar);
int sv = kSchemaVersion; ver.write(H5::PredType::NATIVE_INT, &sv);
auto ed = file.createAttribute("embed_dim", H5::PredType::NATIVE_INT, scalar);
int dim = kEmbedDim; ed.write(H5::PredType::NATIVE_INT, &dim);
auto fps = file.createAttribute("sample_fps", H5::PredType::NATIVE_FLOAT, scalar);
fps.write(H5::PredType::NATIVE_FLOAT, &sample_fps_);
H5::StrType str(H5::PredType::C_S1, H5T_VARIABLE);
auto mv = file.createAttribute("movie", str, scalar);
mv.write(str, movie_);
attr_num(file, "schema_version", H5::PredType::NATIVE_INT, kSchemaVersion);
attr_num(file, "embed_dim", H5::PredType::NATIVE_INT, kEmbedDim);
attr_num(file, "sample_fps", H5::PredType::NATIVE_FLOAT, sample_fps_);
attr_str(file, "movie", movie_);
/// TRACES: GR-004 | SR-001
file.createAttribute("embedder_model", str, scalar).write(str, stamp_.model_name);
file.createAttribute("embedder_sha256", str, scalar).write(str, stamp_.model_sha256);
attr_str(file, "embedder_model", stamp_.model_name);
attr_str(file, "embedder_sha256", stamp_.model_sha256);
/// TRACES: VR-010 | PR-002
attr_str(file, "detector_model", prov_.detector_model.value_or(""));
attr_num(file, "detector_conf", H5::PredType::NATIVE_FLOAT, *prov_.detector_conf);
attr_num(file, "detector_nms", H5::PredType::NATIVE_FLOAT, *prov_.detector_nms);
attr_num(file, "min_face_px", H5::PredType::NATIVE_FLOAT, *prov_.min_face_px);
attr_num(file, "max_faces", H5::PredType::NATIVE_INT, *prov_.max_faces);
attr_num(file, "cut_threshold", H5::PredType::NATIVE_FLOAT, *prov_.cut_threshold);
attr_num(file, "dense_scale", H5::PredType::NATIVE_FLOAT, *prov_.dense_scale);
// Recorded, NOT applied — faces/bbox stays in the detector's own frame
// space so a replay feeds the tracker exactly what the live run fed it.
attr_num(file, "bbox_upscale", H5::PredType::NATIVE_FLOAT,
prov_.bbox_upscale.value_or(1.f));
attr_num(file, "start_sec", H5::PredType::NATIVE_DOUBLE, *prov_.start_sec);
attr_num(file, "end_sec", H5::PredType::NATIVE_DOUBLE, *prov_.end_sec);
attr_num(file, "track_assoc_min_prob", H5::PredType::NATIVE_FLOAT,
*prov_.track_assoc_min_prob);
// 0/1, matching the uint8 booleans in frames/. Tells "TransNetV2 found no
// boundaries" apart from "TransNetV2 never ran", which is/was the same
// all-zero is_scene_boundary array either way.
attr_num(file, "scene_detect", H5::PredType::NATIVE_UINT8,
static_cast<uint8_t>(*prov_.scene_detect ? 1 : 0));
H5::Group frames = file.createGroup("frames");
write_vec(frames, "timestamp_sec", ts_, H5::PredType::NATIVE_DOUBLE);
@@ -123,6 +281,7 @@ private:
std::string path_, movie_;
EmbedderStamp stamp_;
DumpProvenance prov_;
float sample_fps_;
std::atomic<bool>& done_;
std::atomic<bool> written_{false};