Given 3D MRI scans A
, B
, and C
I want to perform an affine (co)registration of B
onto A
, take the transformation affine matrix of the registration and apply it on C
.
My problem is that the affine matrix of the registration transform has the wrong signs. Maybe due to wrong orientation?
The TransformParameters
contain 12 values of which the first 9 are the rotation matrix in row-major order and the last 3 are the translation values.
TransformParameters = [R1, R2, R3, R4, R5, R6, R7, R8, R9, Tx, Ty, Tz]
registration_affine = [[R1, R2, R3, Tx],
[R4, R5, R6, Ty],
[R7, R8, R9, Tz],
[0, 0, 0, 1 ]]
I know that ITK holds images in LPS
orientation and nibabel in RAS
.
So I tried to apply a change with respect to the orientation difference to the transform_affine
but this did not work out.
I can not get the same registration output as ITK, below I will show some number examples and my minimal code example.
To test this I applied an affine transformation to an existing image. The inverse of that transformation matrix is the true affine the registration could find.
array([[ 1.02800583, 0.11462834, -0.11426342, -0.43383606],
[ 0.11462834, 1.02800583, -0.11426342, 0.47954143],
[-0.11426342, -0.11426342, 1.02285268, -0.20457054],
[ 0. , 0. , 0. , 1. ]])
But the affine constructed as explained above, yields:
array([[ 1.02757335, 0.11459412, 0.11448339, 0.23000557],
[ 0.11410441, 1.02746452, 0.11413955, -0.20848751],
[ 0.11398788, 0.11411115, 1.02255042, -0.04884404],
[ 0. , 0. , 0. , 1. ]])
You can see that the values are quite close but that only the signs are wrong. In fact, if I manually set the same signs as in the "true" matrix, the transformation matrix is good.
In the ITK loader of MONAI I found code that suggested to do the following to convert an ITK affine to a nibabel affine:
np.diag([-1, -1, 1, 1]) @ registration_affine
If I use nibabels ornt_transform
methods to get the ornt transform from LPS
to RAS
, this returns [-1, -1, 1]
and matches what is done in the ITK loader of MONAI.
But applying this to the affine from above does not actually yield the correct signs (only in the translation bit):
array([[-1.02757335, -0.11459412, -0.11448339, -0.23000557],
[-0.11410441, -1.02746452, -0.11413955, 0.20848751],
[ 0.11398788, 0.11411115, 1.02255042, -0.04884404],
[ 0. , 0. , 0. , 1. ]])
So I am a bit stuck here.
Here a complete minimal code example to run what I'm doing / trying to do. See below also the example data and package versions.
import nibabel
import numpy as np
from monai.transforms import Affine
from nibabel import Nifti1Image
import itk
# Import Images
moving_image = itk.imread('moving_2mm.nii.gz', itk.F)
fixed_image = itk.imread('fixed_2mm.nii.gz', itk.F)
# Import Default Parameter Map
parameter_object = itk.ParameterObject.New()
affine_parameter_map = parameter_object.GetDefaultParameterMap('affine', 4)
affine_parameter_map['FinalBSplineInterpolationOrder'] = ['1']
parameter_object.AddParameterMap(affine_parameter_map)
# Call registration function
result_image, result_transform_parameters = itk.elastix_registration_method(
fixed_image, moving_image, parameter_object=parameter_object)
parameter_map = result_transform_parameters.GetParameterMap(0)
transform_parameters = np.array(parameter_map['TransformParameters'], dtype=float)
itk.imwrite(result_image, 'reg_itk.nii.gz', compression=True)
# Convert ITK params to affine matrix
rotation = transform_parameters[:9].reshape(3, 3)
translation = transform_parameters[-3:][..., np.newaxis]
reg_affine: np.ndarray = np.append(rotation, translation, axis=1) # type: ignore
reg_affine = np.append(reg_affine, [[0, 0, 0, 1]], axis=0) # type: ignore
# Apply affine transform matrix via MONAI
moving_image_ni: Nifti1Image = nibabel.load('moving_2mm.nii.gz')
fixed_image_ni: Nifti1Image = nibabel.load('fixed_2mm.nii.gz')
moving_image_np: np.ndarray = moving_image_ni.get_fdata() # type: ignore
LPS = nibabel.orientations.axcodes2ornt(('L', 'P', 'S'))
RAS = nibabel.orientations.axcodes2ornt(('R', 'A', 'S'))
ornt_transform = nibabel.orientations.ornt_transform(LPS, RAS)[:, -1] # type: ignore
affine_transform = Affine(affine=np.diag([*ornt_transform, 1]) @ reg_affine, image_only=False)
out_img, out_affine = affine_transform(moving_image_np[np.newaxis, ...])
reg_monai = np.squeeze(out_img)
out = Nifti1Image(reg_monai, fixed_image_ni.affine, header=fixed_image_ni.header)
nibabel.save(out, 'reg_monai.nii.gz')
Input data:
Output data:
Package versions:
itk-elastix==0.12.0
monai==0.8.0
nibabel==3.1.1
numpy==1.19.2
I did ask this question before on the ITKElastix project on GitHub #145 but could not resolve my issues. Thanks to dzenanz and mstaring who tried to help over there.