use ados_protocol::framebus::{BoundingBox, Detection, LockState};
use super::{Appearance, Candidate, SingleObjectTracker, Track, TrackState};
impl SingleObjectTracker {
pub(super) fn max_gate_dist(&self, track: &Track, predicted: &BoundingBox) -> f32 {
let diag = (predicted.width * predicted.width + predicted.height * predicted.height).sqrt();
let base = diag * self.cfg.max_center_dist_diag;
let sigma = self.cfg.gate_sigma * track.kf.center_pos_std();
base + sigma
}
pub(super) fn score_in_gate<'a>(
&self,
track: &Track,
predicted: &BoundingBox,
gate: f32,
candidates: &[&'a Candidate],
) -> Vec<Scored<'a>> {
let (pcx, pcy) = center(predicted);
let mut out: Vec<Scored<'a>> = Vec::new();
for cand in candidates {
let det = &cand.detection;
let Some(det_bbox) = det.bbox.as_ref() else {
continue;
};
let geo_iou = iou(predicted, det_bbox);
let (dcx, dcy) = center(det_bbox);
let dist = ((dcx - pcx).powi(2) + (dcy - pcy).powi(2)).sqrt();
let in_gate = geo_iou >= self.cfg.min_iou || dist <= gate;
if !in_gate {
continue;
}
let motion = motion_fit(geo_iou, dist, gate);
let (appearance, has_app) = match (&track.template, &cand.appearance) {
(Some(t), Some(a)) => (self.appearance.similarity(t, a), true),
_ => (1.0, false),
};
let w = self.cfg.score_weights;
let wsum = (w.appearance + w.motion + w.confidence).max(f32::EPSILON);
let score =
(w.appearance * appearance + w.motion * motion + w.confidence * det.confidence)
/ wsum;
let assoc_confidence = if has_app {
(appearance * 0.5 + motion * 0.3 + det.confidence * 0.2).clamp(0.0, 1.0)
} else {
det.confidence
};
out.push(Scored {
cand,
score,
appearance,
has_app,
assoc_confidence,
});
}
out.sort_by(|a, b| total_cmp(b.score, a.score));
out
}
pub(super) fn decide(&self, track: &Track, scored: &[Scored<'_>]) -> Decision {
if scored.is_empty() {
return Decision::None;
}
let appearance_live = track.template.is_some() && scored.iter().any(|s| s.has_app);
if !appearance_live {
let best = scored
.iter()
.enumerate()
.max_by(|(_, a), (_, b)| total_cmp(a.score, b.score))
.map(|(i, _)| i)
.unwrap_or(0);
return Decision::Associate {
idx: best,
blend: true,
};
}
let admissible: Vec<usize> = scored
.iter()
.enumerate()
.filter(|(_, s)| s.has_app && s.appearance >= self.cfg.min_appearance)
.map(|(i, _)| i)
.collect();
let Some(&best_idx) = admissible
.iter()
.max_by(|&&a, &&b| total_cmp(scored[a].appearance, scored[b].appearance))
else {
return Decision::None;
};
let best_app = scored[best_idx].appearance;
let tie_group: Vec<usize> = admissible
.iter()
.copied()
.filter(|&i| (best_app - scored[i].appearance) <= self.cfg.ambiguous_margin)
.collect();
if tie_group.len() > 1 {
return Decision::Ambiguous((scored[best_idx].assoc_confidence * 0.5).clamp(0.0, 1.0));
}
Decision::Associate {
idx: best_idx,
blend: true,
}
}
pub(super) fn template_for(&self, cand: &Candidate) -> Option<Appearance> {
cand.appearance.clone()
}
pub(super) fn blend_template(&self, track: &mut Track, cand: &Candidate) {
let Some(new_app) = &cand.appearance else {
return;
};
match &mut track.template {
None => track.template = Some(new_app.clone()),
Some(t) if t.features.len() == new_app.features.len() => {
let lr = self.cfg.template_lr.clamp(0.0, 1.0);
for (acc, &x) in t.features.iter_mut().zip(new_app.features.iter()) {
*acc = *acc * (1.0 - lr) + x * lr;
}
}
Some(_) => track.template = Some(new_app.clone()),
}
}
pub(super) fn current_id_for(&self, track: &Track) -> Option<u64> {
match track.state {
TrackState::Confirmed | TrackState::Coasting => Some(track.id),
_ => None,
}
}
}
pub(super) struct Scored<'a> {
pub(super) cand: &'a Candidate,
pub(super) score: f32,
pub(super) appearance: f32,
pub(super) has_app: bool,
pub(super) assoc_confidence: f32,
}
pub(super) enum Decision {
Associate { idx: usize, blend: bool },
Ambiguous(f32),
None,
}
pub(super) fn motion_fit(geo_iou: f32, dist: f32, gate: f32) -> f32 {
if geo_iou > 0.0 {
geo_iou.clamp(0.0, 1.0)
} else if gate > 0.0 {
(1.0 - (dist / gate)).clamp(0.0, 1.0)
} else {
0.0
}
}
pub(super) fn reported(track: &Track, bbox: BoundingBox, lock: LockState, assoc: f32) -> Detection {
Detection {
bbox: Some(bbox),
class_label: track.class_label.clone(),
confidence: track.confidence,
track_id: Some(track.id),
assoc_confidence: Some(assoc.clamp(0.0, 1.0)),
lock_state: Some(lock),
attributes: None,
mask: None,
keypoints: None,
depth: None,
world_pos: None,
}
}
pub(super) fn center(b: &BoundingBox) -> (f32, f32) {
(b.x + b.width / 2.0, b.y + b.height / 2.0)
}
pub(super) fn iou(a: &BoundingBox, b: &BoundingBox) -> f32 {
let ax2 = a.x + a.width;
let ay2 = a.y + a.height;
let bx2 = b.x + b.width;
let by2 = b.y + b.height;
let ix = (ax2.min(bx2) - a.x.max(b.x)).max(0.0);
let iy = (ay2.min(by2) - a.y.max(b.y)).max(0.0);
let inter = ix * iy;
let union = a.width * a.height + b.width * b.height - inter;
if union <= 0.0 {
0.0
} else {
inter / union
}
}
pub(super) fn total_cmp(a: f32, b: f32) -> std::cmp::Ordering {
a.total_cmp(&b)
}