Priority: P0
Size: small
Consensus: Opus + GPT
Files: src/trackers/core/bytetrack/tracker.py, tests under
tests/core/
Current behavior
ByteTrackTracker.update() uses zeros as the default confidence:
confidences = detections.confidence if detections.confidence is not None else np.zeros(len(detections))That routes every no-confidence detection into the low-confidence bucket.
ByteTrack only spawns from unmatched high-confidence detections, so a common
input such as sv.Detections(xyxy=...) silently returns only tracker_id=-1
or an empty result forever.
Why it matters
All other trackers treat missing confidence as "all detections usable" by
defaulting to ones. ByteTrack is the outlier and silently fails on valid
supervision.Detections input.
Suggested fix
Use all-ones confidence:
confidences = detections.confidence if detections.confidence is not None else np.ones(len(detections))Prefer dtype compatible with existing detection arrays if needed.
Suggested tests
- Parametrize no-confidence detections across SORT, ByteTrack, OC-SORT, and BoT-SORT.
- For ByteTrack, call
update()for enough frames to passminimum_consecutive_frames; assert at least one non-negativetracker_id. - Assert behavior matches explicit
confidence=np.ones(...).
Priority: P0
Size: small/medium
Consensus: Opus + GPT
Files:
src/trackers/core/sort/tracker.pysrc/trackers/core/bytetrack/tracker.pysrc/trackers/core/ocsort/tracker.pysrc/trackers/core/botsort/tracker.pysrc/trackers/core/sort/utils.pysrc/trackers/core/bytetrack/utils.pysrc/trackers/core/botsort/utils.py- possibly
src/trackers/core/base.py
Current behavior
All four trackers compute:
int(frame_rate / 30.0 * lost_track_buffer)This floors to zero for low frame rates or small buffers. SORT, ByteTrack, and
BoT-SORT alive helpers then use strict < maximum_frames_without_update, while
OC-SORT uses <=.
Example: frame_rate=10, lost_track_buffer=1 gives
maximum_frames_without_update == 0, so a track can die immediately.
Why it matters
The parameter name implies "number of missed frames to keep a lost track alive." Current behavior is surprising, inconsistent across trackers, and especially bad for mobile / low-FPS streams.
Suggested fix
- Validate
frame_rate > 0. - Validate
lost_track_buffer >= 1. - Compute once with:
maximum_frames_without_update = max(1, math.ceil(frame_rate / 30.0 * lost_track_buffer))- Prefer moving this shared calculation into
BaseTrackeror a small shared helper to prevent future drift. - Use
<= maximum_frames_without_updateconsistently in alive/prune helpers. - Update docstrings to say whether the value is a 30-FPS-normalized buffer or an actual missed-frame count after scaling.
Suggested tests
- Parametrize tracker class x
lost_track_buffer={1, 2, 5}xframe_rate={10, 15, 30, 60}. - Create a track, miss exactly
maximum_frames_without_updateframes, and assert it is still alive. - Miss one more frame and assert it is pruned.
- Add invalid-argument tests for zero/negative frame rate and buffer.
Priority: P0
Size: medium
Consensus: Opus + GPT
Files:
src/trackers/utils/base_tracklet.pysrc/trackers/core/*/tracklet.pysrc/trackers/core/*/tracker.pytests/core/test_tracklets.pytests/core/test_trackers.py
Current behavior
BaseTracklet.count_id is a class variable. get_next_tracker_id() mutates
the class-level counter, and tracker reset() methods reset that global state.
Two tracker instances of the same class in one process share and can reset each
other's ID allocation.
GPT's repro produced two live SORT tracks with ID 0 after resetting a second
tracker instance.
Why it matters
Concurrent trackers are common: multi-camera apps, class-specific trackers, services handling multiple requests, notebooks, and A/B comparisons. Global ID allocation can create collisions and non-local side effects.
Suggested fix
- Move
_next_track_idto each tracker instance. - Add a small instance method such as
_allocate_tracker_id(). - Assign IDs in the tracker rather than inside tracklet class methods, or pass an allocated ID into tracklets when they become confirmed.
- Remove
Tracklet.count_id = 0fromreset()methods. - Update tests that currently pin global counter behavior.
Suggested tests
- Instantiate two trackers of the same class.
- Mature a track in tracker A.
- Reset tracker B.
- Mature another track in tracker A.
- Assert tracker A does not reuse an existing live ID.
- Repeat at least for SORT and ByteTrack; ideally parametrize all trackers.
Priority: P1
Size: medium
Consensus: Opus + GPT, with GPT specifically reproing ByteTrack/BoT-SORT
mid-confidence drops
Files:
src/trackers/core/bytetrack/tracker.pysrc/trackers/core/botsort/tracker.pysrc/trackers/core/ocsort/tracker.py- tests under
tests/core/
Current behavior
The advertised API is effectively: return the same detections with
tracker_id populated. Current behavior diverges:
- ByteTrack and BoT-SORT drop unmatched detections whose confidence is between
high_conf_det_thresholdandtrack_activation_threshold. - ByteTrack's docstring says detection order may differ from input.
- OC-SORT filters low-confidence detections before association and emits rows from the filtered detections, not the original batch.
GPT repro: with high_conf_det_threshold=0.6,
track_activation_threshold=0.7, a detection at confidence 0.65 returned
length 0 for both ByteTrack and BoT-SORT.
Why it matters
Callers often expect output arrays to align with input metadata:
class_id, masks, custom data, and original detection indices. Dropping or
reordering rows makes downstream code silently wrong.
Suggested fix
- Build
tracker_ids = np.full(len(input_detections), -1, dtype=int)in original input order. - Use filtered/high/low subsets only for association and spawn decisions.
- For every matched detection, write its ID into
tracker_ids[original_idx]. - Return an indexed/copy result with all original rows and
tracker_idpopulated, unless the library intentionally chooses a breaking API change. - Gate track spawning separately from output emission: detections below
activation threshold should still be returned with
tracker_id=-1.
Suggested tests
- For each tracker, assert
len(tracker.update(detections)) == len(detections)for empty, low-confidence, mid-confidence, high-confidence, and mixed inputs. - Assert returned
xyxy,confidence,class_id, and custom data preserve original row order. - Assert unmatched rows receive
tracker_id=-1.
Priority: P1
Size: small
Consensus: Opus + GPT
Files: src/trackers/core/ocsort/tracklet.py, tests under
tests/core/
Current behavior
OCSORTTracklet._unfreeze_xcycsr() and _unfreeze_xyxy() loop over
range(time_gap), so the final virtual observation is exactly the real new
observation. update() then calls self.state_estimator.update(bbox) again.
The docstring says interpolation factors should go from 0 to
(time_gap - 1) / time_gap, with the caller responsible for the final real
update. The code currently reaches factor 1.0.
Why it matters
This biases OC-SORT re-association after missed frames and diverges from the intended Observation-Centric Recovery algorithm.
Suggested fix
Change both interpolation loops to avoid the final real observation, e.g.
for i in range(time_gap - 1), and ensure time_gap <= 1 is handled without
extra virtual updates.
Suggested tests
- Add a spy/fake Kalman filter or monkeypatch
kf.update. - Create a miss/reacquire path with
time_gap=3. - Assert virtual updates are applied only for intermediate observations and the
final real bbox is applied exactly once by
update(). - Assert final state matches a hand-computed sequence for a simple box.
Priority: P1
Size: small
Consensus: Opus + GPT
Files:
src/trackers/eval/clear.pysrc/trackers/eval/hota.pysrc/trackers/eval/identity.py- tests under
tests/eval/
Current behavior
The low-level metric functions iterate with zip(gt_ids, tracker_ids).
Mismatched sequence lengths silently truncate to the shorter sequence.
GPT repro: CLEAR reported no false positives for one GT frame and two tracker frames because the trailing tracker frame was ignored.
Why it matters
The higher-level MOT preparation path may align normal MOT inputs, but these metric functions are public enough to be called directly. Silent truncation can inflate published results.
Suggested fix
- Add a shared validation helper for:
len(gt_ids) == len(tracker_ids) == len(similarity_scores)- for each frame,
similarity_scores[t].shape == (len(gt_ids[t]), len(tracker_ids[t]))
- Raise
ValueErrorwith actionable messages.
Suggested tests
- Mismatched frame counts raise.
- Mismatched per-frame similarity shape raises.
- Valid empty-frame combinations still pass.
Priority: P2
Size: small
Consensus: Opus + GPT
Files: src/trackers/core/ocsort/tracklet.py, tests under
tests/core/
Current behavior
- The spawn observation is not recorded in
self.observations. last_observationand history entries store caller-owned arrays by reference.previous_to_last_observationis written but not read.
Why it matters
History aliasing lets external mutation corrupt tracker internals. Missing the initial observation weakens early velocity/history behavior and complicates ORU reasoning.
Suggested fix
- Store defensive copies:
self.last_observation = np.asarray(initial_bbox).copy(). - Seed
self.observations = {0: self.last_observation.copy()}. - Copy every updated bbox before saving it.
- Remove
previous_to_last_observationunless a real consumer is added.
Suggested tests
- Mutate the source bbox after tracklet construction; internals remain stable.
- Mutate the source bbox after update; history remains stable.
- Assert observation key
0exists immediately after construction.
Priority: P2
Size: small
Consensus: Opus + GPT
Files:
src/trackers/utils/converters.pysrc/trackers/utils/state_representations.py- tests under
tests/utils/
Current behavior
xcycsr_to_xyxy() computes:
w = np.sqrt(scale * aspect_ratio)
h = scale / wZero or negative scale/aspect can produce NaN/Inf. The current
XCYCSRStateEstimator.clamp_velocity() only zeros scale velocity when the next
scale would be non-positive; it does not clamp the current state.
Why it matters
The IoU layer now rejects non-finite boxes, which is good, but the tracker can still generate them internally from degenerate Kalman states.
Suggested fix
- Clamp scale and aspect ratio to a small positive epsilon in the converter or before calling it.
- Clamp the actual XCYCSR state in
XCYCSRStateEstimator, not only velocity. - Keep behavior deterministic and documented.
Suggested tests
xcycsr_to_xyxy()with zero scale, negative scale, zero aspect, and negative aspect returns finite boxes or raises a deliberateValueError.- If choosing clamping, assert output width/height are positive finite values.
- Add a state-estimator test proving a degenerate predicted state is repaired before conversion.
Priority: P2
Size: small
Consensus: Opus; consistent with current extensibility goals
Files: src/trackers/core/base.py, tests/core/test_registration.py
Current behavior
BaseTracker.__init_subclass__() silently overwrites
BaseTracker._registry[tracker_id] when a second subclass uses the same
tracker_id.
Why it matters
The package advertises tracker auto-registration and extensibility. Silent overwrite makes import order observable and can break CLI discovery.
Suggested fix
Raise ValueError when registering a duplicate tracker_id, unless the exact
same class is being registered idempotently.
Suggested tests
- Define two temporary subclasses with the same
tracker_id. - Assert the second registration raises and the original registry entry remains unchanged.
Priority: P2
Size: small
Consensus: Opus
Files/directories:
tox.ini- root-level
trackers/ - root-level
test/
Current behavior
The repository contains stale root-level trackers/ and test/ directories
with only __pycache__ artifacts from the old layout. tox.ini has
changedir = test, so tox runs from the stale directory rather than the real
tests/ directory. It also installs only pytest, not the project.
Why it matters
This confuses contributors and can make tox validate little or nothing.
Suggested fix
- Delete stale
trackers/andtest/cache-only directories. - Update
tox.inito run from the repo root. - Install the package in editable mode or install the needed extras/dependency groups.
- Keep tox aligned with
pyproject.tomlpytest config.
Suggested tests
tox -e py310or one available local tox env discovers and runs the real suite.
Priority: P2
Size: small/medium
Consensus: Opus
Files: association helpers in src/trackers/core/*/
Current behavior
Unmatched output order is sorted, but equal-cost assignment ties are still left
to scipy.optimize.linear_sum_assignment.
Why it matters
Golden tests and cross-language rewrites need deterministic ID sequences when cost matrices contain ties or symmetric boxes.
Suggested fix
Add a tiny deterministic perturbation to cost/similarity matrices before assignment, small enough not to change non-tie decisions. Centralize the helper so all trackers use the same tie-break policy.
Suggested tests
- Construct a symmetric/tied association matrix.
- Assert the selected pairs and resulting tracker IDs are stable.
- Use the same fixture later as a Dart/Rust conformance golden.
Priority: P2
Size: small/medium
Consensus: Opus
Files: src/trackers/core/ocsort/tracklet.py, possibly BoT-SORT and
shared tests
Current behavior
OC-SORT has a warmup branch that can grant real IDs before the configured
minimum_consecutive_frames threshold, unlike SORT, ByteTrack, and BoT-SORT.
Why it matters
The same parameter name should not have tracker-specific semantics unless the difference is explicit and tested.
Suggested fix
- Decide one package-wide meaning:
- strict: ID only after the threshold is met, or
- explicit
instant_first_frame_activationstyle parameter.
- Prefer strict consistency for SORT, ByteTrack, and OC-SORT; BoT-SORT already exposes an explicit first-frame activation option.
Suggested tests
- Parametrize all trackers with
minimum_consecutive_frames=3. - Feed a single stable detection over frames.
- Assert IDs remain
-1until the same frame count for every tracker, except any tracker with an explicitly enabled instant-activation parameter.
Priority: P3
Size: small/investigation
Consensus: Opus
Files: src/trackers/core/ocsort/tracker.py,
src/trackers/core/ocsort/tracklet.py, tests under tests/core/
Current behavior
np.array([t.get_state_bbox() for t in self.tracks])produces shape(0,)when there are no tracks. Current downstream guards happen to avoid failure, but the shape is wrong for a(0, 4)box matrix.- OC-SORT freeze timing may capture state after one drift step rather than at the moment of the first miss. This needs a test-first investigation.
Why it matters
Shape drift is a common source of future vectorization bugs. Freeze timing affects OC-SORT recovery quality and should be explicit.
Suggested fix
- Use
np.empty((0, 4))andnp.empty((0, 2))in zero-track paths. - Add a focused freeze-timing test before changing behavior.
Suggested tests
- Empty-tracks/non-empty-detections update path returns a valid result with no
accidental
(0,)arrays reaching IoU or direction-consistency code. - Miss/reacquire fixture documents exactly when
_frozen_stateis captured.
Priority: P3
Size: medium/design
Consensus: Opus
Files:
src/trackers/utils/kalman_filter.pysrc/trackers/utils/state_representations.py- tracker defaults/tests
Current behavior / open questions
- Kalman gain uses
np.linalg.inv(S);np.linalg.solveis usually preferred. - Q-block scaling for the XCYCSR model may double-damp one term; verify against the SORT/OC-SORT reference before changing.
- SORT and ByteTrack now default to
XYXYStateEstimator, which allows independent corner velocities and can shear boxes through occlusions. - OC-SORT direction-normalization epsilon is very small relative to float32 noise.
Why it matters
These are not urgent correctness repros, but they affect long-run numerical stability and the rewrite spec.
Suggested fix
Make this a test-first numerical audit. Do not change defaults without a benchmark or golden-test reason.
Suggested tests
- Kalman predict/update golden cases.
- Degenerate covariance / near-singular innovation case.
- Occlusion fixture comparing XYXY vs XCYCSR/XCYCWH drift.
Priority: P3
Size: medium/design
Consensus: Opus
Files: multiple under src/trackers/core/, src/trackers/eval/,
src/trackers/utils/iou.py
Current behavior
Association helpers have diverging signatures across trackers. The package also has multiple IoU surfaces: pluggable tracker IoU, eval box IoU, and older helper semantics.
Why it matters
Divergence makes the code harder to port and easier to regress. A rewrite should follow one association contract.
Suggested fix
- Add a shared association helper that accepts a similarity matrix, a threshold, and tie-break policy.
- Route tracker helpers through it.
- Consider making eval IoU call into the same validated box math where appropriate, while preserving metric-specific behavior.
Suggested tests
Existing association and IoU tests should continue to pass; add only tests for new shared edge cases.
Priority: P3
Size: review-only first
Consensus: Opus noted limited coverage in this v2 pass
Files: src/trackers/core/botsort/*, tests/core/test_botsort_*
Current status
BoT-SORT was added after the original review and was only spot-checked here. The top shared issues already include BoT-SORT where verified: lost-buffer semantics, global ID counters, and mid-confidence output drops.
Suggested review scope
- CMC numerical stability and failure modes.
- Empty-array shape guards.
- Output-row contract after PR-04.
- Signed-IoU normalization across all association stages.
- Frame requirement/error behavior when CMC is enabled.
- Mobile/rewrite implications of OpenCV-dependent CMC.
For each PR, give a coding agent:
- The PR section from this document.
- The relevant source paths.
- The test requirements.
- The instruction to keep the PR surgical and avoid combining unrelated PRs.
Recommended agent prompt skeleton:
Implement PR-XX from docs/trackers_python_review_v2.md.
Keep the change limited to the files named in that section unless tests show a
tightly coupled fix is required. Add the suggested regression tests. Run the
relevant test subset and report any unrelated baseline failures separately.