diff --git a/README.md b/README.md index 4d95bad..214ec28 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/docsource/source/index.rst b/docsource/source/index.rst index 888d629..baa7003 100644 --- a/docsource/source/index.rst +++ b/docsource/source/index.rst @@ -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 diff --git a/roma/euler.py b/roma/euler.py index 5cba988..d5f32fe 100644 --- a/roma/euler.py +++ b/roma/euler.py @@ -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)] @@ -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: @@ -89,8 +91,7 @@ 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: @@ -98,7 +99,7 @@ def euler_to_rotmat(convention: str, angles, degrees=False, dtype=None, device=N """ 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. @@ -106,12 +107,12 @@ def unitquat_to_euler(convention : str, quat, as_tensor=False, degrees=False, ep 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. @@ -202,12 +203,12 @@ 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. @@ -215,17 +216,17 @@ def rotvec_to_euler(convention : str, rotvec, as_tensor=False, degrees=False, ep 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. @@ -233,12 +234,12 @@ def rotmat_to_euler(convention : str, rotmat, as_tensor=False, degrees=False, ep 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) \ No newline at end of file + return unitquat_to_euler(convention, roma.rotmat_to_unitquat(rotmat), as_tuple=as_tuple, degrees=degrees, epsilon=epsilon) \ No newline at end of file diff --git a/test/test_euler.py b/test/test_euler.py index c6068fe..0b12b48 100644 --- a/test/test_euler.py +++ b/test/test_euler.py @@ -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: @@ -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: @@ -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: @@ -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)