use ados_protocol::framebus::{BoundingBox, Detection, LockState};
use ados_vision::tracker::{
Appearance, Candidate, SingleObjectTracker, TrackerConfig, APPEARANCE_DIM,
};
fn det(x: f32, y: f32, w: f32, h: f32, conf: f32, label: &str) -> Detection {
Detection {
bbox: Some(BoundingBox {
x,
y,
width: w,
height: h,
}),
class_label: label.into(),
confidence: conf,
track_id: None,
assoc_confidence: None,
lock_state: None,
attributes: None,
mask: None,
keypoints: None,
depth: None,
world_pos: None,
}
}
fn target_app() -> Appearance {
let mut v = vec![0.0f32; APPEARANCE_DIM];
v[0] = 1.0;
Appearance::from_features(v)
}
fn app_at_sim(sim: f32) -> Appearance {
let s = sim.clamp(0.0, 1.0);
let ortho = (1.0 - s * s).max(0.0).sqrt();
let mut v = vec![0.0f32; APPEARANCE_DIM];
v[0] = s;
v[1] = ortho;
Appearance::from_features(v)
}
fn center_err(report: &Detection, truth: &Detection) -> f32 {
let rb = report
.bbox
.as_ref()
.expect("reported detection carries a bbox");
let tb = truth.bbox.as_ref().expect("truth detection carries a bbox");
let rx = rb.x + rb.width / 2.0;
let ry = rb.y + rb.height / 2.0;
let tx = tb.x + tb.width / 2.0;
let ty = tb.y + tb.height / 2.0;
((rx - tx).powi(2) + (ry - ty).powi(2)).sqrt()
}
#[test]
fn sole_distractor_cannot_poison_template_enough_to_lock_out_target() {
let cfg = TrackerConfig::default();
let mut t = SingleObjectTracker::new(cfg);
for f in 0..4usize {
t.update_with_appearance(&[Candidate::with_appearance(
det(f as f32 * 8.0, 100.0, 40.0, 40.0, 0.90, "object"),
target_app(),
)]);
}
let target_id = t.current_id().expect("locked");
let n = cfg.max_coast_frames as usize - 1;
for k in 0..n {
let f = 4 + k;
let pred_x = f as f32 * 8.0;
t.update_with_appearance(&[Candidate::with_appearance(
det(pred_x, 100.0, 40.0, 40.0, 0.95, "object"),
app_at_sim(0.55),
)]);
}
let mut relocked_on_target = false;
for k in 0..6usize {
let f = 4 + n + k;
let x = f as f32 * 8.0;
let target =
Candidate::with_appearance(det(x, 100.0, 40.0, 40.0, 0.90, "object"), target_app());
let u = t.update_with_appearance(std::slice::from_ref(&target));
if let Some(rep) = &u.detection {
if u.measured
&& rep.lock_state == Some(LockState::Locked)
&& center_err(rep, &target.detection) < 5.0
{
relocked_on_target = true;
}
}
}
assert!(
relocked_on_target,
"the true target must remain recognisable; a sole admissible distractor must \
not poison the template enough to lock the genuine target out"
);
assert!(
t.current_id().is_some(),
"a lock must exist on the re-appeared target (held id {target_id} or a fresh one)"
);
}
#[test]
fn chameleon_distractor_never_silently_captures_while_target_present() {
let cfg = TrackerConfig::default();
let mut t = SingleObjectTracker::new(cfg);
for f in 0..4usize {
t.update_with_appearance(&[Candidate::with_appearance(
det(f as f32 * 8.0, 100.0, 40.0, 40.0, 0.90, "object"),
target_app(),
)]);
}
let held_id = t.current_id().expect("locked");
let mut dropped = false;
let mut silent_capture: Vec<usize> = Vec::new();
for k in 0..24usize {
let f = 4 + k;
let pred_x = f as f32 * 8.0;
let sim = (1.0 - k as f32 * 0.025).max(0.4);
let target = Candidate::with_appearance(
det(pred_x, 100.0 + k as f32 * 7.0, 40.0, 40.0, 0.90, "object"),
target_app(),
);
let distractor = Candidate::with_appearance(
det(pred_x, 100.0, 40.0, 40.0, 0.99, "object"),
app_at_sim(sim),
);
let u = t.update_with_appearance(&[distractor.clone(), target.clone()]);
match u.track_id {
None => dropped = true,
Some(id) if id != held_id => dropped = true,
Some(_) => {}
}
if let Some(rep) = &u.detection {
let to_t = center_err(rep, &target.detection);
let to_d = center_err(rep, &distractor.detection);
let separated = center_err(&target.detection, &distractor.detection) > 50.0;
let follows_distractor = separated && to_d + 1.0 < to_t;
let clean =
u.measured && rep.lock_state == Some(LockState::Locked) && !u.needs_redesignation;
if !dropped && u.track_id == Some(held_id) && follows_distractor && clean {
silent_capture.push(f);
}
}
}
assert!(
silent_capture.is_empty(),
"SILENT SWAP: the chameleon distractor captured the held id with a clean measured \
Locked (no Uncertain, no re-designation) while the true target was present and \
separated, at frames {silent_capture:?}. The appearance-first association must hold \
the lock on the strictly-more-similar true target or honestly go Uncertain."
);
}
#[test]
fn one_admissible_association_cannot_cross_the_gate_in_one_step() {
let cfg = TrackerConfig::default();
let mut t = SingleObjectTracker::new(cfg);
for f in 0..4usize {
t.update_with_appearance(&[Candidate::with_appearance(
det(f as f32 * 8.0, 100.0, 40.0, 40.0, 0.90, "object"),
target_app(),
)]);
}
let f = 4usize;
t.update_with_appearance(&[Candidate::with_appearance(
det(f as f32 * 8.0, 100.0, 40.0, 40.0, 0.95, "object"),
app_at_sim(0.55),
)]);
let u = t.update_with_appearance(&[Candidate::with_appearance(
det(5.0 * 8.0, 100.0, 40.0, 40.0, 0.90, "object"),
target_app(),
)]);
let rep = u
.detection
.expect("the genuine target must still be reported");
assert!(
u.measured && rep.lock_state == Some(LockState::Locked),
"one admissible-distractor association must not move the template across the \
gate; the genuine target must still associate (lock={:?}, measured={})",
rep.lock_state,
u.measured
);
}