简体   繁体   English

如何在仿射变换矩阵中考虑 ITK CenterOfRotationPoint?

[英]How to factor the ITK CenterOfRotationPoint in an affine transformation matrix?

We are using the registration algorithm of ITK but we only want the affine transformation matrix and not directly apply the registration.我们正在使用 ITK 的配准算法,但我们只想要仿射变换矩阵,而不是直接应用配准。 In a previous issues we already solved a misunderstanding regarding the image/transform orientation: How to get transformation affine from ITK registration?上一期我们已经解决了一个关于image/transform orientation的误解: 如何从ITK配准得到transformation affine?

We did now run into a sample where the current solution does not properly work.我们现在确实遇到了一个示例,其中当前的解决方案无法正常工作。 The rotation is good but the result is slightly translated.旋转很好,但结果略有平移。 The image output of ITK is perfect, so we know that the registration worked. ITK 的图像 output 是完美的,所以我们知道注册成功了。 That's why we will reduce the problem description below to the affine calculation with the specific matrices.这就是为什么我们将下面的问题描述简化为具有特定矩阵的仿射计算。

From the ITK registration we get/read the following parameters:从 ITK 注册我们得到/读取以下参数:

parameter_map = result_transform_parameters.GetParameterMap(0)

rot00, rot01, rot02, rot10, rot11, rot12, rot20, rot21, rot22 = parameter_map[
    'TransformParameters'][:9]
A = np.array([
    [rot00, rot01, rot02, 0],
    [rot10, rot11, rot12, 0],
    [rot20, rot21, rot22, 0],
    [    0,     0,     0, 1],
], dtype=float)  # yapf: disable

tx, ty, tz = parameter_map['TransformParameters'][9:]
t = np.array([
    [1, 0, 0, tx],
    [0, 1, 0, ty],
    [0, 0, 1, tz],
    [0, 0, 0,  1],
], dtype=float)  # yapf: disable

# In world coordinates
cx, cy, cz = parameter_map['CenterOfRotationPoint']
c = np.array([
    [1, 0, 0, cx],
    [0, 1, 0, cy],
    [0, 0, 1, cz],
    [0, 0, 0,  1],
], dtype=float)  # yapf: disable

ox, oy, oz = parameter_map['Origin']
o = np.array([
    [1, 0, 0, ox],
    [0, 1, 0, oy],
    [0, 0, 1, oz],
    [0, 0, 0,  1],
], dtype=float)  # yapf: disable

moving_ras = moving_image.affine

Where A is the direction/rotation matrix, t the translation matrix, c the center of rotation (CoR), and moving_ras the affine of the moving image in RAS orientation.其中A是方向/旋转矩阵, t是平移矩阵, c是旋转中心 (CoR), moving_ras是运动图像在 RAS 方向上的仿射。

The translation and direction matrix can be combined to one transform matrix:平移和方向矩阵可以合并为一个变换矩阵:

transform = t @ A

We are not sure how to factor in the CenterOfRotationPoint .我们不确定如何考虑CenterOfRotationPoint Based on this , this , and that exchange questions, I thought one might need to do it like that:基于thisthisthat交换问题,我认为可能需要这样做:

transform = c @ transform @ np.linalg.inv(c)

Finally, we need to add the orientation flip between RAS and LPS:最后,我们需要添加 RAS 和 LPS 之间的方向翻转:

registration = FLIPXY_44 @ transform @ FLIPXY_44

But this does not result in the correct transformation affine.但这不会导致正确的仿射变换。

On the ITK docs and in a GitHub issue we got this formula to apply the above parameters to points:在 ITK 文档和 GitHub 问题中,我们得到了将上述参数应用于点的公式:

T(x) = A ( x - c ) + (t + c)

While we can not directly use that since we do not want to directly transform the image but we only want to calculate the correct affine transformation matrix, one can see how the formula is pretty similar to what we are already doing as explained above.虽然我们不能直接使用它,因为我们不想直接变换图像,而只想计算正确的仿射变换矩阵,但可以看出该公式与我们已经在做的事情非常相似,如上所述。

We are again at a dead end with our knowledge.我们再次陷入了知识的死胡同。

