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
¶
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 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 |
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 |
False
|
degrees
|
bool
|
If True, interpret |
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 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 |
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
|
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
¶
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 |
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 |
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. |
required |
degrees
|
bool
|
If True, interpret |
False
|
pivot
|
('origin', 'root')
|
World-space point the motion turns about. |
"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:
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 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 |
required |
up_axis
|
str or None
|
Signed axis string (e.g. |
None
|
degrees
|
bool
|
If True, interpret |
False
|
pivot
|
('origin', 'root')
|
World-space point the motion turns about. Default |
"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,
|
(-pi, pi)
|
up_axis
|
str or None
|
Signed axis string. Auto-detected if None. |
None
|
degrees
|
bool
|
If True, interpret |
False
|
pivot
|
('origin', 'root')
|
World-space point to turn about, as in :func: |
"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 |
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. |
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)
|
|
mirror(bvh: Bvh, lr_mapping: dict[str, str] | None = None, lateral_axis: str | None = None, inplace: bool = False) -> Bvh | None
¶
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
|
|
None
|
lateral_axis
|
str or None
|
Axis perpendicular to the mirror plane, e.g. |
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 |
reorient_world_up(bvh: Bvh, new_up: str, inplace: bool = False) -> Bvh | None
¶
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. |
required |
inplace
|
bool
|
|
False
|
Returns:
| Type | Description |
|---|---|
Bvh or None
|
|
reorient_rest_up(bvh: Bvh, new_up: str, inplace: bool = False) -> Bvh | None
¶
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. |
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
¶
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. |
required |
inplace
|
bool
|
|
False
|
Returns:
| Type | Description |
|---|---|
Bvh or None
|
|
Raises:
| Type | Description |
|---|---|
ValueError
|
If |