Skip to content

Rotations & SE(3)

rotations

Rotation & rigid-transform math for skeleton-based motion data.

All functions are batch-vectorized using NumPy and operate on arrays where the leading dimensions are batch dimensions.

Supported representations: - Euler angles: (, 3) in degrees or radians - Rotation matrices: (, 3, 3) - 6D rotation (Zhou et al., CVPR 2019): (, 6) — continuous representation - Quaternions: (, 4) in (w, x, y, z) scalar-first convention - Axis-angle: (, 3) — rotation axis scaled by rotation angle in radians - Rigid transforms (SE(3)): (, 4, 4) homogeneous matrices, with the matching se(3) twist coordinates [ω(3), v(3)] (rotation-first, V-Jacobian-coupled) — see :func:se3_exp / :func:se3_log.

Convention note: Euler angles in BVH files use intrinsic rotations with pre-multiplication: R = R_first @ R_second @ R_third where the order comes from the joint's rot_channels (e.g., ['Z','Y','X']). Angles are in degrees in BVH files, but most functions here work in radians unless stated otherwise.

Representation conversions

Batch-vectorized conversions between Euler angles, rotation matrices, quaternions, 6D, and axis-angle.

euler_to_rotmat(angles: npt.ArrayLike, order: Union[str, Sequence[str]], degrees: bool = False) -> npt.NDArray[np.float64]

Convert Euler angles to rotation matrices (batch).

Parameters:

Name Type Description Default
angles array_like, shape (*, 3) or (*, J, 3)

Euler angles. Each row is (angle1, angle2, angle3) following the axis order given by order. When order is a per-joint sequence of length J, the second-to-last axis is the joint axis.

required
order str or sequence of strings
  • 'ZYX' or ['Z', 'Y', 'X'] — single global order applied to every entry. R = R1 @ R2 @ R3 (intrinsic, pre-multiplied).
  • ['ZYX', 'ZYX', 'ZXY', ...] — per-joint orders, one entry per joint along axis -2 of angles. Input shape must satisfy angles.shape[-2] == len(order). Joints sharing an order are grouped so the rotation math vectorizes inside each group.
required
degrees bool

If True, Euler angles are in degrees. Default False (radians).

False

Returns:

Name Type Description
R ndarray, shape (*, 3, 3) or (*, J, 3, 3)

Rotation matrices, one per input entry.

rotmat_to_euler(R: npt.ArrayLike, order: Union[str, Sequence[str]], degrees: bool = False) -> npt.NDArray[np.float64]

Convert rotation matrices to Euler angles (batch).

Uses the convention of intrinsic rotations with pre-multiplication.

Every rotation has two equivalent Euler decompositions (middle angle reflected, both outer angles shifted by π). This function always returns the branch with the middle angle in [-π/2, π/2] for Tait-Bryan orders (distinct axes) or [0, π] for proper Euler orders (first axis repeated), outer angles in [-π, π] — so a triple authored outside those ranges round-trips to the equivalent in-range triple, not to itself.

