介紹如何在 Python 中使用 ITK 函式庫的 v4 影像對準框架處理二維影像的對準,並提供 hello world 範例程式碼。
ITKv4 影像對準框架
ITKv4 影像對準框架跟上一版比較起來更為彈性,主要差異就是對準過程都在新的虛擬影像(virtual image)中進行,固定影像(fixed image)與調動影像(moving image)都透過轉換(transforms)與內插(interpolators)在虛擬影像空間以 metric 衡量配適程度,然後將結果交給 optimizer 更新轉換參數,重複這個過程直到結果收斂。
在這張 ITKv4 影像對準框架圖中,虛線的項目代表資料物件(data objects),實線的項目代表處理物件(process objects)。
設定影像類型
在 ITKv4 的架構下要進行影像對準時,首先要決定影像的類型,包含像素的類型以及影像的維度。
import itk
import matplotlib.pyplot as plt
# 影像像素資料類型
PixelType = itk.F
# 實體座標系統用的資料類型
ScalarType = itk.D
# 影像維度
Dimension = 2
# 影像類型
FixedImageType = itk.Image[PixelType, Dimension]
MovingImageType = itk.Image[PixelType, Dimension]
讀取影像
根據影像類型建立影像 reader,並且讀取固定影像與調動影像。
# 建立影像 Reader
FixedReaderType = itk.ImageFileReader[FixedImageType]
fixedImageReader = FixedReaderType.New()
fixedImageReader.SetFileName("BrainProtonDensitySliceBorder20.mhd")
fixedImageReader.Update()
# 讀取 Fixed 影像
fixedImage = fixedImageReader.GetOutput()
# 建立影像 Reader
MovingReaderType = itk.ImageFileReader[MovingImageType]
movingImageReader = MovingReaderType.New()
movingImageReader.SetFileName("BrainProtonDensitySliceShifted13x17y.mhd")
movingImageReader.Update()
# 讀取 Moving 影像
movingImage = movingImageReader.GetOutput()
這裡影像 reader 所指定的影像類型是指讀取影像之後所輸出的影像類型,如果這個類型跟影像檔案中實際的資料不符,影像 reader 會自動處理影像類型的轉換。
若要查看影像內部的詳細資訊,可以直接用 print 輸出影像物件:
# 查看影像詳細資訊
print(fixedImage)
Image (0x25a89c0)
RTTI typeinfo: itk::Image<float, 2u>
Reference Count: 2
Modified Time: 246
Debug: Off
Object Name:
Observers:
none
Source: (0x2960380)
Source output name: Primary
Release Data: Off
Data Released: False
Global Release Data: Off
PipelineMTime: 59
UpdateMTime: 247
RealTimeStamp: 0 seconds
LargestPossibleRegion:
Dimension: 2
Index: [0, 0]
Size: [221, 257]
BufferedRegion:
Dimension: 2
Index: [0, 0]
Size: [221, 257]
RequestedRegion:
Dimension: 2
Index: [0, 0]
Size: [221, 257]
Spacing: [1, 1]
Origin: [0, 0]
Direction:
1 0
0 1
IndexToPointMatrix:
1 0
0 1
PointToIndexMatrix:
1 0
0 1
Inverse Direction:
1 0
0 1
PixelContainer:
ImportImageContainer (0x3dd22d0)
RTTI typeinfo: itk::ImportImageContainer<unsigned long, float>
Reference Count: 1
Modified Time: 244
Debug: Off
Object Name:
Observers:
none
Pointer: 0x3fed990
Container manages memory: true
Size: 56797
Capacity: 56797使用 matplotlib 的 imshow 顯示兩張影像的內容。
# 分開顯示兩張影像
fig, axs = plt.subplots(1, 2)
axs[0].imshow(itk.GetArrayViewFromImage(fixedImage), cmap='gray')
axs[0].set_title('Fixed Image')
axs[1].imshow(itk.GetArrayViewFromImage(movingImage), cmap='gray')
axs[1].set_title('Moving Image')
plt.show()

