Skip to content

Analysis

analysis

Motion analysis for BVH data.

Velocities, accelerations, angular velocities, root trajectory, and foot contacts. Every function takes a :class:~pybvh.bvh.Bvh object as its first argument; thin wrapper methods on the Bvh class delegate here.

Feature-array export for ML pipelines lives in :mod:pybvh.features.

Velocities & accelerations

The finite-difference kinematics ladder over FK positions and rotations.

node_velocities(bvh: Bvh, centered: str = 'world', in_frames: bool = False, coords: npt.NDArray[np.float64] | None = None, stencil: str = 'central', pad: str = 'edge') -> npt.NDArray[np.float64]

Compute per-node position velocities (joints + end sites).

Two orthogonal choices:

  • stencil picks the finite-difference method — central (second-order accurate, symmetric) or forward (first-order, causal).
  • pad picks the boundary-handling convention — "edge" fills the boundary so the output has the same shape as the input; "none" drops the boundary frames that the stencil can't define.

Parameters:

Name Type Description Default
bvh Bvh

Input motion.

required
centered str

Coordinate centering mode (default "world"). Ignored if coords is providedcoords takes precedence. Note: "world" and "first" produce identical velocities (constant offsets vanish under differentiation); only "skeleton" is meaningfully different here.

'world'
in_frames bool

If True, return velocity in units/frame. If False (default), return velocity in units/second.

False
coords (ndarray, shape(F, N, 3))

Pre-computed spatial coordinates. If None, computed internally via :meth:Bvh.node_positions.

None
stencil ('central', 'forward')

"central" (default): v[i] = (pos[i+1] - pos[i-1]) / (2·dt). Second-order accurate at interior frames. "forward": v[i] = (pos[i+1] - pos[i]) / dt. First-order accurate; matches the convention common in many ML papers.

"central"
pad ('edge', 'none')

"edge" (default): output shape equals input shape (F, N, 3). For stencil="central" the first/last frames use a one-sided difference (np.gradient template); for stencil="forward" the trailing frame replicates the last valid forward-diff value. "none": drop boundary frames where the stencil is undefined — shape (F-2, N, 3) for central, (F-1, N, 3) for forward.

"edge"

Returns:

Type Description
ndarray

Shape depends on stencil × pad:

========= ====== ================ stencil pad shape ========= ====== ================ central edge (F, N, 3) central none (F-2, N, 3) forward edge (F, N, 3) forward none (F-1, N, 3) ========= ====== ================

See Also

joint_velocities : Same data restricted to non-end-site joints ((F, J, 3)). Use that when the output should index-align with :attr:Bvh.joint_angles / :func:angular_velocities.

Raises:

Type Description
ValueError

If the clip is too short for the chosen combination, frame_time == 0 when in_frames=False, or either parameter is invalid. stencil="central" requires at least 3 frames; stencil="forward" requires at least 2.

joint_velocities(bvh: Bvh, centered: str = 'world', in_frames: bool = False, coords: npt.NDArray[np.float64] | None = None, stencil: str = 'central', pad: str = 'edge') -> npt.NDArray[np.float64]

Compute per-joint position velocities (end sites excluded).

Returns the joint-axis subset of :func:node_velocities — same finite-difference math, but restricted to non-end-site joints so the output indexes match :attr:Bvh.joint_angles and :func:angular_velocities. Output shape is (F, J, 3) (or the appropriate trimmed variant per stencil × pad).

See :func:node_velocities for the full parameter / shape docs.

Raises:

Type Description
ValueError

If coords is not node-shaped (F, N, 3) (in addition to the :func:node_velocities conditions).

node_accelerations(bvh: Bvh, centered: str = 'world', in_frames: bool = False, coords: npt.NDArray[np.float64] | None = None, stencil: str = 'central', pad: str = 'edge') -> npt.NDArray[np.float64]

Compute per-node position accelerations (joints + end sites).

Applies the chosen stencil twice to the input positions.

Parameters:

Name Type Description Default
bvh Bvh

Input motion.

required
centered str

Coordinate centering mode (default "world"). Ignored if coords is providedcoords takes precedence. Note: "world" and "first" produce identical accelerations (constant offsets vanish under differentiation); only "skeleton" is meaningfully different here.

'world'
in_frames bool

If True, return acceleration in units/frame^2. If False (default), return in units/second^2.

False
coords (ndarray, shape(F, N, 3))

Pre-computed spatial coordinates. If None, computed internally via :meth:Bvh.node_positions.

None
stencil ('central', 'forward')

Finite-difference method applied twice. Default "central".

"central"
pad ('edge', 'none')

Boundary handling. "edge" (default): output shape equals input shape (F, N, 3). "none": drop boundary frames the stencil can't define — central drops 4 frames total (F-4, N, 3); forward drops 2 (F-2, N, 3).

"edge"

Returns:

Type Description
ndarray

Shape depends on stencil × pad:

========= ====== ================ stencil pad shape ========= ====== ================ central edge (F, N, 3) central none (F-4, N, 3) forward edge (F, N, 3) forward none (F-2, N, 3) ========= ====== ================

Composition identity: ``np.gradient(node_velocities(), dt)`` equals
``node_accelerations()`` exactly under the defaults
(``stencil="central"``, ``pad="edge"``). Not guaranteed for other
combinations.
See Also

joint_accelerations : Same data restricted to non-end-site joints ((F, J, 3)).

Raises:

Type Description
ValueError

If the clip is too short for the chosen combination, frame_time == 0 when in_frames=False, or either parameter is invalid. Minimum frames: 3 for central+edge, forward+edge, and forward+none; 5 for central+none.

joint_accelerations(bvh: Bvh, centered: str = 'world', in_frames: bool = False, coords: npt.NDArray[np.float64] | None = None, stencil: str = 'central', pad: str = 'edge') -> npt.NDArray[np.float64]

Compute per-joint position accelerations (end sites excluded).

Returns the joint-axis subset of :func:node_accelerations — same twice-applied finite-difference math, restricted to non-end-site joints so output indexes match :attr:Bvh.joint_angles. Output shape is (F, J, 3) (or the appropriate trimmed variant per stencil × pad).

See :func:node_accelerations for the full parameter / shape docs.

Raises:

Type Description
ValueError

If coords is not node-shaped (F, N, 3) (in addition to the :func:node_accelerations conditions).

node_speed_derivative(bvh: Bvh, centered: str = 'world', in_frames: bool = False, coords: npt.NDArray[np.float64] | None = None, stencil: str = 'central', pad: str = 'edge') -> npt.NDArray[np.float64]

Per-node rate of change of speed d‖v‖/dt (joints + end sites).

Computes the per-node speed ‖v‖ from :func:node_velocities, then applies the same finite-difference stencil once more to that scalar series. Positive values mean the node is speeding up, negative values mean it is slowing down — the natural "is the movement accelerating or braking" signal, independent of direction changes.

This is the tangential acceleration a_t = d‖v‖/dt of the node's trajectory. The complementary normal (centripetal) component is a_n = ‖v‖² · κ with κ the trajectory curvature (:func:pybvh.geometry.curvature), and the two decompose the full acceleration vector: a_t² + a_n² = ‖a‖².

It is not recoverable from :func:node_accelerations: this is the difference of the norm (Δ‖v⃗‖), not the norm of the difference (‖Δv⃗‖ = ‖a⃗‖). A direction change at constant speed gives ‖a⃗‖ > 0 but d‖v‖/dt = 0.

Parameters:

Name Type Description Default
bvh Bvh

Input motion.

required
centered str

Coordinate centering mode (default "world"). Ignored if coords is providedcoords takes precedence.

'world'
in_frames bool

If True, return the rate in units/frame^2. If False (default), return in units/second^2.

False
coords (ndarray, shape(F, N, 3))

Pre-computed spatial coordinates. If None, computed internally via :meth:Bvh.node_positions.

None
stencil ('central', 'forward')

Finite-difference method, applied at both stages (positions → velocities, speed → its derivative). Default "central".

"central"
pad ('edge', 'none')

Boundary handling, applied at both stages. "edge" (default): output shape equals input length (F, N). "none": drop boundary frames the repeated stencil can't define — central drops 4 frames total (F-4, N); forward drops 2 (F-2, N).

