Skip to content

API Reference

Find a function

The fastest route from "I want to…" to the exact call. Everything visual is also drawn, one picture per feature, in the Gallery.

I want to… Call Reference
Load / write a BVH file pybvh.read_bvh_file(path) / bvh.write(path) I/O
Load a whole directory read_bvh_directory("data/", parallel=True) Batch
Reconcile mixed skeletons / fps / up-axes harmonize(clips) batch.harmonize
Get 3D joint positions (forward kinematics) bvh.node_positions() Bvh.node_positions
Convert rotation representations bvh.to_quat(), bvh.to_6d(), … Rotations
Interpolate rotations / rigid transforms rotations.quat_slerp(...) / rotations.screw_interpolate(...) Rotations & SE(3)
Detect foot contacts bvh.foot_contacts() analysis.foot_contacts
Measure gait (cadence, stride, symmetry) bvh.gait_parameters() analysis.gait_parameters
Score movement smoothness (SPARC, jerk) bvh.smoothness(joint, metric="sparc") analysis.smoothness
Velocities / accelerations / jerk bvh.joint_velocities()bvh.node_jerk() Analysis
Trajectory shape (curvature, path length, …) bvh.curvature(joint), bvh.path_length(joint) Geometry
Pose extent & centre of mass bvh.bounding_box(), bvh.center_of_mass() Geometry
Relative pose of two segments, SE(3) twists rotations.relative_transform(...), se3_log Rotations & SE(3)
Augment data (mirror, rotate, noise, …) bvh.mirror(), bvh.rotate_vertical(a), … Transforms
Export one ML-ready feature array bvh.to_feature_array(representation="6d") features.to_feature_array
Stack many clips into one array batch_to_numpy(clips, pad=True) batch.batch_to_numpy
Smooth / differentiate / FFT a signal signal.box_filter_smooth(...), signal.fft_magnitude(...) Signal
Visualize (snapshot, video, interactive) bvh.plot_frame(), bvh.render("out.mp4"), bvh.play() Visualization
Edit the skeleton (retarget, scale, subset) bvh.retarget(ref), bvh.extract_joints([...]) Bvh.retarget
Slice / concatenate / resample frames bvh[10:50], bvh + other, bvh.resample(30) Bvh Class
Fix the up axis or facing convention bvh.reorient_world_up('+z'), bvh.reorient_rest_up('+z') Bvh.reorient_world_up
Round-trip through pandas bvh.to_df_dict() / pybvh.df_to_bvh(...) Bvh Class

Modules at a glance

Module Owns Page
pybvh.Bvh the central container: skeleton + motion, with every high-level method Bvh Class
pybvh.io reading and writing .bvh files I/O
pybvh.rotations conversions between all rotation representations, SLERP, SE(3) rigid-transform math Rotations & SE(3)
pybvh.geometry array-pure position descriptors: trajectories, bounding volumes, centre of mass Geometry
pybvh.transforms augmentation: mirror, rotate, translate, noise, speed, dropout Transforms
pybvh.analysis motion dynamics: velocities → jerk, foot contacts, gait, smoothness, covariance Analysis
pybvh.features the flat (F, D) ML feature-array export and its column layout Features
pybvh.signal array-pure signal utilities: finite differences, stats, smoothing, FFT Signal
pybvh.batch directory-level loading, harmonization, batched NumPy export Batch
pybvh.bvhplot visualization: snapshots, video/GIF export, interactive playback Visualization

Top-level exports

pybvh

HarmonizeReport(kept_indices: list[int] = list(), kept_sources: list[str | None] = list(), dropped_indices: list[int] = list(), dropped_sources: list[str | None] = list(), drop_reasons: list[str] = list(), applied_stages: list[dict[str, str]] = list()) dataclass

Per-call summary of what :func:harmonize did to each clip.

All fields use JSON-native types so the report can be serialized directly with json.dumps(dataclasses.asdict(report)) and embedded as audit metadata alongside a preprocessed dataset.

Attributes:

Name Type Description
kept_indices list of int

Indices (into the input clips list) of clips that survived the topology gate.

kept_sources list of str or None

source_path of each kept clip, aligned with kept_indices.

dropped_indices list of int

Indices of clips that were dropped by the topology gate.

dropped_sources list of str or None

source_path of each dropped clip, aligned with dropped_indices.

drop_reasons list of str

One human-readable reason per dropped clip, aligned with dropped_indices.

applied_stages list of dict

