pythonscipyeuler-anglesrotational-matricessimpleitk

Difference between SimpleITK.Euler3DTransform and scipy.spatial.transform.Rotation.from_euler?


Using these two library functions:

to create a simple rotation matrix from Euler Angles:

import numpy as np
import SimpleITK as sitk
from scipy.spatial.transform import Rotation
from math import pi

euler_angles = [pi / 10, pi / 18, pi / 36]

sitk_matrix = sitk.Euler3DTransform((0, 0, 0), *euler_angles).GetMatrix()
sitk_matrix = np.array(sitk_matrix).reshape((3,3))
print(np.array_str(sitk_matrix, precision=3, suppress_small=True))

order = 'XYZ' # Different results for any order in ['XYZ','XZY','YZX','YXZ','ZXY','ZYX','xyz','xzy','yzx','yxz','zxy','zyx']
scipy_matrix = Rotation.from_euler(order, euler_angles).as_matrix()
print(np.array_str(scipy_matrix, precision=3, suppress_small=True))

I get two different results:

[[ 0.976 -0.083  0.2  ]
 [ 0.139  0.947 -0.288]
 [-0.165  0.309  0.937]]
[[ 0.981 -0.086  0.174]
 [ 0.136  0.943 -0.304]
 [-0.138  0.322  0.937]]

Why? How can I compute the same matrix as SimpleITK using scipy?


Solution

  • The issue is that the itk.Euler3DTransform class by default applies the rotation matrix multiplications in Z @ X @ Y order and the Rotation.from_euler function in Z @ Y @ X order.

    Note that this is independent of the order you specified. The order you specify refers to the order of the angles, not the order of the matrix multiplications.

    If you are using the itk.Euler3DTransform directly as you showed in your example, you can actually change the default behavior for itk to perform the matrix multiplication in Z @ Y @ X order.

    I never worked with sitk but in theory and based on the documentation, something like this should work:

    euler_transform = sitk.Euler3DTransform((0, 0, 0), *euler_angles)
    euler_transform.SetComputeZYX(True)
    sitk_matrix = euler_transform.GetMatrix()
    

    Alternatively, I wrote a function which is similar to Rotation.from_euler but has the option to specify the rotation order as well:

    from typing import List
    from numpy.typing import NDArray
    
    def build_rotation_3d(radians: NDArray,
                          radians_oder: str = 'XYZ',
                          rotation_order: str = 'ZYX',
                          dims: List[str] = ['X', 'Y', 'Z']) -> NDArray:
        x_rad, y_rad, z_rad = radians[(np.searchsorted(dims, list(radians_oder)))]
        x_cos, y_cos, z_cos = np.cos([x_rad, y_rad, z_rad], dtype=np.float64)
        x_sin, y_sin, z_sin = np.sin([x_rad, y_rad, z_rad], dtype=np.float64)
        x_rot = np.asarray([
            [1.0, 0.0, 0.0, 0.0],
            [0.0, x_cos, -x_sin, 0.0],
            [0.0, x_sin, x_cos, 0.0],
            [0.0, 0.0, 0.0, 1.0],
        ])
        y_rot = np.asarray([
            [y_cos, 0.0, y_sin, 0.0],
            [0.0, 1.0, 0.0, 0.0],
            [-y_sin, 0.0, y_cos, 0.0],
            [0.0, 0.0, 0.0, 1.0],
        ])
        z_rot = np.asarray([
            [z_cos, -z_sin, 0.0, 0.0],
            [z_sin, z_cos, 0.0, 0.0],
            [0.0, 0.0, 1.0, 0.0],
            [0.0, 0.0, 0.0, 1.0],
        ])
    
        rotations = np.asarray([x_rot, y_rot, z_rot])[(np.searchsorted(dims, list(rotation_order)))]
    
        return rotations[0] @ rotations[1] @ rotations[2]
    

    To get the respective rotation matrix which matches the EulerTransform:

    rotation_matrix = build_rotation_3d(numpy.array(euler_angles),
                                        radians_oder = 'XYZ',
                                        rotation_order = 'ZXY')