[英]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
。 基於this 、 this和that交換問題,我認為可能需要這樣做:
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 配准。 為簡潔起見,省略了 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
我想這不是解決方案,但是這個簡單的代碼/轉換似乎讓圖像指向相同的方向並且幾乎對齊,這讓我懷疑是否真的是LPS
到RAS
,因為這看起來是一個完全不同的軸轉換:
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.