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 |
required |
order
|
str or sequence of strings
|
|
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 |
required |
order
|
str or sequence of strings
|
|
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 ( |
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 q1 —
quat_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. |
required |
q2
|
array_like, shape (*, 4)
|
Quaternions in (w, x, y, z) scalar-first convention. Leading
dimensions broadcast (e.g. |
required |
Returns:
| Name | Type | Description |
|---|---|---|
q |
ndarray, shape (*, 4)
|
The Hamilton product |
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
|
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 |
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 |
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 |
required |
from_repr
|
str
|
One of |
required |
to_repr
|
str
|
One of |
required |
order
|
str or sequence of strings
|
Euler rotation order(s). Required when |
None
|
degrees
|
bool
|
Interpret/emit Euler angles in degrees (default |
False
|
Returns:
| Type | Description |
|---|---|
ndarray
|
Converted data. Shape depends on |
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 |
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 |
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 |
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 |
required |
seg_n
|
array_like, shape (*, 2, 3)
|
Segment endpoint pairs |
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 |
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 |
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).