Things we noticed that might make issues here:我们注意到的事情可能会在这里产生问题:

  • Orientation方向
    • ITK uses LPS orientation for images and transforms ITK 对图像和变换使用 LPS 方向
    • Monai/Nibabel uses RAS orientation for images and transforms Monai/Nibabel 对图像和变换使用 RAS 方向
  • Center of Rotation旋转中心
    • ITK provides the used center of rotation ITK 提供使用的旋转中心
    • Monai implicitly assumes the center of rotation to be the center of the image Monai 隐含地假设旋转中心是图像的中心
  • World space vs. Index space.世界空间与索引空间。
    • All transforms and points from ITK are in world space. ITK 的所有变换和点都在世界空间中。
    • Monai seems to operate directly on the image.莫奈似乎直接对图像进行操作。
  • (0, 0, 0) Corner - ITK and Monai seem to use the opposit corner for coordinates - eg in a 4x4x4 image, position (0, 0, 0) in ITK is position (3, 3, 3) in Monai. (0, 0, 0) 角 - ITK 和 Monai 似乎使用对角作为坐标 - 例如,在 4x4x4 图像中,ITK 中的 position (0, 0, 0) 是 Monai 中的 position (3, 3, 3)。

EDIT : I noticed that my current minimal code example is not quite comprehensive.编辑:我注意到我当前的最小代码示例并不十分全面。 Therefore here an update.因此这里更新。 The included affine matrices are taken from the ITK coregistration.包含的仿射矩阵取自 ITK 配准。 The ITK code was omitted for brevity.为简洁起见,省略了 ITK 代码。

Here with new test data (you can view these images via MRIcoGL):这里有新的测试数据(您可以通过 MRIcoGL 查看这些图像):

Here a minimal code example:这是一个最小的代码示例:

from pathlib import Path

import nibabel
import numpy as np
from monai.transforms.spatial.array import Affine
from monai.utils.enums import GridSampleMode, GridSamplePadMode
from nibabel import Nifti1Image

np.set_printoptions(suppress=True)  # type: ignore

folder = Path('.')

FLIPXY_44 = np.diag([-1, -1, 1, 1])

# rot00, rot01, rot02, rot10, rot11, rot12, rot20, rot21, rot22 = parameter_map['TransformParameters'][:9]
A = np.array([[ 1.02380734, -0.05137566, -0.00766465,  0.        ],
              [ 0.01916231,  0.93276486, -0.23453097,  0.        ],
              [ 0.01808809,  0.2667324 ,  0.94271694,  0.        ],
              [ 0.        ,  0.        ,  0.        ,  1.        ]]) # yapf: disable

# tx, ty, tz = parameter_map['TransformParameters'][9:]
t = np.array([[ 1.        ,  0.        ,  0.        ,  1.12915465  ],
              [ 0.        ,  1.        ,  0.        , 11.76880151  ],
              [ 0.        ,  0.        ,  1.        , 41.54685788  ],
              [ 0.        ,  0.        ,  0.        ,  1.          ]]) # yapf: disable

# cx, cy, cz = parameter_map['CenterOfRotationPoint']
c = np.array([[ 1.        ,  0.        ,  0.        ,  -0.1015625  ],
              [ 0.        ,  1.        ,  0.        , -24.5521698  ],
              [ 0.        ,  0.        ,  1.        ,   0.1015625  ],
              [ 0.        ,  0.        ,  0.        ,   1.         ]]) # yapf: disable

# Moving image affine
x = np.array([[ 2.        ,  0.        ,  0.        , -125.75732422],
              [ 0.        ,  2.        ,  0.        , -125.23828888],
              [ 0.        ,  0.        ,  2.        ,  -99.86506653],
              [ 0.        ,  0.        ,  0.        ,    1.        ]]) # yapf: disable

o = np.array([
    [1., 0., 0., 126.8984375],
    [0., 1., 0., 102.4478302],
    [0., 0., 1., -126.8984375],
    [0., 0., 0., 1.],
])

moving_ras = x

# Combine the direction and translation
transform = t @ A

# Factor in the center of rotation
# transform = c @ transform @ np.linalg.inv(c)

# Switch from LPS to RAS orientation
registration = FLIPXY_44 @ transform @ FLIPXY_44

y = np.array([[ 2.        ,  0.        ,  0.        , -126.8984375 ],
              [ 0.        ,  2.        ,  0.        , -102.4478302 ],
              [ 0.        ,  0.        ,  2.        , -126.8984375 ],
              [ 0.        ,  0.        ,  0.        ,    1.        ]]) # yapf: disable

