Skip to content

Commit

Permalink
Euler: changed as_tensor to as_tuple and updated doc
Browse files Browse the repository at this point in the history
  • Loading branch information
Romain BRÉGIER committed Apr 25, 2024
1 parent 0768dfd commit 4af5c29
Show file tree
Hide file tree
Showing 4 changed files with 32 additions and 31 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ rotvec = torch.randn(batch_shape + (3,))
q = roma.rotvec_to_unitquat(rotvec)
R = roma.unitquat_to_rotmat(q)
Rbis = roma.rotvec_to_rotmat(rotvec)
euler_angles = roma.unitquat_to_euler('xyz', q, as_tensor=True, degrees=True)
euler_angles = roma.unitquat_to_euler('xyz', q, degrees=True)

# Regression of a rotation from an arbitrary input:
# Special Procrustes orthonormalization of a 3x3 matrix
Expand Down
4 changes: 2 additions & 2 deletions docsource/source/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -57,9 +57,9 @@ Rotation matrix (rotmat)
- Encoded as a ...xDxD tensor (D=3 for 3D rotations).
- We use column-vector convention, i.e. :math:`R X` is the transformation of a 1xD vector :math:`X` by a rotation matrix :math:`R`.

Euler and Tait-Bryan angles (euler)
Euler angles and Tait-Bryan angles (euler)
- Encoded as a ...xD tensor or a list of D tensors corresponding to each angle (D=3 for typical Euler angles conventions).
- We provide mappings between Euler angles and other rotation representations. To perform actual computations, use an other representation.
- We provide mappings between Euler angles and other rotation representations. Euler angles suffer from shortcomings such as gimbal lock, and we recommend using quaternions or rotation matrices to perform actual computations.