One dict per kept clip, aligned with kept_indices. Each dict records which harmonization stages ran for that clip, with before→after where meaningful. Possible keys: "retarget", "resample", "world_up", "rest_up", "rest_forward", "euler_order". Empty dict means the clip passed the gate without needing any transformation.

FkTopology

Bases: _FkTopologyFields

A skeleton's topology as plain arrays — everything forward kinematics reads.

The array-signature counterpart of a node tree: :func:frames_to_node_positions accepts one of these in place of a :class:~pybvh.Bvh or a list[BvhNode], so a caller holding only arrays (a data loader reading a preprocessed dataset, an augmentation step that has no source file open) can run forward kinematics without reconstructing node objects.

This is an FK input bundle, not a skeleton descriptor: it carries what the FK loop reads and nothing else — no names, no orientation axes, no channel layout. Every field is independently serializable (three arrays and a list of strings), so a preprocessing step can store the four values in whatever container it already uses; the type itself is not a serialization format. For anything else about a skeleton, keep the :class:~pybvh.Bvh.

Instances validate on construction (see Raises), because the train-time caller builds one from arrays with no node tree to check against. A malformed topology raises here rather than producing silently wrong geometry: two of the failure modes are indistinguishable from valid input downstream — a root marked as an end site reads joint_idx == -1 as a negative index into the rotation array, and a node parented to an end site reads uninitialized memory for its parent's accumulated rotation.

Attributes:

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

Each node's rest-pose offset from its parent, in the skeleton's length unit. Node order is the caller's; it need not be depth-first, but parents must precede children (see Raises).

parent_idx (ndarray, shape(N))

Index of each node's parent, -1 for the root. Exactly one -1, and it is necessarily at index 0.

joint_idx (ndarray, shape(N))

Index of each node's rotation along joint_angles axis 1, and -1 for end sites — which is also what marks a node as an end site; there is no separate flag.

euler_orders list of str

Per-joint Euler order, e.g. ['ZYX', ...], length J.

Indexed by joint column, not by node. euler_orders[j] is the order of the node whose joint_idx == j. The two coincide only when joint_idx counts up in node order, which is what :meth:from_nodes and :attr:Bvh.fk_topology produce. A caller that permutes the joint columns — packing vertices into a graph layout, say — must permute this list the same way. No check can catch a mismatch: every permutation is a valid topology, just not the one you meant.

Raises:

Type Description
ValueError

If the arrays disagree on N, offsets is not (N, 3), an Euler order is not a permutation of 'XYZ', or any of the following invariants is broken:

  • Parents precede children (parent_idx[i] < i). The FK loop fills node i while reading its parent's already-written row, so a forward reference reads an unwritten one.
  • Exactly one root (one -1 in parent_idx).
  • The root is a joint (joint_idx[0] >= 0). A -1 there is a valid negative index into the rotation array, so it silently applies the last joint's rotation to the whole skeleton.
  • No node is parented to an end site. End sites accumulate no rotation, so their children would read an uninitialized frame.
  • Joint columns are a complete range — the non-negative joint_idx values are exactly 0..J-1, each once — and len(euler_orders) == J.
See Also

Bvh.fk_topology : Derive one from a loaded skeleton. from_nodes : Derive one from a bare node list. frames_to_node_positions : The FK entry point that consumes it.

Example

topology = bvh.fk_topology # at preprocessing time np.savez(path, offsets=topology.offsets, parent_idx=topology.parent_idx, ... joint_idx=topology.joint_idx, euler_orders=topology.euler_orders) ... # at train time, no Bvh in sight d = np.load(path) topology = FkTopology(d['offsets'], d['parent_idx'], d['joint_idx'], ... list(d['euler_orders'])) coords = frames_to_node_positions(topology, root_pos, joint_angles)

from_nodes(nodes: list[BvhNode]) -> FkTopology classmethod

Derive a topology from a node tree.

Parameters:

Name Type Description Default
nodes list of BvhNode

Nodes in an order where every parent precedes its children — the depth-first order of :attr:Bvh.nodes, for instance.

required

Returns:

Type Description
FkTopology

joint_idx counts up in node order, so euler_orders matches :attr:Bvh.euler_orders element for element.

Notes

