この記事では、ツールを介して直接表示する1つの方法と、手動で描画する2つの方法を含め、医療画像レジストレーションにおける変形フィールドの視覚化について説明します。
まず、変形場を紹介します。[W、H]のサイズの2次元画像の変形場は[W、H、2]であり、3次元のサイズは2であり、 x軸とy軸方向の変位。同様に、[D、W、H]のサイズの3次元画像に対応する変形フィールドのサイズは[D、W、H、3]であり、3次元のサイズは3です。これらはそれぞれx軸とy軸で表されます。軸とz軸の方向の変位。下の図は、2次元の脳画像の登録後に得られた変形フィールドです。
冒頭で述べたように、以下では変形フィールドを視覚化する3つの方法を紹介します。
1.ITK-Snapを使用して変形フィールドを視覚化します
この方法は最も便利で、私の意見では最高の効果ですが、3次元の変形フィールドにのみ適しています。まず、変形フィールドを「.nii」画像として保存し、ITK-Snapツールで開きます。まず、下図の赤い枠の位置にしばらくカーソルを合わせ、下図のアイコンが表示されたらクリックします。 「マルチコンポーネントディスプレイの下」グリッドを選択すると、次の図に示す効果が表示されます。
2.マオマオの方法
具体的なアイデアについては、Maomao Great Godの記事plt.contourDrawing Image Deformation Field(Deformation Field)を参照してください。実際、具体的な考え方がよくわかりませんでした。まずは規則的なグリッドを生成し、変形フィールドを追加して表示します。ただし、グリッドが[W、H]サイズで、変形フィールドが[W、H、2]サイズの場合、追加する次元を1つしか選択できないため、少し奇妙に感じます。コードは次のように表示されます。
import matplotlib.pyplot as plt
import SimpleITK as sitk
import numpy as np
def grid2contour(grid, title):
'''
grid--image_grid used to show deform field
type: numpy ndarray, shape: (h, w, 2), value range:(-1, 1)
'''
assert grid.ndim == 3
x = np.arange(-1, 1, 2.0 / grid.shape[1])
y = np.arange(-1, 1, 2.0 / grid.shape[0])
X, Y = np.meshgrid(x, y)
Z1 = grid[:, :, 0] + 2 # remove the dashed line
Z1 = Z1[::-1] # vertical flip
Z2 = grid[:, :, 1] + 2
plt.figure()
plt.contour(X, Y, Z1, 15, levels=50, colors='k') #改变levels的值,可以改变形变场的密集程度
plt.contour(X, Y, Z2, 15, levels=50, colors='k')
plt.xticks(()), plt.yticks(()) # remove x, y ticks
plt.title(title)
plt.show()
def show_grid():
img = sitk.ReadImage("./2D1.nii")
img_arr = sitk.GetArrayFromImage(img)
img_shape = img_arr.shape
# 起点、终点、步长(可为小数)
x = np.arange(-1, 1, 2 / img_shape[1])
y = np.arange(-1, 1, 2 / img_shape[0])
X, Y = np.meshgrid(x, y)
regular_grid = np.stack((X, Y), axis=2)
grid2contour(regular_grid, "regular_grid")
rand_field = np.random.rand(*img_shape[:2], 2) # 参数前加*是以元组形式导入
rand_field_norm = rand_field.copy()
rand_field_norm[:, :, 0] = rand_field_norm[:, :, 0] * 2 / img_shape[1]
rand_field_norm[:, :, 1] = rand_field_norm[:, :, 1] * 2 / img_shape[0]
sampling_grid = regular_grid + rand_field_norm
grid2contour(sampling_grid, "sampling_grid")
img_arr[..., 0] = img_arr[..., 0] * 2 / img_shape[1]
img_arr[..., 1] = img_arr[..., 1] * 2 / img_shape[0]
img_grid = regular_grid + img_arr
grid2contour(img_grid, "img_grid")
if __name__ == "__main__":
show_grid()
print("end")
コードの20行目で、レベルの値を変更すると、変形フィールドのグリッドの密度が変更される可能性があります。
3.最初に通常のグリッドを生成し、次に空間変換ネットワークを使用して変形します
この方法は、登録グループの友人によって提案され、自分で実装しました。アイデアは、最初に通常のグリッドを生成してそれを画像として保存し、次に空間変換ネットワークを使用してそれを変形することです。コードは次のように表示されます。
import numpy as np
import SimpleITK as sitk
import matplotlib.pyplot as plt
import torch
import torch.nn as nn
import torch.nn.functional as F
# 空间转换网络
class SpatialTransformer(nn.Module):
# 1.生成网格grid;2.new_grid=grid+flow,即旧网格加上一个位移;3.将网格规范化到[-1,1];4.根据新网格对原图进行采样
def __init__(self, size, mode='bilinear'):
"""
Instiatiate the block
:param size: size of input to the spatial transformer block
:param mode: method of interpolation for grid_sampler
"""
super(SpatialTransformer, self).__init__()
# Create sampling grid
vectors = [torch.arange(0, s) for s in size]
grids = torch.meshgrid(vectors)
grid = torch.stack(grids) # y, x, z
grid = torch.unsqueeze(grid, 0) # add batch
grid = grid.type(torch.FloatTensor)
self.register_buffer('grid', grid)
self.mode = mode
def forward(self, src, flow):
"""
Push the src and flow through the spatial transform block
:param src: the original moving image
:param flow: the output from the U-Net
"""
new_locs = self.grid + flow
shape = flow.shape[2:]
# Need to normalize grid values to [-1, 1] for resampler
for i in range(len(shape)):
new_locs[:, i, ...] = 2 * (new_locs[:, i, ...] / (shape[i] - 1) - 0.5)
if len(shape) == 2:
new_locs = new_locs.permute(0, 2, 3, 1) # 维度置换,变为0,2,3,1
new_locs = new_locs[..., [1, 0]]
elif len(shape) == 3:
new_locs = new_locs.permute(0, 2, 3, 4, 1)
new_locs = new_locs[..., [2, 1, 0]]
return F.grid_sample(src, new_locs, mode=self.mode)
# 生成网格图片
def create_grid(size, path):
num1, num2 = (size[0] + 10) // 10, (size[1] + 10) // 10 # 改变除数(10),即可改变网格的密度
x, y = np.meshgrid(np.linspace(-2, 2, num1), np.linspace(-2, 2, num2))
plt.figure(figsize=((size[0] + 10) / 100.0, (size[1] + 10) / 100.0)) # 指定图像大小
plt.plot(x, y, color="black")
plt.plot(x.transpose(), y.transpose(), color="black")
plt.axis('off') # 不显示坐标轴
# 去除白色边框
plt.gca().xaxis.set_major_locator(plt.NullLocator())
plt.gca().yaxis.set_major_locator(plt.NullLocator())
plt.subplots_adjust(top=1, bottom=0, left=0, right=1, hspace=0, wspace=0)
plt.margins(0, 0)
plt.savefig(path) # 保存图像
# plt.show()
if __name__ == "__main__":
out_path = r"C:\Users\zuzhiang\Desktop\new_img.jpg" # 图片保存路径
# 读入形变场
phi = sitk.ReadImage("./2D1.nii") # [324,303,2]
phi_arr = torch.from_numpy(sitk.GetArrayFromImage(phi)).float()
phi_shape = phi_arr.shape
# 产生网格图片
create_grid(phi_shape, out_path)
img = sitk.GetArrayFromImage(sitk.ReadImage(out_path))[..., 0]
img = np.squeeze(img)[np.newaxis, np.newaxis, :phi_shape[0], :phi_shape[1]]
# 用STN根据形变场对网格图片进行变形
STN = SpatialTransformer(phi_shape[:2])
phi_arr = phi_arr.permute(2, 0, 1)[np.newaxis, ...]
warp = STN(torch.from_numpy(img).float(), phi_arr)
# 保存图片
warp_img = sitk.GetImageFromArray(warp[0, 0, ...].numpy().astype(np.uint8))
sitk.WriteImage(warp_img, out_path)
print("end")
グリッド画像のサイズは正確に決定できないため、生成時にさらに10ピクセルが生成され、読み取られるときにトリミングされることに注意してください。上記のコードの57行目の除数(10)を変更すると、グリッド線の密度を変更できます。
この方法はもう少しエレガントな感じがしますが、効果は2番目の方法と似ています。2番目と3番目の方法の効果を次の図に示します。