影像對準
進行影像對準時,需要定義好 transform(影像轉換,例如線性或非線性轉換)、optimizer(尋找最佳解的方法)、metric(衡量兩張影像距離的方法)、interpolator(計算內差的方法)的類型,接著將各元件串接起來,進行影像對準。
# 影像轉換類型
TransformType = itk.TranslationTransform[ScalarType, Dimension]
# 建立初始轉換
initialTransform = TransformType.New()
# 建立 Optimizer 物件
OptimizerType = itk.RegularStepGradientDescentOptimizerv4[ScalarType]
optimizer = OptimizerType.New()
# 建立 Metric 物件
MetricType = itk.MeanSquaresImageToImageMetricv4[FixedImageType, MovingImageType]
metric = MetricType.New()
# 影像對準物件
RegistrationType = itk.ImageRegistrationMethodv4[FixedImageType, MovingImageType]
registration = RegistrationType.New()
# 設定 Metric
registration.SetMetric(metric)
# 設定 Optimizer
registration.SetOptimizer(optimizer)
# 設定初始轉換
registration.SetInitialTransform(initialTransform)
# 設定輸入影像
registration.SetFixedImage(fixedImage)
registration.SetMovingImage(movingImage)
# Interpolator 類型
FixedLinearInterpolatorType = itk.LinearInterpolateImageFunction[FixedImageType, ScalarType]
MovingLinearInterpolatorType = itk.LinearInterpolateImageFunction[FixedImageType, ScalarType]
# 建立 Interpolator 物件
fixedInterpolator = FixedLinearInterpolatorType.New()
movingInterpolator = MovingLinearInterpolatorType.New()
# 設定 Interpolator
metric.SetFixedInterpolator(fixedInterpolator)
metric.SetMovingInterpolator(movingInterpolator)
設定調動影像的初始轉換,先取得預設參數,檢查參數數量。
# Moving 影像所使用的初始轉換
movingInitialTransform = TransformType.New()
initialParameters = movingInitialTransform.GetParameters()
# 查看參數數量
print(initialParameters.GetNumberOfElements())
2
設定初始轉換的參數,此處兩個參數分別代表 X 與 Y 軸方向的位移。
# 設定初始轉換參數
initialParameters[0] = 0
initialParameters[1] = 0
movingInitialTransform.SetParameters(initialParameters)
registration.SetMovingInitialTransform(movingInitialTransform)
固定影像所使用的初始轉換通常都是 identity(維持不變),由於預設值就是 identity,所以亦可省略不寫:
# Fixed 影像所使用的初始轉換
identityTransform = TransformType.New()
identityTransform.SetIdentity()
registration.SetFixedInitialTransform(identityTransform)
optimizer 的 learning rate 是非常重要的參數,其指定 optimizer 每次的步長(step length),不適當的 learning rate 會讓影像對準無法收斂。
learning rate 如果設定太大,會造成 optimizer 不穩定,若設定太小則會讓收斂過程非常緩慢,簡易的設定方式是一開始先設定為較小的數值(例如 1.0 到 5.0 左右),等到其他參數都調整好、整體都可以穩定收斂之後,再慢慢升高 learning rate 直到開始出現收斂不穩定的狀況為止,最佳的 learning rate 就是讓疊代次數最少,但依然維持收斂的穩定性。
learning rate 在實際運算時會跟 metric 的梯度(gradient)相乘產生步長,所以步長也會跟 metric 的數值成正比,在 metric 數值尺度比較大的狀況下,要維持相同的 optimizer 行為,就需要較小的 learning rate。
# 設定 Learning Rate
optimizer.SetLearningRate(4)
每當導數(derivative)方向發生劇烈改變時,通常就是代表剛越過局部極值點(local extrema),此時 optimizer 就會降低 learning,而降低 learning rate 的比例就是由 relaxation factor 參數來控制,這個參數值介於 0 到 1 之間,預設值是 0.5。
# 設定 Relaxation Factor
optimizer.SetRelaxationFactor(0.5)
隨著影像對準的過程,learning rate 會漸漸減小,當其減小到非常小的值的時候,就代表已經收斂了,這個判斷收斂的門檻值可以透過 minimum step length 參數來設定,預設值為 1e-4。
# 設定 Minimum Step Length
optimizer.SetMinimumStepLength(0.001)
在疊代的過程也有可能會出現無法收斂的狀況,因此會設定疊代次數的上限值。
# 設定最高疊代次數
optimizer.SetNumberOfIterations(200)
ITKv4 支援多層級的影像對準(multi-level registration),在不同階段中採用不同解析度的虛擬空間(virtual space),並搭配不同的平滑度(smoothness)的固定影像與調動影像,而這些條件必須在影像對準計算開始之前就設定好,否則就只能採用預設值進行影像對準。
這裡我們採用最簡單的單一層級對準方式,亦不使用任何的降解析度(shrinking)或平滑化(smoothing)。
# 設定 Levels 相關參數
registration.SetNumberOfLevels(1)
registration.SetSmoothingSigmasPerLevel([0])
registration.SetShrinkFactorsPerLevel([1])
所有的組件與參數都設定好之後,就可以實際進行影像對準的計算了。
# 進行影像對準
registration.Update()
計算完成之後,取得影像對準結果並輸出幾個重要的參數值。
# 取得影像對準結果
transform = registration.GetTransform()
finalParameters = transform.GetParameters()
translationAlongX = finalParameters.GetElement(0)
translationAlongY = finalParameters.GetElement(1)
numberOfIterations = optimizer.GetCurrentIteration()
bestMetricValue = optimizer.GetValue()
# 輸出結果
print("Registration result: ")
print(" Translation X = " + str(translationAlongX))
print(" Translation Y = " + str(translationAlongY))
print(" Iterations = " + str(numberOfIterations))
print(" Metric value = " + str(bestMetricValue))
Registration result: Translation X = 13.001531179313957 Translation Y = 16.99948401485064 Iterations = 21 Metric value = 0.0012263178458750189
TranslationTransform 轉換的參數意義非常單純,兩個參數就是代表 X 軸與 Y 軸的位移。這裡的 Iterations 就是實際計算所使用的疊代次數,這個數字如果很大,可能代表 learning rate 設定太小,會造成計算時間拉長,或是結果尚未收斂。
轉換影像
在影像對準計算完成之後,只是得到了轉換函數的各個參數值,如果要得到調動影像轉換後的結果,必須把轉換函數套用至調動影像上進行轉換。
由於此處我們有指定調動影像的初始轉換,因此這裡必須以 CompositeTransform 將初始轉換與最終計算出來的轉換結合,再套用至調動影像上,才能正確將調動影像轉換至固定影像的空間中。
# 建立對準結果的轉換
CompositeTransformType = itk.CompositeTransform[ScalarType, Dimension]
outputCompositeTransform = CompositeTransformType.New()
outputCompositeTransform.AddTransform(movingInitialTransform)
outputCompositeTransform.AddTransform(registration.GetModifiableTransform())
轉換影像的工作必須使用 resampler 搭配轉換函數來處理:
# 建立輸出對準結果用的 Resampler
ResampleFilterType = itk.ResampleImageFilter
resampler = ResampleFilterType[MovingImageType, FixedImageType].New()
resampler.SetInput(movingImage)
resampler.SetTransform(outputCompositeTransform)
resampler.SetUseReferenceImage(True)
resampler.SetReferenceImage(fixedImage)
resampler.SetDefaultPixelValue(100)
顯示影像對準的結果:
# 分開顯示兩張影像
fig, axs = plt.subplots(1, 2)
axs[0].imshow(itk.GetArrayViewFromImage(fixedImage), cmap='gray')
axs[0].set_title('Fixed Image')
axs[1].imshow(itk.GetArrayViewFromImage(resampler.GetOutput()), cmap='gray')
axs[1].set_title('Result Image')
plt.show()