Mappings between rotation representations
Expand Down
45 changes: 23 additions & 22 deletions roma/euler.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,16 +24,19 @@ def euler_to_unitquat(convention: str, angles, degrees=False, normalize=True, dt
Convert Euler angles to unit quaternion representation.
Args:
convention (string): string defining a sequence of rotation axes ('XYZ' or 'xzx' for example).
convention (string): string defining a sequence of D rotation axes ('XYZ' or 'xzx' for example).
The sequence of rotation is expressed either with respect to a global 'extrinsic' coordinate system (in which case axes are denoted in lowercase: 'x', 'y', or 'z'),
or with respect to an 'intrinsic' coordinates system attached to the object under rotation (in which case axes are denoted in uppercase: 'X', 'Y', 'Z').
Intrinsic and extrinsic conventions cannot be mixed.
angles (list of floats, list of tensors, or tensor): a list of angles associated to each axis, expressed in radians by default.
If a single tensor is provided, Euler angles are assumed to be stacked along the last dimension.
angles (...xD tensor, or tuple/list of D floats or ... tensors): a list of angles associated to each axis, expressed in radians by default.
degrees (bool): if True, input angles are assumed to be expressed in degrees.
normalize (bool): if True, normalize the returned quaternion to compensate potential numerical.
Returns:
A batch of unit quaternions (...x4 tensor, XYZW convention).
Warning:
Case is important: 'xyz' and 'XYZ' denote different conventions.
"""
if type(angles) == torch.Tensor:
angles = [t.squeeze(dim=-1) for t in torch.split(angles, split_size_or_sections=1, dim=-1)]
Expand Down Expand Up @@ -74,8 +77,7 @@ def euler_to_rotvec(convention: str, angles, degrees=False, dtype=None, device=N
Args:
convention (string): 'xyz' for example. See :func:`~roma.euler.euler_to_unitquat()`.
angles (list of floats, list of tensors, or tensor): a list of angles associated to each axis, expressed in radians by default.
If a single tensor is provided, Euler angles are assumed to be stacked along the last dimension.
angles (...xD tensor, or tuple/list of D floats or ... tensors): a list of angles associated to each axis, expressed in radians by default.
degrees (bool): if True, input angles are assumed to be expressed in degrees.
Returns:
Expand All @@ -89,29 +91,28 @@ def euler_to_rotmat(convention: str, angles, degrees=False, dtype=None, device=N
Args:
convention (string): 'xyz' for example. See :func:`~roma.euler.euler_to_unitquat()`.
angles (list of floats, list of tensors, or tensor): a list of angles associated to each axis, expressed in radians by default.
If a single tensor is provided, Euler angles are assumed to be stacked along the last dimension.
angles (...xD tensor, or tuple/list of D floats or ... tensors): a list of angles associated to each axis, expressed in radians by default.
degrees (bool): if True, input angles are assumed to be expressed in degrees.
Returns:
a batch of rotation matrices (...x3x3 tensor).
"""
return roma.unitquat_to_rotmat(euler_to_unitquat(convention=convention, angles=angles, degrees=degrees, dtype=dtype, device=device))

def unitquat_to_euler(convention : str, quat, as_tensor=False, degrees=False, epsilon=1e-7):
def unitquat_to_euler(convention : str, quat, as_tuple=False, degrees=False, epsilon=1e-7):
"""
Convert unit quaternion to Euler angles representation.
Args:
convention (str): string of 3 characters belonging to {'x', 'y', 'z'} for extrinsic rotations, or {'X', 'Y', 'Z'} for intrinsic rotations.
Consecutive axes should not be identical.
quat (...x4 tensor, XYZW convention): input batch of unit quaternion.
as_tensor (boolean): if True, angles are returned as a stacked ...x3 tensor.
as_tuple (boolean): if True, angles are not stacked but returned as a tuple of tensors.
degrees (bool): if True, angles are returned in degrees.
epsilon (float): a small value used to detect degenerate configurations.
Returns:
A list of 3 tensors corresponding to each Euler angle, expressed by default in radians.
A stacked ...x3 tensor corresponding to Euler angles, expressed by default in radians.
In case of gimbal lock, the third angle is arbitrarily set to 0.
"""
# Code adapted from scipy.spatial.transform.Rotation.
Expand Down Expand Up @@ -202,43 +203,43 @@ def unitquat_to_euler(convention : str, quat, as_tensor=False, degrees=False, ep
foo = torch.rad2deg(foo)
angles[idx] = roma.internal.unflatten_batch_dims(foo, batch_shape)

if as_tensor:
angles = torch.stack(angles, dim=-1)

return angles
if as_tuple:
return tuple(angles)
else:
return torch.stack(angles, dim=-1)

def rotvec_to_euler(convention : str, rotvec, as_tensor=False, degrees=False, epsilon=1e-7):
def rotvec_to_euler(convention : str, rotvec, as_tuple=False, degrees=False, epsilon=1e-7):
"""
Convert rotation vector to Euler angles representation.
Args:
convention (str): string of 3 characters belonging to {'x', 'y', 'z'} for extrinsic rotations, or {'X', 'Y', 'Z'} for intrinsic rotations.
Consecutive axes should not be identical.
rotvec (...x3 tensor): input batch of rotation vectors.
as_tensor (boolean): if True, angles are returned as a stacked ...x3 tensor.
as_tuple (boolean): if True, angles are not stacked but returned as a tuple of tensors.
degrees (bool): if True, angles are returned in degrees.
epsilon (float): a small value used to detect degenerate configurations.
Returns:
A list of 3 tensors corresponding to each Euler angle, expressed by default in radians.
A stacked ...x3 tensor corresponding to Euler angles, expressed by default in radians.
In case of gimbal lock, the third angle is arbitrarily set to 0.
"""
return unitquat_to_euler(convention, roma.rotvec_to_unitquat(rotvec), degrees=degrees, epsilon=epsilon)
return unitquat_to_euler(convention, roma.rotvec_to_unitquat(rotvec), as_tuple=as_tuple, degrees=degrees, epsilon=epsilon)

def rotmat_to_euler(convention : str, rotmat, as_tensor=False, degrees=False, epsilon=1e-7):
def rotmat_to_euler(convention : str, rotmat, as_tuple=False, degrees=False, epsilon=1e-7):
"""
Convert rotation matrix to Euler angles representation.
Args:
convention (str): string of 3 characters belonging to {'x', 'y', 'z'} for extrinsic rotations, or {'X', 'Y', 'Z'} for intrinsic rotations.
Consecutive axes should not be identical.
rotmat (...x3x3 tensor): input batch of rotation matrices.
as_tensor (boolean): if True, angles are returned as a stacked ...x3 tensor.
as_tuple (boolean): if True, angles are not stacked but returned as a tuple of tensors.
degrees (bool): if True, angles are returned in degrees.
epsilon (float): a small value used to detect degenerate configurations.
Returns:
A list of 3 tensors corresponding to each Euler angle, expressed by default in radians.
A stacked ...x3 tensor corresponding to Euler angles, expressed by default in radians.
In case of gimbal lock, the third angle is arbitrarily set to 0.
"""
return unitquat_to_euler(convention, roma.rotmat_to_unitquat(rotmat), degrees=degrees, epsilon=epsilon)
return unitquat_to_euler(convention, roma.rotmat_to_unitquat(rotmat), as_tuple=as_tuple, degrees=degrees, epsilon=epsilon)
12 changes: 6 additions & 6 deletions test/test_euler.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ def test_euler_unitquat_consistency(self):
if intrinsics:
convention = convention.upper()
q = roma.random_unitquat(batch_shape, device=device, dtype=dtype)
angles = roma.unitquat_to_euler(convention, q, degrees=degrees)
angles = roma.unitquat_to_euler(convention, q, degrees=degrees, as_tuple=True)
self.assertTrue(len(angles) == 3)
self.assertTrue(all([angle.shape == batch_shape for angle in angles]))
if degrees:
Expand All @@ -52,7 +52,7 @@ def test_euler_rotvec_consistency(self):
if intrinsics:
convention = convention.upper()
q = roma.random_rotvec(batch_shape, device=device, dtype=dtype)
angles = roma.rotvec_to_euler(convention, q, degrees=degrees)
angles = roma.rotvec_to_euler(convention, q, degrees=degrees, as_tuple=True)
self.assertTrue(len(angles) == 3)
self.assertTrue(all([angle.shape == batch_shape for angle in angles]))
if degrees:
Expand All @@ -74,7 +74,7 @@ def test_euler_rotmat_consistency(self):
if intrinsics:
convention = convention.upper()
q = roma.random_rotmat(batch_shape, device=device, dtype=dtype)
angles = roma.rotmat_to_euler(convention, q, degrees=degrees)
angles = roma.rotmat_to_euler(convention, q, degrees=degrees, as_tuple=True)
self.assertTrue(len(angles) == 3)
self.assertTrue(all([angle.shape == batch_shape for angle in angles]))
if degrees:
Expand All @@ -101,9 +101,9 @@ def test_euler_tensor(self):
dtype = torch.float64
q = roma.random_unitquat(batch_shape, device=device, dtype=dtype)
convention = 'xyz'
angles = roma.unitquat_to_euler(convention, q)
angles_tensor = roma.unitquat_to_euler(convention, q, as_tensor=True)
assert type(angles) == list
angles = roma.unitquat_to_euler(convention, q, as_tuple=True)
angles_tensor = roma.unitquat_to_euler(convention, q)
assert type(angles) == tuple
assert type(angles_tensor) == torch.Tensor
q1 = roma.euler_to_unitquat(convention, angles)
q2 = roma.euler_to_unitquat(convention, angles_tensor)
Expand Down

0 comments on commit 4af5c29

Please sign in to comment.