mod advance;
mod appearance;
mod config;
mod kalman;
mod scoring;
pub use appearance::{Appearance, AppearanceModel, Candidate, HistogramAppearance, APPEARANCE_DIM};
pub use config::{ScoreWeights, TrackState, TrackUpdate, TrackerConfig};
use ados_protocol::framebus::{Detection, LockState};
use kalman::BoxKalman;
use scoring::{reported, total_cmp};
pub struct SingleObjectTracker {
cfg: TrackerConfig,
track: Option<Track>,
next_id: u64,
appearance: Box<dyn AppearanceModel>,
needs_redesignation: bool,
}
impl std::fmt::Debug for SingleObjectTracker {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("SingleObjectTracker")
.field("cfg", &self.cfg)
.field("track", &self.track)
.field("next_id", &self.next_id)
.field("needs_redesignation", &self.needs_redesignation)
.finish_non_exhaustive()
}
}
#[derive(Debug)]
struct Track {
id: u64,
kf: BoxKalman,
state: TrackState,
hits: u32,
misses: u32,
class_label: String,
confidence: f32,
assoc_confidence: f32,
template: Option<Appearance>,
reconfirming: bool,
coast_ambiguous: bool,
operator_designated: bool,
}
impl SingleObjectTracker {
pub fn new(cfg: TrackerConfig) -> Self {
Self::with_appearance_model(cfg, Box::new(HistogramAppearance))
}
pub fn with_appearance_model(cfg: TrackerConfig, appearance: Box<dyn AppearanceModel>) -> Self {
Self {
cfg,
track: None,
next_id: 1,
appearance,
needs_redesignation: false,
}
}
pub fn current_id(&self) -> Option<u64> {
self.track.as_ref().and_then(|t| match t.state {
TrackState::Confirmed | TrackState::Coasting => Some(t.id),
_ => None,
})
}
pub fn state(&self) -> TrackState {
self.track.as_ref().map_or(TrackState::Idle, |t| t.state)
}
pub fn needs_redesignation(&self) -> bool {
self.needs_redesignation
}
pub fn redesignate(&mut self) {
self.needs_redesignation = false;
}
pub fn designate(&mut self, detection: &Detection) -> Option<u64> {
self.track = None;
let cand = Candidate::motion_only(detection.clone());
let _ = self.seed(&[&cand], true);
self.track.as_ref().map(|t| t.id)
}
pub fn update(&mut self, detections: &[Detection]) -> TrackUpdate {
let candidates: Vec<Candidate> = detections
.iter()
.cloned()
.map(Candidate::motion_only)
.collect();
self.update_with_appearance(&candidates)
}
pub fn update_with_appearance(&mut self, candidates: &[Candidate]) -> TrackUpdate {
let eligible: Vec<&Candidate> = candidates
.iter()
.filter(|c| {
c.detection.confidence >= self.cfg.min_confidence && c.detection.bbox.is_some()
})
.collect();
match self.track.take() {
None => self.seed(&eligible, false),
Some(track) => self.advance(track, &eligible),
}
}
fn seed(&mut self, candidates: &[&Candidate], operator_designated: bool) -> TrackUpdate {
self.needs_redesignation = false;
let best = candidates
.iter()
.copied()
.max_by(|a, b| total_cmp(a.detection.confidence, b.detection.confidence));
match best {
None => TrackUpdate {
state: TrackState::Idle,
detection: None,
track_id: None,
measured: false,
needs_redesignation: false,
operator_designated: false,
},
Some(cand) => {
let det = &cand.detection;
let id = self.next_id;
self.next_id += 1;
let template = self.template_for(cand);
let track = Track {
id,
kf: BoxKalman::new(
det.bbox
.as_ref()
.expect("eligible candidate carries a bbox"),
&self.cfg,
),
state: if self.cfg.confirm_hits <= 1 {
TrackState::Confirmed
} else {
TrackState::Tentative
},
hits: 1,
misses: 0,
class_label: det.class_label.clone(),
confidence: det.confidence,
assoc_confidence: det.confidence,
template,
reconfirming: false,
coast_ambiguous: false,
operator_designated,
};
let state = track.state;
let report = if state == TrackState::Confirmed {
Some(reported(
&track,
track.kf.bbox(),
LockState::Locked,
det.confidence,
))
} else {
None
};
let id_out = self.current_id_for(&track);
self.track = Some(track);
TrackUpdate {
state,
detection: report,
track_id: id_out,
measured: true,
needs_redesignation: false,
operator_designated,
}
}
}
}
}
#[cfg(test)]
mod tests;