fixed_image_affine = y

moving_image_ni: Nifti1Image = nibabel.load(folder / 'real_moving.nii.gz')
moving_image_np: np.ndarray = moving_image_ni.get_fdata()  # type: ignore

affine_transform = Affine(affine=registration,
                          image_only=True,
                          mode=GridSampleMode.NEAREST,
                          padding_mode=GridSamplePadMode.BORDER)
reg_monai = np.squeeze(affine_transform(moving_image_np[np.newaxis, ...]))

out = Nifti1Image(reg_monai, fixed_image_affine)

nibabel.save(out, folder / 'reg_monai.nii.gz')

When you executed this code, the resulting reg_monai.nii.gz should match the real_fixed.nii.gz (in position and outline - not in the actual content).当您执行此代码时,生成的reg_monai.nii.gz应该与real_fixed.nii.gz匹配(在 position 和大纲中 - 不在实际内容中)。

Currently the result looks like this (viewed via MRIcoGL):目前结果看起来像这样(通过 MRIcoGL 查看):

配准图像不匹配

But the result should look like this (this is the direct ITK registration output where the hardcoded affine matrices come from - which should prove that the registration worked and that the parameters generally should be good):但结果应该是这样的(这是硬编码仿射矩阵来自的直接 ITK 注册 output - 这应该证明注册有效并且参数通常应该是好的):

配准图像确实匹配


For the sake of completeness, here also the code to perform the ITK registration and to get the above affine matrices:为了完整起见,这里还有执行 ITK 注册和获取上述仿射矩阵的代码:

from pathlib import Path

import itk
import numpy as np

np.set_printoptions(suppress=True)  # type: ignore

folder = Path('.')

