Pose Kinematics#
Implemented in C++ (mosaic::compute_kinematics(),
src/analysis/pose_kinematics.hpp/.cpp) — not in
pose.human_pose, which only produces the raw keypoint detections
this module derives Speed/Acceleration from. Used by the Analysis tab’s
Pose plugin (User guide) to plot a keypoint’s derived motion
alongside its raw position.
Gap-tolerant finite differences#
A naive velocity estimate assumes a constant nominal frame rate. But real detections have gaps: a keypoint can be temporarily occluded or low-confidence for several frames, and simply skipping those samples (rather than interpolating a fabricated path through them) means the time between two valid consecutive samples isn’t always one nominal frame period — it can span a real gap. Using each sample’s own real timestamp handles this correctly:
where \(\tilde p_i\) is the (optionally smoothed — see below) position at valid sample \(i\), and \(a_i\) uses the same \(\Delta t_i\) as the speed sample it’s paired with. A keypoint below the visibility threshold, or a frame with no detected subject at all, is excluded from the sample sequence entirely rather than interpolated.
Centered moving-average smoothing#
Raw frame-to-frame keypoint jitter makes the acceleration estimate above particularly noisy (a second derivative amplifies noise more than a first derivative does). An optional centered moving average smooths positions before the finite differences above are computed:
using a window half-width \(h\), clamped at the sequence’s edges (no padding — an edge sample averages over however many neighbours actually exist). Centered (not trailing) is deliberately used since this is offline analysis over an already-fully-recorded trajectory — there’s no reason to accept a causal filter’s phase lag when the whole trajectory is already available.
Why average speed is time-weighted#
The reported average speed is not the arithmetic mean of the per-interval speed samples \(v_i\) above. It’s the total path length over the total elapsed time:
using the unsmoothed positions for distance (smoothing is meant to stabilize the derivative estimate, not misreport the actual path length travelled).
Note
A naive mean of \(v_i\) samples silently over- or under-weights long gaps: since each \(v_i\) already divides by its own \(\Delta t_i\), an unweighted mean of speeds treats a 1-second-long low-visibility gap’s single speed sample exactly the same as a 40ms interval’s speed sample — even though the gap-spanning sample represents 25× more real time. This project’s own code review caught exactly this discrepancy (an unweighted mean understated true average speed whenever a low-visibility gap was present) — the time-weighted formula above is the fix, and is the reason gap-tolerant sampling (above) matters for more than just correctness of the instantaneous samples.
Practical recommendations#
Raising Smoothing (e.g. to 5) meaningfully stabilizes a noisy Acceleration curve at the cost of blurring genuinely fast transients. Leave it at 1 (off) first and only raise it once you’ve confirmed the raw signal actually needs it — don’t smooth by default just because it looks cleaner.
Without a real calibration linking pixels to physical distance, the
Scale (mm/px) field is exactly as accurate as the value you
type into it — a wrong or guessed scale produces a confidently
wrong physical speed. Leave it at 1.0 (px units) unless you
have an actual measured reference distance in the frame to compute
the real ratio from.
…but “honestly” doesn’t mean “meaningfully.” A single reported average speed spanning a multi-second occlusion gap is mathematically correct (see the time-weighted formula above) but may not reflect anything a reader would call the subject’s “real” average speed during that gap — treat any large gap in the exported CSV’s timestamps as a flag to sanity-check the surrounding numbers, not just trust them.
Kinematics are computed against subject index 0 specifically — a multi-subject session’s kinematics are only meaningful if subject 0 genuinely refers to the same physical individual for the whole analyzed span, which this plugin cannot verify on its own.