Skip to content

Transforms

transforms

Spatial augmentation transforms for BVH motion data.

Bvh-level API — All transforms operate on :class:~pybvh.bvh.Bvh objects and follow the inplace=False convention: by default they return a new object, leaving the original unchanged.

NumPy-level API — Lower-level functions (mirror_angles, rotate_angles_vertical) accept raw arrays + minimal metadata for users who work with pre-extracted arrays.

translate_root(bvh: Bvh, offset: npt.ArrayLike, inplace: bool = False) -> Bvh | None

translate_root(bvh: Bvh, offset: npt.ArrayLike, *, inplace: Literal[True]) -> None
translate_root(bvh: Bvh, offset: npt.ArrayLike, inplace: Literal[False] = ...) -> Bvh

Shift the root position by a constant 3-D offset.

Parameters:

Name Type Description Default
bvh Bvh

Input motion.

required
offset array_like of shape (3,)

Translation vector (dx, dy, dz).

required
inplace bool

If True, modify bvh and return None.

False

Returns:

Type Description
Bvh or None

random_translate_root(bvh: Bvh, range_xyz: tuple[float, float] = (-100.0, 100.0), rng: np.random.Generator | None = None) -> Bvh

Translate root by a random offset sampled uniformly per axis.

Parameters:

Name Type Description Default
bvh Bvh

Input motion.

required
range_xyz tuple of (low, high)

Uniform sampling range applied to each axis independently.

(-100.0, 100.0)
rng Generator or None

Random generator for reproducibility.

None

Returns:

Type Description
Bvh

add_noise(bvh: Bvh, sigma_deg: float, sigma_pos: float = 0.0, rng: np.random.Generator | None = None, inplace: bool = False, wrap: bool = True) -> Bvh | None

add_noise(bvh: Bvh, sigma_deg: float, *, sigma_pos: float = ..., rng: np.random.Generator | None = ..., inplace: Literal[True]) -> None
add_noise(bvh: Bvh, sigma_deg: float, sigma_pos: float = ..., rng: np.random.Generator | None = ..., inplace: Literal[False] = ...) -> Bvh

Add Gaussian noise to joint rotation angles.

Parameters:

Name Type Description Default
bvh Bvh

Input motion.

required
sigma_deg float

Standard deviation of noise in degrees, added to joint_angles.

required
sigma_pos float

Standard deviation of noise added to root_pos (default 0 — no position noise).

0.0
rng Generator or None

Random generator for reproducibility.

None
inplace bool

If True, modify bvh and return None.

False
wrap bool

If True (default), wrap noised angles to [-π, π] (radians) so downstream Euler-to-rotmat round-trips don't see discontinuities. Set to False if the consumer handles angle ranges itself.

True

Returns:

Type Description
Bvh or None

perturb_speed(bvh: Bvh, factor: float) -> Bvh

Change motion speed by resampling.

A factor of 2.0 makes the motion twice as fast (fewer frames); 0.5 makes it half as fast (more frames). Uses the existing :meth:Bvh.resample which performs quaternion SLERP for rotations.

Parameters:

Name Type Description Default
bvh Bvh

Input motion.

required
factor float

Speed multiplier (must be > 0).

required

Returns:

Type Description
Bvh

New Bvh with adjusted frame count and frame_time.

random_perturb_speed(bvh: Bvh, factor_range: tuple[float, float] = (0.8, 1.2), rng: np.random.Generator | None = None) -> Bvh

Apply a random speed change sampled uniformly from factor_range.

Parameters:

Name Type Description Default
bvh Bvh

Input motion.

required
factor_range tuple of (low, high)

Range for the speed factor.

(0.8, 1.2)
rng Generator or None

Random generator for reproducibility.

None

Returns:

Type Description
Bvh

drop_frames(bvh: Bvh, drop_rate: float, rng: np.random.Generator | None = None, inplace: bool = False) -> Bvh | None

drop_frames(bvh: Bvh, drop_rate: float, *, rng: np.random.Generator | None = ..., inplace: Literal[True]) -> None
drop_frames(bvh: Bvh, drop_rate: float, rng: np.random.Generator | None = ..., inplace: Literal[False] = ...) -> Bvh

Replace randomly selected frames with SLERP-interpolated values.

Dropped frames are filled by spherical linear interpolation (SLERP) of the nearest kept neighbours' quaternion rotations and linear interpolation of root positions. The output has the same frame count as the input.

Parameters:

Name Type Description Default
bvh Bvh

Input motion.

required
drop_rate float

Fraction of frames to drop, in (0, 1). First and last frames are always kept.

required
rng Generator or None

Random generator for reproducibility.

None
inplace bool

If True, modify bvh and return None.

False

Returns:

Type Description
Bvh or None