moving_image = itk.imread(str(folder / 'real_moving.nii.gz'), itk.F)
fixed_image = itk.imread(str(folder / 'real_fixed.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']
affine_parameter_map['MaximumNumberOfIterations'] = ['512']
parameter_object.AddParameterMap(affine_parameter_map)

# Call registration function
result_image, result_transform_parameters = itk.elastix_registration_method(  # type: ignore
    fixed_image, moving_image, parameter_object=parameter_object)

itk.imwrite(result_image, str(folder / 'real_reg_itk.nii.gz'), compression=True)

parameter_map = result_transform_parameters.GetParameterMap(0)

rot00, rot01, rot02, rot10, rot11, rot12, rot20, rot21, rot22 = parameter_map['TransformParameters'][:9]
A = np.array([
    [rot00, rot01, rot02, 0],
    [rot10, rot11, rot12, 0],
    [rot20, rot21, rot22, 0],
    [    0,     0,     0, 1],
], dtype=float)  # yapf: disable

tx, ty, tz = parameter_map['TransformParameters'][9:]
t = np.array([
    [1, 0, 0, tx],
    [0, 1, 0, ty],
    [0, 0, 1, tz],
    [0, 0, 0,  1],
], dtype=float)  # yapf: disable

# In world coordinates
cx, cy, cz = parameter_map['CenterOfRotationPoint']
c = np.array([
    [1, 0, 0, cx],
    [0, 1, 0, cy],
    [0, 0, 1, cz],
    [0, 0, 0,  1],
], dtype=float)  # yapf: disable

ox, oy, oz = parameter_map['Origin']
o = np.array([
    [1, 0, 0, ox],
    [0, 1, 0, oy],
    [0, 0, 1, oz],
    [0, 0, 0,  1],
], dtype=float)  # yapf: disable

Package versions: Package 版本:

itk-elastix==0.12.0
monai==0.8.0
nibabel==3.1.1
numpy==1.19.2

I guess this is not the solution, but this easy code/ transformation seems to let the image pointing in the same direction and almost aligned, which makes me question if is really LPS to RAS because this looks a completely different transformation of axis:我想这不是解决方案,但是这个简单的代码/转换似乎让图像指向相同的方向并且几乎对齐,这让我怀疑是否真的是LPSRAS ,因为这看起来是一个完全不同的轴转换:

transform_matrix= np.array([
    [0, 0, 1, 0],
    [0, 1, 0, 0],
    [1, 0, 0, 0],
    [0, 0, 0,  1]], dtype=float)

to_transform: Nifti1Image = nibabel.load('file/real_moving.nii.gz')
to_transform: np.ndarray = to_transform.get_fdata() 

affine_transform = Affine(affine=transform_matrix, image_only=True,
                          mode=GridSampleMode.NEAREST, padding_mode=GridSamplePadMode.BORDER)
transformed_img = np.squeeze(affine_transform(to_transform[np.newaxis, ...])) 

On the other hand I was not able to find the proper order of the parameters in the parameter_map (on the documentation).另一方面,我无法在 parameter_map(在文档中)中找到参数的正确顺序。 Are you sure that A and t are multiplied and not summed (with a zero diagonal on t), maybe you can point me to the documentation where that is written.你确定 A 和 t 相乘而不是相加(t 上的对角线为零),也许你可以指向我写的文档。

On the center of rotation I found this , which as far as I know will mean:在旋转中心,我发现了这个,据我所知这意味着:

transform = c @ transform @ c_minus

Where c have again no diagonal, whether if this should be applied after or before the t, I have no answer, but for me no option worked for me, as I couldn't even reproduce your images with this data set. c 再次没有对角线,是否应该在 t 之后或之前应用,我没有答案,但对我来说没有任何选择对我有用,因为我什至无法用这个数据集重现你的图像。

I found some useful information with jupyter examples at the documentation of itk-elastix here我在这里的 itk-elastix 文档中找到了一些关于 jupyter 示例的有用信息

This is the result of the first piece of code, but the images doesn't seem to be the same as yours.这是第一段代码的结果,但是图像似乎和你的不一样。

I let you some pictures of how the data appears into my machine with the input, transformed image and reference image at the end.我给你一些图片,说明数据如何出现在我的机器中,最后是输入、转换图像和参考图像。

Before transformation改造前改造前

Transformed image:转换图像: 变换图像

objective image客观形象客观形象

I know this is not a final solution, but hope it is still useful.我知道这不是最终解决方案,但希望它仍然有用。

What I see is that the image registration process is not actually working.我看到的是图像注册过程实际上没有工作。

def registration_test(moving_image, fixed_image, niter=512):
  # Import Default Parameter Map
  parameter_object = itk.ParameterObject.New()
  affine_parameter_map = parameter_object.GetDefaultParameterMap('affine', 4)
  affine_parameter_map['FinalBSplineInterpolationOrder'] = ['1']
  affine_parameter_map['MaximumNumberOfIterations'] = [str(niter)]
  parameter_object.AddParameterMap(affine_parameter_map)

  # Call registration function
  result_image, result_transform_parameters = itk.elastix_registration_method(  # type: ignore
      fixed_image, moving_image, parameter_object=parameter_object)
  #transform_parameters = parameter_map['TransformParameters']
  #transform_origin = parameter_map['CenterOfRotationPoint']
  transform_parameters = result_transform_parameters.GetParameter(0, 'TransformParameters')
  transform_origin = result_transform_parameters.GetParameter(0, 'CenterOfRotationPoint')
  r = np.asarray(transform_parameters).reshape(4, 3)
  c = np.asarray(transform_origin, dtype=float)
  A = np.eye(4)
  A[:3,3] = r[3]
  A[:3,:3] = r[:3].T
  print(A, c)
  C = np.eye(4)
  C[:3, 3] = c;
  C_inv = np.eye(4)
  C_inv[:3,3] = -c;
  
  affine_transform = Affine(affine=C @ A @ C_inv,
                          image_only=True,
                          mode=GridSampleMode.NEAREST,
                          padding_mode=GridSamplePadMode.BORDER)
  
  moving_image_np = np.asarray(moving_image)
  reg_monoai = affine_transform(moving_image_np[..., np.newaxis])
  obtained = reg_monoai[..., 0]
  print(obtained.shape)
  plt.figure(figsize=(9,9))
  plt.subplot(331)
  plt.imshow(fixed_image[64,:,:], origin='lower'); plt.xticks([]); plt.yticks([])
  plt.ylabel('fixed_image'); plt.title('plane 0')
  plt.subplot(334)
  plt.imshow(obtained[64,:,:], origin='lower'); plt.xticks([]); plt.yticks([])
  plt.ylabel('result')
  plt.subplot(337)
  plt.imshow(moving_image[64,:,:], origin='lower'); plt.xticks([]); plt.yticks([])
  plt.ylabel('moving_image');
  plt.subplot(332)
  plt.imshow(fixed_image[:,64,:], origin='lower'); plt.xticks([]); plt.yticks([])
  plt.title('plane 1')
  plt.subplot(335)
  plt.imshow(obtained[:,64,:], origin='lower'); plt.xticks([]); plt.yticks([])
  plt.subplot(338)
  plt.imshow(moving_image[:,64,:], origin='lower'); plt.xticks([]); plt.yticks([])
  plt.subplot(333)
  plt.title('plane 2');
  plt.imshow(fixed_image[:,:,64], origin='lower'); plt.xticks([]); plt.yticks([])
  plt.subplot(336)
  plt.imshow(obtained[:,:,64], origin='lower'); plt.xticks([]); plt.yticks([])
  plt.subplot(339)
  plt.imshow(moving_image[:,:,64], origin='lower'); plt.xticks([]); plt.yticks([])

Then with the pair you sent, that are very close to each other if I run 1000 iterations, here is what I have然后你发送的那对,如果我运行 1000 次迭代,它们彼此非常接近,这就是我所拥有的

%%time
registration_test(moving_image, fixed_image, 1000)
[[ 1.02525991  0.01894165  0.02496272  1.02504064]
 [-0.05196394  0.93458484  0.26571434 11.92591955]
 [-0.00407657 -0.23543312  0.94091849 41.62065545]
 [ 0.          0.          0.          1.        ]] [ -0.1015625 -24.5521698   0.1015625]
(128, 128, 128)
CPU times: user 15.9 s, sys: 654 ms, total: 16.6 s
Wall time: 10.9 s

在此处输入图像描述

test with a slight rotation轻微旋转测试

Using this function to rotate around one axis使用这个 function 绕一个轴旋转

def imrot(im, angle, axis=1):
  x,y = [i for i in range(3) if i != axis]
  A = np.eye(4)
  A[x,x] = np.cos(angle);
  A[x,y] = np.sin(angle);
  A[y,x] = -np.sin(angle);
  A[y,y] = np.cos(angle);
  f = Affine(affine=A,
        image_only=True,
        mode=GridSampleMode.NEAREST,
        padding_mode=GridSamplePadMode.BORDER)
  return itk.image_from_array(f(np.asarray(im)[np.newaxis])[0])

I see that over 10 iterations the moving_image is not significantly modified我看到超过 10 次迭代moving_image没有显着修改

%%time
e2e_test(moving_image, imrot(fixed_image, 0.5), 10)
[[ 0.9773166  -0.05882861 -0.09435328 -8.29016604]
 [ 0.01960457  1.01097845 -0.06601224 -4.62307826]
 [ 0.09305988  0.07375327  1.06381763  0.74783361]
 [ 0.          0.          0.          1.        ]] [63.5 63.5 63.5]
(128, 128, 128)
CPU times: user 3.57 s, sys: 148 ms, total: 3.71 s
Wall time: 2.24 s

在此处输入图像描述

But if I increase the number of iterations to 100, instead of approximating the fixed image as I would expect it seems lost但是如果我将迭代次数增加到 100,而不是像我期望的那样近似固定图像,它似乎丢失了

[[  1.12631932  -0.33513615  -0.70472146 -31.57349579]
 [ -0.07239085   1.08080123  -0.42268541 -28.72943354]
 [ -0.24096706  -0.08024728   0.80870164  -5.86050765]
 [  0.           0.           0.           1.        ]] [63.5 63.5 63.5]

在此处输入图像描述

After 1000 iterations 1000次迭代后

[[  1.28931626  -0.36533121  -0.52561289 -37.00919916]
 [  0.02204954   1.23661994  -0.29418401 -34.36979156]
 [ -0.32713001  -0.13135651   0.96500969   2.75931824]
 [  0.           0.           0.           1.        ]] [63.5 63.5 63.5]

在此处输入图像描述

After 10000 iterations 10000次迭代后

[[  1.46265277   0.02692694   0.14337441 -61.37788428]
 [ -0.15334478   1.37362513   0.16242297 -52.59833838]
 [ -0.53333714  -0.51411401   0.80381994  -4.97349468]
 [  0.           0.           0.           1.        ]] [63.5 63.5 63.5]

在此处输入图像描述

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM