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 |
kept_sources |
list of str or None
|
|
dropped_indices |
list of int
|
Indices of clips that were dropped by the topology gate. |
dropped_sources |
list of str or None
|
|
drop_reasons |
list of str
|
One human-readable reason per dropped clip, aligned with
|
applied_stages |
list of dict
|
One dict per kept clip, aligned with |
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, |
joint_idx |
(ndarray, shape(N))
|
Index of each node's rotation along |
euler_orders |
list of str
|
Per-joint Euler order, e.g. Indexed by joint column, not by node. |
Raises:
| Type | Description |
|---|---|
ValueError
|
If the arrays disagree on
|
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: |
required |
Returns:
| Type | Description |
|---|---|
FkTopology
|
|
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'
|
warn_on_world_up_disagreement
|
bool
|
If True (default) and |
True
|
lr_mapping
|
dict or None
|
Explicit left/right joint pair mapping
( |
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 / Nwhen the file's value is within 0.01% of one — salvaging the ubiquitous0.033333truncation, at the cost ofbvh.frame_timenot 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 |
required |
verbose
|
bool
|
If True, print a one-line confirmation to stdout on success.
Default |
False
|
overwrite
|
bool
|
If True (default), replace an existing file at |
True
|
Raises:
| Type | Description |
|---|---|
ValueError
|
If the file extension is not |
FileNotFoundError
|
If the parent directory does not exist. |
FileExistsError
|
If |
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'
|
sort
|
bool or {'lexicographic', 'natural'}
|
File ordering. |
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 |
None
|
world_up
|
str
|
World vertical axis applied to every loaded file.
|
'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 |
False
|
Returns:
| Type | Description |
|---|---|
list of Bvh
|
One Bvh object per successfully loaded file. Shorter than the
set of matched files when |
Raises:
| Type | Description |
|---|---|
FileNotFoundError
|
If |
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'
|
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 |
False
|
pad_value
|
float
|
Value to use for padding (default |
0.0
|
Returns:
| Type | Description |
|---|---|
ndarray or list of ndarray
|
If |
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:
- Topology check vs
reference(if provided). On mismatch, the clip is dropped or raises peron_incompatible. - Bone-proportion retargeting to
reference(if provided). - Frame-rate resampling to
target_fps(if provided and the clip's current fps differs by more than0.01). - World-up reorientation to
target_world_up(if provided andbvh.world_up != target_world_up). Rotates the entire scene — affects offsets, root_pos, and theworld_upflag. - Rest-up reorientation to
target_rest_up(if provided andbvh.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. - Rest-forward reorientation to
target_rest_forward(if provided andbvh.rest_forward != target_rest_forward). Rotates rest-pose offsets around the vertical axis so the skeleton's rest-pose facing matches. - 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 |
None
|
target_fps
|
float or None
|
Target frame rate in Hz. Clips whose fps differs by more than
|
None
|
target_world_up
|
str or None
|
Signed-axis string ( |
None
|
target_rest_up
|
str or None
|
Signed-axis string. Clips whose |
None
|
target_rest_forward
|
str or None
|
Signed-axis string. Clips whose |
None
|
target_euler_order
|
str or None
|
Three-character order like |
None
|
on_incompatible
|
('drop', 'raise')
|
Behavior on topology mismatch with |
"drop"
|
verbose
|
bool
|
If True (default), emit a single |
True
|
return_report
|
bool
|
If True, return |
False
|
Returns:
| Type | Description |
|---|---|
list of Bvh, or (list of Bvh, HarmonizeReport)
|
Harmonized clips (and optional report). The list may be shorter
than |
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
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: Renamed from |
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: |
None
|
centered
|
str
|
|
'world'
|
up
|
str or None
|
Signed world-up axis string (e.g. |
None
|
Returns:
| Type | Description |
|---|---|
(ndarray, shape(F, N, 3) or (N, 3))
|
Spatial coordinates for all nodes (including end sites).
Returns 2-D |
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 |
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 |
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 |
Notes
Source: Troje 2002 (pose normalization); Umeyama 1991 for the centered convention.