use crate::core::{Detection, Detections};
use crate::tracker::assignment::assign_by_iou;
use crate::tracker::kalman::KalmanBoxFilter;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum TrackState {
Tracked,
Lost,
}
struct Track {
id: usize,
filter: KalmanBoxFilter,
state: TrackState,
time_since_update: usize,
}
pub struct ByteTracker {
tracks: Vec<Track>,
next_id: usize,
track_activation_threshold: f32,
high_confidence_matching_threshold: f32,
low_confidence_matching_threshold: f32,
lost_track_buffer: usize,
}
impl ByteTracker {
pub fn new(
track_activation_threshold: f32,
lost_track_buffer: usize,
high_confidence_matching_threshold: f32,
low_confidence_matching_threshold: f32,
) -> Self {
Self {
tracks: Vec::new(),
next_id: 1,
track_activation_threshold,
high_confidence_matching_threshold,
low_confidence_matching_threshold,
lost_track_buffer,
}
}
pub fn active_tracks(&self) -> usize {
self.tracks.len()
}
pub fn update(&mut self, detections: &Detections) -> Detections {
let predicted_boxes: Vec<[f32; 4]> =
self.tracks.iter_mut().map(|t| t.filter.predict()).collect();
let (high_idx, low_idx): (Vec<usize>, Vec<usize>) = (0..detections.len())
.partition(|&i| detections.detections[i].confidence >= self.track_activation_threshold);
let high_boxes: Vec<[f32; 4]> = high_idx
.iter()
.map(|&i| detections.detections[i].bbox)
.collect();
let low_boxes: Vec<[f32; 4]> = low_idx
.iter()
.map(|&i| detections.detections[i].bbox)
.collect();
let mut track_matched = vec![false; self.tracks.len()];
let mut assigned_track_id: Vec<Option<usize>> = vec![None; detections.len()];
let stage1 = assign_by_iou(
&predicted_boxes,
&high_boxes,
self.high_confidence_matching_threshold,
);
for (k, matched_track) in stage1.iter().enumerate() {
if let Some(track_index) = matched_track {
let detection_index = high_idx[k];
let track = &mut self.tracks[*track_index];
track
.filter
.update(detections.detections[detection_index].bbox);
track.time_since_update = 0;
track.state = TrackState::Tracked;
assigned_track_id[detection_index] = Some(track.id);
track_matched[*track_index] = true;
}
}
let stage2_track_indices: Vec<usize> = (0..self.tracks.len())
.filter(|&i| !track_matched[i] && self.tracks[i].state == TrackState::Tracked)
.collect();
let stage2_track_boxes: Vec<[f32; 4]> = stage2_track_indices
.iter()
.map(|&i| predicted_boxes[i])
.collect();
let stage2 = assign_by_iou(
&stage2_track_boxes,
&low_boxes,
self.low_confidence_matching_threshold,
);
for (k, matched_track) in stage2.iter().enumerate() {
if let Some(local_index) = matched_track {
let track_index = stage2_track_indices[*local_index];
let detection_index = low_idx[k];
let track = &mut self.tracks[track_index];
track
.filter
.update(detections.detections[detection_index].bbox);
track.time_since_update = 0;
track.state = TrackState::Tracked;
assigned_track_id[detection_index] = Some(track.id);
track_matched[track_index] = true;
}
}
for (i, track) in self.tracks.iter_mut().enumerate() {
if !track_matched[i] {
track.time_since_update += 1;
track.state = TrackState::Lost;
}
}
self.tracks
.retain(|t| t.time_since_update <= self.lost_track_buffer);
for (k, matched_track) in stage1.iter().enumerate() {
if matched_track.is_none() {
let detection_index = high_idx[k];
let id = self.next_id;
self.next_id += 1;
let filter = KalmanBoxFilter::new(detections.detections[detection_index].bbox);
self.tracks.push(Track {
id,
filter,
state: TrackState::Tracked,
time_since_update: 0,
});
assigned_track_id[detection_index] = Some(id);
}
}
let output: Vec<Detection> = detections
.iter()
.enumerate()
.filter_map(|(i, d)| {
assigned_track_id[i].map(|id| {
let mut d = d.clone();
d.tracker_id = Some(id);
d
})
})
.collect();
Detections::new(output)
}
}
impl Default for ByteTracker {
fn default() -> Self {
Self::new(0.25, 30, 0.3, 0.2)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::core::Detection;
fn det(bbox: [f32; 4], confidence: f32) -> Detection {
Detection::new(bbox, confidence, 0)
}
#[test]
fn high_confidence_detection_spawns_a_track() {
let mut tracker = ByteTracker::default();
let detections = Detections::new(vec![det([0.0, 0.0, 10.0, 10.0], 0.9)]);
let out = tracker.update(&detections);
assert_eq!(out.len(), 1);
assert!(out.detections[0].tracker_id.is_some());
assert_eq!(tracker.active_tracks(), 1);
}
#[test]
fn low_confidence_unmatched_detection_is_dropped_and_spawns_nothing() {
let mut tracker = ByteTracker::default();
let detections = Detections::new(vec![det([0.0, 0.0, 10.0, 10.0], 0.1)]);
let out = tracker.update(&detections);
assert_eq!(out.len(), 0);
assert_eq!(tracker.active_tracks(), 0);
}
#[test]
fn matching_high_confidence_detection_keeps_same_id() {
let mut tracker = ByteTracker::default();
let first = Detections::new(vec![det([0.0, 0.0, 10.0, 10.0], 0.9)]);
let out1 = tracker.update(&first);
let id = out1.detections[0].tracker_id.unwrap();
let second = Detections::new(vec![det([1.0, 1.0, 11.0, 11.0], 0.9)]);
let out2 = tracker.update(&second);
assert_eq!(out2.detections[0].tracker_id, Some(id));
}
#[test]
fn low_confidence_detection_recovers_an_existing_track() {
let mut tracker = ByteTracker::default();
let first = Detections::new(vec![det([0.0, 0.0, 10.0, 10.0], 0.9)]);
let out1 = tracker.update(&first);
let id = out1.detections[0].tracker_id.unwrap();
let second = Detections::new(vec![det([1.0, 1.0, 11.0, 11.0], 0.15)]);
let out2 = tracker.update(&second);
assert_eq!(out2.len(), 1);
assert_eq!(out2.detections[0].tracker_id, Some(id));
}
#[test]
fn track_survives_briefly_missing_and_is_dropped_after_lost_track_buffer() {
let mut tracker = ByteTracker::new(0.25, 2, 0.3, 0.2);
let first = Detections::new(vec![det([0.0, 0.0, 10.0, 10.0], 0.9)]);
tracker.update(&first);
assert_eq!(tracker.active_tracks(), 1);
for _ in 0..2 {
tracker.update(&Detections::empty());
}
assert_eq!(
tracker.active_tracks(),
1,
"should survive up to lost_track_buffer misses"
);
tracker.update(&Detections::empty());
assert_eq!(
tracker.active_tracks(),
0,
"should be dropped once lost_track_buffer is exceeded"
);
}
#[test]
fn two_independent_objects_maintain_separate_identities() {
let mut tracker = ByteTracker::default();
let first = Detections::new(vec![
det([0.0, 0.0, 10.0, 10.0], 0.9),
det([100.0, 0.0, 110.0, 10.0], 0.9),
]);
let out1 = tracker.update(&first);
let id_left = out1.detections[0].tracker_id.unwrap();
let id_right = out1.detections[1].tracker_id.unwrap();
assert_ne!(id_left, id_right);
let second = Detections::new(vec![
det([2.0, 0.0, 12.0, 10.0], 0.9),
det([102.0, 0.0, 112.0, 10.0], 0.9),
]);
let out2 = tracker.update(&second);
assert_eq!(out2.detections[0].tracker_id, Some(id_left));
assert_eq!(out2.detections[1].tracker_id, Some(id_right));
}
}