Skip to main content

scirs2_vision/tracking/
types.rs

1//! Core types for multi-object tracking (SORT / ByteTrack).
2
3/// An axis-aligned bounding box with detection score and optional class ID.
4#[non_exhaustive]
5#[derive(Debug, Clone)]
6pub struct BoundingBox {
7    /// Left edge (x-coordinate of top-left corner).
8    pub x1: f32,
9    /// Top edge (y-coordinate of top-left corner).
10    pub y1: f32,
11    /// Right edge (x-coordinate of bottom-right corner).
12    pub x2: f32,
13    /// Bottom edge (y-coordinate of bottom-right corner).
14    pub y2: f32,
15    /// Detection confidence score in `[0, 1]`.
16    pub score: f32,
17    /// Optional class identifier.
18    pub class_id: Option<usize>,
19}
20
21impl BoundingBox {
22    /// Create a new bounding box.
23    pub fn new(x1: f32, y1: f32, x2: f32, y2: f32, score: f32, class_id: Option<usize>) -> Self {
24        Self {
25            x1,
26            y1,
27            x2,
28            y2,
29            score,
30            class_id,
31        }
32    }
33
34    /// Area of the bounding box in pixels².
35    pub fn area(&self) -> f32 {
36        let w = (self.x2 - self.x1).max(0.0);
37        let h = (self.y2 - self.y1).max(0.0);
38        w * h
39    }
40
41    /// Intersection-over-Union with another bounding box.
42    ///
43    /// Returns 0 for non-overlapping boxes and 1 for identical boxes.
44    pub fn iou(&self, other: &BoundingBox) -> f32 {
45        let inter_x1 = self.x1.max(other.x1);
46        let inter_y1 = self.y1.max(other.y1);
47        let inter_x2 = self.x2.min(other.x2);
48        let inter_y2 = self.y2.min(other.y2);
49
50        if inter_x2 <= inter_x1 || inter_y2 <= inter_y1 {
51            return 0.0;
52        }
53
54        let inter_area = (inter_x2 - inter_x1) * (inter_y2 - inter_y1);
55        let union_area = self.area() + other.area() - inter_area;
56
57        if union_area <= 0.0 {
58            0.0
59        } else {
60            inter_area / union_area
61        }
62    }
63
64    /// Convert to centre-x, centre-y, width, height representation.
65    pub fn to_xywh(&self) -> (f32, f32, f32, f32) {
66        let cx = (self.x1 + self.x2) * 0.5;
67        let cy = (self.y1 + self.y2) * 0.5;
68        let w = self.x2 - self.x1;
69        let h = self.y2 - self.y1;
70        (cx, cy, w, h)
71    }
72}
73
74// ---------------------------------------------------------------------------
75// TrackState
76// ---------------------------------------------------------------------------
77
78/// Life-cycle state of a tracked object.
79#[non_exhaustive]
80#[derive(Debug, Clone, PartialEq, Eq)]
81pub enum TrackState {
82    /// Newly created track; not yet confirmed by enough consecutive matches.
83    Tentative,
84    /// Track has received enough consecutive matches to be considered reliable.
85    Confirmed,
86    /// Track was not matched in the most recent frame but has not yet aged out.
87    Lost,
88    /// Track has exceeded `max_age` without a match and will be removed.
89    Deleted,
90}
91
92// ---------------------------------------------------------------------------
93// Track
94// ---------------------------------------------------------------------------
95
96/// A single tracked object with its current state and bounding box.
97#[non_exhaustive]
98#[derive(Debug, Clone)]
99pub struct Track {
100    /// Globally unique identifier assigned at creation.
101    pub track_id: u64,
102    /// Current life-cycle state.
103    pub state: TrackState,
104    /// Most recent (predicted or updated) bounding box.
105    pub bbox: BoundingBox,
106    /// Total number of frames since this track was created.
107    pub age: usize,
108    /// Number of consecutive frames in which this track was matched.
109    pub hits: usize,
110    /// Number of frames since the last successful match.
111    pub time_since_update: usize,
112}
113
114// ---------------------------------------------------------------------------
115// SortConfig
116// ---------------------------------------------------------------------------
117
118/// Configuration for the SORT tracker.
119#[non_exhaustive]
120#[derive(Debug, Clone)]
121pub struct SortConfig {
122    /// Maximum number of frames a track can go un-matched before deletion.
123    pub max_age: usize,
124    /// Minimum number of consecutive matches required to confirm a track.
125    pub min_hits: usize,
126    /// IoU threshold used during Hungarian assignment (lower = more permissive).
127    pub iou_threshold: f32,
128}
129
130impl Default for SortConfig {
131    fn default() -> Self {
132        Self {
133            max_age: 3,
134            min_hits: 3,
135            iou_threshold: 0.3,
136        }
137    }
138}
139
140impl SortConfig {
141    /// Create a fully-specified `SortConfig`.
142    pub fn new(max_age: usize, min_hits: usize, iou_threshold: f32) -> Self {
143        Self {
144            max_age,
145            min_hits,
146            iou_threshold,
147        }
148    }
149}
150
151// ---------------------------------------------------------------------------
152// ByteTrackConfig
153// ---------------------------------------------------------------------------
154
155/// Configuration for the ByteTrack tracker (Zhang et al. 2022).
156#[non_exhaustive]
157#[derive(Debug, Clone)]
158pub struct ByteTrackConfig {
159    /// Score threshold separating *high-confidence* from *low-confidence* detections.
160    pub high_thresh: f32,
161    /// Minimum score; detections below this are discarded entirely.
162    pub low_thresh: f32,
163    /// IoU threshold for both association stages.
164    pub match_thresh: f32,
165    /// Maximum frames a lost track survives without a match.
166    pub max_age: usize,
167    /// Minimum consecutive matches required to confirm a new track.
168    pub min_hits: usize,
169}
170
171impl Default for ByteTrackConfig {
172    fn default() -> Self {
173        Self {
174            high_thresh: 0.6,
175            low_thresh: 0.1,
176            match_thresh: 0.8,
177            max_age: 30,
178            min_hits: 3,
179        }
180    }
181}
182
183impl ByteTrackConfig {
184    /// Create a fully-specified `ByteTrackConfig`.
185    pub fn new(
186        high_thresh: f32,
187        low_thresh: f32,
188        match_thresh: f32,
189        max_age: usize,
190        min_hits: usize,
191    ) -> Self {
192        Self {
193            high_thresh,
194            low_thresh,
195            match_thresh,
196            max_age,
197            min_hits,
198        }
199    }
200}
201
202// ---------------------------------------------------------------------------
203// TrackerResult
204// ---------------------------------------------------------------------------
205
206/// Output produced by a tracker after processing a single frame.
207pub struct TrackerResult {
208    /// All currently active (non-deleted) tracks, filtered by confirmation policy.
209    pub tracks: Vec<Track>,
210    /// Index of the frame that produced this result.
211    pub frame_id: usize,
212}