Skip to content

Features

features

ML pipeline feature extraction for BVH motion data.

All functions are standalone and take a :class:~pybvh.bvh.Bvh object as their first argument. Thin wrapper methods on the Bvh class delegate to these functions.

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.

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.

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.

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(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.

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', centered: str = 'world', coords: npt.NDArray[np.float64] | None = None, *, vel_threshold: float | None = None, height_threshold: float | None = None, floor: float | str = 'auto', min_contact_duration: float = 0.1, min_gap_duration: float = 0.1, 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"
centered str

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

'world'
coords (ndarray, shape(F, N, 3))

Pre-computed spatial coordinates.

None
vel_threshold (float or None, keyword - only)

Speed threshold in world units per frame. Defaults to 0.004 × skeleton_scale where skeleton_scale is the mean rest-pose distance from the root to the foot joints. 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
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 or ``"auto"``, 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. 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
return_info (bool, keyword - only)

If True, return (contacts, info) where info is a dict holding the detected joints, the thresholds actually applied, the estimated floor, the skeleton scale used for auto- calibration, and the method used. Useful for debugging and for downstream pipelines that need to record the detection parameters. "skeleton_scale" is only present when auto-calibration ran (i.e., at least one threshold was left at its default).

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 is unknown.
  • If floor is a string other than "auto".
  • If no foot joints can be found or any named joint is missing from the skeleton.
  • When the height signal is involved and bvh.world_up is inconsistent with rest-pose geometry (feet above hips).

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.

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.

feature_array_layout(*, num_joints: int, num_feet: int = 0, representation: str = '6d', include_root_pos: bool = True, include_velocities: bool = False, include_foot_contacts: bool = False) -> dict[str, slice]

Column layout of the array returned by :func:to_feature_array.

Returns a dict mapping block name to column slice so callers can write feat[:, layout['rotations']] without counting columns. Pure function — no :class:~pybvh.bvh.Bvh required; useful for model-shape setup before any data is loaded.

Parameters:

Name Type Description Default
num_joints int

Number of joints (excluding end sites). Used for both the rotation and velocity blocks: velocities are per-joint (not per-node), aligning with the rotation block's joint axis.

required
num_feet int

Number of foot joints for contact detection. Required (>0) when include_foot_contacts=True.

0
representation str

Rotation representation: 'euler', 'axisangle' (3 values per joint), 'quaternion' (4), '6d' (6, default), 'rotmat' (9).

'6d'
include_root_pos bool

Mirror the flags of :func:to_feature_array.

True
include_velocities bool

Mirror the flags of :func:to_feature_array.

True
include_foot_contacts bool

Mirror the flags of :func:to_feature_array.

True

Returns:

Type Description
dict

{block_name: slice} with keys drawn from {"root_pos", "rotations", "velocities", "foot_contacts"} depending on the flags.

Raises:

Type Description
ValueError

If representation is unknown, or if include_foot_contacts=True but num_feet == 0.

to_feature_array(bvh: Bvh, representation: str = '6d', include_root_pos: bool = True, include_velocities: bool = False, include_foot_contacts: bool = False, centered: str = 'world', foot_joints: list[str] | None = None, stencil: str = 'central', pad: str = 'edge') -> npt.NDArray[np.float64]

Export motion as a single flat feature array for ML pipelines.

Composes root position, joint rotations, velocities, and foot contacts into a single (F, D) array ready for model input.

Parameters:

Name Type Description Default
bvh Bvh

Input motion.

required
representation str

Rotation representation: 'euler', '6d' (default), 'quaternion', 'axisangle', or 'rotmat' (9 values per joint as a flattened 3×3).

'6d'
include_root_pos bool

If True (default), include root position (3 columns).

True
include_velocities bool

If True, include joint velocity features.

False
include_foot_contacts bool

If True, include foot contact labels.

False
centered str

Coordinate centering mode (default "world").

'world'
foot_joints list of str or None

Foot joints for contact detection. Only used when include_foot_contacts=True.

None
stencil optional

Only affect output when include_velocities=True. Same semantics as :func:joint_velocities. The root-position, rotation, and foot-contact blocks are trimmed in time so all blocks align with the velocity shape.

'central'
pad optional

Only affect output when include_velocities=True. Same semantics as :func:joint_velocities. The root-position, rotation, and foot-contact blocks are trimmed in time so all blocks align with the velocity shape.

'central'

Returns:

Type Description
ndarray, shape (F, D), (F-1, D), or (F-2, D)

See :func:feature_array_layout for the column layout. Leading dimension depends on include_velocities and the stencil × pad combination.

Raises:

Type Description
ValueError

If representation is unknown, or stencil / pad is invalid.