Feat/dynamic frame rate integration#446
Open
AlexBodner wants to merge 8 commits into
Open
Conversation
…ame rate (PR1) (#438) * feat(trackers): time-parameterized Kalman foundations for variable frame rate Establish the Kalman filter machinery needed to advance state by an arbitrary time step, while leaving every existing call site behaving byte-for-byte identically. * `KalmanFilter` gains an optional `predict(dt=1.0)` and a `set_motion_model_builders(F_builder, Q_builder)` opt-in. Callers that do not register builders ignore `dt` entirely. The first time-aware call with `dt = 1.0` preserves the caller-supplied reference Q so existing calibration survives. * `BaseStateEstimator` declares abstract `build_F(dt)` / `build_Q(dt)` / `_kinematic_indices` and registers them with the underlying KF on construction. `set_kf_covariances(Q=...)` back-calibrates a per-coordinate acceleration variance `σ_a²` from the velocity diagonal of the supplied Q and captures any non-kinematic diagonal entries (e.g. XCYCSR aspect-ratio random walk) for restoration on rebuild. * XYXY, XCYCSR, and XCYCWH state estimators each implement the Discrete White Noise Acceleration (DWNA) Q(dt) discretization and the matching constant-velocity F(dt). * `BaseTracklet.predict` and all four concrete tracklets (SORT, ByteTrack, OC-SORT, BoT-SORT) accept and forward `dt=1.0`. Public API surface unchanged. Default `dt=1.0` everywhere reproduces previous behaviour; the full existing test suite (567 tests) passes unmodified. Adds 19 new tests covering backward compatibility, DWNA structure, back-calibration, and a synthetic constant-velocity trajectory under non-uniform sampling intervals. See `docs/design/dynamic-frame-rate.md` for the full feature spec and the phased rollout plan (this commit lands PR 1 of 3 in the MVP spike). Co-authored-by: Cursor <cursoragent@cursor.com> * fix(pre_commit): 🎨 auto format pre-commit hooks * refactor(kalman): gate dt-aware predict with flag set at registration Replace the dual None-check on _F_builder/_Q_builder in predict() with _motion_model_builders_enabled, flipped on in set_motion_model_builders(). The API always registers both builders atomically, so the hot path no longer re-validates what registration already guarantees. Co-authored-by: Cursor <cursoragent@cursor.com> * refactor(kalman): install dt-aware sync hook at registration time Replace builder None-checks and flags in predict() with a _sync_motion_model callable defaulting to a no-op. set_motion_model_builders() installs the real sync closure; predict() always calls it unconditionally. Co-authored-by: Cursor <cursoragent@cursor.com> * refactor(kalman): use instance state for motion-model dt cache Replace closure/nonlocal cached_dt with self._cached_dt and a plain _sync_motion_model method. Builders are stored on the instance at registration time. Co-authored-by: Cursor <cursoragent@cursor.com> * added clarifying comment * chore(docs): stop tracking internal dynamic-frame-rate design spec Remove docs/design/dynamic-frame-rate.md from the repo while keeping it local via .gitignore. Point in-repo references to the user guide instead. Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: Cursor <cursoragent@cursor.com> Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
…ckers (PR2) (#439) * feat(trackers): timestamp plumbing and time-based pruning for SORT and ByteTrack PR 2 of the dynamic frame-rate feature series. - BaseTracker.update() abstract signature extended with optional `timestamp` argument (float | None); all four concrete trackers updated accordingly. - BaseTracker._compute_dt() helper: derives per-step dt from wall-clock timestamps, bootstraps to 1/frame_rate on first call, returns 1.0 in fixed-rate mode, emits a once-per-instance UserWarning and returns 0.0 on non-monotonic timestamps (predict skipped for that frame). - BaseTracklet gains time_since_update_seconds (float), incremented by dt in predict() and reset to 0.0 in update(); mirroring the integer frame counter. - SORTTracker and ByteTrackTracker: store _frame_rate, _last_timestamp, _dt_nonmonotonic_warned; compute dt via _compute_dt; pass it to tracklet.predict(dt); switch pruning to seconds-based (time_since_update_seconds < maximum_time_without_update) when timestamps are active, falling back to frame-count otherwise. - sort/utils.py and bytetrack/utils.py: _get_alive_tracklets accepts optional maximum_time_without_update; switches criterion accordingly. - OCSORTTracker and BoTSORTTracker: accept timestamp kwarg, emit a one-time UserWarning and ignore it (variable-rate support deferred to a later PR). - 24 new tests covering: backward compat, _compute_dt bootstrap/non-monotonic, time accumulation, seconds-based pruning, and OC-SORT/BoT-SORT warn policy. - All 721 tests pass. Co-authored-by: Cursor <cursoragent@cursor.com> * docs(learn): add variable frame rate guide for timestamp-based tracking Document the optional timestamp argument on update(), supported trackers, and fixed vs dynamic behaviour for SORT and ByteTrack. Co-authored-by: Cursor <cursoragent@cursor.com> * Document dual dt convention for fixed vs dynamic frame rate. * fix(pre_commit): 🎨 auto format pre-commit hooks * dynamic frame rate working in the 'tidy' way + tests * fix(pre_commit): 🎨 auto format pre-commit hooks * Add motion model, predict timing, and lifecycle modules omitted from prior commit * fix(pre_commit): 🎨 auto format pre-commit hooks * apply fixes for timestamp plumbing PR. * fixed ruff/mypy things * Extend timestamp mode to all trackers and refactor Kalman motion models * fix(pre_commit): 🎨 auto format pre-commit hooks --------- Co-authored-by: Cursor <cursoragent@cursor.com> Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Contributor
There was a problem hiding this comment.
Pull request overview
This PR adds optional timestamp-driven “dynamic frame rate” support across the tracker stack (trackers → tracklets → state estimators → Kalman motion/noise), enabling variable time gaps between update() calls to scale Kalman prediction and to prune lost tracks using a seconds-based budget while preserving fixed-rate behavior when timestamp is omitted.
Changes:
- Adds
timestamp: float | None = Noneplumbing toBaseTracker.update()and concrete trackers, plus aPredictTiminghelper to convert elapsed seconds into Kalman “frame-step” units. - Introduces a motion-model layer (
KalmanMotionModel+ScalableProcessNoise) to buildF(frame_step)and DWNA-scaledQ(frame_step)for gaps. - Adds docs + extensive unit/integration tests for timing, pruning, and motion/noise scaling.
Reviewed changes
Copilot reviewed 25 out of 26 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| CHANGELOG.md | Documents the new optional timestamp= API and motion-model additions. |
| docs/learn/track.md | Adds user-facing documentation for variable frame rate behavior and per-call timestamp mode. |
| src/trackers/core/base.py | Centralizes timestamp bookkeeping (_predict_timing) and predict dispatch to tracklets. |
| src/trackers/core/sort/tracker.py | Threads timestamp through SORT update flow and time-based pruning budget. |
| src/trackers/core/bytetrack/tracker.py | Threads timestamp through ByteTrack update flow and time-based pruning budget. |
| src/trackers/core/ocsort/tracker.py | Threads timestamp through OC-SORT update flow and adds seconds-budget pruning path. |
| src/trackers/core/botsort/tracker.py | Threads timestamp through BoTSORT update flow and time-based pruning budget. |
| src/trackers/core/sort/tracklet.py | Uses PredictTiming to scale predict steps and advance seconds miss clock. |
| src/trackers/core/bytetrack/tracklet.py | Uses PredictTiming to scale predict steps and advance seconds miss clock. |
| src/trackers/core/ocsort/tracklet.py | Uses PredictTiming to scale predict steps and advance seconds miss clock (plus ORU behavior). |
| src/trackers/core/botsort/tracklet.py | Uses PredictTiming to scale predict steps and advance seconds miss clock. |
| src/trackers/utils/base_tracklet.py | Adds shared seconds-based miss clock + unified “within budget” helper. |
| src/trackers/utils/predict_timing.py | Introduces PredictTiming and FIXED_RATE_TIMING. |
| src/trackers/utils/motion_models.py | Adds KalmanMotionModel and ScalableProcessNoise for F/Q scaling. |
| src/trackers/utils/state_representations.py | Routes frame_step into motion application before kf.predict(). |
| src/trackers/utils/kalman_filter.py | Ensures predict/update algebra supports “no observation” updates cleanly. |
| tests/core/test_timestamp_plumbing.py | Integration tests for timestamp timing conversion and time-based pruning behavior. |
| tests/core/test_tracklets.py | Validates OC-SORT ORU unfreeze behavior stays fixed-rate (unit-step predicts). |
| tests/core/test_registration.py | Updates/extends registration coverage for new API surface (diff truncated in prompt). |
| tests/utils/test_state_estimators.py | Smoke tests for estimator predict defaults and motion-cache reset. |
| tests/utils/test_motion_models.py | Unit tests for F scaling and DWNA Q polynomial scaling properties. |
| tests/utils/test_kalman_filter.py | Unit tests for predict using stored matrices and update(None) semantics. |
Comment on lines
+410
to
+429
| last = self._last_timestamp | ||
| self._last_timestamp = timestamp | ||
|
|
||
| if last is None: | ||
| # Bootstrap: no prior timestamp, so we cannot compute t - t_prev. | ||
| # Use one nominal frame period (1 / frame_rate) so the first Kalman | ||
| # step is frame_step=1.0 — matching fixed-rate behaviour rather than | ||
| # using the absolute timestamp value (e.g. 37.2 s would break tuning). | ||
| return 1.0 / self._frame_rate | ||
|
|
||
| elapsed = timestamp - last | ||
| if elapsed <= 0: | ||
| warnings.warn( | ||
| f"{type(self).__name__}: non-positive elapsed={elapsed:.6f}s from " | ||
| "non-monotonic timestamp. Skipping predict for this step.", | ||
| UserWarning, | ||
| stacklevel=3, | ||
| ) | ||
| return 0.0 | ||
| return elapsed |
Collaborator
Author
There was a problem hiding this comment.
I think its better to assume monotonicity of inputs. Wdyt @Borda ?
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Extract the full variable-frame-rate documentation into a dedicated learn page and link to it from track.md. Co-authored-by: Cursor <cursoragent@cursor.com>
Assert frame_step=1.0 in ORU sub-step test, restore BaseStateEstimator noise-configuration note, and clarify monotonic timestamp requirement in the dynamic frame rate guide. Co-authored-by: Cursor <cursoragent@cursor.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What does this PR do?
This PR introduces Dynamic frame rate feature, which is activated by passing a timestamp to the tracker update function.
It is implemented in the State Estimator layer via an interaction with the new class KalmanMotionModel which handles the logic of swapping the motion model and noise matrices for the used model. Then, this is dispatched until tracklet predict level.
When using dynamic frame rate, we still use the linear velocity model, but we scale it by the delta time. To do the analog for the covariance matrix, it is not enough to just scale it, but we use the Discrete White Noise Acceleration Process matrix, which is the proper adjustment. This logic is implement in the ScalableProcessNoise class in src/trackers/utils/motion_models.py.
How much does this improve?
To evaluate this we tried by dropping frames trying to model network failures.
Here we get:
Then, we can model burst loss by correlating the frame drops.
Here we get:

And the mean per dataset is:

We are seeing delta HOTA between using the new feature and not using it in pp. So we expect to see all positive results, but in DanceTrack static is performing up to 2pp better. This can happen due to usage of parameters tuned for static usage. Though in soccernet and sportsmot improves usage up to 6pps!
Here we see HOTA, but this gives an improvement to IDF1 and MOTA as well. Here we have a plot with ALL the results. Full is without frame dropping, to see the ceiling, dynamic is using this PRs feature, and static is tracking naively.

Type of Change
Testing
Checklist
Additional Context