use std::collections::VecDeque;
use crate::core::Detections;
use crate::tracker::assignment::assign_by_cost;
use crate::tracker::kalman::KalmanBoxFilter;
struct Track {
id: usize,
filter: KalmanBoxFilter,
time_since_update: usize,
features: VecDeque<Vec<f32>>,
}
fn cosine_distance(a: &[f32], b: &[f32]) -> f32 {
let dot: f32 = a.iter().zip(b).map(|(x, y)| x * y).sum();
let norm_a = a.iter().map(|x| x * x).sum::<f32>().sqrt();
let norm_b = b.iter().map(|x| x * x).sum::<f32>().sqrt();
if norm_a <= 0.0 || norm_b <= 0.0 {
return 1.0;
}
(1.0 - dot / (norm_a * norm_b)).clamp(0.0, 2.0)
}
const HARD_GATE_COST: f32 = 1e6;
fn nearest_neighbor_distance(gallery: &VecDeque<Vec<f32>>, query: &[f32]) -> f32 {
gallery
.iter()
.map(|f| cosine_distance(f, query))
.fold(f32::INFINITY, f32::min)
}
pub struct DeepSortTracker {
tracks: Vec<Track>,
next_id: usize,
max_age: usize,
iou_gate: f32,
appearance_weight: f32,
max_cosine_distance: f32,
feature_gallery_size: usize,
}
impl DeepSortTracker {
pub fn new(max_age: usize, iou_gate: f32) -> Self {
Self {
tracks: Vec::new(),
next_id: 1,
max_age,
iou_gate,
appearance_weight: 0.5,
max_cosine_distance: 0.2,
feature_gallery_size: 30,
}
}
pub fn with_appearance_weight(mut self, weight: f32) -> Self {
self.appearance_weight = weight.clamp(0.0, 1.0);
self
}
pub fn with_max_cosine_distance(mut self, distance: f32) -> Self {
self.max_cosine_distance = distance;
self
}
pub fn with_feature_gallery_size(mut self, size: usize) -> Self {
self.feature_gallery_size = size.max(1);
self
}
pub fn active_tracks(&self) -> usize {
self.tracks.len()
}
pub fn update(&mut self, detections: &mut Detections, embeddings: &[Vec<f32>]) {
assert_eq!(
detections.len(),
embeddings.len(),
"embeddings must have exactly one feature vector per detection"
);
let predicted_boxes: Vec<[f32; 4]> =
self.tracks.iter_mut().map(|t| t.filter.predict()).collect();
let detection_boxes: Vec<[f32; 4]> = detections.detections.iter().map(|d| d.bbox).collect();
let no_match_cost = self.appearance_weight * self.max_cosine_distance
+ (1.0 - self.appearance_weight) * (1.0 - self.iou_gate);
let tracks = &self.tracks;
let iou_gate = self.iou_gate;
let appearance_weight = self.appearance_weight;
let matches = assign_by_cost(
predicted_boxes.len(),
detection_boxes.len(),
no_match_cost,
|i, j| {
let iou = crate::core::bbox_iou(predicted_boxes[i], detection_boxes[j]);
if iou < iou_gate {
return HARD_GATE_COST;
}
let appearance = nearest_neighbor_distance(&tracks[i].features, &embeddings[j]);
appearance_weight * appearance + (1.0 - appearance_weight) * (1.0 - iou)
},
);
let num_tracks = self.tracks.len();
let mut track_matched = vec![false; num_tracks];
for (detection_index, track_index) in matches.iter().enumerate() {
if let Some(track_index) = track_index {
let track = &mut self.tracks[*track_index];
track
.filter
.update(detections.detections[detection_index].bbox);
track.time_since_update = 0;
track
.features
.push_back(embeddings[detection_index].clone());
while track.features.len() > self.feature_gallery_size {
track.features.pop_front();
}
detections.detections[detection_index].tracker_id = Some(track.id);
track_matched[*track_index] = true;
}
}
for (track, matched) in self.tracks.iter_mut().zip(track_matched.iter()) {
if !matched {
track.time_since_update += 1;
}
}
for (detection_index, track_index) in matches.iter().enumerate() {
if track_index.is_none() {
let id = self.next_id;
self.next_id += 1;
let filter = KalmanBoxFilter::new(detections.detections[detection_index].bbox);
let mut features = VecDeque::with_capacity(self.feature_gallery_size);
features.push_back(embeddings[detection_index].clone());
self.tracks.push(Track {
id,
filter,
time_since_update: 0,
features,
});
detections.detections[detection_index].tracker_id = Some(id);
}
}
self.tracks.retain(|t| t.time_since_update <= self.max_age);
}
}
impl Default for DeepSortTracker {
fn default() -> Self {
Self::new(30, 0.3)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::core::Detection;
fn embedding(direction: &[f32]) -> Vec<f32> {
direction.to_vec()
}
#[test]
fn new_detection_gets_a_fresh_tracker_id() {
let mut tracker = DeepSortTracker::default();
let mut detections = Detections::new(vec![Detection::new([0.0, 0.0, 10.0, 10.0], 0.9, 0)]);
tracker.update(&mut detections, &[embedding(&[1.0, 0.0])]);
assert!(detections.detections[0].tracker_id.is_some());
assert_eq!(tracker.active_tracks(), 1);
}
#[test]
#[should_panic(expected = "one feature vector per detection")]
fn mismatched_embeddings_length_panics() {
let mut tracker = DeepSortTracker::default();
let mut detections = Detections::new(vec![Detection::new([0.0, 0.0, 10.0, 10.0], 0.9, 0)]);
tracker.update(&mut detections, &[]);
}
#[test]
fn matching_detection_with_similar_appearance_keeps_same_id() {
let mut tracker = DeepSortTracker::new(5, 0.3);
let mut first = Detections::new(vec![Detection::new([0.0, 0.0, 10.0, 10.0], 0.9, 0)]);
tracker.update(&mut first, &[embedding(&[1.0, 0.0, 0.0])]);
let id = first.detections[0].tracker_id.unwrap();
let mut second = Detections::new(vec![Detection::new([1.0, 1.0, 11.0, 11.0], 0.9, 0)]);
tracker.update(&mut second, &[embedding(&[0.9, 0.1, 0.0])]);
assert_eq!(second.detections[0].tracker_id, Some(id));
}
#[test]
fn appearance_disambiguates_two_tracks_with_identical_motion() {
let mut tracker = DeepSortTracker::new(5, 0.1).with_appearance_weight(0.9);
let mut frame1 = Detections::new(vec![
Detection::new([0.0, 0.0, 10.0, 10.0], 0.9, 0),
Detection::new([0.0, 0.0, 10.0, 10.0], 0.9, 0),
]);
tracker.update(
&mut frame1,
&[embedding(&[1.0, 0.0, 0.0]), embedding(&[0.0, 1.0, 0.0])],
);
let id_a = frame1.detections[0].tracker_id.unwrap();
let id_b = frame1.detections[1].tracker_id.unwrap();
assert_ne!(id_a, id_b);
let mut frame2 = Detections::new(vec![
Detection::new([0.0, 0.0, 10.0, 10.0], 0.9, 0),
Detection::new([0.0, 0.0, 10.0, 10.0], 0.9, 0),
]);
tracker.update(
&mut frame2,
&[embedding(&[0.0, 1.0, 0.0]), embedding(&[1.0, 0.0, 0.0])],
);
assert_eq!(frame2.detections[0].tracker_id, Some(id_b));
assert_eq!(frame2.detections[1].tracker_id, Some(id_a));
}
#[test]
fn feature_gallery_is_capped_at_the_configured_size() {
let mut tracker = DeepSortTracker::new(30, 0.3).with_feature_gallery_size(3);
let mut detections = Detections::new(vec![Detection::new([0.0, 0.0, 10.0, 10.0], 0.9, 0)]);
for _ in 0..10 {
tracker.update(&mut detections, &[embedding(&[1.0, 0.0])]);
}
assert_eq!(tracker.tracks[0].features.len(), 3);
}
#[test]
fn track_is_removed_after_max_age_missed_frames() {
let mut tracker = DeepSortTracker::new(2, 0.3);
let mut first = Detections::new(vec![Detection::new([0.0, 0.0, 10.0, 10.0], 0.9, 0)]);
tracker.update(&mut first, &[embedding(&[1.0, 0.0])]);
assert_eq!(tracker.active_tracks(), 1);
for _ in 0..3 {
let mut empty = Detections::empty();
tracker.update(&mut empty, &[]);
}
assert_eq!(tracker.active_tracks(), 0);
}
}