Python

Python ITK 2D 影像對準 Hello World 教學與範例

介紹如何在 Python 中使用 ITK 函式庫的 v4 影像對準框架處理二維影像的對準,並提供 hello world 範例程式碼。

ITKv4 影像對準框架

ITKv4 影像對準框架跟上一版比較起來更為彈性,主要差異就是對準過程都在新的虛擬影像(virtual image)中進行,固定影像(fixed image)與調動影像(moving image)都透過轉換(transforms)與內插(interpolators)在虛擬影像空間以 metric 衡量配適程度,然後將結果交給 optimizer 更新轉換參數,重複這個過程直到結果收斂。

ITKv4 影像對準框架

在這張 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

使用 matplotlibimshow 顯示兩張影像的內容。

# 分開顯示兩張影像
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.05.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 參數來控制,這個參數值介於 01 之間,預設值是 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()
影像對準結果

參考資料:ImageRegistration1.cxx

Share
Published by
Office Guide
Tags: ITK

Recent Posts

Python 使用 PyAutoGUI 自動操作滑鼠與鍵盤

本篇介紹如何在 Python ...

1 年 ago

Ubuntu Linux 以 WireGuard 架設 VPN 伺服器教學與範例

本篇介紹如何在 Ubuntu ...

1 年 ago

Linux 網路設定 ip 指令用法教學與範例

本篇介紹如何在 Linux 系...

1 年 ago

Linux 以 Cryptsetup、LUKS 加密 USB 隨身碟教學與範例

介紹如何在 Linux 系統中...

1 年 ago