feat: banded admission for the per-subject embedding store
AR-018 — an embedding joins a track's store only if its similarity to something already there falls inside a band, rather than merely being far from the gallery. Above the upper bound it is redundant: another look at a pose the store already covers, teaching the annex nothing while costing a slot a novel view could have used. Below the lower bound it is suspect: within one track every face is the same person by construction, so an embedding unlike everything else on the track is evidence that construction failed — a track-ID collision or a bad detection. Admitting it is exactly how an actor's annex gets poisoned with someone else's face. The old gate had only the upper half of that idea, expressed as a raw cosine against the gallery. Both bounds are now calibrated probabilities (AR-024), so the same number means the same thing here as in association and evidence weighting rather than three different things. This catches track-ID collisions EARLIER than the spread gate did — at the door rather than at promotion — so the buffer never becomes two-person in the first place. The spread gate stays as a second line for a track that drifts gradually instead of jumping. The existing test was asserting the mechanism rather than the outcome, so it was rewritten to assert what actually matters: whichever gate fires, the outsider must not reach the annex. Rejections are counted. A store that admits nothing is as broken as one that admits everything, and neither is visible otherwise. Band defaults 0.90-0.95 are working values pending VR-007; the two bounds fail in opposite directions and must be swept separately. Suite: 92 cases, 6133 assertions. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> TRACES: AR-018, AR-024 | SR-005
This commit is contained in:
@@ -2,6 +2,8 @@
|
||||
#include "types.hpp"
|
||||
#include "config.hpp"
|
||||
|
||||
#include <functional>
|
||||
|
||||
#include <cmath>
|
||||
#include <cstdio>
|
||||
#include <iostream>
|
||||
@@ -117,6 +119,17 @@ struct TrackGallery {
|
||||
// matcher when it observes a cut or track disappearance.
|
||||
void forget(int track_id) { tracks_.erase(track_id); }
|
||||
|
||||
/// TRACES: AR-024 | SR-005
|
||||
/// Supply the calibration belonging to the active embedder. Without it the
|
||||
/// band falls back to treating cosine as probability, which is wrong but
|
||||
/// bounded — and the default is loud in the header rather than silent.
|
||||
void set_calibration(std::function<float(float)> c) { calibrate_ = std::move(c); }
|
||||
void set_band(float lo, float hi) { band_lo_ = lo; band_hi_ = hi; }
|
||||
|
||||
/// Embeddings the band refused. A store that admits nothing is as wrong as
|
||||
/// one that admits everything, and neither is visible without this.
|
||||
std::size_t band_rejected() const { return rejected_; }
|
||||
|
||||
// Drop every track buffer (scene cut / EOF). Mirrors face_tracker's clear.
|
||||
void clear_tracks() { tracks_.clear(); }
|
||||
|
||||
@@ -134,9 +147,40 @@ private:
|
||||
bool promoted{false};
|
||||
};
|
||||
|
||||
/// TRACES: AR-018, AR-024 | SR-005
|
||||
/// Banded admission: an embedding joins the store only if its similarity to
|
||||
/// something already there falls **inside a band**.
|
||||
///
|
||||
/// above the upper bound → redundant. It is another look at a pose the
|
||||
/// store already covers, and adding it teaches the annex nothing while
|
||||
/// costing a slot that a novel view could have used.
|
||||
/// below the lower bound → suspect. Within one track every face is the
|
||||
/// same person by construction, so an embedding unlike everything else
|
||||
/// on the track is evidence the construction failed — a track-ID
|
||||
/// collision or a bad detection. Admitting it is how an actor's annex
|
||||
/// gets poisoned with someone else's face.
|
||||
///
|
||||
/// Both bounds are calibrated probabilities, never raw cosines (AR-024): a
|
||||
/// bare similarity threshold means something different for every model and
|
||||
/// every face size, and this gate has to hold across both.
|
||||
///
|
||||
/// The first embedding is always admitted — there is nothing for it to be
|
||||
/// redundant with, and nothing to contradict it.
|
||||
bool admit(const TrackState& ts, const Embedding& emb) const {
|
||||
if (ts.buf.empty()) return true;
|
||||
|
||||
float p_max = 0.f;
|
||||
for (const auto& b : ts.buf)
|
||||
p_max = std::max(p_max, calibrate_(cosine_similarity(b.emb, emb)));
|
||||
|
||||
return p_max >= band_lo_ && p_max <= band_hi_;
|
||||
}
|
||||
|
||||
void insert_into_buffer(TrackState& ts, const Embedding& emb,
|
||||
float gal_sim, const cv::Mat& crop)
|
||||
{
|
||||
if (!admit(ts, emb)) { ++rejected_; return; }
|
||||
|
||||
BufEntry e;
|
||||
e.emb = emb;
|
||||
e.gal_sim = gal_sim;
|
||||
@@ -231,6 +275,13 @@ private:
|
||||
#endif
|
||||
}
|
||||
|
||||
/// cosine → P(same person). The one probability space the pipeline reasons
|
||||
/// in; see gallery_calibration.hpp's same_person_probability.
|
||||
std::function<float(float)> calibrate_{[](float c) { return std::max(0.f, c); }};
|
||||
float band_lo_{0.90f};
|
||||
float band_hi_{0.95f};
|
||||
std::size_t rejected_{0}; ///< admissions refused by the band
|
||||
|
||||
bool enabled_;
|
||||
int buffer_size_;
|
||||
float novelty_sim_;
|
||||
|
||||
Reference in New Issue
Block a user