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.

All angle parameters are in radians (pybvh's internal convention); functions that rotate accept a degrees=True flag matching the convention in :mod:~pybvh.rotations.

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, offset_range: 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
offset_range 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_rotation_noise(bvh: Bvh, sigma: float, rng: np.random.Generator | None = None, inplace: bool = False, wrap: bool = False, degrees: bool = False) -> Bvh | None

add_rotation_noise(bvh: Bvh, sigma: float, *, rng: np.random.Generator | None = ..., inplace: Literal[True], wrap: bool = ..., degrees: bool = ...) -> None
add_rotation_noise(bvh: Bvh, sigma: float, rng: np.random.Generator | None = ..., inplace: Literal[False] = ..., wrap: bool = ..., degrees: bool = ...) -> Bvh

Add zero-mean Gaussian noise to joint rotation angles.

Parameters:

Name Type Description Default
bvh Bvh

Input motion.

required
sigma float

Standard deviation of the rotation noise in radians (or degrees if degrees=True), added to joint_angles. 0 is a no-op and draws nothing from rng.

required
rng Generator or None

Random generator for reproducibility.

None
inplace bool

If True, modify bvh and return None.

False
wrap bool

If True, wrap noised angles to [-π, π]. Default False: BVH channels can legitimately hold values outside that range (accumulated rotations spanning multiple turns), and wrapping those would corrupt them.

False
degrees bool

If True, interpret sigma in degrees. Default False (radians).

False

Returns:

Type Description
Bvh or None
See Also

add_position_noise : The root-translation counterpart.

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

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

Add zero-mean Gaussian noise to the root translation.

Parameters:

Name Type Description Default
bvh Bvh

Input motion.

required
sigma float

Standard deviation of the noise added to root_pos, in the skeleton's length unit — the same unit as the bone offsets, so it scales with the file rather than being absolute. There is no degrees= here because this is a length, not an angle. 0 is a no-op and draws nothing from rng.

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
See Also

add_rotation_noise : The joint-angle counterpart.

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 an adjusted frame count and the original frame_time — the clip's duration changes, its playback rate does not.

Notes

This resamples: the motion is re-interpolated onto a new time grid (see :meth:Bvh.resample), so joint rotations pass through SLERP and the frame count changes. The alternative convention — scale frame_time by 1 / factor and leave the samples untouched — is lossless and changes duration too, but yields a non-standard frame rate rather than a resampled clip. Use that directly when you want playback-rate change without interpolation.

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, and kept frames are preserved exactly (bit-for-bit) — only the dropped frames are re-synthesized.

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: float, up_idx: int, root_order: str, degrees: bool = False, pivot: str | npt.ArrayLike = 'origin') -> 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 float

Rotation angle in radians (or degrees if degrees=True).

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
degrees bool

If True, interpret angle in degrees. Default False (radians).

False
pivot ('origin', 'root')

World-space point the motion turns about. "origin" (default) turns about the world origin, so a motion standing away from it sweeps through space along an arc; "root" turns about the first frame's root position, i.e. turn-in-place; an explicit (3,) point turns about a fixed landmark. Only the two horizontal components are read — every point on a vertical line spans the same rotation axis — so "root" is equivalently the first-frame root projected to the ground plane.

"origin"

Returns:

Type Description
(new_joint_angles, new_root_pos)

Copies with the rotation applied. Angles in radians.

Notes

The pivot only translates the root trajectory: rotating about p is the same as root_pos - p → rotate about the origin → + p, and the root's world rotation is identical either way. So a pipeline that already centers its clips on the first-frame root gets turn-in-place from the default "origin" and needs no pivot=.

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, np.pi / 2, up, order)

rotate_vertical(bvh: Bvh, angle: float, up_axis: str | None = None, degrees: bool = False, pivot: str | npt.ArrayLike = 'origin', inplace: bool = False) -> Bvh | None

rotate_vertical(bvh: Bvh, angle: float, *, up_axis: str | None = ..., degrees: bool = ..., pivot: str | npt.ArrayLike = ..., inplace: Literal[True]) -> None
rotate_vertical(bvh: Bvh, angle: float, up_axis: str | None = ..., degrees: bool = ..., pivot: str | npt.ArrayLike = ..., 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 float

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

required
up_axis str or None

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

None
degrees bool

If True, interpret angle in degrees. Default False (radians).

False
pivot ('origin', 'root')

World-space point the motion turns about. Default "origin", the world origin: a character standing away from it sweeps through space along an arc rather than turning where it stands. "root" is that turn-in-place — it pivots about the first frame's root position, projected to the ground plane. An explicit (3,) point pivots about a fixed landmark (a dataset capture centre, a stage mark). Only the two horizontal components of a point are read: every point on a vertical line spans the same rotation axis, so the up-axis component is dropped and heights come through untouched.

"origin"
inplace bool

If True, modify bvh and return None.

False

Returns:

Type Description
Bvh or None
Notes

The pivot moves only the root trajectory — the root's world rotation, and every child joint's parent-local angles, are identical for any pivot. Equivalently, pivot="root" is center on the first-frame root → rotate about the origin → un-center: a pipeline that already centers its clips gets turn-in-place from the default and needs no pivot= at all.

Randomized pivots are deliberately not offered here (unlike angle, which has :func:random_rotate_vertical). A random pivot is a rotation composed with a translation, so it is already expressible as rotate_vertical + :func:translate_root, and which distribution to draw from is a pipeline's decision, not a motion's.

random_rotate_vertical(bvh: Bvh, angle_range: tuple[float, float] = (-np.pi, np.pi), up_axis: str | None = None, degrees: bool = False, pivot: str | npt.ArrayLike = 'origin', 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 radians (default full circle, (-π, π)), or in degrees if degrees=True.

(-pi, pi)
up_axis str or None

Signed axis string. Auto-detected if None.

None
degrees bool

If True, interpret angle_range in degrees. Default False (radians).

False
pivot ('origin', 'root')

World-space point to turn about, as in :func:rotate_vertical. Only the angle is random; the pivot is fixed for the call.

"origin"
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_pairs(bvh: Bvh) -> list[tuple[int, int]]

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

Converts the joint name pairs of :attr:Bvh.lr_mapping 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, lr_mapping: dict[str, str] | None = None, lateral_axis: str | None = None, inplace: bool = False) -> Bvh | None

mirror(bvh: Bvh, *, lr_mapping: dict[str, str] | None = ..., lateral_axis: str | None = ..., inplace: Literal[True]) -> None
mirror(bvh: Bvh, lr_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
lr_mapping dict or None

{"LeftArm": "RightArm", ...}. Defaults to :attr:Bvh.lr_mapping (auto-detected or user-set).

None
lateral_axis str or None

Axis perpendicular to the mirror plane, e.g. 'x' or '+x' (the sign is irrelevant for mirroring). Auto-detected if None, and the detection is usually the right thing to use: it averages the left-minus-right rest-pose offsets over the L/R joint pairs and takes the dominant axis, so it measures how this skeleton is actually built rather than assuming a convention. Pass an explicit axis only to override that, or when the detection raises because the L/R offsets are degenerate (parallel to the up axis, or zero).

None
inplace bool

If True, modify bvh and return None.

False

Returns:

Type Description
Bvh or None

Raises:

Type Description
ValueError

If no left/right pairs are available, or if an explicitly passed lr_mapping names joints that don't exist.

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

Raises:

Type Description
ValueError

If the rest pose is degenerate (no up axis can be inferred).

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.