Search code examples
pythonimage-processing3dimage-rotationeuler-angles

Apply rotation defined by Euler angles to 3D image, in python


I'm working with 3D images and have to rotate them according to Euler angles (phi,psi,theta) in 'zxz' convention (these Euler angles are part of a dataset, so I have to use that convention). I found the function scipy.ndimage.rotate that seems useful in that regard.

arrayR = scipy.ndimage.rotate(array , phi, axes=(0,1), reshape=False)
arrayR = scipy.ndimage.rotate(arrayR, psi, axes=(1,2), reshape=False)
arrayR = scipy.ndimage.rotate(arrayR, the, axes=(0,1), reshape=False)

Sadly, this does not do what intended. This is why:

Definition:

In the z-x-z convention, the x-y-z frame is rotated three times: first about the z-axis by an angle phi; then about the new x-axis by an angle psi; then about the newest z-axis by an angle theta.

However with above code, the rotations are always with respect to the original axes. Which is why obtained rotations are not correct. Anyone has a suggestion to obtain correct rotations, as explained in the definition?

In other words, in the present 'zxz' convention the rotations are intrinsic (rotations about the axes of the rotating coordinate system XYZ, solidary with the moving body, which changes its orientation after each elemental rotation). If I use the above code, the rotations are extrinsic (rotations about the axes xyz of the original coordinate system, which is assumed to remain motionless). I need a way for doing extrinsic rotations, in python.


Solution

  • I found a satisfying solution following this link: https://nbviewer.jupyter.org/gist/lhk/f05ee20b5a826e4c8b9bb3e528348688

    This method uses np.meshgrid, scipy.ndimage.map_coordinates. The above link uses some third party library for generating the rotation matrix, however I use scipy.spatial.transform.Rotation. This function allows to define both intrinsic and extrinsic rotations: see description of scipy.spatial.transform.Rotation.from_euler.

    Here is my function:

    import numpy as np
    from scipy.spatial.transform import Rotation as R
    from scipy.ndimage import map_coordinates
    
    # Rotates 3D image around image center
    # INPUTS
    #   array: 3D numpy array
    #   orient: list of Euler angles (phi,psi,the)
    # OUTPUT
    #   arrayR: rotated 3D numpy array
    # by E. Moebel, 2020
    def rotate_array(array, orient):
        phi = orient[0]
        psi = orient[1]
        the = orient[2]
    
        # create meshgrid
        dim = array.shape
        ax = np.arange(dim[0])
        ay = np.arange(dim[1])
        az = np.arange(dim[2])
        coords = np.meshgrid(ax, ay, az)
    
        # stack the meshgrid to position vectors, center them around 0 by substracting dim/2
        xyz = np.vstack([coords[0].reshape(-1) - float(dim[0]) / 2,  # x coordinate, centered
                         coords[1].reshape(-1) - float(dim[1]) / 2,  # y coordinate, centered
                         coords[2].reshape(-1) - float(dim[2]) / 2])  # z coordinate, centered
    
        # create transformation matrix
        r = R.from_euler('zxz', [phi, psi, the], degrees=True)
        mat = r.as_matrix()
    
        # apply transformation
        transformed_xyz = np.dot(mat, xyz)
    
        # extract coordinates
        x = transformed_xyz[0, :] + float(dim[0]) / 2
        y = transformed_xyz[1, :] + float(dim[1]) / 2
        z = transformed_xyz[2, :] + float(dim[2]) / 2
    
        x = x.reshape((dim[1],dim[0],dim[2]))
        y = y.reshape((dim[1],dim[0],dim[2]))
        z = z.reshape((dim[1],dim[0],dim[2])) # reason for strange ordering: see next line
    
        # the coordinate system seems to be strange, it has to be ordered like this
        new_xyz = [y, x, z]
    
        # sample
        arrayR = map_coordinates(array, new_xyz, order=1)
    

    Note: You can also use this function for intrinsic rotations, simply adapt the first argument of 'from_euler' to your Euler convention. In this case, you obtain equivalent result than in my 1st post (using scipy.ndimage.rotate). However I noticed that the present code is 3x faster (0.01s for 40^3 volume) than when using scipy.ndimage.rotate (0.03s for 40^3 volume).

    Hope this will help someone!