"edge"

Returns:

Type Description
ndarray

Shape depends on stencil × pad — the frame trimming of :func:node_accelerations, without the trailing 3-axis:

========= ====== ================ stencil pad shape ========= ====== ================ central edge (F, N) central none (F-4, N) forward edge (F, N) forward none (F-2, N) ========= ====== ================

See Also

joint_speed_derivative : Same data restricted to non-end-site joints ((F, J)). node_accelerations : The vector second derivative — its norm is the full ‖a‖, of which this is the tangential component. velocity_reductions : peak_acceleration / peak_deceleration are the extrema of this series (stencil="forward", pad="none").

Raises:

Type Description
ValueError

If the clip is too short for the chosen combination, frame_time == 0 when in_frames=False, or either parameter is invalid. Minimum frames match :func:node_accelerations (two stencil applications): 3 for central+edge, forward+edge, and forward+none; 5 for central+none.

Notes

Source: Hachimura et al. 2005 (Time Effort).

joint_speed_derivative(bvh: Bvh, centered: str = 'world', in_frames: bool = False, coords: npt.NDArray[np.float64] | None = None, stencil: str = 'central', pad: str = 'edge') -> npt.NDArray[np.float64]

Per-joint rate of change of speed d‖v‖/dt (end sites excluded).

Returns the joint-axis subset of :func:node_speed_derivative — same two-stage finite-difference math, restricted to non-end-site joints so output indexes match :attr:Bvh.joint_angles. Output shape is (F, J) (or the appropriate trimmed variant per stencil × pad).

See :func:node_speed_derivative for the full parameter / shape / sign-semantics docs.

Raises:

Type Description
ValueError

If coords is not node-shaped (F, N, 3) (in addition to the :func:node_speed_derivative conditions).

angular_velocities(bvh: Bvh, in_frames: bool = False, stencil: str = 'central', pad: str = 'edge', degrees: bool = False) -> npt.NDArray[np.float64]

Compute per-joint angular velocities via rotation matrix log map.

These are the rates of each joint's parent-relative (local) rotation — the BVH channels themselves — expressed in the joint's own (body) frame: ω[i] = log(R_i^T @ R_{i+1}) / dt on the local rotation matrices, no forward kinematics involved. Two published alternatives differ: the world-frame angular velocity of a segment (the biomechanics convention) composes rotations down the chain first and diverges for every joint whose parent is itself rotating; and the spatial-frame rate log(R_{i+1} @ R_i^T) differs from the body-frame rate by conjugation with R — same magnitude, rotated axis. The local convention is what BVH data natively parameterizes; compose Bvh.to_rotmat output through the hierarchy yourself if you need segment rates.

Parameters:

Name Type Description Default
bvh Bvh

Input motion.

required
in_frames bool

If True, return angular velocity in radians/frame (or degrees/frame if degrees=True). If False (default), return in radians/second (or degrees/ second if degrees=True).

False
degrees bool

If True, convert the final output from radians to degrees. Default False (radians). Consistent with the degrees= flag on :mod:pybvh.rotations functions.

False
stencil ('central', 'forward')

"central" (default): two-step relative rotation R_rel = R_{i-1}^T @ R_{i+1}, ω[i] = log(R_rel) / 2. Spans 2·dt so the short-way angle cap is 360°/frame. "forward": one-step ω[i] = log(R_i^T @ R_{i+1}). Spans dt so the cap is 180°/frame; matches the common one-step-rotation convention in motion capture literature.

"central"
pad ('edge', 'none')

"edge" (default): output shape (F, J, 3). For stencil="central" the first/last frames use a one-sided one-step forward/backward rotation (same template as np.gradient); for stencil="forward" the trailing frame replicates the last valid forward value. "none": drop boundary frames the stencil can't define — central returns (F-2, J, 3), forward returns (F-1, J, 3).

"edge"

Returns:

Type Description
ndarray

Shape depends on stencil × pad:

========= ====== ================ stencil pad shape ========= ====== ================ central edge (F, J, 3) central none (F-2, J, 3) forward edge (F, J, 3) forward none (F-1, J, 3) ========= ====== ================

Direction is the rotation axis; magnitude is the rotation angle (radians or radians/second). Angles are clamped to [0, π] — rotations exceeding the short-way angle wrap.

Raises:

Type Description
ValueError

If fewer than 2 frames (stencil="forward") or 3 frames (stencil="central"), frame_time == 0 when in_frames=False, or either parameter is invalid.

Root trajectory & foot contacts

Ground-plane root features and binary contact labels — the contact signals are drawn in the Gallery.

root_trajectory(bvh: Bvh, up_axis: str | None = None, include_velocities: bool = False, stencil: str = 'central', pad: str = 'edge', degrees: bool = False) -> npt.NDArray[np.float64]

Extract root trajectory features commonly used in motion ML.

Returns the root's ground-plane position and heading angle (as sin/cos pair). Optionally appends ground-plane and heading velocities.

The heading reference is the rest-pose forward direction — derived from the skeleton's L/R lateral geometry crossed with world_up (see :func:pybvh.tools._compute_forward_at). This means "heading = rest-pose forward" at any frame whose root rotation is identity, regardless of what rotation the clip starts with.

The heading is orientation-derived — the root bone's rotation applied to the rest forward, projected to the ground plane — not the direction of travel (a side-stepping character keeps its heading). It therefore inherits pelvis twist, and its ground-plane projection shrinks toward numerical ambiguity when the character bends far forward; :func:facing_frame is the whole-body, yaw-only alternative that avoids both. For the direction of motion, differentiate the ground position (the ground_*_vel columns).

Parameters:

Name Type Description Default
bvh Bvh

Input motion.

required
up_axis str or None

Signed axis string (e.g. '+y', '+z'). If None, uses bvh.world_up.

None
include_velocities bool

If True, append [ground_a_vel, ground_b_vel, heading_vel] to the output. Velocities are in coordinate-units/second and radians/second (heading is unwrapped before differentiating to avoid ±π jumps).

False
stencil optional

Only used with include_velocities=True. Same semantics as :func:joint_velocities — see that docstring for the full matrix. Default stencil="central", pad="edge" returns shape (F, 7); stencil="forward", pad="none" returns (F-1, 7); stencil="central", pad="none" returns (F-2, 7).

'central'
pad optional

Only used with include_velocities=True. Same semantics as :func:joint_velocities — see that docstring for the full matrix. Default stencil="central", pad="edge" returns shape (F, 7); stencil="forward", pad="none" returns (F-1, 7); stencil="central", pad="none" returns (F-2, 7).

'central'
degrees bool

If True, convert the heading_vel column from radians/second to degrees/second. Default False (radians). ground_*_vel columns are linear positions per second and are unaffected. Only used when include_velocities=True.

False

Returns:

Type Description
ndarray

Shape (F, 4) when include_velocities=False. When include_velocities=True the trailing 3 columns are [ground_a_vel, ground_b_vel, heading_vel] and the leading 4-column base is trimmed to match the chosen stencil × pad shape.

Columns: [ground_pos_a, ground_pos_b, heading_sin, heading_cos], optionally followed by [ground_a_vel, ground_b_vel, heading_vel]. a and b are the two ground-plane axes (non-up axes in the natural x, y, z order with the up axis removed).

foot_contacts(bvh: Bvh, foot_joints: list[str] | None = None, method: str = 'combined', coords: npt.NDArray[np.float64] | None = None, *, vel_threshold: float | None = None, vel_smooth_duration: float = 1.0 / 30.0, height_threshold: float | None = None, floor: float | str = 'auto', min_contact_duration: float = 0.1, min_gap_duration: float = 0.1, hysteresis: float = 0.25, adaptive: bool = False, height_reference: str = 'velocity', return_info: bool = False) -> npt.NDArray[np.float64] | tuple[npt.NDArray[np.float64], dict]

Detect binary foot contact labels per frame.

The default combines a velocity check (foot not moving) and a height check (foot near the ground) following the HuMoR heuristic — each signal catches a different failure mode of the other. method="velocity" and method="height" remain available as single-signal escape hatches.

