- P2-6 tracking: global greedy IoU matching (all pairs, best overlap first) replaces track-iteration-order greedy, so a crossing worker's box can no longer be stolen by whichever track came first. Any track in a contended match (person overlapping 2 tracks, or track overlapping 2 persons) is reported via tracker.ambiguous(); run_once resets its detector + smoother state so FALLEN/baseline/immobility never ride an id swap onto the wrong person. A genuinely fallen worker re-confirms within prone_confirm_s. - P2-7 fall: STANDING -> FALLING now requires sustained prone (falling_confirm_s, 1.0s). One prone-looking frame (a bend, a bbox glitch) no longer arms the ghost detector's CRITICAL if the person then leaves. FALLEN / no_motion timing is unchanged (keyed off prone_since). - P2-8 app: run_once feeds the engine and smoothers only tracks re-detected this tick; a track coasting inside max_age with a frozen frame is no-data, so prone-confirm / no-motion / immobility timers cannot accrue on stale data. Re-detection still confirms the fall (dropout pauses evidence). TDD: 7 new tests verified failing on the pre-fix code (git stash), then 203 passed / 2 skipped (hardware) with the fixes.
68 lines
2.5 KiB
Python
68 lines
2.5 KiB
Python
from edge.models import Person, BBox, Keypoint
|
|
from edge.tracking import IouTracker
|
|
|
|
|
|
def _person(x1, y1, x2, y2):
|
|
kps = [Keypoint((x1 + x2) / 2, (y1 + y2) / 2, 0.9) for _ in range(17)]
|
|
return Person(BBox(x1, y1, x2, y2), kps)
|
|
|
|
|
|
def test_new_person_gets_track_id():
|
|
tr = IouTracker()
|
|
tracks = tr.update([_person(0, 0, 10, 20)], t=0.0)
|
|
assert len(tracks) == 1
|
|
assert tracks[0].track_id == 1
|
|
|
|
|
|
def test_overlapping_person_keeps_same_id():
|
|
tr = IouTracker(iou_thr=0.3)
|
|
tr.update([_person(0, 0, 10, 20)], t=0.0)
|
|
tracks = tr.update([_person(1, 1, 11, 21)], t=0.1) # high overlap
|
|
assert tracks[0].track_id == 1
|
|
|
|
|
|
def test_disappeared_track_is_reported_lost():
|
|
tr = IouTracker(max_age=0.2)
|
|
tr.update([_person(0, 0, 10, 20)], t=0.0)
|
|
tr.update([], t=0.5) # nothing detected, past max_age
|
|
lost = tr.lost()
|
|
assert len(lost) == 1
|
|
assert lost[0].track_id == 1
|
|
|
|
|
|
def test_history_accumulates_frames():
|
|
tr = IouTracker()
|
|
tr.update([_person(0, 0, 10, 20)], t=0.0)
|
|
tracks = tr.update([_person(0, 0, 10, 20)], t=0.1)
|
|
assert len(tracks[0].frames) == 2
|
|
assert tracks[0].latest.t == 0.1
|
|
|
|
|
|
def test_crossing_keeps_ids_via_global_best_match():
|
|
# Track-iteration-order greedy let track 1 steal the box that overlaps track 2 far more
|
|
# (0.48 vs 0.74), leaving track 2 unmatched and its person re-created as a NEW id —
|
|
# FALLEN state and frozen baseline would ride along to the wrong person. Global greedy
|
|
# must assign the highest-IoU pair first.
|
|
tr = IouTracker(iou_thr=0.3)
|
|
tr.update([_person(0, 0, 10, 20), _person(5, 0, 15, 20)], t=0.0)
|
|
tracks = tr.update([_person(3.5, 0, 13.5, 20), _person(-4, 0, 6, 20)], t=0.1)
|
|
by_id = {t.track_id: t for t in tracks}
|
|
assert set(by_id) == {1, 2} # no phantom new track
|
|
assert by_id[2].latest.bbox.x1 == 3.5
|
|
assert by_id[1].latest.bbox.x1 == -4
|
|
|
|
|
|
def test_contended_match_is_flagged_ambiguous():
|
|
# One person overlapping BOTH tracks (a crossing): whichever track wins, the match may
|
|
# be a swap, so every contended track must be reported for a safety-state reset.
|
|
tr = IouTracker(iou_thr=0.3)
|
|
tr.update([_person(0, 0, 10, 20), _person(5, 0, 15, 20)], t=0.0)
|
|
tr.update([_person(2, 0, 12, 20)], t=0.1)
|
|
assert tr.ambiguous() == {1, 2}
|
|
|
|
|
|
def test_clean_match_is_not_ambiguous():
|
|
tr = IouTracker(iou_thr=0.3)
|
|
tr.update([_person(0, 0, 10, 20), _person(100, 0, 110, 20)], t=0.0)
|
|
tr.update([_person(1, 1, 11, 21), _person(101, 1, 111, 21)], t=0.1)
|
|
assert tr.ambiguous() == set()
|