diff --git a/src/config.hpp b/src/config.hpp index 3284f32..761e0ef 100644 --- a/src/config.hpp +++ b/src/config.hpp @@ -85,40 +85,26 @@ struct Config { std::string arcface_model; std::string arcface_engine; // optional path to a pre-built TRT engine; bypasses ORT int embed_batch_size{4}; // max faces per ORT Run() call — bounds per-call latency - float match_prior{0.5f}; // base-rate prior; 0.5 = use calibrated sigmoid directly + float match_prior{0.433f}; // base-rate prior; 10-knob DE optimum (was 0.5) // Tuned by Differential Evolution against Amazon X-Ray per-second presence - // over the 4-film rep4 matrix. Best model+mode: LVFace-B_Glint360K, full - // gallery, expansion on. Supersedes an earlier 9-film scene-union tuning - // (0.76); that metric hid out-of-cast false positives. + // over ALL 9 films (opencv5 build, LVFace-B_Glint360K, full gallery, + // expansion on), a 10-parameter sweep — see docs/model-bakeoff.md. The + // per-second misID-weighted macro-F1 optimum is 64.0% (P 79.0%, R 61.1%). // - // **Read the provenance before trusting the value.** Two things about it: + // This is a permissive operating point: the sweep discovered that with + // flood-fill presence recovering recall, a LOW threshold pays off. It + // supersedes the earlier 0.754, which came from a 4-film subset under the + // now-withdrawn anneal/extinction windows and was never re-derived after a + // scoring-bug fix. The full-9-film sweep at 0.485 beats it. // - // 1. The document it came from no longer exists under that name. It was - // docs/rep4-optimizer-results.md, renamed to docs/model-bakeoff.md and - // then rewritten (0bd2747). This comment pointed at the dead path for - // long enough that the number looked unsourced. The original is still - // readable at `git show d340da7:docs/rep4-optimizer-results.md`, where - // the shipped triple appears as - // `prob_threshold=0.754, anneal_sec=35.5`. - // - // 2. **0.754 predates a scoring bug fix and was never re-derived.** That - // same rewrite reports finding "a real scoring bug in optimize.py: a - // candidate whose hardest film's replay timed out was averaged over - // survivors instead of penalized, silently rewarding partial coverage. - // Affected 3 of 16 training combos". The corrected sweep converged - // somewhere else — the surviving document records anneal_sec=59.2, - // extinction_sec=59.2 against the 35.5/57.4 shipped alongside this - // threshold — and no corrected prob_threshold is recorded anywhere. - // (The other two constants are now withdrawn outright, which is why - // only this one still matters.) - // - // The doc is also candid that the optimum "generalizes unevenly — strong on - // 3 of 5 held-out films, badly broken on 2 (one with a 974-count misID - // blowup)", and that it is shipped anyway because it still beats the old - // defaults on average. That is a defensible call and not a settled, - // film-agnostic optimum; it should be visible here rather than only in a - // document this comment used to point at incorrectly. - float prob_threshold{0.754f}; // posterior P(match | sim, prior) threshold + // Caveat, still true: the optimum generalises unevenly. It is strong on 7 of + // 9 films (F1 62–80%) and weak on two — The Many Saints of Newark (an + // ensemble of look-alikes; nearly all the run's misIDs land here) and + // Scarface (sparse cuts, so flood-fill over-extends: R 95% / P 26%). Both + // were the low outliers in every prior run too. Shipped because it wins on + // average and on the misID-weighted objective; not a settled, film-agnostic + // constant. + float prob_threshold{0.485f}; // posterior P(match | sim, prior) threshold // TRACES: AR-024 | SR-002 // match_threshold (0.45), match_ratio (0.80) and match_ratio_ceil (0.65) are // RETIRED, joining track_max_embed_dist, cut_revive_sim, expand_novelty_sim @@ -131,7 +117,12 @@ struct Config { // ── Presence derivation ────────────────────────────────────────────────── // How accepted frames become a reported window. flood requires scene_detect. - PresenceMode presence_mode{PresenceMode::track_extent}; + // Default flood: the 10-knob DE optimum uses it — snapping presence to the + // shot recovers enough recall against X-Ray's scene-level cast to win the + // misID-weighted F1, at a precision cost that is a net gain on 7 of 9 films. + // Falls back to track_extent per claim when no boundaries exist. See + // docs/model-bakeoff.md and PresenceMode above. + PresenceMode presence_mode{PresenceMode::flood}; // ── Cut detection ──────────────────────────────────────────────────────── float cut_threshold{0.70f}; // grayscale histogram correlation below this → hard cut @@ -180,7 +171,7 @@ struct Config { // frame-dependent (AR-007): on is_cut / is_scene_boundary, and for any track // that is no longer on screen, it drops to 0 (embedding only), because // position carries no information across a viewpoint change or a gap. - float track_alpha{0.4f}; // base cost weight: 0=embedding only, 1=spatial only + float track_alpha{0.435f}; // base cost weight: 0=embedding only, 1=spatial only (10-knob DE optimum) float track_min_iou{0.1f}; // IoU below which spatial link alone is rejected // Minimum P(same person) for an association to be admissible on appearance // alone. This replaces track_max_embed_dist (a raw cosine distance, AR-024). @@ -193,7 +184,7 @@ struct Config { // Replaces track_max_frames_missing: a frame count silently changed meaning // with sample_fps, and the same number had to be guessed twice (once for an // ordinary miss, once for a cut). Seconds mean one thing at any sample rate. - double track_extinction_sec{5.0}; + double track_extinction_sec{31.0}; // 10-knob DE optimum (was 5.0) // ── Ownership and evidence accumulation (AR-025) ────────────────────────── // TRACES: AR-025, AR-017 | SR-002 @@ -207,8 +198,10 @@ struct Config { // ownership_logodds is arguably the most consequential constant in the // pipeline after prob_threshold: below it a track produces no presence // claim at all, so it decides whether an actor is reported rather than how - // confidently. 2.0 is a posterior of ~0.88. Unswept. - float ownership_logodds{2.0f}; + // confidently. 1.72 is a posterior of ~0.85 — the 10-knob DE optimum (was + // an unswept 2.0 ≈ 0.88); slightly more permissive, consistent with the + // low-threshold operating point the sweep converged on. + float ownership_logodds{1.72f}; // How much a single observation may move a track's belief. n_eff = // n / (1 + (n-1)·rho), so rho_max caps what a repeated view can ever be @@ -217,10 +210,11 @@ struct Config { // detection, alignment and noise realisation, so a little independent // evidence survives. Setting it to 1 freezes belief after the first frame, // which is the bug this replaced. - float evidence_rho_max{0.5f}; + float evidence_rho_max{0.204f}; // 10-knob DE optimum (was 0.5): weights a + // held pose closer to a single observation // P(same view) below this and the observation counts as a genuinely new // look, so it joins the per-track view set. - float evidence_admit_below{0.6f}; + float evidence_admit_below{0.784f}; // 10-knob DE optimum (was 0.6) // Distinct views remembered per track, which bounds the novelty comparison. int evidence_max_views{8}; @@ -268,10 +262,10 @@ struct Config { // at promotion time — see track_gallery.hpp. This is the only threshold the // expansion path has: it replaces the raw-cosine expand_novelty_sim (0.55) // and expand_track_spread_max (0.60), which are retired (AR-024). - // Working values pending VR-007; sweep both bounds, they fail in opposite - // directions. - float expand_band_lo{0.90f}; - float expand_band_hi{0.95f}; + // 10-knob DE optimum (was 0.90/0.95). The sweep widened the band — a lower lo + // admits more pose-varied views into the annex — which the optimum preferred. + float expand_band_lo{0.804f}; + float expand_band_hi{0.952f}; int expand_min_anchor_frames{3}; // require ≥N accepted frames naming the actor before // the track is confirmed and its buffer promoted std::string expand_debug_dir; // if set, dump promoted mugshots + embeddings here