In gimbal lock (middle angle at that range's boundary) only a combination of the two outer angles is determined; this function sets the first angle to 0 and folds the whole residual into the third. SciPy's Rotation.as_euler makes the opposite choice (third angle zeroed). Both describe the input rotation exactly — the round-trip is unaffected — and differ only in how the locked pair is split between the first and third slots.

Parameters:

Name Type Description Default
R array_like, shape (*, 3, 3) or (*, J, 3, 3)

Rotation matrices. When order is a per-joint sequence of length J, the third-to-last axis is the joint axis.

required
order str or sequence of strings
  • 'ZYX' or ['Z', 'Y', 'X'] — single global order.
  • ['ZYX', 'ZYX', ...] — per-joint orders, one entry per joint along axis -3 of R. Must satisfy R.shape[-3] == len(order).
required
degrees bool

If True, Euler angles are in degrees. Default False (radians).

False

Returns:

Name Type Description
angles ndarray, shape (*, 3) or (*, J, 3)

Euler angles in the specified order.

rotmat_to_rot6d(R: npt.ArrayLike) -> npt.NDArray[np.float64]

Convert rotation matrices to 6D representation.

The 6D representation consists of the first two columns of the rotation matrix, concatenated into a 6-vector.

Parameters:

Name Type Description Default
R array_like, shape (*, 3, 3)

Rotation matrices.

required

Returns:

Name Type Description
rot6d ndarray, shape (*, 6)

6D rotation vectors [col0 | col1].

rot6d_to_rotmat(rot6d: npt.ArrayLike) -> npt.NDArray[np.float64]

Convert 6D rotation representation to rotation matrices using Gram-Schmidt orthogonalization (Zhou et al., CVPR 2019).

Parameters:

Name Type Description Default
rot6d array_like, shape (*, 6)

6D rotation vectors [a1 | a2] where a1 and a2 are 3-vectors.

required

Returns:

Name Type Description
R ndarray, shape (*, 3, 3)

Rotation matrices (proper rotations, det = +1).

euler_to_rot6d(angles: npt.ArrayLike, order: Union[str, Sequence[str]], degrees: bool = False) -> npt.NDArray[np.float64]

Convert Euler angles to 6D rotation representation.

Parameters:

Name Type Description Default
angles array_like, shape (*, 3)

Euler angles.

required
order str or list

Rotation axis order, e.g. 'ZYX'.

required
degrees bool

If True, Euler angles are in degrees. Default False (radians).

False

Returns:

Name Type Description
rot6d ndarray, shape (*, 6)

rot6d_to_euler(rot6d: npt.ArrayLike, order: Union[str, Sequence[str]], degrees: bool = False) -> npt.NDArray[np.float64]

Convert 6D rotation representation to Euler angles.

Parameters:

Name Type Description Default
rot6d array_like, shape (*, 6)

6D rotation vectors.

required
order str or list

Rotation axis order, e.g. 'ZYX'.

required
degrees bool

If True, Euler angles are in degrees. Default False (radians).

False

Returns:

Name Type Description
angles ndarray, shape (*, 3)

rotmat_to_quat(R: npt.ArrayLike) -> npt.NDArray[np.float64]

Convert rotation matrices to quaternions (batch).

Uses the Shepperd method for numerical stability.

Parameters:

Name Type Description Default
R array_like, shape (*, 3, 3)

Rotation matrices.

required

Returns:

Name Type Description
q ndarray, shape (*, 4)

Unit quaternions in (w, x, y, z) scalar-first convention, in canonical form (w >= 0) — see Notes.

Notes

q and -q are the same rotation, so a convention is needed to pick one. This returns the w >= 0 half, applied per element: the alternative is to choose signs so a sequence stays in one hemisphere, and the two disagree whenever a rotation passes through 180°, where w crosses zero.

The consequence is that a converted sequence is not guaranteed to be temporally continuous: a joint sweeping smoothly through 180° flips sign between adjacent frames. The rotations remain exact — this round-trips through :func:quat_to_rotmat — but code that differences or measures distance on the raw values sees a jump that is not in the motion. Pass the result through :func:quat_unwrap when you need sequence continuity.

quat_to_rotmat(q: npt.ArrayLike) -> npt.NDArray[np.float64]

Convert quaternions to rotation matrices (batch).

Parameters:

Name Type Description Default
q array_like, shape (*, 4)

Quaternions in (w, x, y, z) scalar-first convention. Need not be unit quaternions (will be normalized).

required

Returns:

Name Type Description
R ndarray, shape (*, 3, 3)

Rotation matrices.

Raises:

Type Description
ValueError

If any input quaternion has zero norm (no rotation is defined).

euler_to_quat(angles: npt.ArrayLike, order: Union[str, Sequence[str]], degrees: bool = False) -> npt.NDArray[np.float64]

Convert Euler angles to quaternions.

Parameters:

Name Type Description Default
angles array_like, shape (*, 3)

Euler angles.

required
order str or list, e.g. 'ZYX'

Rotation axis order.

required
degrees bool

If True, Euler angles are in degrees. Default False (radians).

False

Returns:

Name Type Description
q ndarray, shape (*, 4)

Quaternions (w, x, y, z).

quat_to_euler(q: npt.ArrayLike, order: Union[str, Sequence[str]], degrees: bool = False) -> npt.NDArray[np.float64]

Convert quaternions to Euler angles.

Parameters:

Name Type Description Default
q array_like, shape (*, 4)

Quaternions (w, x, y, z).

required
order str or list, e.g. 'ZYX'

Rotation axis order.

required
degrees bool

If True, Euler angles are in degrees. Default False (radians).

False

Returns:

Name Type Description
angles ndarray, shape (*, 3)

Euler angles.

rotmat_to_axisangle(R: npt.ArrayLike) -> npt.NDArray[np.float64]

Convert rotation matrices to axis-angle representation (batch).

The axis-angle vector is the unit rotation axis scaled by the rotation angle (in radians). For the identity rotation the zero vector is returned.

This is the SO(3) log map, routed through the quaternion: angle = 2·atan2(‖q_vec‖, q_w) with the axis from the quaternion's vector part. Unlike the classic arccos((trace−1)/2) form, this stays machine-precise everywhere — including near 180°, where the trace route is ill-conditioned. The returned angle is in [0, π] (at exactly π the axis sign is inherently ambiguous).

Parameters:

Name Type Description Default
R array_like, shape (*, 3, 3)

Rotation matrices.

required

Returns:

Name Type Description
aa ndarray, shape (*, 3)

Axis-angle vectors (axis × angle_radians).

axisangle_to_rotmat(aa: npt.ArrayLike) -> npt.NDArray[np.float64]

Convert axis-angle vectors to rotation matrices using Rodrigues' formula (batch).

Parameters:

Name Type Description Default
aa array_like, shape (*, 3)

Axis-angle vectors (axis × angle_radians). Zero vector maps to identity.

required

Returns:

Name Type Description
R ndarray, shape (*, 3, 3)

Rotation matrices.

euler_to_axisangle(angles: npt.ArrayLike, order: Union[str, Sequence[str]], degrees: bool = False) -> npt.NDArray[np.float64]

Convert Euler angles to axis-angle vectors.

Parameters:

Name Type Description Default
angles array_like, shape (*, 3)

Euler angles.

required
order str or list, e.g. 'ZYX'

Rotation axis order.

required
degrees bool

If True, Euler angles are in degrees. Default False (radians).

False

Returns:

Name Type Description
aa ndarray, shape (*, 3)

Axis-angle vectors (axis × angle_radians).

axisangle_to_euler(aa: npt.ArrayLike, order: Union[str, Sequence[str]], degrees: bool = False) -> npt.NDArray[np.float64]

Convert axis-angle vectors to Euler angles.

Parameters:

Name Type Description Default
aa array_like, shape (*, 3)

Axis-angle vectors (axis × angle_radians).

required
order str or list, e.g. 'ZYX'

Rotation axis order.

required
degrees bool

If True, Euler angles are in degrees. Default False (radians).

False

Returns:

Name Type Description
angles ndarray, shape (*, 3)

Euler angles.

Quaternion utilities

Composition and spherical interpolation.

quat_multiply(q1: npt.ArrayLike, q2: npt.ArrayLike) -> npt.NDArray[np.float64]

Hamilton product of quaternions (batch).

Composes rotations: the result rotates by q2 first, then q1quat_to_rotmat(quat_multiply(q1, q2)) == quat_to_rotmat(q1) @ quat_to_rotmat(q2). Inputs broadcast against each other over the leading dimensions and are not normalized; multiply unit quaternions to compose rotations.

Parameters:

Name Type Description Default
q1 array_like, shape (*, 4)

Quaternions in (w, x, y, z) scalar-first convention. Leading dimensions broadcast (e.g. (F, J, 4) with (4,)).

required
q2 array_like, shape (*, 4)

Quaternions in (w, x, y, z) scalar-first convention. Leading dimensions broadcast (e.g. (F, J, 4) with (4,)).

required

Returns:

Name Type Description
q ndarray, shape (*, 4)

The Hamilton product q1 * q2.

quat_slerp(q1: npt.ArrayLike, q2: npt.ArrayLike, t: float | npt.ArrayLike, shortest: bool = True) -> npt.NDArray[np.float64]

Spherical linear interpolation between quaternions.

Parameters:

Name Type Description Default
q1 array_like, shape (*, 4)

Start quaternions (w, x, y, z).

required
q2 array_like, shape (*, 4)

End quaternions (w, x, y, z).

required
t float or array_like

Interpolation parameter(s) in [0, 1].

required
shortest bool

Which of the two arcs joining the rotations to travel. True (default) takes the short way round, never turning more than 180°. False takes the arc the given quaternions describe, which is the long way whenever dot(q1, q2) < 0. See Notes.

True

Returns:

Name Type Description
q ndarray, shape (*, 4)

Interpolated unit quaternions.

Notes

q and -q are the same rotation but describe opposite arcs between the same endpoints, so "interpolate between these two rotations" has two answers and a convention has to pick one. shortest=True is what resampling and blending want — a motion should not detour the long way just because a sign flipped, and :meth:Bvh.resample relies on it. shortest=False preserves a turn that genuinely exceeds 180°, such as a wind-up or a full spin, which the shortest arc would silently shorten.

With shortest=False and near-antipodal inputs (dot(q1, q2) near -1) the two rotations are half a turn apart and every great circle between them is equally valid; the result there is numerically unstable and not meaningful. shortest=True cannot reach that regime.

quat_unwrap(q: npt.ArrayLike, axis: int = 0) -> npt.NDArray[np.float64]

Make a quaternion sequence continuous by flipping signs along axis.

q and -q are the same rotation, so a per-frame canonical form — such as the w >= 0 one :func:rotmat_to_quat produces — can jump sign between adjacent frames while the motion itself is smooth. The rotations are correct either way, but the representation is discontinuous, which corrupts anything that differences, interpolates, or measures distance on the raw values: quaternion velocities, feature arrays fed to a model, naive L2 rotation distances.

This flips each element so consecutive quaternions lie in the same hemisphere, choosing the sign that keeps dot(q[i], q[i-1]) >= 0. The first element along axis is left as-is and sets the branch.

Parameters:

Name Type Description Default
q array_like, shape (*, 4)

Quaternions in (w, x, y, z) scalar-first convention. The sequence runs along axis; all other leading axes are independent sequences (e.g. (F, J, 4) unwraps each joint separately).

required
axis int

The time/sequence axis (default 0). May not be the trailing quaternion axis.

0

Returns:

Type Description
ndarray, shape (*, 4)

A new array; the input is not modified. Every element is either the input or its negation, so each still represents exactly the same rotation.

Raises:

Type Description
ValueError

If the trailing axis is not length 4, or axis refers to it.

Examples:

>>> _, quats = bvh.to_quat()             # (F, J, 4), canonical w >= 0
>>> quats = rotations.quat_unwrap(quats)  # continuous along frames
See Also

rotmat_to_quat : Produces the w >= 0 canonical form this undoes. quat_slerp : Handles the hemisphere itself; needs no unwrapping.

The convert dispatcher

One entry point that routes between any pair of representations by name.

convert(data: npt.ArrayLike, from_repr: str, to_repr: str, *, order: Union[str, Sequence[str], None] = None, degrees: bool = False) -> npt.NDArray[np.float64]

Convert rotation data between representations via a string alias.

Pivots through rotation matrices internally, so every pair of representations is reachable. When from_repr or to_repr is "euler", the order argument is required (and accepts the same single-string / per-joint-sequence forms as :func:euler_to_rotmat).

Parameters:

Name Type Description Default
data array_like

Input data. Shape depends on from_repr — see :data:REPRESENTATION_CHANNELS for the channel count.

required
from_repr str

One of "euler", "rotmat", "6d", "quat", "axisangle".

required
to_repr str

One of "euler", "rotmat", "6d", "quat", "axisangle".

required
order str or sequence of strings

Euler rotation order(s). Required when from_repr == "euler" or to_repr == "euler"; ignored otherwise.

None
degrees bool

Interpret/emit Euler angles in degrees (default False). Ignored when neither side is Euler.

False

Returns:

Type Description
ndarray

Converted data. Shape depends on to_repr.

SE(3) rigid transforms

Twists, the exp/log maps, screw interpolation, and segment-relative poses — each drawn in the Gallery.

se3_exp(twist: npt.ArrayLike) -> npt.NDArray[np.float64]

Exponential map se(3) → SE(3): twist [ω, v] → 4×4 transform (batch).

R = exp([ω]×) and d = V(ω) · v, where V is the SO(3) left Jacobian. The linear part v is therefore screw-coupled, not the raw translation (they coincide only when ω = 0).

Parameters:

Name Type Description Default
twist array_like, shape (*, 6)

se(3) coordinates [ω(3), v(3)], rotation-first.

required

Returns:

Name Type Description
T ndarray, shape (*, 4, 4)

Homogeneous rigid transforms.

See Also

se3_log : Inverse map. screw_interpolate : SE(3) geodesic blend.

Notes

Source: Modern Robotics (Lynch & Park); Vemulapalli et al. 2014.

se3_log(transform: npt.ArrayLike) -> npt.NDArray[np.float64]

Logarithm map SE(3) → se(3): 4×4 transform → twist [ω, v] (batch).

Inverse of :func:se3_exp: ω = log(R) and v = V⁻¹(ω) · d.

Parameters:

Name Type Description Default
transform array_like, shape (*, 4, 4)

Homogeneous rigid transforms.

required

Returns:

Name Type Description
twist ndarray, shape (*, 6)

se(3) coordinates [ω(3), v(3)], rotation-first.

Notes

At a rotation angle of exactly π the axis sign is ambiguous (as for any SO(3) log); the round-trip se3_exp(se3_log(T)) == T holds regardless because v is recomputed consistently with whichever ω is chosen.

Source: Modern Robotics; Vemulapalli et al. 2014.

se3_inverse(transform: npt.ArrayLike) -> npt.NDArray[np.float64]

Closed-form inverse of rigid transforms (batch).

[R, d]⁻¹ = [Rᵀ, −Rᵀd] — exact and cheaper than a general matrix inverse, since the rotation block of a rigid transform is orthogonal.

Parameters:

Name Type Description Default
transform array_like, shape (*, 4, 4)

Homogeneous rigid transforms.

required

Returns:

Type Description
ndarray, shape (*, 4, 4)

The inverse transforms.

See Also

se3_exp, se3_log : SE(3) exp/log maps. screw_interpolate : SE(3) geodesic blend (uses this inverse).

screw_interpolate(T0: npt.ArrayLike, T1: npt.ArrayLike, t: float | npt.ArrayLike) -> npt.NDArray[np.float64]

Screw-motion interpolation between two rigid transforms.

T0 · exp(t · log(T0⁻¹ T1)) — the SE(3) geodesic, the rigid-transform analogue of quaternion SLERP. Rotation and translation advance together along a constant screw axis. t = 0 returns T0; t = 1 returns T1.

Parameters:

Name Type Description Default
T0 array_like, shape (*, 4, 4)

Endpoint transforms.

required
T1 array_like, shape (*, 4, 4)

Endpoint transforms.

required
t float or array_like

Interpolation parameter(s) (typically in [0, 1], extrapolates outside). An array t broadcasts against the batch shape like :func:quat_slerp — e.g. a single transform pair with t of shape (K,) yields (K, 4, 4).

required

Returns:

Type Description
ndarray, shape (*, 4, 4)

The interpolated transform.

Notes

Source: Vemulapalli et al. 2014 (Lie-group skeletal features).

relative_transform(seg_m: npt.ArrayLike, seg_n: npt.ArrayLike) -> npt.NDArray[np.float64]

Rigid transform of segment n in segment m's local frame.

The geometry→SE(3) bridge: each segment (a pair of endpoint positions) defines a local coordinate frame — origin at its start, x-axis along the segment, the remaining axes completed orthonormally — and the result is T_m⁻¹ · T_n, the pose of segment n relative to segment m. Feed the resulting transforms to :func:se3_log for Lie-group features.

Parameters:

Name Type Description Default
seg_m array_like, shape (*, 2, 3)

Segment endpoint pairs [start, end]; each must have nonzero length (coincident endpoints have no frame and yield nan).

required
seg_n array_like, shape (*, 2, 3)

Segment endpoint pairs [start, end]; each must have nonzero length (coincident endpoints have no frame and yield nan).

required

Returns:

Type Description
ndarray, shape (*, 4, 4)

The relative rigid transform.

Notes

Source: Vemulapalli et al. 2014.

rotation_geodesic_distance(R1: npt.ArrayLike, R2: npt.ArrayLike) -> npt.NDArray[np.float64]

Geodesic (angular) distance between rotations, in radians (batch).

‖log(R1ᵀ R2)‖ — the angle of the relative rotation, the shortest arc on SO(3). Equivalent to 2·arccos(|⟨q1, q2⟩|) on quaternions. Result is in [0, π].

Parameters:

Name Type Description Default
R1 array_like, shape (*, 3, 3)

Rotation matrices.

required
R2 array_like, shape (*, 3, 3)

Rotation matrices.

required

Returns:

Type Description
ndarray, shape (*)

Geodesic distance in radians.

Notes

Source: Aristidou et al. 2017/2018 (orientation-space metrics).

mean_rotation(R: npt.ArrayLike) -> npt.NDArray[np.float64]

Chordal (Frobenius) mean of a set of rotation matrices (batch).

The rotation minimizing Σᵢ ‖R − Rᵢ‖²_F over SO(3) — the projection of the arithmetic matrix mean back onto the rotation group: M = mean(Rᵢ), M = U Σ Vᵀ (SVD), result U diag(1, 1, det(U Vᵀ)) Vᵀ. The determinant guard keeps the result right-handed (a proper rotation, never a reflection). For rotations within ~π/2 of each other this closely tracks the geodesic (Karcher) mean while staying closed-form and batch-vectorized.

Parameters:

Name Type Description Default
R (array_like, shape(..., N, 3, 3))

Rotation matrices; the mean is taken over axis -3 (the N axis). Leading batch axes are preserved.

required

Returns:

Type Description
(ndarray, shape(..., 3, 3))

The mean rotation per batch entry.

Raises:

Type Description
ValueError

If the input is not at least 3-D with trailing (3, 3) shape, or if N == 0 (the mean of an empty set is undefined; catching it here beats the NaN-mean warning and the cryptic SVD failure it would otherwise become).

See Also

rotation_geodesic_distance : Use it to check the spread of the inputs before trusting the mean. pybvh.analysis.facing_frame : Per-frame facing bases — averaging a clip's facing (via the frames' rotation matrices) is the motivating use.

Notes

Degenerate spreads. When the inputs are spread wide on SO(3) (angles approaching π apart — e.g. antipodal pairs), the arithmetic mean M loses rank and the SVD's choice of singular vectors in the collapsed subspace is arbitrary: the returned matrix is still a valid right-handed rotation, but its orientation within the collapsed plane is meaningless and numerically unstable (a tiny input perturbation can swing it). Check the spread with :func:rotation_geodesic_distance against the returned mean before interpreting it.

Source: Moakher 2002 (means on SO(3)); Hartley, Trumpf, Dai & Li 2013 (rotation averaging).