Parameters:

Name Type Description Default
bvh Bvh

Input motion.

required
foot_joints list of str or None

Explicit foot joints (recommended). If None, falls back to :func:auto_detect_foot_joints which matches "foot"/ "toe" substrings then filters by skeletal topology.

None
method ('combined', 'velocity', 'height')

"combined" (default): foot is in contact when speed is below vel_threshold and height above floor is below height_threshold. "velocity" / "height": single signal.

"combined"
coords (ndarray, shape(F, N, 3))

Pre-computed spatial coordinates. Must be world-frame positions or a constant translation thereof (e.g. centered="first" output) — per-frame centerings such as centered="skeleton" distort the foot-speed signal. If None, world-frame positions are computed internally.

None
vel_threshold (float or None, keyword - only)

Speed threshold in world units per second. Defaults to 0.12 × skeleton_scale u/s where skeleton_scale is the mean rest-pose distance from the root to the foot joints (equivalent to the pre-v0.8 0.004 × skeleton_scale per frame at 30 fps). Scale-invariant across cm- and m-scale skeletons, and unaffected by finger/spine subdivision (unlike a median-bone-length reference, which shrinks when a skeleton has many short finger bones).

None
vel_smooth_duration (float, keyword - only)

Physical time span (seconds) the foot-speed estimator is conditioned over before thresholding. Displacement vectors are box-averaged over max(1, round(vel_smooth_duration / frame_time)) frames (capped at F - 1) before taking the norm — for interior frames this equals differencing positions ~vel_smooth_duration apart, making the signal (not just the threshold units) frame-rate independent: adjacent-frame differencing at 120 fps picks up high-frequency jitter that 30 fps differencing averages out, splitting genuine stance phases. Averaging the vectors (norm-of-mean, not mean-of-norms) lets oscillatory jitter cancel. Default 1/30 s — a 1-frame no-op at ≤ 30 fps (labels identical to the raw signal there), 4 frames at 120 fps. Set 0.0 to disable (raw adjacent-frame differencing). Trade-off: a single-frame glitch smears over up to one window (~33 ms), the same timescale the duration filters already treat as noise. Applies to the velocity signal only; the height signal is position-level and is never smoothed.

1.0 / 30.0
height_threshold (float or None, keyword - only)

Clearance above the estimated floor, in world units. Defaults to 0.013 × skeleton_scale. A foot is "low enough" when foot_height − floor < height_threshold.

None
floor float, ``"auto"`` or ``"min"``, keyword-only

Floor height along the raw world_up axis. "auto" (default) estimates it as the 2nd percentile of the per-frame minimum foot height — robust to occasional spurious low frames — always from the coords actually in use (the cached :attr:Bvh.floor_height fills in / is filled from this estimate on the default world-coords + auto-feet path). "min" uses the true minimum instead — exact, but a single glitched-low frame drags the floor down with it; it never reads or writes the cache. The two diverge on clips with marker noise or long airborne phases. Pass a float to pin the floor explicitly (e.g. floor=0.0 when the rig is already ground-aligned).

'auto'
min_contact_duration (float, keyword - only)

Morphological open: contact runs shorter than this many seconds are set to 0. Default 0.1 s (3 frames at 30 fps) — removes contact flickers shorter than 100 ms, which are physically implausible. Set to 0.0 to disable. Internally converted to frames via max(1, round(duration / frame_time)).

0.1
min_gap_duration (float, keyword - only)

Morphological close: non-contact gaps shorter than this many seconds are filled (set to 1). Default 0.1 s — bridges short interruptions in an otherwise continuous contact phase, catching pivot-foot artefacts where the joint briefly exceeds the velocity threshold even though the physical foot is planted. Set to 0.0 to disable.

0.1
hysteresis (float, keyword - only)

Schmitt-trigger band fraction (default 0.25). A frame enters swing only when its signal rises above threshold*(1+hysteresis) and a contact is kept only if it ever drops below threshold*(1-hysteresis) — so an isolated dip near the threshold no longer flips the label. Strictly suppresses boundary flicker (it cannot invent contacts). Set to 0.0 for the plain single-threshold behaviour.

0.25
adaptive (bool, keyword - only)

If True, derive each foot's thresholds from its own signal distribution (Otsu's bimodal split between the stance and swing clusters) instead of the fixed scale fraction, falling back to the fixed default for any foot whose signal is not convincingly bimodal (e.g. standing, or a foot that never clearly swings). Default False; recommended for known-locomotion clips where the fixed threshold under- or over-detects. :func:gait_parameters enables this by default — calling it declares the clip is locomotion.

False
height_reference ('velocity', 'floor')

How the default height_threshold is anchored (only used with method="combined" when height_threshold is None). "velocity" (default): per-foot stance-median calibration — each foot's threshold is set from the median of its clearance over the frames where that foot is slow (speed below vel_threshold, default 0.12 × skeleton_scale u/s), so retargeted mocap whose feet hover above the estimated floor still detects stance; on rigs whose feet reach the floor this reduces to the "floor" margin. "floor": the fixed 0.013 × skeleton_scale margin above the estimated floor — no per-foot calibration. The stance calibration presumes the joints contact the ground regularly; for arbitrary (non-foot) joint sets, :func:ground_contacts defaults to "floor" for exactly that reason.

"velocity"
return_info (bool, keyword - only)

If True, return (contacts, info) where info holds the detected joints, method, thresholds actually applied, estimated floor, skeleton scale, the hysteresis band, a per-foot confidence in [0, 1] (detection decisiveness), and unsupervised quality diagnostics: foot_skate (mean/max horizontal drift of a planted foot, ÷ skeleton scale — should be ~0), airborne_fraction (frames with no foot down — a false-negative signal), and height_at_contact (mean clearance during contact, per foot). With adaptive=True it also reports per-foot thresholds and adaptive_used_* flags. "skeleton_scale" is only present when auto-calibration ran. The velocity-smoothing span is echoed as vel_smooth_duration, with the effective window in frames as vel_smooth_frames (present whenever the velocity signal was computed).

False

Returns:

Type Description
ndarray of shape ``(F, num_foot_joints)``, or
tuple ``(ndarray, dict)`` when ``return_info=True``.

Binary contact labels (1.0 = contact, 0.0 = no contact). For "velocity"/"combined", frame 0 is propagated from frame 1 because velocity is undefined at frame 0. Column order matches foot_joints (or, for auto-detection, the order returned by :func:auto_detect_foot_joints).

Raises:

Type Description
ValueError
  • If method or height_reference is unknown.
  • If floor is a string other than "auto" or "min".
  • If no foot joints can be found or any named joint is missing from the skeleton.
  • If foot_joints is explicitly an empty list — contact detection needs at least one joint.
  • If vel_smooth_duration is negative.
  • When the height signal is involved and bvh.world_up is inconsistent with rest-pose geometry (feet above hips).
  • If frame_time == 0 and the velocity signal (units/second) or a nonzero duration filter needs a time base.
See Also

ground_contacts : The same detection engine for arbitrary (non-foot) joint sets — hands, knees, props. auto_detect_foot_joints : The detection used when foot_joints is None.

ground_contacts(bvh: Bvh, joints: Sequence[str | int], method: str = 'combined', coords: npt.NDArray[np.float64] | None = None, *, vel_threshold: float | None = None, vel_smooth_duration: float = 1.0 / 30.0, height_threshold: float | None = None, floor: float | str = 'auto', min_contact_duration: float = 0.1, min_gap_duration: float = 0.1, hysteresis: float = 0.25, adaptive: bool = False, height_reference: str = 'floor', return_info: bool = False) -> npt.NDArray[np.float64] | tuple[npt.NDArray[np.float64], dict]

Detect ground-contact labels for an arbitrary set of joints.

The same detection engine as :func:foot_contacts — velocity and/or clearance-above-floor thresholding with hysteresis and duration filters — but for any joint set: hands during floor work, knees in a crawl, a prop bone. Because the joints are not assumed to be feet, three foot-specific behaviors are dropped: there is no rest-pose "feet below hips" sanity check, the call never reads or writes the cached :attr:Bvh.floor_height (the floor estimated from an arbitrary joint set describes those joints, not the scene), and height_reference defaults to "floor" instead of :func:foot_contacts' "velocity" — the per-joint stance calibration behind "velocity" presumes regular ground contact, which feet in locomotion have and a hand that touches the floor twice does not.

