Skip to content

ngperception/tracking: SORT baseline, evaluator and a dataset-free harness - #2

Open
egeboy35 wants to merge 1 commit into
lkk688:mainfrom
egeboy35:feat/tracking-sort-baseline
Open

ngperception/tracking: SORT baseline, evaluator and a dataset-free harness#2
egeboy35 wants to merge 1 commit into
lkk688:mainfrom
egeboy35:feat/tracking-sort-baseline

Conversation

@egeboy35

Copy link
Copy Markdown

ngperception/README.md lists Tracking (MOT) as planned, with SORT as the basic tier and DeepSORT → ByteTrack → OC-SORT above it. The folder does not exist yet. This adds it.

It follows the three-layer shape the rest of the suite uses (trackers / datasets / evaluator) and consumes ngdet.Detection rather than detecting anything itself — so a sequence can be re-tracked under a different detector without touching the tracker, which is what keeps the ablation about association rather than detection.

Layout

tracking/
├── trackers/base.py    TrackResult + BaseTracker + TRACKER_REGISTRY + build_tracker + iou_matrix
├── trackers/sort.py    @register("sort") — Kalman (cx,cy,area,aspect) + Hungarian on IoU
├── datasets.py         MOTChallenge csv reader + a dataset-free synthetic generator
├── evaluator.py        CLEAR-MOT: MOTA / MOTP / IDF1 / ID-switches
├── run_eval.py         CLI, same shape as depth/run_eval.py
└── tests/              37 tests, ~1.2 s

numpy and scipy only — no network, no weights, no GPU, nothing to download. That is deliberate: the tracking arm stays reproducible by anyone, and a later appearance-based backend gets a like-for-like reference to beat.

Runs with no dataset

python -m DeepDataMiningLearning.ngperception.tracking.run_eval \
    --trackers sort --synthetic --min-hits 1
  tracker           MOTA    MOTP    IDF1   IDSW      FP      FN
  sort           +1.0000  1.0000  1.0000      0       0       0

Dial in the failure the tracker is supposed to survive (2 × 30 frames × 4 objects, --min-hits 1 --max-age 3):

condition MOTA MOTP IDF1 IDSW FN
clean +1.0000 1.0000 1.0000 0 0
6 px detector jitter +1.0000 0.9187 1.0000 0 0
10 % missed detections +0.9000 1.0000 0.9474 0 24
15 % missed + 8 px jitter +0.8583 0.8914 0.7175 0 34

Jitter costs MOTP and leaves MOTA's detection terms alone; missed detections cost MOTA in proportion. Reporting both keeps those separable.

On a MOTChallenge dataset

python -m DeepDataMiningLearning.ngperception.tracking.run_eval \
    --trackers sort --root /data/MOT17 --sequences MOT17-02 MOT17-04

datasets.py does the two conversions that format needs, in one place, tested: frames are 1-indexed on disk, 0-indexed in the API, and boxes are xywh on disk, xyxy everywhere in ngdet/ngperception (x2 = left + width, not left + width − 1). Rows whose conf is 0 are MOTChallenge's ignore flag and are dropped by default.

Algorithm

A constant-velocity Kalman filter per track over [cx, cy, area, aspect] with aspect held constant — SORT's own simplification, kept so this is the published baseline and not a private variant — plus Hungarian assignment on IoU between each track's predicted box and each detection. Matches below the gate are rejected after the assignment, not before: filtering first would let a leftover pair win an assignment the optimal solution had given to a better one.

Tests

37 tests, ~1.2 s, no GPU and no dataset. Line coverage is 100 % of the importable code in all four modules — the only uncovered lines are the if __name__ self-test blocks, and each of those runs under python -m.

They were checked against 18 seeded mutations: gate removal, assignment sign, id reuse, area/aspect swap, the area clamp, the IoU union term, the ID-switch counter, MOTA's terms, and both MOTChallenge conversions. All 18 turn the suite red.

Two of those started out undetected and the tests were strengthened until they were not — removing the association gate, and removing the area clamp. The first needed a case where a far-away detection must not claim an existing track; the second needed a box collapsing fast enough that the predicted area passes through zero (with the clamp it stays ~240 px wide, without it every corner lands on the same point).

What this baseline does not do

Stated in the README rather than left implicit: SORT has no re-identification, so an object that leaves and returns gets a new id and the sequence takes an ID-switch; it has no occlusion reasoning beyond coasting for max_age frames; and it inherits every miss of the detector it is given.

The README task-table row moves from planned to ✅ baseline, with the three appearance/association backends above it still marked planned.

🤖 Generated with Claude Code

The README's task table listed Tracking (MOT) as planned with SORT as the
basic tier; the folder did not exist. This adds it, following the same
three-layer shape the rest of the suite uses (trackers / datasets /
evaluator) and consuming ngdet.Detection rather than detecting anything
itself, so a sequence can be re-tracked under a different detector
without touching the tracker.

Contents
  trackers/base.py   TrackResult + BaseTracker + TRACKER_REGISTRY +
                     build_tracker + iou_matrix, mirroring
                     ngdet.detectors.base and depth.estimators.base
  trackers/sort.py   @register("sort") -- constant-velocity Kalman filter
                     over (cx, cy, area, aspect) plus Hungarian assignment
                     on IoU. Matches below the gate are rejected AFTER the
                     assignment so it stays globally optimal
  datasets.py        MOTChallenge csv reader plus a synthetic generator, so
                     the module runs with no dataset at all
  evaluator.py       CLEAR-MOT: MOTA / MOTP / IDF1 / ID-switches, counters
                     summed across sequences and the ratios formed once
                     (the MOTChallenge convention)
  run_eval.py        CLI in the same shape as depth/run_eval.py

numpy and scipy only: no network, no weights, no GPU, nothing to download.
That is deliberate -- the tracking arm stays reproducible by anyone, and a
later appearance-based backend has a like-for-like reference to beat.

Measured, 2 x 30 frames x 4 objects, --min-hits 1 --max-age 3:

  condition                   MOTA     MOTP     IDF1   IDSW   FN
  clean                    +1.0000   1.0000   1.0000      0    0
  6 px detector jitter     +1.0000   0.9187   1.0000      0    0
  10 % missed detections   +0.9000   1.0000   0.9474      0   24
  15 % missed + 8 px       +0.8583   0.8914   0.7175      0   34

Jitter costs MOTP and leaves MOTA's detection terms alone; missed
detections cost MOTA in proportion. Reporting both keeps those separable.

Tests: 37, ~1.2 s, no GPU and no dataset. Line coverage is 100% of the
importable code in all four modules (the only uncovered lines are the
`if __name__` self-test blocks, which each run under `python -m`). They
were checked against 18 seeded mutations -- gate removal, assignment sign,
id reuse, area/aspect swap, the area clamp, IoU union, the ID-switch
counter, MOTA's terms, both MOTChallenge conversions -- and all 18 turn
the suite red.

Two limits are stated in the README rather than left implicit: SORT has no
re-identification, so an object that leaves and returns takes an ID-switch;
and it inherits every miss of the detector it is given.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants