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}