.. warning:: With the default floor="auto" the floor is estimated from the given joints' own trajectories (2nd percentile of their per-frame minimum height). A joint set that never actually grounds makes its lowest hover point the "floor" and fabricates contacts there. Unless the joints genuinely reach the ground for a meaningful fraction of the clip, pass floor=bvh.floor_height (the scene floor from the feet) or an explicit float.

Parameters:

Name Type Description Default
bvh Bvh

Input motion.

required
joints sequence of str or int

The joints to test, as node names and/or node-space indices (rows of :meth:Bvh.node_positions; NumPy-style negative indices allowed). End sites are legal — fingertips and toe tips are often exactly the grounding points. Output column order matches this sequence.

required
method ('combined', 'velocity', 'height')

Same meaning as in :func:foot_contacts.

"combined"
coords (ndarray, shape(F, N, 3))

Pre-computed world-frame positions (or a constant translation thereof), as in :func:foot_contacts.

None
vel_threshold keyword - only

Same meaning and defaults as in :func:foot_contacts. The auto thresholds scale by the mean rest-pose distance from the root to the given joints (the same skeleton-scale rule, applied to this joint set) — so the defaults stay proportionate for short chains like hands. Mind the floor="auto" warning above.

None
vel_smooth_duration keyword - only

Same meaning and defaults as in :func:foot_contacts. The auto thresholds scale by the mean rest-pose distance from the root to the given joints (the same skeleton-scale rule, applied to this joint set) — so the defaults stay proportionate for short chains like hands. Mind the floor="auto" warning above.

None
height_threshold keyword - only

Same meaning and defaults as in :func:foot_contacts. The auto thresholds scale by the mean rest-pose distance from the root to the given joints (the same skeleton-scale rule, applied to this joint set) — so the defaults stay proportionate for short chains like hands. Mind the floor="auto" warning above.

None
floor keyword - only

Same meaning and defaults as in :func:foot_contacts. The auto thresholds scale by the mean rest-pose distance from the root to the given joints (the same skeleton-scale rule, applied to this joint set) — so the defaults stay proportionate for short chains like hands. Mind the floor="auto" warning above.

None
min_contact_duration keyword - only

Same meaning and defaults as in :func:foot_contacts. The auto thresholds scale by the mean rest-pose distance from the root to the given joints (the same skeleton-scale rule, applied to this joint set) — so the defaults stay proportionate for short chains like hands. Mind the floor="auto" warning above.

None
min_gap_duration keyword - only

Same meaning and defaults as in :func:foot_contacts. The auto thresholds scale by the mean rest-pose distance from the root to the given joints (the same skeleton-scale rule, applied to this joint set) — so the defaults stay proportionate for short chains like hands. Mind the floor="auto" warning above.

None
hysteresis keyword - only

Same meaning and defaults as in :func:foot_contacts. The auto thresholds scale by the mean rest-pose distance from the root to the given joints (the same skeleton-scale rule, applied to this joint set) — so the defaults stay proportionate for short chains like hands. Mind the floor="auto" warning above.

None
adaptive keyword - only

Same meaning and defaults as in :func:foot_contacts. The auto thresholds scale by the mean rest-pose distance from the root to the given joints (the same skeleton-scale rule, applied to this joint set) — so the defaults stay proportionate for short chains like hands. Mind the floor="auto" warning above.

None
height_reference ('floor', 'velocity')

Default "floor" — the fixed 0.013 × skeleton_scale margin above the floor. "velocity" enables :func:foot_contacts' per-joint stance calibration; only use it when the joints ground regularly enough to have stance statistics.

"floor"
return_info (bool, keyword - only)

As in :func:foot_contacts; info["joints"] always holds the resolved names (indices are mapped back through bvh.nodes).

False

Returns:

Type Description
ndarray of shape ``(F, num_joints)``, or tuple ``(ndarray, dict)`` when ``return_info=True``.

Binary contact labels (1.0 = contact); columns follow joints order.

Raises:

Type Description
ValueError
  • If method or height_reference is unknown.
  • If floor is a string other than "auto" or "min".
  • If joints is empty — contact detection needs at least one joint.
  • If a joint name is not in the skeleton.
  • If vel_smooth_duration is negative.
  • If frame_time == 0 and the velocity signal (units/second) or a nonzero duration filter needs a time base.
TypeError

If a joints entry is neither a str nor an int (bool is rejected explicitly — True/False silently indexing nodes 1/0 would be a trap).

IndexError

If a node index is out of range for bvh.nodes.

See Also

foot_contacts : The foot-specialized entry point (auto-detection, rest-pose sanity check, floor-height caching, height_reference="velocity" default).

auto_detect_foot_joints(bvh: Bvh, *, _rest_coords: npt.NDArray[np.float64] | None = None) -> list[str]

Auto-detect foot joint names by topology.

Algorithm:

  1. Substring match: candidates are joints whose names contain "foot" or "toe" (case-insensitive).
  2. Tip-descendant filter: keep only candidates that have an end site or a toe-named child. This drops IK helpers, which typically have no children.
  3. Most-distal filter: drop candidates whose subtree (any depth) contains another candidate. On a rig with Foot → ToeBase → EndSite, this keeps only ToeBase — the more distal, ground-contacting joint.
  4. Deterministic order: sort by rest-pose height along bvh.world_up (lowest first); alphabetical name within ties so the output is stable across runs.

If step 1 produces matches but step 2 drops all of them, the tip filter is skipped with a UserWarning (better than returning nothing for unusual rigs).

Parameters:

Name Type Description Default
bvh Bvh

Input skeleton.

required

Returns:

Type Description
list of str

