#pragma once /// TRACES: AR-024, AR-025 | SR-002 /// /// EvidenceDiscounter — how much a single observation is allowed to move a /// track's belief. /// /// **The independence problem.** Per-frame identity evidence is accumulated as /// log-odds along a track (AR-025), which is only valid for *independent* /// observations. Consecutive frames of one track are nothing of the kind: near /// identical pose, lighting and expression. Treating them as independent drives /// the posterior to certainty on what is effectively one measurement — thirty /// frames of the same face at the same angle is not thirty pieces of evidence. /// /// The mitigation is to weight each observation by how much it *adds*: a view /// the track has already contributed is discounted toward zero, a genuinely new /// pose counts in full. This reuses the same judgement the diversity buffer /// makes for gallery expansion (AR-019) — which embeddings on a track are /// mutually distinct — rather than inventing a second notion of novelty. /// /// Owned by TrackRegistry rather than left to callers. A caller that forgot to /// discount, or applied it twice, would silently produce confident wrong /// answers, and the registry is the one place where all evidence converges. /// /// **Similarity enters as a calibrated probability, never a raw cosine** /// (AR-024): "is this the same view" is a decision, and a bare cosine threshold /// means something different for every model and every face size. #include "types.hpp" #include #include #include #include class EvidenceDiscounter { public: /// cosine similarity → P(same view). Supplied by the caller so the /// calibration fitted for the active embedder is used (AR-023/AR-024). using Calibrate = std::function; struct Config { int max_views{8}; ///< distinct views remembered per track float admit_below{0.6f}; ///< P(same view) under this ⇒ a new view float floor{0.0f}; ///< minimum weight for a redundant observation }; // Two constructors rather than a defaulted argument: `Config{}` as a default // argument would reference Config's own member initializers before the // enclosing class is complete, which is ill-formed. explicit EvidenceDiscounter(Calibrate cal) : cal_(std::move(cal)), cfg_() {} EvidenceDiscounter(Calibrate cal, Config cfg) : cal_(std::move(cal)), cfg_(cfg) {} /// Weight in [0,1] for one observation, updating `views` when the /// observation is novel enough to count as a distinct look at the subject. /// /// The first observation on a track always counts in full: there is nothing /// for it to be redundant with. float weight(std::vector& views, const Embedding& e) const { if (views.empty()) { views.push_back(e); return 1.0f; } float p_same = 0.0f; for (const auto& v : views) p_same = std::max(p_same, cal_(cosine_similarity(v, e))); // Weight is the probability this is *not* a repeat of something already // counted. A near-duplicate contributes ~0; an unseen pose ~1. const float w = std::max(cfg_.floor, 1.0f - p_same); if (p_same < cfg_.admit_below && static_cast(views.size()) < cfg_.max_views) { views.push_back(e); } return w; } private: Calibrate cal_; Config cfg_; };