Skip to content

I/O

io

BVH file I/O — reading and writing .bvh motion capture files.

Public functions:

  • :func:read_bvh_file — parse a .bvh file into a :class:~pybvh.bvh.Bvh
  • :func:write_bvh_file — write a :class:~pybvh.bvh.Bvh to a .bvh file

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.