Joint names in deterministic order. Empty if no candidates. Returning [] — never raising — is the contract for footless rigs (no "foot"/"toe" names, or matches with no tip descendants that step 2's fallback also rejects).

Notes

This is the same detection used internally by :func:foot_contacts when foot_joints=None. Call it directly to preview the detection or to feed an explicit list back in.

An empty result is a report, not an error: this function never raises on footless rigs. The contact detectors are where emptiness becomes fatal — :func:foot_contacts and :func:ground_contacts both raise ValueError when handed an empty joint list, so a silent [] cannot flow into a zero-column contact array.

Facing frame

The continuous per-frame facing basis — the vector form of the snapped Bvh.forward_at / Bvh.left_at axis labels.

FacingFrame = namedtuple('FacingFrame', ['forward', 'left', 'up', 'valid']) module-attribute

facing_frame(bvh: Bvh, coords: npt.NDArray[np.float64] | None = None) -> FacingFrame

Per-frame facing basis of the character, as continuous unit vectors.

Returns the orthonormal right-handed triple (forward, left, up) for every frame — the continuous form of the axis-label pair :meth:Bvh.forward_at / :meth:Bvh.left_at, which snap exactly this basis to the nearest signed world axis. Use the labels for rest-pose canonicalization and dataset-convention checks; use this function whenever the actual facing direction matters — the snapped label stays constant while a character turns through less than 90°, the vectors track the rotation frame by frame.

This is a yaw-only, gravity-aligned facing frame: up is the exact :attr:Bvh.world_up unit vector on every frame, so the basis only ever rotates about the vertical — deliberately not a pelvis orientation (no roll or pitch; a character bending forward keeps their facing frame level). It is also distinct from :func:root_trajectory's heading, which is a second facing estimate built differently — the root bone's rotation applied to the rest forward — not a direction of motion: that heading inherits pelvis twist and collapses toward numerical ambiguity when the character bends far forward, while this basis measures the whole-body L/R geometry and stays yaw-only. The two diverge whenever the pelvis turns or tilts relative to the body. Neither is velocity-based; for the direction of travel, differentiate ground position (root_trajectory's ground_*_vel columns).

Construction (per frame, all in world space): the leftward direction is the average of (left_pos - right_pos) over the L/R joint pairs in :attr:Bvh.lr_mapping, projected onto the horizontal plane and normalized; forward = leftward × up; left = up × forward (re-orthogonalized). The triple satisfies forward × left = up.

Fallback policy (mirrors :meth:Bvh.forward_at): frames with no usable L/R direction — the skeleton has no lr_mapping, or the frame's horizontal (left - right) average nearly vanishes / is nearly parallel to world_up (norm below 1e-6) — receive a constant fallback basis instead: forward from the rest-pose leftward crossed with world_up, or the arbitrary-but-stable per-up-axis default ('+z' for a y-up world) when no rest-pose L/R geometry exists either; left = up × forward keeps the triple orthonormal. The valid field reports exactly those frames: False where the basis is the fallback, not a measurement (all-False when the skeleton has no L/R pairs at all — the per-frame refinement of :attr:Bvh.has_lr_geometry). The fallback rows still carry the usable constant basis rather than nan, so rendering-style consumers can ignore valid; anything measuring facing should mask by it. The bvhplot follow camera applies the same policy — it holds its orientation on such frames rather than inventing a rotation.

Parameters:

Name Type Description Default
bvh Bvh

Input motion.

required
coords (ndarray, shape(F, N, 3))

Pre-computed spatial coordinates for all frames (as returned by Bvh.node_positions()). When provided, skips the per-call forward kinematics.

None

Returns:

Type Description
FacingFrame

Named tuple (forward, left, up, valid): three (F, 3) float64 arrays of per-frame unit vectors forming a right-handed orthonormal basis, and a (F,) bool array, False on frames whose basis is the constant fallback rather than a measurement (see the fallback policy above).

See Also

Bvh.forward_at, Bvh.left_at : The categorical (snapped axis label) form of the same construction. root_trajectory : Root-bone-orientation heading (a different facing estimate — see Notes above; not a travel direction).

Jerk

The third rung of the velocity → acceleration → jerk ladder.

node_jerk(bvh: Bvh, centered: str = 'world', in_frames: bool = False, coords: npt.NDArray[np.float64] | None = None, stencil: str = 'central', pad: str = 'edge') -> npt.NDArray[np.float64]

Compute per-node position jerk (third derivative) — (F, N, 3).

Applies the chosen stencil three times to the positions, the next rung of the velocity → acceleration → jerk ladder. The jerk magnitude np.linalg.norm(node_jerk(...), axis=-1) is the usual smoothness signal.

Parameters:

Name Type Description Default
bvh Bvh

Input motion.

required
centered str

Coordinate centering mode (default "world"). Ignored if coords is given.

'world'
in_frames bool

If True, units/frame³; else units/second³ (default).

False
coords (ndarray, shape(F, N, 3))

Pre-computed positions; computed via :meth:Bvh.node_positions if None.

None
stencil ('central', 'forward')

Finite-difference method applied three times. Default "central".

"central"
pad ('edge', 'none')

"edge" (default): output shape (F, N, 3). "none": drop boundary frames the stencil can't define — central drops 6 (F-6, N, 3); forward drops 3 (F-3, N, 3).

"edge"

Returns:

Type Description
ndarray

Per-node jerk. Composition identity: np.gradient(node_accelerations(), dt) equals node_jerk() exactly under the defaults (stencil="central", pad="edge").

Raises:

Type Description
ValueError

Too-short clip, frame_time == 0 with in_frames=False, or invalid parameter. Minimum frames: 7 for central+none, 4 for forward, 3 otherwise.

joint_jerk(bvh: Bvh, centered: str = 'world', in_frames: bool = False, coords: npt.NDArray[np.float64] | None = None, stencil: str = 'central', pad: str = 'edge') -> npt.NDArray[np.float64]

Per-joint position jerk (end sites excluded) — (F, J, 3).

The joint-axis subset of :func:node_jerk, index-aligned with :attr:Bvh.joint_angles. See :func:node_jerk for full docs. Raises ValueError if coords is not node-shaped (F, N, 3).

Smoothness metrics

Array-pure kernels on a speed profile — (T,) for a scalar, or (T, K) reduced per column — plus the smoothness(metric=…) dispatcher.

sparc(speed: npt.NDArray[np.float64], fs: float, padlevel: int = 4, fc: float = 10.0, amp_th: float = 0.05) -> float | npt.NDArray[np.float64]

Spectral arc length (SPARC) smoothness of a speed profile.

The negative arc length of the normalized Fourier magnitude spectrum over [0, fc] Hz — a smoothness measure that is robust to noise and invariant to amplitude/duration. Values are ≤ 0; closer to 0 is smoother.

Parameters:

Name Type Description Default
speed (ndarray, shape(T) or (T, K))

Speed profile, or a signed scalar velocity — the spectrum is taken of the values as given, not their magnitude, so the two are genuinely different inputs here (matching the reference implementation). A 2-D input is K independent profiles in columns, reduced per column (the FFT is batched along the time axis; each column's result is identical to the 1-D call on that column).

required
fs float

Sampling rate in Hz.

required
padlevel int

Zero-padding exponent: nfft = 2**(ceil(log2(T)) + padlevel) (default 4).

4
fc float

Upper cutoff frequency in Hz (default 10.0).

10.0
amp_th float

Normalized amplitude threshold selecting the spectral band (default 0.05).

0.05

Returns:

Type Description
float or ndarray

The spectral arc length (SAL) — a scalar for (T,) input, a (K,) array for (T, K). nan for a zero speed profile (a perfectly still joint), whose spectrum carries no energy and whose smoothness is therefore undefined.

Notes

Source: Balasubramanian et al. 2015, "On the analysis of movement smoothness." Reimplemented in NumPy; validated against the authors' reference output (see tests/test_smoothness_golden.py).

dimensionless_jerk(speed: npt.NDArray[np.float64], fs: float, normalize: str = 'peak_speed', amplitude: float | npt.NDArray[np.float64] | None = None) -> float | npt.NDArray[np.float64]

Dimensionless jerk (DLJ) smoothness of a speed profile.

Integrated squared jerk made scale-invariant by dividing out movement duration and size — -(duration³ / peak²) · ∫ (d²v/dt²)² dt at the default. More negative is less smooth.

Parameters:

Name Type Description Default
speed (ndarray, shape(T) or (T, K))

Speed profile, or a signed scalar velocity (Hogan & Sternad define the measure on x(t), "any scalar coordinate", whose derivative carries a sign). The signal is differentiated as given; only the normalizer takes magnitudes. A 2-D input is K independent profiles in columns, reduced per column.

required
fs float

Sampling rate in Hz.

required
normalize ('peak_speed', 'mean_speed', 'amplitude')

Which published normalizer to apply to the jerk integral (default "peak_speed"). All three are dimensionless and keep the same negative-is-less-smooth sign; they differ in magnitude only. The two speed-based ones take magnitudes (max|v|, mean|v|), so a signed velocity normalizes by its excursion rather than by a mean that cancels on an out-and-back. See the Notes for how to choose.

"peak_speed"
amplitude float or ndarray

The movement extent A — required by normalize="amplitude" and rejected by the other two. Scalar, or shape (K,) to match a (T, K) speed input.

None

Returns:

Type Description
float or ndarray

The dimensionless jerk (≤ 0) — a scalar for (T,) input, a (K,) array for (T, K). nan when the normalizing extent is zero (an all-zero speed profile, or amplitude=0), matching the degenerate-input convention of :func:sparc and :func:speed_metric.

Raises:

Type Description
ValueError

If normalize is unknown, if normalize="amplitude" is used without amplitude=, or if amplitude= is passed with one of the speed-based normalizers.

Notes

Hogan & Sternad 2009 give the measure in three forms::

∫(d²v/dt²)² dt · D⁵/A²   ≡   · D³/v_mean²   |   variant: · D³/v_peak²

The first two are the same measure — A = v_mean · D — so "mean_speed" is exactly "amplitude" evaluated at the arc length the speed profile itself implies, A = ∫|v| dt. Passing amplitude= is only a different measure when the extent comes from somewhere else, most usefully the endpoint displacement ‖p_T − p_0‖, which is smaller than the arc length for any path that is not straight. (For the arc length of a trajectory, prefer "mean_speed" over amplitude=geometry.path_length(traj): the chord sum uses a different quadrature than this function's integral and will not agree exactly.)

"peak_speed" — the default, and what the widely used reference implementation computes — is genuinely distinct from the other two. It differs by a factor of (v_mean / v_peak)², which depends on the shape of the speed profile rather than being constant, so a bell-shaped reach and a plateaued sustained movement of equal extent and duration rank differently under it. That factor is :func:speed_metric, so dimensionless_jerk(v, fs, "mean_speed") equals dimensionless_jerk(v, fs) / speed_metric(v)**2.

Reproducing a published DLJ figure therefore means matching its convention; the default is unchanged from earlier pybvh releases.

Source: Hogan & Sternad 2009; Balasubramanian et al. (the "peak_speed" form). Validated against the reference output.

log_dimensionless_jerk(speed: npt.NDArray[np.float64], fs: float, normalize: str = 'peak_speed', amplitude: float | npt.NDArray[np.float64] | None = None) -> float | npt.NDArray[np.float64]

Log dimensionless jerk (LDLJ) — -ln|DLJ|.

The log transform of :func:dimensionless_jerk, the form most used in practice. More negative is less smooth.

Parameters:

Name Type Description Default
speed (ndarray, shape(T) or (T, K))

Speed profile; a 2-D input is K independent profiles in columns, reduced per column.

required
fs float

Sampling rate in Hz.

required
normalize ('peak_speed', 'mean_speed', 'amplitude')

Normalizer forwarded to :func:dimensionless_jerk (default "peak_speed"). Under the log transform the choice becomes an additive offset of -ln of the normalizer ratio.

"peak_speed"
amplitude float or ndarray

Movement extent forwarded to :func:dimensionless_jerk; required by normalize="amplitude" and rejected by the other two.

None

Returns:

Type Description
float or ndarray

-ln|DLJ| — a scalar for (T,) input, a (K,) array for (T, K). A zero-jerk (constant-speed) profile is perfectly smooth and returns +inf.

Notes

Source: Balasubramanian et al. Validated against the reference output.

number_of_peaks(speed: npt.NDArray[np.float64], min_height: float | None = None) -> int | npt.NDArray[np.int_]

Number of local maxima in a speed profile.

A simple smoothness proxy — a single smooth movement has one velocity peak; more peaks mean more sub-movements.

Parameters:

Name Type Description Default
speed (ndarray, shape(T) or (T, K))

Speed profile; a 2-D input is K independent profiles in columns, counted per column.

required
min_height float or None

Minimum height a maximum must reach to be counted, in the same units as speed. None (default) counts every strict local maximum, however small. See Notes — the default makes this metric sensitive to noise, and the literature says so.

None

Returns:

Type Description
int or ndarray

Count of qualifying strict interior local maxima — a scalar for (T,) input, a (K,) array for (T, K).

Notes

Strictness. A sample counts when it is strictly greater than both neighbours. The alternative, allowing ties, would count every sample of a flat-topped peak separately, which is worse; the cost of strictness is that an exactly flat maximum — two or more equal adjacent samples — counts as zero peaks rather than one. Exact ties are rare in float data but reachable after quantization or box-filter smoothing.

Sign. The comparison is on the values as given, not their magnitude, so for a signed velocity this counts maxima of the signed signal — velocity peaks in one direction — and a trough at -5 beside neighbours at -6 is a maximum. That is the right reading for a scalar velocity and a no-op for a speed profile; use min_height if you want only the positive-going peaks.

Height threshold. With no threshold, every micro-fluctuation is a "sub-movement", so on noisy data the count tracks the noise rather than the movement — Hogan & Sternad 2009 warn about exactly this (peak counting is "prone to spurious peaks", and separately blind to arrests). The default is unthresholded because that is what the metric means in its source literature, not because it is the better choice for real data; set min_height when the profile is noisy. It is absolute rather than a fraction of the peak so it composes with whatever normalization you have already applied — for a relative threshold, pass min_height=0.05 * speed.max().

Source: Balasubramanian et al. (number-of-peaks metric); Hogan & Sternad 2009 (its noise sensitivity). Note the pinned SPARC reference implementation ships no peak-counting function, so unlike :func:sparc and :func:dimensionless_jerk this metric has no golden-reference test — only the properties asserted in tests/test_analysis_primitives.py.

speed_metric(speed: npt.NDArray[np.float64]) -> float | npt.NDArray[np.float64]

Mean-to-peak speed ratio — mean|v| / max|v|, in [0, 1].

A bell-shaped (smooth) speed profile has a low ratio; a flat plateau approaches 1.

Parameters:

Name Type Description Default
speed (ndarray, shape(T) or (T, K))

Speed profile, or a signed scalar velocity; a 2-D input is K independent profiles in columns, reduced per column.

required

Returns:

Type Description
float or ndarray

The mean/peak ratio — a scalar for (T,) input, a (K,) array for (T, K). nan for an all-zero profile.

Notes

Both reductions take the magnitude. For a non-negative speed profile that is simply mean(v) / max(v); for a signed velocity it is what keeps the documented [0, 1] range true, since a raw mean cancels toward zero on an out-and-back movement and would yield a ratio at or below zero against a positive peak.

The same magnitude convention is used by :func:dimensionless_jerk's "mean_speed" normalizer, which makes this the exact conversion factor between its two distinct normalizers: dimensionless_jerk(v, fs, "mean_speed") == dimensionless_jerk(v, fs) / speed_metric(v)**2.

Source: Balasubramanian et al. (speed-metric); Flash & Hogan.

integrated_squared_jerk(speed: npt.NDArray[np.float64], fs: float) -> float | npt.NDArray[np.float64]

Integrated squared jerk — ∫ (d²v/dt²)² dt (dimensional).

Accepts (T,) (scalar out) or (T, K) ((K,) out, reduced per column).

mean_squared_jerk(speed: npt.NDArray[np.float64], fs: float) -> float | npt.NDArray[np.float64]

Mean squared jerk — mean((d²v/dt²)²).

Accepts (T,) (scalar out) or (T, K) ((K,) out, reduced per column).

rms_squared_jerk(speed: npt.NDArray[np.float64], fs: float) -> float | npt.NDArray[np.float64]

Root-mean-square jerk — sqrt(mean((d²v/dt²)²)).

Accepts (T,) (scalar out) or (T, K) ((K,) out, reduced per column).

smoothness(speed: npt.NDArray[np.float64], fs: float, metric: str = 'sparc', **kwargs: Any) -> float | npt.NDArray[np.float64]

Dispatch to a named smoothness metric on a speed profile.

Parameters:

Name Type Description Default
speed (ndarray, shape(T) or (T, K))

Speed profile; a 2-D input is K independent profiles in columns, reduced per column.

required
fs float

Sampling rate in Hz.

required
metric str

One of "sparc" (default), "dimensionless_jerk", "log_dimensionless_jerk", "integrated_squared_jerk", "mean_squared_jerk", "rms_squared_jerk", "number_of_peaks", "speed_metric".

'sparc'
**kwargs Any

Metric-specific options: padlevel / fc / amp_th for "sparc"; normalize / amplitude for "dimensionless_jerk" and "log_dimensionless_jerk"; min_height for "number_of_peaks". The remaining metrics take none. See each kernel's own docstring for defaults.

{}

Returns:

Type Description
float or ndarray

The selected smoothness value — a scalar for (T,) input, a (K,) array for (T, K).

Raises:

Type Description
ValueError

If metric is unknown.

Signal reductions

Scalar summaries of a speed or activity signal.

VelocityReductions = namedtuple('VelocityReductions', ['peak', 'mean', 'peak_to_mean', 'peak_acceleration', 'peak_deceleration']) module-attribute

velocity_reductions(speed: npt.NDArray[np.float64], fs: float) -> VelocityReductions

Scalar reductions of a speed profile.

Parameters:

Name Type Description Default
speed (ndarray, shape(T) or (T, K))

Speed profile; a 2-D input is K independent profiles in columns, reduced per column (every field of the result becomes a (K,) array).

required
fs float

Sampling rate in Hz; scales peak_acceleration and peak_deceleration into units/second². Required — like the other array-pure kernels (:func:sparc, :func:smoothness), the time base must be stated explicitly.

required

Returns:

Type Description
VelocityReductions

Named tuple (peak, mean, peak_to_mean, peak_acceleration, peak_deceleration) — floats for (T,) input, (K,) arrays for (T, K). peak_acceleration is the largest instantaneous rate of speed increase and peak_deceleration the largest rate of speed decrease; both are >= 0 (0 when the speed never rises / never falls).

Notes

All fields reduce the values as given — none takes a magnitude first. For a genuine (non-negative) speed profile that is the only reading; for a signed scalar velocity it means peak is the signed maximum (not the excursion max|v|) and peak_to_mean is unbounded (a raw mean cancels toward zero on an out-and-back movement, going nan at exactly zero). This differs from :func:speed_metric, whose documented [0, 1] range forces the magnitude convention mean|v| / max|v| — so for signed input peak_to_mean is not 1 / speed_metric. Take np.abs first if you want magnitude reductions of a signed velocity.

peak_acceleration and peak_deceleration are the positive and negative extrema (clamped at 0) of the per-frame speed-derivative series d‖v‖/dt: when speed is the norm of :func:node_velocities with stencil="forward", pad="none", they equal the extrema of :func:node_speed_derivative under the same convention (asserted in the test suite so the two can't drift).

Source: Pollick et al., Halovic & Kroos, Samadani et al.

zero_crossings(signal: npt.NDArray[np.float64], axis: int = 0) -> npt.NDArray[np.int_]

Count sign changes of a signal along an axis.

Strict crossings only — consecutive samples with a product < 0; exact zeros are not counted as crossings. The consequence: a sign change that passes through an exact zero sample (+1, 0, -1) counts zero crossings, not one. The alternative convention — sign-change counting, np.diff(np.sign(x)) != 0 — counts it (twice, unless zeros are carried forward). Exact zeros are rare in float data but reachable after quantization, rectification, or box-filter smoothing.

Parameters:

Name Type Description Default
signal ndarray

Input signal.

required
axis int

Axis along which to count (default 0).

0

Returns:

Type Description
ndarray

Crossing counts with axis removed (a scalar for 1-D input).

Notes

Source: Zhao & Badler (motion feature counts).

active_segments(speed: npt.NDArray[np.float64], threshold: float) -> npt.NDArray[np.bool_]

Boolean mask of "active" (above-threshold) samples.

Parameters:

Name Type Description Default
speed ndarray

Speed (or any non-negative activity) signal.

required
threshold float

Activity threshold; samples strictly above it are active. No hidden default — the caller picks the threshold, keeping this a theory-neutral primitive.

required

Returns:

Type Description
ndarray of bool

speed > threshold.

Notes

Source: Pollick et al., Bernhardt & Robinson.

active_duration(speed: npt.NDArray[np.float64], threshold: float, fs: float) -> float | npt.NDArray[np.float64]

Total time spent active — active sample count / fs.

Parameters:

Name Type Description Default
speed (ndarray, shape(T) or (T, K))

Speed signal; a 2-D input is K independent signals in columns, reduced per column.

required
threshold float

Activity threshold (see :func:active_segments).

required
fs float

Sampling rate in Hz. Required — the time base must be stated explicitly, matching the other array-pure kernels.

required

Returns:

Type Description
float or ndarray

Active duration in seconds — a scalar for (T,) input, a (K,) array for (T, K).

Energy, gait & range of motion

Kinetic energy and the spatiotemporal gait parameters (all computed in one pass by gait_parameters).

kinetic_energy(bvh: Bvh, masses: npt.NDArray[np.float64] | Mapping[str, float] | None = None, centered: str = 'world', stencil: str = 'central', pad: str = 'edge') -> npt.NDArray[np.float64]

Per-frame kinetic energy summed over joints.

With masses, Σ_j ½ m_j ‖v_j‖² (true kinetic energy). Without, Σ_j ‖v_j‖² (unit-mass energy proxy) — pybvh ships no segment-mass model, so pass anatomical masses for physical energy. This is a point-mass-at-joints model; rigid-body energy (segment-CoM masses, rotational inertia) is not supported.

Parameters:

Name Type Description Default
bvh Bvh

Input motion.

required
masses ndarray of shape (J,), or mapping {joint_name: mass}

Per-joint masses (end sites excluded). A mapping keyed by joint name is validated for exact coverage and is the safer form — an array relies on matching Bvh.joint_names order. Default None → unit-mass proxy.

None
centered str

Centering mode for the velocities (default "world").

'world'
stencil optional

Velocity finite-difference convention (see :func:joint_velocities).

'central'
pad optional

Velocity finite-difference convention (see :func:joint_velocities).

'central'

Returns:

Type Description
ndarray

Per-frame energy; leading length follows the velocity stencil × pad shape.

Raises:

Type Description
ValueError

If a masses mapping does not cover the joints exactly, a masses array has the wrong length, or the masses do not sum to a positive total (an all-zero mass vector would silently zero the energy). The unit-mass masses=None default is unaffected.

Notes

Source: Głowinski et al., Piana et al., Lu et al. 2025.

cadence(bvh: Bvh, foot_joints: list[str] | None = None, *, contacts: npt.NDArray[np.float64] | None = None) -> float

Step rate — foot-contact onsets per second.

A projection of :func:gait_parameters (the single definition of every gait scalar).

Parameters:

Name Type Description Default
bvh Bvh

Input motion.

required
foot_joints list of str

Foot joints; auto-detected if None.

None
contacts (ndarray, shape(F, n_feet))

Pre-computed contact labels (see :func:gait_parameters).

None

Returns:

Type Description
float

Steps per second (nan if the clip has no duration — the rate is undefined, not zero).

Notes

Unit. Steps per second, consistent with every other rate in pybvh (Hz, units/s). The clinical gait literature — including the sources below — reports cadence in steps per minute; multiply by 60 to compare against published figures. Onsets are pooled over all contact columns, so passing more than two contact-bearing joints per foot inflates the count proportionally.

Source: Crane & Gross, Gross et al. 2012, Karg et al. 2010.

stride_length(bvh: Bvh, foot_joints: list[str] | None = None, *, contacts: npt.NDArray[np.float64] | None = None) -> float

Mean stride length — distance between successive same-foot landings.

The standard, foot-measured stride: for each foot, the horizontal distance between its position at consecutive contact onsets, pooled over feet and averaged. A projection of :func:gait_parameters — see that for the full spatiotemporal set (variability, step length, stance, double-support, asymmetry) computed in one pass.

Parameters:

Name Type Description Default
bvh Bvh

Input motion.

required
foot_joints list of str

Foot joints; auto-detected if None.

None
contacts (ndarray, shape(F, n_feet))

Pre-computed contact labels (see :func:gait_parameters).

None

Returns:

Type Description
float

Mean stride length in skeleton units (nan if no foot completes a stride — fewer than two contacts).

Notes

Source: Crane & Gross, Gross et al. 2012, Karg et al. 2010.

walking_pace(bvh: Bvh) -> float

Mean horizontal speed — root ground-path length per second.

Same definition as the walking_pace field of :func:gait_parameters, but computed directly from the root path so it needs no foot joints or contact detection. Note it only approximates stride_length × cadence / 2 (exact for straight, steady, symmetric gait; it diverges on curved or irregular walking because the root path and the foot landings measure different things).

Parameters:

Name Type Description Default
bvh Bvh

Input motion.

required

Returns:

Type Description
float

Horizontal units per second (nan if the clip has no duration — the rate is undefined, not zero).

Notes

Source: Crane & Gross, Gross et al. 2012.

GaitParameters = namedtuple('GaitParameters', ['cadence', 'walking_pace', 'stride_length', 'stride_cv', 'step_length', 'stance_fraction', 'double_support_fraction', 'asymmetry']) module-attribute

gait_parameters(bvh: Bvh, foot_joints: list[str] | None = None, *, contacts: npt.NDArray[np.float64] | None = None) -> GaitParameters

Spatiotemporal gait parameters in one pass.

Bundles the foot-measured gait analysis: cadence (onsets/s), walking_pace (root ground speed), stride_length and its coefficient of variation stride_cv (landing→next-same-foot-landing), step_length (forward advance between successive any-foot landings, measured along the direction of travel so step width is excluded), stance_fraction (mean fraction of a cycle a foot is planted), double_support_fraction (fraction of frames with ≥2 feet planted), and asymmetry (left/right stride difference). Underdetermined fields are nan — uniformly, across all eight (e.g. asymmetry without one identifiable left and right foot, stride_length if no foot completes two contacts, step_length if there is no net travel, cadence and walking_pace on a zero-duration clip).

These are kinematic — computed from foot positions and contact timing alone. Dynamic gait analysis (joint torques, ground-reaction force, mechanical work) needs a physical model and is out of scope.

Conventions worth naming, since published gait figures differ on all three:

  • stride_cv pools each foot's deviations from that same foot's mean before taking the standard deviation, then divides by the overall mean stride. The plainer alternative — the CV of the pooled stride sample — folds left/right asymmetry into "variability"; here the two are separate fields, and they diverge exactly in proportion to asymmetry.
  • asymmetry is |mean_L − mean_R| / (½(mean_L + mean_R)): unsigned and unitless. Robinson's Symmetry Index, standard in the gait literature, is the same ratio kept signed and ×100 — multiply by 100 for SI magnitude; the direction of the asymmetry is not recoverable from this field.
  • step_length projects onto a single whole-clip progression chord (the root's first→last horizontal displacement), not a per-step heading. On a curved walk this systematically shortens the value, and a closed loop has no net travel and returns nan. stride_length by contrast is the full Euclidean distance between same-foot landings, so it includes lateral step width while step_length excludes it — the two are not the 2:1 pair a clinical report would use on curved or wide-stance gait.

Two fields pick one convention among published ones. stride_cv is a within-foot CV: each foot's deviations from its own mean stride are pooled, and their std is divided by the overall mean — so a steady but asymmetric gait reads as low-variability, with the L/R difference reported separately in asymmetry. The plain pooled CV (std / mean over all strides regardless of foot), the other common definition, folds as

Parameters:

Name Type Description Default
bvh Bvh

Input motion.

required
foot_joints list of str

Foot joints; auto-detected if None.

None
contacts (ndarray, shape(F, n_feet))

Pre-computed contact labels (column order matching foot_joints); otherwise :func:foot_contacts is run with adaptive=True — gait input is locomotion by definition, which is the documented precondition for the adaptive per-foot thresholds (the fixed defaults under-detect stance on retargeted mocap whose feet hover above the estimated floor). Pass explicit contacts for full control over the detection.

None

Returns:

Type Description
GaitParameters

Named tuple of the eight parameters above.

Raises:

Type Description
ValueError

If no foot joints can be found.

Notes

Source: Crane & Gross, Gross et al. 2012, Karg et al. 2010.

range_of_motion(signal: npt.NDArray[np.float64], axis: int = 0) -> npt.NDArray[np.float64]

Peak-to-peak range of a signal — max − min along an axis.

For a joint-angle channel this is its range of motion over the clip.

Parameters:

Name Type Description Default
signal ndarray

Input signal (e.g. a joint-angle time series).

required
axis int

Axis to reduce over (default 0, the frame axis).

0

Returns:

Type Description
ndarray

The peak-to-peak range with axis removed.

Notes

Source: gait / biomechanics range-of-motion descriptors.

Scale & covariance descriptors

Skeleton-size normalization and fixed-size sequence statistics.

skeleton_size(bvh: Bvh, foot_joints: list[str] | None = None) -> float

Absolute skeleton scale — mean rest-pose root-to-foot distance.

A size proxy that scales linearly with the whole skeleton (≈ half the standing height for a humanoid). Only the leg chain contributes, so finger/spine subdivision does not affect it. This is the public name for the scale foot_contacts uses internally to set its thresholds; use it for size normalization. For the relative scale between two skeletons, see :func:relative_scale_factor.

A skeleton whose size cannot be measured — auto-detection finds no feet, or every foot sits exactly on the root — raises rather than returning a substitute: any fabricated number (1.0 reads as a plausible metre-scale humanoid, nan poisons everything scaled by it) would be indistinguishable from, or worse than, a measurement. Catch the error and choose your own scale if a total function is needed. (foot_contacts' internal threshold scale keeps a private 1.0 fallback for degenerate rigs — a threshold must exist even where a measurement does not.)

Parameters:

Name Type Description Default
bvh Bvh

Input skeleton.

required
foot_joints list of str

Foot joints; auto-detected from topology if None. Explicitly passed names must exist in the skeleton (ValueError otherwise).

None

Returns:

Type Description
float

Mean rest-pose distance from the root to the foot joints (always > 0).

Raises:

Type Description
ValueError

If an explicitly passed foot joint name is not in the skeleton; if auto-detection finds no foot joints; or if all foot joints coincide with the root in the rest pose (no measurable size).

Notes

Source: gait/biomech normalization (Troje, Karg et al.).

relative_scale_factor(reference: npt.NDArray[np.float64], target: npt.NDArray[np.float64], *, centered: bool = False) -> float

Least-squares uniform scale matching target to reference.

The scalar s minimizing ‖reference − s·target‖² over all coordinates — i.e. s = ⟨reference, target⟩ / ⟨target, target⟩ (Troje- style size normalization between two skeletons or poses). Both arrays must share shape (e.g. two (N, 3) rest poses, or (F, N, 3) sequences).

This is the relative scale between skeletons; for a single skeleton's absolute size, see :func:skeleton_size.

By default the fit is taken about the coordinate origin — neither array is mean-centered first. The Procrustes/Umeyama convention centers both point sets on their centroids before fitting scale; the two agree only when the poses are already centered, and diverge in proportion to the centroid offset. That makes the origin form right for rest poses (root at the origin) and wrong for world-frame sequences with a translated root — pass centered=True for those. Note centered=True is the scale-only Procrustes fit: no rotation is estimated, so it is not the full Umeyama similarity estimate, whose scale differs once a rotation is jointly fitted.

Parameters:

Name Type Description Default
reference ndarray

The pose/sequence to match.

required
target ndarray

The pose/sequence being scaled. Same shape as reference.

required
centered (bool, keyword - only)

If True, subtract each array's centroid — the mean over every axis except the last (i.e. over all points, and all frames for a sequence) — before fitting. Default False (fit about the origin).

False

Returns:

Type Description
float

The optimal scale s (nan if target is all-zero — or, with centered=True, constant, since a centered constant array is all-zero).

Notes

Source: Troje 2002 (pose normalization); Umeyama 1991 for the centered convention.

cov3dj(pos: npt.NDArray[np.float64]) -> npt.NDArray[np.float64]

Covariance of 3D joint positions over time (Cov3DJ).

Flattens each frame's joints to a 3N vector and returns the (3N, 3N) population covariance across frames (divides by F, not F − 1) — a fixed-size pose descriptor independent of sequence length.

Parameters:

Name Type Description Default
pos (ndarray, shape(F, N, 3))

Per-frame joint positions.

required

Returns:

Type Description
ndarray, shape (3N, 3N)

The population covariance matrix.

Notes

Source: Hussein et al. (Cov3DJ).

lagged_covariance(signal: npt.NDArray[np.float64], lag: int) -> npt.NDArray[np.float64]

Lagged covariance matrix — M(l) = (1/(T−l)) Σ_t (v_{t+l} − v̄)(v_t − v̄)ᵀ.

Captures temporal structure between channels at a fixed lag: the covariance between the signal and itself lag samples earlier, averaged over the T − l overlapping sample pairs (so every entry is a mean, independent of the lag). The signal is centered on its temporal mean first — a true covariance, so a constant offset contributes nothing. lag=0 reduces to the ordinary population covariance of the channels (cf. :func:cov3dj).

Parameters:

Name Type Description Default
signal (ndarray, shape(T, D))

Multichannel signal (time × channels).

required
lag int

Non-negative lag in samples.

required

Returns:

Type Description
(ndarray, shape(D, D))

The lagged covariance.

Raises:

Type Description
ValueError

If lag is negative or >= T.

Notes

Source: Venture et al. (lagged covariance descriptors).