use std::collections::{HashMap, HashSet, VecDeque};
use crate::core::Detections;
pub struct DetectionsSmoother {
length: usize,
history: HashMap<usize, VecDeque<[f32; 4]>>,
}
impl DetectionsSmoother {
pub fn new(length: usize) -> Self {
assert!(length > 0, "smoothing window length must be at least 1");
Self {
length,
history: HashMap::new(),
}
}
pub fn update(&mut self, detections: &Detections) -> Detections {
let mut seen = HashSet::new();
let mut output = Vec::with_capacity(detections.len());
for detection in detections.iter() {
let Some(tracker_id) = detection.tracker_id else {
output.push(detection.clone());
continue;
};
seen.insert(tracker_id);
let track = self.history.entry(tracker_id).or_default();
track.push_back(detection.bbox);
while track.len() > self.length {
track.pop_front();
}
let mut smoothed = detection.clone();
smoothed.bbox = average_bbox(track);
output.push(smoothed);
}
self.history
.retain(|tracker_id, _| seen.contains(tracker_id));
Detections::new(output)
}
}
impl Default for DetectionsSmoother {
fn default() -> Self {
Self::new(5)
}
}
fn average_bbox(boxes: &VecDeque<[f32; 4]>) -> [f32; 4] {
let n = boxes.len() as f32;
let mut sum = [0.0f32; 4];
for bbox in boxes {
for i in 0..4 {
sum[i] += bbox[i];
}
}
[sum[0] / n, sum[1] / n, sum[2] / n, sum[3] / n]
}
#[cfg(test)]
mod tests {
use super::*;
use crate::core::Detection;
fn tracked(bbox: [f32; 4], tracker_id: usize) -> Detection {
let mut d = Detection::new(bbox, 0.9, 0);
d.tracker_id = Some(tracker_id);
d
}
#[test]
fn averages_a_single_tracks_box_over_the_window() {
let mut smoother = DetectionsSmoother::new(3);
smoother.update(&Detections::new(vec![tracked([0.0, 0.0, 10.0, 10.0], 1)]));
smoother.update(&Detections::new(vec![tracked([10.0, 0.0, 20.0, 10.0], 1)]));
let out = smoother.update(&Detections::new(vec![tracked([20.0, 0.0, 30.0, 10.0], 1)]));
assert_eq!(out.detections[0].bbox, [10.0, 0.0, 20.0, 10.0]);
}
#[test]
fn window_caps_at_configured_length() {
let mut smoother = DetectionsSmoother::new(2);
smoother.update(&Detections::new(vec![tracked([0.0, 0.0, 10.0, 10.0], 1)]));
smoother.update(&Detections::new(vec![tracked(
[100.0, 0.0, 110.0, 10.0],
1,
)]));
let out = smoother.update(&Detections::new(vec![tracked(
[200.0, 0.0, 210.0, 10.0],
1,
)]));
assert_eq!(out.detections[0].bbox[0], 150.0);
}
#[test]
fn detections_without_a_tracker_id_pass_through_unsmoothed() {
let mut smoother = DetectionsSmoother::new(3);
let out = smoother.update(&Detections::new(vec![Detection::new(
[5.0, 5.0, 15.0, 15.0],
0.9,
0,
)]));
assert_eq!(out.detections[0].bbox, [5.0, 5.0, 15.0, 15.0]);
}
#[test]
fn missing_a_frame_resets_a_tracks_history() {
let mut smoother = DetectionsSmoother::new(5);
smoother.update(&Detections::new(vec![tracked([0.0, 0.0, 10.0, 10.0], 1)]));
smoother.update(&Detections::empty());
let out = smoother.update(&Detections::new(vec![tracked(
[100.0, 0.0, 110.0, 10.0],
1,
)]));
assert_eq!(out.detections[0].bbox, [100.0, 0.0, 110.0, 10.0]);
}
#[test]
fn two_tracks_are_smoothed_independently() {
let mut smoother = DetectionsSmoother::new(2);
smoother.update(&Detections::new(vec![
tracked([0.0, 0.0, 10.0, 10.0], 1),
tracked([0.0, 100.0, 10.0, 110.0], 2),
]));
let out = smoother.update(&Detections::new(vec![
tracked([20.0, 0.0, 30.0, 10.0], 1),
tracked([20.0, 100.0, 30.0, 110.0], 2),
]));
assert_eq!(out.detections[0].bbox[0], 10.0);
assert_eq!(out.detections[1].bbox[0], 10.0);
}
#[test]
#[should_panic]
fn zero_length_panics() {
DetectionsSmoother::new(0);
}
}