edgefirst_tracker/lib.rs
1// SPDX-FileCopyrightText: Copyright 2025 Au-Zone Technologies
2// SPDX-License-Identifier: Apache-2.0
3
4//! Multi-object tracking for EdgeFirst inference pipelines.
5//!
6//! This crate provides the [`Tracker`] trait and a concrete implementation,
7//! [`ByteTrack`], which associates per-frame detections
8//! into persistent tracks using two-pass IoU matching and Kalman-smoothed
9//! trajectory prediction.
10//!
11//! # Quick start
12//!
13//! ```rust
14//! use edgefirst_tracker::{bytetrack::ByteTrackBuilder, Tracker, MockDetection};
15//!
16//! let mut tracker = ByteTrackBuilder::new().build();
17//!
18//! // Timestamps are nanoseconds; here we use small integers for clarity.
19//! let frame1 = vec![MockDetection::new([0.1, 0.1, 0.5, 0.5], 0.9, 0)];
20//! let infos = tracker.update(&frame1, 1_000_000);
21//!
22//! // Each element corresponds to the detection at the same index; `Some` means
23//! // the detection was either matched to an existing track or started a new one.
24//! if let Some(info) = infos[0] {
25//! println!("track {} created at t={}", info.uuid, info.created);
26//! }
27//! ```
28//!
29//! # Terminology
30//!
31//! - **Tracklet** — an internal state machine (Kalman filter + metadata) that
32//! follows one object across frames.
33//! - **Detection** — a single-frame bounding box with confidence score and
34//! class label, produced by a model decoder.
35//! - **Track** — the public view of a tracklet: a [`TrackInfo`] value returned
36//! by [`Tracker::update`] or [`Tracker::get_active_tracks`].
37//!
38//! # Architecture
39//!
40//! See [`crates/tracker/ARCHITECTURE.md`](https://github.com/EdgeFirstAI/hal/blob/main/crates/tracker/ARCHITECTURE.md)
41//! for the full design description, Mermaid class diagram, tracking flow, and
42//! Kalman filter state documentation.
43
44use std::fmt::Debug;
45
46pub use uuid::Uuid;
47pub mod bytetrack;
48pub use bytetrack::{ByteTrack, ByteTrackBuilder};
49pub mod kalman;
50
51/// A bounding box produced by a detection model, suitable for use with a [`Tracker`].
52///
53/// Implement this trait on your model's output type to feed it directly into
54/// [`ByteTrack`] without copying. The tracker crate has
55/// no dependency on `edgefirst-decoder`; any type that can report an XYXY box,
56/// a confidence score, and a class label satisfies this contract.
57///
58/// # Coordinates
59///
60/// `bbox()` must return `[xmin, ymin, xmax, ymax]` in normalised `[0, 1]`
61/// coordinates or in pixel coordinates — the tracker does not prescribe units,
62/// but all detections passed to a single [`Tracker::update`] call must use the
63/// same coordinate space.
64///
65/// # Example
66///
67/// ```rust
68/// use edgefirst_tracker::DetectionBox;
69///
70/// #[derive(Debug, Clone)]
71/// struct MyDetection { bbox: [f32; 4], score: f32, label: usize }
72///
73/// impl DetectionBox for MyDetection {
74/// fn bbox(&self) -> [f32; 4] { self.bbox }
75/// fn score(&self) -> f32 { self.score }
76/// fn label(&self) -> usize { self.label }
77/// }
78/// ```
79pub trait DetectionBox: Debug + Clone {
80 /// Bounding box in `[xmin, ymin, xmax, ymax]` (XYXY) format.
81 fn bbox(&self) -> [f32; 4];
82
83 /// Confidence score in `[0, 1]`. Used by ByteTrack to separate
84 /// high-confidence detections (first-pass matching) from low-confidence
85 /// ones (second-pass matching).
86 fn score(&self) -> f32;
87
88 /// Class index (0-based). Stored in the tracklet for callers that need
89 /// per-class track metadata; the tracker itself does not gate matching
90 /// on label — a tracklet may receive detections from different classes.
91 fn label(&self) -> usize;
92}
93
94/// A minimal detection box for unit tests and doc examples.
95///
96/// This type is **not** intended for production use. It is `#[doc(hidden)]` in
97/// the public docs but available as `edgefirst_tracker::MockDetection` so test
98/// code in downstream crates does not need to define its own dummy type.
99#[doc(hidden)]
100#[derive(Debug, Clone, Copy)]
101pub struct MockDetection {
102 bbox: [f32; 4],
103 score: f32,
104 label: usize,
105}
106
107impl MockDetection {
108 /// Creates a new mock detection with the given bounding box, score, and label.
109 pub fn new(bbox: [f32; 4], score: f32, label: usize) -> Self {
110 Self { bbox, score, label }
111 }
112}
113
114impl DetectionBox for MockDetection {
115 fn bbox(&self) -> [f32; 4] {
116 self.bbox
117 }
118
119 fn score(&self) -> f32 {
120 self.score
121 }
122
123 fn label(&self) -> usize {
124 self.label
125 }
126}
127
128/// Per-track metadata returned by [`Tracker::update`] and [`Tracker::get_active_tracks`].
129///
130/// Every active track carries a stable [`Uuid`] assigned at track creation and
131/// preserved until the track is deleted. Callers can use the UUID to correlate
132/// tracks across frames or to drive per-track rendering (e.g. per-UUID colour
133/// assignment in `ColorMode::Track`).
134///
135/// # Timestamps
136///
137/// `created` and `last_updated` store the raw `timestamp` argument passed to
138/// [`Tracker::update`] at the corresponding event. The tracker does not
139/// interpret these values — they are caller-supplied nanoseconds in typical
140/// usage, but any monotonically increasing unit works.
141#[derive(Debug, Clone, Copy, PartialEq)]
142pub struct TrackInfo {
143 /// Globally unique identifier for this track, assigned when the tracklet is
144 /// first created and never reused.
145 pub uuid: Uuid,
146
147 /// Kalman-smoothed predicted location of the object in XYXY format
148 /// `[xmin, ymin, xmax, ymax]`. This is the **predicted** position for the
149 /// current frame, not the raw detection box — it may differ from the
150 /// detection that triggered the update. For the raw detection see
151 /// [`ActiveTrackInfo::last_box`].
152 pub tracked_location: [f32; 4],
153
154 /// Number of times this track has been matched to a detection. Starts at 1
155 /// on the frame the track is created. Useful as a confidence signal —
156 /// tracks with a low count are newly created and may be false positives.
157 pub count: i32,
158
159 /// Timestamp (as supplied to [`Tracker::update`]) of the frame on which
160 /// this track was first created.
161 pub created: u64,
162
163 /// Timestamp of the most recent [`Tracker::update`] call that matched a
164 /// detection to this track. Tracklets whose `last_updated` is older than
165 /// `now - extra_lifespan` are deleted on the next update.
166 pub last_updated: u64,
167}
168
169/// A [`TrackInfo`] paired with the most recent raw detection box for that track.
170///
171/// Returned by [`Tracker::get_active_tracks`] for callers that need both the
172/// Kalman-smoothed position (in `info.tracked_location`) and the unsmoothed,
173/// model-output bounding box (`last_box`) — for example, to draw the exact
174/// predicted anchor alongside the smoothed track centre.
175#[derive(Debug, Clone, Copy, PartialEq)]
176pub struct ActiveTrackInfo<T: DetectionBox> {
177 /// Smoothed track metadata.
178 pub info: TrackInfo,
179
180 /// The last raw detection box associated with this track, as returned by
181 /// [`DetectionBox::bbox`] — before any Kalman smoothing. This reflects the
182 /// model's output on the most recent frame the track was matched, not the
183 /// current Kalman prediction.
184 pub last_box: T,
185}
186
187/// Multi-object tracker interface.
188///
189/// Implementations associate a stream of per-frame detection boxes into
190/// persistent tracks. The tracker owns all internal state (tracklet filters,
191/// timestamps, IDs) and is mutable — `update` takes `&mut self`.
192///
193/// # Contract
194///
195/// - The returned `Vec` from `update` is parallel to the input `boxes` slice:
196/// `result[i]` is `Some(info)` when detection `i` was matched to a track or
197/// spawned a new one, and `None` when it was too low-confidence to initiate or
198/// continue a track.
199/// - `get_active_tracks` returns every tracklet that has not yet expired,
200/// including those not matched in the most recent frame (they remain alive
201/// until `extra_lifespan` elapses without a match).
202/// - Callers must not share a single `Tracker` across concurrent threads without
203/// external synchronization — `update` takes `&mut self`.
204pub trait Tracker<T: DetectionBox> {
205 /// Process one frame of detections and advance all internal tracklet states.
206 ///
207 /// `boxes` is the set of detections for this frame. `timestamp` is a
208 /// monotonically increasing value (typically nanoseconds from a wall clock
209 /// or pipeline counter) used for tracklet expiry.
210 ///
211 /// Returns a `Vec<Option<TrackInfo>>` of the same length as `boxes`.
212 fn update(&mut self, boxes: &[T], timestamp: u64) -> Vec<Option<TrackInfo>>;
213
214 /// Return all currently active tracklets, including those not matched in
215 /// the most recent frame but not yet expired.
216 fn get_active_tracks(&self) -> Vec<ActiveTrackInfo<T>>;
217}