rotate_angles_vertical(joint_angles: npt.NDArray[np.float64], root_pos: npt.NDArray[np.float64], angle_deg: float, up_idx: int, root_order: str) -> tuple[npt.NDArray[np.float64], npt.NDArray[np.float64]]

Rotate motion around the vertical axis (NumPy-level).

Only modifies root_pos and the root joint's Euler angles (index 0 in joint_angles). Non-root joints are in parent-local coordinates and are unaffected.

Parameters:

Name Type Description Default
joint_angles ndarray of shape (F, J, 3)

Euler angles in radians (pybvh's internal convention).

required
root_pos ndarray of shape (F, 3)

Root translation per frame.

required
angle_deg float

Rotation angle in degrees (user-facing — converted to radians internally).

required
up_idx int

Index of the up axis (0=X, 1=Y, 2=Z).

required
root_order str

Euler order of the root joint, e.g. 'ZYX'.

required

Returns:

Type Description
(new_joint_angles, new_root_pos)

Copies with the rotation applied. Angles in radians.

See Also

rotate_vertical : Bvh-level wrapper that auto-detects up_idx and root_order from the skeleton.

Examples:

>>> angles = bvh.joint_angles          # (F, J, 3) radians
>>> pos = bvh.root_pos                 # (F, 3)
>>> up = {'x': 0, 'y': 1, 'z': 2}[bvh.world_up[1]]
>>> order = ''.join(bvh.root.rot_channels)
>>> new_angles, new_pos = rotate_angles_vertical(
...     angles, pos, 90.0, up, order)

rotate_vertical(bvh: Bvh, angle_deg: float, up_axis: str | None = None, inplace: bool = False) -> Bvh | None

rotate_vertical(bvh: Bvh, angle_deg: float, *, up_axis: str | None = ..., inplace: Literal[True]) -> None
rotate_vertical(bvh: Bvh, angle_deg: float, up_axis: str | None = ..., inplace: Literal[False] = ...) -> Bvh

Rotate the entire motion around the vertical (up) axis.

Only the root joint's world-space rotation and root position are modified. Child joints are in parent-local coordinates and are unaffected.

Parameters:

Name Type Description Default
bvh Bvh

Input motion.

required
angle_deg float

Rotation angle in degrees (positive = counter-clockwise when viewed from above).

required
up_axis str or None

Signed axis string (e.g. '+y'). Auto-detected if None.

None
inplace bool

If True, modify bvh and return None.

False

Returns:

Type Description
Bvh or None

random_rotate_vertical(bvh: Bvh, angle_range: tuple[float, float] = (-180.0, 180.0), up_axis: str | None = None, rng: np.random.Generator | None = None) -> Bvh

Rotate motion by a random angle around the vertical axis.

Parameters:

Name Type Description Default
bvh Bvh

Input motion.

required
angle_range tuple of (low, high)

Angle sampling range in degrees.

(-180.0, 180.0)
up_axis str or None

Signed axis string. Auto-detected if None.

None
rng Generator or None

Random generator for reproducibility.

None

Returns:

Type Description
Bvh

mirror_angles(joint_angles: npt.NDArray[np.float64], root_pos: npt.NDArray[np.float64], lr_joint_pairs: list[tuple[int, int]], lateral_idx: int, rot_channels: list[list[str]]) -> tuple[npt.NDArray[np.float64], npt.NDArray[np.float64]]

Mirror joint angles and root position (NumPy-level).

Performs the array-level operations of mirroring: negating the lateral component of root_pos, swapping L/R joint angle columns, and negating Euler angle components whose rotation axis is not the lateral axis.

This function does not modify skeleton offsets (bone geometry). For a complete mirror that also adjusts the skeleton, use the Bvh-level :func:mirror function.

Parameters:

Name Type Description Default
joint_angles ndarray of shape (F, J, 3)

Euler angles in radians (pybvh's internal convention).

required
root_pos ndarray of shape (F, 3)

Root translation per frame.

required
lr_joint_pairs list of (left_idx, right_idx)

Index pairs into the joint axis of joint_angles.

required
lateral_idx int

Index of the lateral axis (0=X, 1=Y, 2=Z).

required
rot_channels list of list of str

Per-joint Euler channel order, e.g. [['Z','Y','X'], ...]. Length must equal J.

required

Returns:

Type Description
(new_joint_angles, new_root_pos)

Copies with the mirroring applied.

See Also

mirror : Bvh-level wrapper that also mirrors skeleton offsets and auto-detects lr_joint_pairs, lateral_idx, and rot_channels from the skeleton.

Examples:

>>> angles = bvh.joint_angles
>>> pos = bvh.root_pos
>>> pairs = transforms.auto_detect_lr_pairs(bvh)
>>> lat_idx = {'x': 0, 'y': 1, 'z': 2}[bvh.left_at(frame=0)[1]]
>>> channels = [n.rot_channels for n in bvh.nodes if not n.is_end_site()]
>>> new_angles, new_pos = mirror_angles(
...     angles, pos, pairs, lat_idx, channels)

auto_detect_lr_mapping(bvh: Bvh) -> dict[str, str]

Return the L/R joint-name mapping for this skeleton.

Thin wrapper around :attr:Bvh.lr_mapping that returns an empty dict (instead of None) when no pairs are available — back-compat shape for code that expected a dict.

Parameters:

Name Type Description Default
bvh Bvh

Input BVH with named joints.

required

Returns:

Type Description
dict

{"LeftArm": "RightArm", ...}. Empty if no pairs available.

auto_detect_lr_pairs(bvh: Bvh) -> list[tuple[int, int]]

Auto-detect left/right joint pairs as index tuples.

Wraps :func:auto_detect_lr_mapping and converts joint name pairs to index pairs in joint_angles index space (axis 1 of bvh.joint_angles).

Parameters:

Name Type Description Default
bvh Bvh

Input BVH with named joints.

required

Returns:

Type Description
list of (int, int)

[(left_idx, right_idx), ...] in joint_angles index space. Empty if no pairs found.

mirror(bvh: Bvh, left_right_mapping: dict[str, str] | None = None, lateral_axis: str | None = None, inplace: bool = False) -> Bvh | None

mirror(bvh: Bvh, *, left_right_mapping: dict[str, str] | None = ..., lateral_axis: str | None = ..., inplace: Literal[True]) -> None
mirror(bvh: Bvh, left_right_mapping: dict[str, str] | None = ..., lateral_axis: str | None = ..., inplace: Literal[False] = ...) -> Bvh

Mirror (reflect) the motion across the lateral plane.

Swaps left/right joint data and negates the appropriate rotation and position components so that the skeleton appears as a mirror image.

Parameters:

Name Type Description Default
bvh Bvh

Input motion.

required
left_right_mapping dict or None

{"LeftArm": "RightArm", ...}. Auto-detected if None.

None
lateral_axis str or None

Axis perpendicular to the mirror plane, e.g. 'x'. Auto-detected if None (the axis that is neither forward nor upward).

None
inplace bool

If True, modify bvh and return None.

False

Returns:

Type Description
Bvh or None

Raises:

Type Description
ValueError

If auto-detection finds no left/right pairs.

reorient_world_up(bvh: Bvh, new_up: str, inplace: bool = False) -> Bvh | None

reorient_world_up(bvh: Bvh, new_up: str, *, inplace: Literal[True]) -> None
reorient_world_up(bvh: Bvh, new_up: str, inplace: Literal[False] = ...) -> Bvh

Change the world coordinate system's vertical axis.

Applies a global rotation to the entire animation (root translation, skeleton offsets, root joint rotations) so the world vertical axis changes from the current bvh.world_up to new_up. The character looks visually identical; only the coordinate system changes.

Restricted to axis-aligned rotations (multiples of 90 degrees) for lossless transformation.

Parameters:

Name Type Description Default
bvh Bvh
required
new_up str

Target up axis, e.g. '+y'.

required
inplace bool
False

Returns:

Type Description
Bvh or None

reorient_rest_up(bvh: Bvh, new_up: str, inplace: bool = False) -> Bvh | None

reorient_rest_up(bvh: Bvh, new_up: str, *, inplace: Literal[True]) -> None
reorient_rest_up(bvh: Bvh, new_up: str, inplace: Literal[False] = ...) -> Bvh

Rotate the skeleton's rest-pose offsets so its topological up aligns with new_up, compensating all joint rotations so that FK positions are unchanged.

This fixes files where the rest pose and animation disagree on the up axis (e.g. rest pose authored in Y-up but animation plays in Z-up). After this call, the disagreement warning disappears.

The world coordinate system is unchanged: root_pos and world_up are NOT modified.

Parameters:

Name Type Description Default
bvh Bvh
required
new_up str

Target rest-pose up axis, e.g. '+y'.

required
inplace bool
False

Returns:

Type Description
Bvh or None

reorient_rest_forward(bvh: Bvh, new_forward: str, inplace: bool = False) -> Bvh | None

reorient_rest_forward(bvh: Bvh, new_forward: str, *, inplace: Literal[True]) -> None
reorient_rest_forward(bvh: Bvh, new_forward: str, inplace: Literal[False] = ...) -> Bvh

Rotate the skeleton's rest-pose offsets so the character faces new_forward, compensating all joint rotations so FK positions are unchanged.

The rotation is around the world up axis (a rotation in the ground plane). new_forward must not be parallel to world_up.

Parameters:

Name Type Description Default
bvh Bvh
required
new_forward str

Target rest-pose forward axis, e.g. '+y' or '-z'.

required
inplace bool
False

Returns:

Type Description
Bvh or None

Raises:

Type Description
ValueError

If new_forward is parallel to world_up.