繁体   English   中英

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

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

我们正在使用 ITK 的配准算法,但我们只想要仿射变换矩阵,而不是直接应用配准。 上一期我们已经解决了一个关于image/transform orientation的误解: 如何从ITK配准得到transformation affine?

我们现在确实遇到了一个示例,其中当前的解决方案无法正常工作。 旋转很好,但结果略有平移。 ITK 的图像 output 是完美的,所以我们知道注册成功了。 这就是为什么我们将下面的问题描述简化为具有特定矩阵的仿射计算。

从 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

其中A是方向/旋转矩阵, t是平移矩阵, c是旋转中心 (CoR), moving_ras是运动图像在 RAS 方向上的仿射。

平移和方向矩阵可以合并为一个变换矩阵:

transform = t @ A

我们不确定如何考虑CenterOfRotationPoint 基于thisthisthat交换问题,我认为可能需要这样做:

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

最后,我们需要添加 RAS 和 LPS 之间的方向翻转:

registration = FLIPXY_44 @ transform @ FLIPXY_44

但这不会导致正确的仿射变换。

在 ITK 文档和 GitHub 问题中,我们得到了将上述参数应用于点的公式:

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

虽然我们不能直接使用它,因为我们不想直接变换图像,而只想计算正确的仿射变换矩阵,但可以看出该公式与我们已经在做的事情非常相似,如上所述。

我们再次陷入了知识的死胡同。

我们注意到的事情可能会在这里产生问题:

  • 方向
    • ITK 对图像和变换使用 LPS 方向
    • Monai/Nibabel 对图像和变换使用 RAS 方向
  • 旋转中心
    • ITK 提供使用的旋转中心
    • Monai 隐含地假设旋转中心是图像的中心
  • 世界空间与索引空间。
    • ITK 的所有变换和点都在世界空间中。
    • 莫奈似乎直接对图像进行操作。
  • (0, 0, 0) 角 - ITK 和 Monai 似乎使用对角作为坐标 - 例如,在 4x4x4 图像中,ITK 中的 position (0, 0, 0) 是 Monai 中的 position (3, 3, 3)。

编辑:我注意到我当前的最小代码示例并不十分全面。 因此这里更新。 包含的仿射矩阵取自 ITK 配准。 为简洁起见,省略了 ITK 代码。

这里有新的测试数据(您可以通过 MRIcoGL 查看这些图像):

这是一个最小的代码示例:

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')

当您执行此代码时,生成的reg_monai.nii.gz应该与real_fixed.nii.gz匹配(在 position 和大纲中 - 不在实际内容中)。

目前结果看起来像这样(通过 MRIcoGL 查看):

配准图像不匹配

但结果应该是这样的(这是硬编码仿射矩阵来自的直接 ITK 注册 output - 这应该证明注册有效并且参数通常应该是好的):

配准图像确实匹配


为了完整起见,这里还有执行 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 版本:

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

我想这不是解决方案,但是这个简单的代码/转换似乎让图像指向相同的方向并且几乎对齐,这让我怀疑是否真的是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, ...])) 

另一方面,我无法在 parameter_map(在文档中)中找到参数的正确顺序。 你确定 A 和 t 相乘而不是相加(t 上的对角线为零),也许你可以指向我写的文档。

在旋转中心,我发现了这个,据我所知这意味着:

transform = c @ transform @ c_minus

c 再次没有对角线,是否应该在 t 之后或之前应用,我没有答案,但对我来说没有任何选择对我有用,因为我什至无法用这个数据集重现你的图像。

我在这里的 itk-elastix 文档中找到了一些关于 jupyter 示例的有用信息

这是第一段代码的结果,但是图像似乎和你的不一样。

我给你一些图片,说明数据如何出现在我的机器中,最后是输入、转换图像和参考图像。

改造前改造前

转换图像: 变换图像

客观形象客观形象

我知道这不是最终解决方案,但希望它仍然有用。

我看到的是图像注册过程实际上没有工作。

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([])

然后你发送的那对,如果我运行 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

在此处输入图像描述

轻微旋转测试

使用这个 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])

我看到超过 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

在此处输入图像描述

但是如果我将迭代次数增加到 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]

在此处输入图像描述

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]

在此处输入图像描述

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