Skip to main content

ados_vision/tracker/
config.rs

1//! Tracker tuning and per-frame output types.
2
3use ados_protocol::framebus::Detection;
4
5/// Tuning for the tracker. Defaults are chosen for a hand-held / airborne
6/// camera at a few tens of frames per second; all are overridable.
7#[derive(Debug, Clone, Copy)]
8pub struct TrackerConfig {
9    /// Minimum IoU between a candidate detection and the predicted box for the
10    /// candidate to be eligible for the lock. The primary geometric gate.
11    pub min_iou: f32,
12    /// Fallback center-distance gate, as a multiple of the predicted box's
13    /// diagonal. A fast/weaving target can move enough that its boxes barely
14    /// overlap frame-to-frame; an in-diagonal center still associates. This is
15    /// the *base* multiplier; the live gate widens with the filter's position
16    /// uncertainty (see `max_gate_dist`).
17    pub max_center_dist_diag: f32,
18    /// How strongly the filter's own position uncertainty widens the gate.
19    /// During a coast the position covariance grows, so the gate should grow
20    /// with it (a missed object can be further from the last prediction). In
21    /// units of standard deviations of the position estimate added to the base
22    /// gate. Zero reproduces a fixed gate.
23    pub gate_sigma: f32,
24    /// Detections below this confidence are ignored entirely.
25    pub min_confidence: f32,
26    /// Consecutive associated frames before a tentative track is confirmed.
27    pub confirm_hits: u32,
28    /// Consecutive missed frames a confirmed track coasts (predict-only) before
29    /// it is declared lost. Brief occlusion stays inside this budget and keeps
30    /// the id; exceeding it drops the lock.
31    pub max_coast_frames: u32,
32    /// Measurement noise: how much a single detection box is trusted versus the
33    /// prediction. Higher ⇒ smoother (trust the model more), lower ⇒ snappier
34    /// (trust the detection more). In the same units as the bbox (pixels).
35    pub measurement_noise: f32,
36    /// Process noise: how much the motion model is allowed to drift per frame.
37    /// Higher ⇒ the filter adapts faster to acceleration / turns.
38    pub process_noise: f32,
39    /// Largest velocity (pixels/frame, per box component) a single Kalman update
40    /// may inject into the estimate. One jittered detection box must not be able
41    /// to slam a huge velocity into the filter (which would then overshoot the
42    /// next prediction and shake the lock loose). Clamping the per-update
43    /// velocity change keeps a half-box-width jitter from poisoning the model.
44    pub max_velocity: f32,
45    /// Minimum appearance similarity for a candidate to be eligible as the
46    /// locked object. A candidate that looks too unlike the template is rejected
47    /// even if it sits dead-center in the motion gate (the shadowing-distractor
48    /// defence). Only applied when a template + the candidate's descriptor are
49    /// both present.
50    pub min_appearance: f32,
51    /// Appearance margin (best similarity minus second-best similarity) below
52    /// which two candidates are treated as appearance-AMBIGUOUS: the lock will
53    /// not silently pick one, it goes Uncertain and requests re-designation.
54    /// This is the never-silent guard for two identical-looking objects.
55    pub ambiguous_margin: f32,
56    /// Template update rate (EMA factor in `0..=1`) applied to the locked
57    /// object's appearance on a confident measured association. Small ⇒ the
58    /// template adapts slowly to lighting / pose, so a one-frame wrong match
59    /// cannot snap the template onto a distractor.
60    pub template_lr: f32,
61    /// Weights for the combined association score: (appearance, motion, detector
62    /// confidence). Used only as the TIE-BREAK among candidates whose appearance
63    /// similarity is within [`Self::ambiguous_margin`] of the best, appearance
64    /// alone owns the primary ranking (see `SingleObjectTracker::decide`), so a
65    /// louder/closer distractor cannot out-rank a strictly-more-similar candidate
66    /// through this score. Normalized internally, so only the ratio matters.
67    pub score_weights: ScoreWeights,
68}
69
70/// Relative weights of the three association cues, applied only as the tie-break
71/// among appearance-indistinguishable candidates. Appearance owns the primary
72/// ranking outright, so these never let a geometric or confidence coincidence
73/// outvote a clear appearance preference.
74#[derive(Debug, Clone, Copy)]
75pub struct ScoreWeights {
76    pub appearance: f32,
77    pub motion: f32,
78    pub confidence: f32,
79}
80
81impl Default for ScoreWeights {
82    fn default() -> Self {
83        Self {
84            appearance: 0.6,
85            motion: 0.3,
86            confidence: 0.1,
87        }
88    }
89}
90
91impl Default for TrackerConfig {
92    fn default() -> Self {
93        Self {
94            min_iou: 0.1,
95            max_center_dist_diag: 1.5,
96            gate_sigma: 3.0,
97            min_confidence: 0.0,
98            confirm_hits: 2,
99            max_coast_frames: 8,
100            measurement_noise: 4.0,
101            process_noise: 1.0,
102            max_velocity: 40.0,
103            min_appearance: 0.5,
104            ambiguous_margin: 0.08,
105            template_lr: 0.1,
106            score_weights: ScoreWeights::default(),
107        }
108    }
109}
110
111/// The observable lifecycle of the single locked track.
112#[derive(Debug, Clone, Copy, PartialEq, Eq)]
113pub enum TrackState {
114    /// No track. The next confident detection seeds a tentative one.
115    Idle,
116    /// A track exists but has not yet been confirmed (too few hits). It is not
117    /// reported on the wire so a spurious one-frame blob never claims an id.
118    Tentative,
119    /// Confirmed and reported. The id is live and the identity is trusted.
120    Confirmed,
121    /// Confirmed but currently coasting on prediction through a miss, OR held on
122    /// the prediction because the frame was appearance-ambiguous, OR re-confirming
123    /// after a coast (a re-association coming out of a coast spends at least one
124    /// frame here, reported Uncertain, before it is trusted as Confirmed again, so
125    /// a handover laundered during a coast surfaces doubt for a frame). The id is
126    /// held; downstream sees the predicted/measured box flagged Uncertain.
127    Coasting,
128}
129
130/// What a single advance produced.
131#[derive(Debug, Clone)]
132pub struct TrackUpdate {
133    /// The lifecycle state after this frame.
134    pub state: TrackState,
135    /// The reported detection for the locked object this frame, with its
136    /// `track_id`, `lock_state`, and `assoc_confidence` filled, or `None` when
137    /// there is nothing live to report (idle, or tentative-not-yet-confirmed).
138    /// For a coasting / ambiguous frame this is the predicted box.
139    pub detection: Option<Detection>,
140    /// The track id the lock currently holds, if any.
141    pub track_id: Option<u64>,
142    /// True when this update came from a measured (and accepted) detection, not
143    /// a coast / ambiguous hold.
144    pub measured: bool,
145    /// True when the tracker could not safely resolve the identity this frame
146    /// (two appearance-ambiguous candidates) and is asking the operator layer to
147    /// re-designate the target. A silent capture never happens; this flag is how
148    /// the ambiguity surfaces.
149    pub needs_redesignation: bool,
150    /// True only when the reported track was seeded by an explicit operator
151    /// [`SingleObjectTracker::designate`](super::SingleObjectTracker::designate) call. A consumer publish path gates the
152    /// wire `lock_state` on this: an auto-seeded track (`false`) is tracked and
153    /// carries an id, but is never presented as locked, mirroring the offload
154    /// publish path. `false` on a frame with nothing to report.
155    pub operator_designated: bool,
156}