Parents are resolved by object identity, never by name: node names need not be unique (the parser generates end-site display names from the parent's name, so two end sites under one joint collide), and a name-keyed lookup would silently attach a limb to the wrong parent.

read_bvh_file(filepath: str | Path, world_up: str = 'auto', warn_on_world_up_disagreement: bool = True, lr_mapping: dict[str, str] | None = None) -> Bvh

Parse a BVH motion capture file and return a Bvh object.

Parameters:

Name Type Description Default
filepath str or Path

Path to the BVH file.

required
world_up str

World vertical axis. "auto" (default) auto-detects from animation data. Pass a signed axis string like "+y" to skip auto-detection and suppress the disagreement warning.

'auto'
warn_on_world_up_disagreement bool

If True (default) and world_up="auto", emit a UserWarning when rest-pose and first-frame inferences disagree.

True
lr_mapping dict or None

Explicit left/right joint pair mapping ({"arm.L": "arm.R", ...}). If provided, skips the name-based auto-detection for this file. Use for skeletons whose naming conventions the heuristic can't parse.

None

Returns:

Name Type Description
bvh Bvh

A Bvh object containing the skeleton hierarchy, root positions, joint angles, and frame time.

Notes

BVH files store joint angles in degrees; pybvh holds them in radians on :attr:Bvh.joint_angles. This function converts on read; :func:write_bvh_file converts back on write.

Three things the reader normalizes rather than preserving verbatim, so a round-trip is lossless in motion but not byte-for-byte:

  • Frame time is snapped to an exact 1 / N when the file's value is within 0.01% of one — salvaging the ubiquitous 0.033333 truncation, at the cost of bvh.frame_time not being the literal file value. Non-integer rates (23.976 fps) are left alone, and no other parser does this snap.
  • Root channels are reordered to position-first. A file declaring rotations before positions parses correctly but writes back in pybvh's canonical order.
  • Offsets and motion values are written at 6 decimal places (frame time at full precision), so re-reading a written file quantizes at ~1e-6 in the file's own units.

Values pybvh infers or you set — world_up, lr_mapping — have nowhere to live in the BVH format and are lost on write; re-apply them after reading.

write_bvh_file(bvh: Bvh, filepath: str | Path, verbose: bool = False, overwrite: bool = True) -> None

Write a Bvh object to a .bvh file.

Parameters:

Name Type Description Default
bvh Bvh

The motion data to write.

required
filepath str or Path

Destination file path. Must have a .bvh extension.

required
verbose bool

If True, print a one-line confirmation to stdout on success. Default False — preprocessing loops that write many files shouldn't flood the terminal by default.

False
overwrite bool

If True (default), replace an existing file at filepath. Pass False to refuse instead, raising FileExistsError — worth doing when the destination is hand-authored data rather than a regenerable output.

True

Raises:

Type Description
ValueError

If the file extension is not .bvh.

FileNotFoundError

If the parent directory does not exist.

FileExistsError

If filepath exists and overwrite=False.

Notes

pybvh stores joint angles in radians, but the BVH format requires degrees; this function converts on write.

Offsets and motion values are written with 6 decimal places, the de facto BVH convention (and what most DCC tools emit) — motion data re-read from a written file is therefore quantized at ~1e-6 in the skeleton's length unit and in degrees. Frame Time is the exception: it is written at full precision (%.10g), because a truncated frame time compounds across every frame of a resample while a truncated coordinate does not.

The BVH format carries no place for world_up, lr_mapping, or source_path; those are lost on write and re-inferred on read. Root channels are always written position-first, whatever order the source file declared.

read_bvh_directory(dirpath: str | Path, pattern: str = '*.bvh', sort: bool | str = True, parallel: bool = False, max_workers: int | None = None, world_up: str = 'auto', lr_mapping: dict[str, str] | None = None, skip_errors: bool = False) -> list[Bvh]

Load all BVH files from a directory.

Parameters:

Name Type Description Default
dirpath str or Path

Directory to search for BVH files.

required
pattern str

Glob pattern to filter files (default "*.bvh").

'*.bvh'
sort bool or {'lexicographic', 'natural'}

File ordering. True (default) and "lexicographic" sort by full path string — the same order Python's sorted() over paths produces, so a list built elsewhere with sorted() (labels, split manifests) stays index-aligned; note it puts file10.bvh before file2.bvh. "natural" compares embedded digit runs numerically (file2 before file10, case-insensitive) — opt-in, because silently diverging from the ecosystem's lexicographic default would misalign such parallel lists. False keeps the filesystem's glob order (non-deterministic across platforms).

True
parallel bool

If True, load files in parallel using threads. Parsing is CPU-bound and GIL-limited, so this mainly helps on slow storage (network filesystems, cold disks); expect little speedup on a warm local disk.

False
max_workers int or None

Maximum number of threads when parallel=True. None defers to the ThreadPoolExecutor default.

None
world_up str

World vertical axis applied to every loaded file. "auto" (default) auto-detects per file. Pass e.g. "+y" to override all files uniformly.

'auto'
lr_mapping dict or None

Explicit left/right joint pair mapping applied to every loaded file. Useful when a whole dataset shares an unusual naming convention the auto-detect heuristic can't parse.

None
skip_errors bool

If True, files that fail to load emit a UserWarning and are skipped. If False (default), the first failure propagates as the original exception. Use True when robustness against occasional corrupt files matters more than strict verification.

False

Returns:

Type Description
list of Bvh

One Bvh object per successfully loaded file. Shorter than the set of matched files when skip_errors=True and some failed.

Raises:

Type Description
FileNotFoundError

If dirpath does not exist.

batch_to_numpy(bvh_list: list[Bvh], representation: str = 'euler', include_root_pos: bool = True, pad: bool = False, pad_value: float = 0.0) -> npt.NDArray[np.float64] | list[npt.NDArray[np.float64]]

Convert a list of Bvh objects to NumPy arrays.

All Bvh objects must share the same skeleton hierarchy. For representations whose channel layout depends on the source Euler order ('euler', 'axisangle'), all clips must additionally share the same per-joint Euler orders. For rotation-invariant representations ('6d', 'quat', 'rotmat') the Euler-order check is skipped.

Parameters:

Name Type Description Default
bvh_list list of Bvh

BVH objects to convert.

required
representation str

Rotation representation: 'euler' (default), '6d', 'quat', 'axisangle', or 'rotmat'.

'euler'
include_root_pos bool

If True (default), prepend root position (3 columns) to the rotation data.

True
pad bool

If True, zero-pad shorter sequences to the maximum length and return a single 3-D array (B, F_max, D). If False (default), return a list of 2-D arrays.

False
pad_value float

Value to use for padding (default 0.0).

0.0

Returns:

Type Description
ndarray or list of ndarray

If pad=True: array of shape (B, F_max, D). If pad=False: list of arrays, each (F_i, D).

Raises:

Type Description
ValueError

If skeletons are incompatible or representation is unknown.

harmonize(clips: list[Bvh], *, reference: Bvh | None = None, target_fps: float | None = None, target_world_up: str | None = None, target_rest_up: str | None = None, target_rest_forward: str | None = None, target_euler_order: str | None = None, on_incompatible: Literal['drop', 'raise'] = 'drop', verbose: bool = True, return_report: bool = False) -> list[Bvh] | tuple[list[Bvh], HarmonizeReport]

harmonize(clips: list[Bvh], *, reference: Bvh | None = ..., target_fps: float | None = ..., target_world_up: str | None = ..., target_rest_up: str | None = ..., target_rest_forward: str | None = ..., target_euler_order: str | None = ..., on_incompatible: Literal['drop', 'raise'] = ..., verbose: bool = ..., return_report: Literal[False] = ...) -> list[Bvh]
harmonize(clips: list[Bvh], *, reference: Bvh | None = ..., target_fps: float | None = ..., target_world_up: str | None = ..., target_rest_up: str | None = ..., target_rest_forward: str | None = ..., target_euler_order: str | None = ..., on_incompatible: Literal['drop', 'raise'] = ..., verbose: bool = ..., return_report: Literal[True]) -> tuple[list[Bvh], HarmonizeReport]

Apply dataset-level harmonization to a list of clips.

For each clip, applies in order:

  1. Topology check vs reference (if provided). On mismatch, the clip is dropped or raises per on_incompatible.
  2. Bone-proportion retargeting to reference (if provided).
  3. Frame-rate resampling to target_fps (if provided and the clip's current fps differs by more than 0.01).
  4. World-up reorientation to target_world_up (if provided and bvh.world_up != target_world_up). Rotates the entire scene — affects offsets, root_pos, and the world_up flag.
  5. Rest-up reorientation to target_rest_up (if provided and bvh.rest_up != target_rest_up). Rotates only the rest-pose offsets and compensates joint rotations so FK positions are unchanged; the world frame is untouched.
  6. Rest-forward reorientation to target_rest_forward (if provided and bvh.rest_forward != target_rest_forward). Rotates rest-pose offsets around the vertical axis so the skeleton's rest-pose facing matches.
  7. Euler-order re-expression to target_euler_order (if provided and any per-joint Euler order differs). Re-expresses each joint's stored Euler angles in the target order while preserving the underlying rotations.

The ordering matters: world-up is "heaviest" (touches everything), rest-up modifies only rest-pose offsets (leaving world frame intact), and rest-forward is a further rotation of those same offsets around the vertical. Euler-order re-expression runs last because it only rewrites channel layout, not geometry.

Any of the reference / target_* kwargs may be None to skip that stage. Passing all as None returns a shallow copy of clips (no-op).

Parameters:

Name Type Description Default
clips list of Bvh

Input clips.

required
reference Bvh or None

If provided, every clip must match reference.matches_hierarchy (same joints, same parent structure — rest offsets are allowed to differ since retargeting will overwrite them next). Kept clips are then retargeted to reference's bone offsets.

None
target_fps float or None

Target frame rate in Hz. Clips whose fps differs by more than 0.01 are resampled via quaternion SLERP.

None
target_world_up str or None

Signed-axis string ('+y', '-z', ...). Clips whose world_up differs are rotated via reorient_world_up.

None
target_rest_up str or None

Signed-axis string. Clips whose rest_up differs are corrected via reorient_rest_up. Typically used to fix files whose rest pose and animation disagree on the up axis.

None
target_rest_forward str or None

Signed-axis string. Clips whose rest_forward differs are rotated via reorient_rest_forward so the rest pose faces a consistent direction across the dataset.

None
target_euler_order str or None

Three-character order like 'XYZ' / 'ZYX'. When set, clips with any joint whose Euler order differs are re-expressed in the target order via :meth:Bvh.change_euler_order. This is orientation-preserving: underlying rotations are unchanged; only the channel layout is rewritten. Numerical drift can occur on gimbal-lock-adjacent rotations — for bit-exact round-trips across the conversion, prefer rotation-invariant representations ('6d' / 'quat') downstream.

None
on_incompatible ('drop', 'raise')

Behavior on topology mismatch with reference. "drop" (default) skips the clip; "raise" raises ValueError at the first mismatch.

"drop"
verbose bool

If True (default), emit a single UserWarning at end of call when one or more clips were dropped, summarizing how many were dropped and identifying the first few. Set to False to silence the summary entirely.

True
return_report bool

If True, return (clips, report) where report is a :class:HarmonizeReport describing every stage applied to every kept clip plus per-clip drop reasons. Default False keeps the return type as a plain list[Bvh].

False

Returns:

Type Description
list of Bvh, or (list of Bvh, HarmonizeReport)

Harmonized clips (and optional report). The list may be shorter than clips if any were dropped.

Raises:

Type Description
ValueError

If on_incompatible is not one of the accepted values, or if on_incompatible='raise' and a clip mismatches reference.

frames_to_node_positions(skeleton: Union[Bvh, list[BvhNode], FkTopology], root_pos: npt.ArrayLike | None = None, joint_angles: npt.ArrayLike | None = None, centered: str = 'world', up: str | None = None) -> npt.NDArray[np.float64]

Return spatial coordinates of all nodes for one or multiple frames.

Parameters:

Name Type Description Default
skeleton Bvh or list of BvhNode or FkTopology

The skeleton to pose. Pass an :class:FkTopology to run forward kinematics from arrays alone, with no node objects — see :attr:Bvh.fk_topology for producing one.

Renamed from nodes_container in 0.8.2, when the array form was added and the old name stopped being true.

required
root_pos (ndarray, shape(F, 3) or (3,))

Root position per frame. If None, extracted from skeleton (which must then be a Bvh object).

None
joint_angles (ndarray, shape(F, J, 3) or (J, 3))

Euler angles in radians per joint per frame (pybvh's internal convention; matches :attr:Bvh.joint_angles). If None, extracted from skeleton. A non-Euler stream converts first — see :mod:pybvh.rotations.

None
centered str

"world" – root at its actual position. "skeleton" – root at origin in every frame. "first" – ground-plane centering: the first frame's root position is subtracted in the two non-up axes only, so the motion starts above the origin at its original height.

'world'
up str or None

Signed world-up axis string (e.g. '+y') — only read by centered="first", to decide which coordinate is left untouched. Defaults to the skeleton's own world_up when skeleton is a :class:~pybvh.Bvh. There is no default for the other two input forms: a node list and an FkTopology carry no gravity direction, and guessing one silently mis-centers every skeleton that does not happen to match the guess.

None

Returns:

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

Spatial coordinates for all nodes (including end sites). Returns 2-D (N, 3) when a single frame is provided, 3-D (F, N, 3) otherwise.

Raises:

Type Description
ValueError

If centered is not one of the three modes; if root_pos / joint_angles are omitted for a skeleton that carries no motion; if they disagree on frame count or on the joint count the skeleton declares; or if centered="first" is requested without an up axis and the skeleton cannot supply one.

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.