Behaviour of affine transform on 3D image with non-uniform resolution with Scipy

孤街浪徒 提交于 2021-02-08 08:43:18

问题


I'm looking to apply an affine transformation, defined in homogeneous coordinates on images of different resolutions, but I encounter an issue when one ax is of different resolution of the others.

Normally, as only the translation part of the affine is dependent of the resolution, I normalize the translation part by the resolution and apply the corresponding affine on the image, using scipy.ndimage.affine_transform.

If the resolution of the image is the same for all axes, it works perfectly, you can see below images of the same transformation (being a scale+translation, or rotation+translation, see code below) being applied to an image, its downsampled version (meaning at a lower resolution). Images match (almost) perfectly, differences in voxel values are mainly caused as far as I know by interpolation errors.

But you can see that the shapes overlay between the downsampled transformed image and the transformed (downsampled for comparison) image

Scale affine transformation applied on the same image, at two different (uniform) resolutions

Rotation affine transformation applied on the same image, at two different (uniform) resolutions

Unfortunately, if one of the image axis has a different resolution than the other (see code below), it works well with affine transform with null non-diagonal terms (like translation, or scaling) but the result of the transformation gives a completely wrong result.

Rotation affine transformation applied on the same image, at two different (non-uniform) resolutions

Here you can see a minimal working example of the code:


import numpy as np
import nibabel as nib
from scipy.ndimage import zoom
from scipy.ndimage import affine_transform
import matplotlib.pyplot as plt

################################
#### LOAD ANY 3D IMAGE HERE ####
################################
#%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% TO BE DEFINED BY USER
orig_img = np.zeros((100, 100, 100))
orig_img[25:75, 25:75, 25:75] = 1

ndim = orig_img.ndim

################################
##### DEFINE RESOLUTIONS #######

#%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% TO BE DEFINED BY USER
# Comment/uncomment to choose the resolutions (in mm) of the images
# ORIG_RESOLUTION = [1., 1., 1.]
# TARGET_RESOLUTION = [2., 2., 2.]

ORIG_RESOLUTION = [1., 0.5, 1.]
TARGET_RESOLUTION = [2., 2., 2.]

#####################################
##### DEFINE AFFINE TRANSFORM #######

affine_scale_translation = np.array([[1.5, 0.0, 0.0, -25.],
                                     [0.0, 0.8, 0.0, 0. ],
                                     [0.0, 0.0, 1.0, 0.  ],
                                     [0.0, 0.0, 0.0, 1.0]])
a = np.sqrt(2)/2.
affine_rotation_translation = np.array([[a  , a  , 0.0, -25.],
                                     [-a , a  , 0.0, 50. ],
                                     [0.0, 0.0, 1.0, 0.0 ],
                                     [0.0, 0.0, 0.0, 1.0]])

# #%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% TO BE DEFINED BY USER
# Comment/uncomment to choose the transformation to be applied 

# affine_tf, name_affine = affine_scale_translation, "Tf scale"
affine_tf, name_affine = affine_rotation_translation, "Tf rotation"

######################################################
######## DOWNSAMPLE IMAGE TO LOWER RESOLUTION ########
######################################################

downsample_img = zoom(orig_img,
                      zoom=np.array(ORIG_RESOLUTION)/np.array(TARGET_RESOLUTION),
                      prefilter=False, order=1)

##############################################################################
######## APPLY AFFINE TRANSFORMATION TO ORIGINAL AND DOWNSAMPLE IMAGE ########
##############################################################################

affine_st_full_res, affine_st_low_res = affine_tf.copy(), affine_tf.copy()

# Inverse transform as affine_transform apply the tf from the target space to the original space
affine_st_full_res, affine_st_low_res = np.linalg.inv(affine_st_full_res), np.linalg.inv(affine_st_low_res)

# Normalize translation part (normally expressed in millimeters) for the resolution
affine_st_full_res[:ndim, ndim] = affine_st_full_res[:ndim, ndim] / ORIG_RESOLUTION
affine_st_low_res[:ndim, ndim] = affine_st_low_res[:ndim, ndim] / TARGET_RESOLUTION


# Apply transforms on images of different resolutions

orig_tf_img = affine_transform(orig_img, affine_st_full_res, prefilter=False, order=1)
downsample_tf_img = affine_transform(downsample_img, affine_st_low_res, prefilter=False, order=1)


# Downsample result at full resolution to be compared to result on downsample image

downsample_orig_tf_img = zoom(orig_tf_img, zoom=np.array(
                              ORIG_RESOLUTION)/np.array(TARGET_RESOLUTION),
                              prefilter=False, order=1)

# print(orig_img.shape)
# print(downsample_img.shape)
# print(orig_tf_img.shape)
# print(downsample_orig_tf_img.shape)

###############################
######## VISUALISATION ########
###############################
# We'll visualize in 2D the slice at the middle of the z (third) axis of the image, in both resolution
mid_z_slice_full, mid_z_slice_low = orig_img.shape[2]//2, downsample_img.shape[2]//2

fig, ((ax1, ax2, ax3), (ax4, ax5, ax6)) = plt.subplots(nrows=2, ncols=3)

ax1.imshow(orig_img[:, :, mid_z_slice_full], cmap='gray')
ax1.axis('off')
ax1.set_title('1/ Origin image, at full res: {}'.format(ORIG_RESOLUTION))

ax2.imshow(downsample_img[:, :, mid_z_slice_low], cmap='gray')
ax2.axis('off')
ax2.set_title('2/ Downsampled image, at low res: {}'.format(TARGET_RESOLUTION))

ax3.imshow(downsample_tf_img[:, :, mid_z_slice_low], cmap='gray')
ax3.axis('off')
ax3.set_title('3/ Transformed downsampled image')

ax4.imshow(orig_tf_img[:, :, mid_z_slice_full], cmap='gray')
ax4.axis('off')
ax4.set_title('4/ Transformed original image')

ax5.imshow(downsample_orig_tf_img[:, :, mid_z_slice_low], cmap='gray')
ax5.axis('off')
ax5.set_title('5/ Downsampled transformed image')


error = ax6.imshow(np.abs(downsample_tf_img[:, :, mid_z_slice_low] -\
                          downsample_orig_tf_img[:, :, mid_z_slice_low]), cmap='hot')
ax6.axis('off')
ax6.set_title('Error map between 3/ and 5/')

fig.colorbar(error)

plt.suptitle('Result for {} applied on {} and {} resolution'.format(name_affine, ORIG_RESOLUTION, TARGET_RESOLUTION))
plt.tight_layout()

plt.show()



回答1:


The downsampling basically changes the basis vectors of the image. This makes the downsampling act like a translation.

But rotation and translation are not commutative (not interchangeable).

https://gamedev.stackexchange.com/questions/16719/what-is-the-correct-order-to-multiply-scale-rotation-and-translation-matrices-f

So after your downsampling you have to translate the content of the image back to the original position. Which can be done by composing a affine transformation matrix using ORIG_RESOLUTION and applying it before the actual transformation. Or use a matrix multipication (e.g. with np.dot) to modify the actual transformation.



来源:https://stackoverflow.com/questions/59846059/behaviour-of-affine-transform-on-3d-image-with-non-uniform-resolution-with-scipy

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!