介紹如何使用 ITK 的樣條曲線(BSpline)轉換對 2D 影像進行非剛性影像對準。
這個範例中使用
BSplineTransform
轉換對 2D 的影像進行非剛性影像對準(non-rigid image registration),由於樣條曲線(BSpline)轉換的參數數量很龐大,所以這個例子中採用 LBFGSOptimizerv4
而不用普通的 RegularStepGradientDescentOptimizer
或 ConjugateGradientLineSearchOptimizer
。
由於 BSplineTransform
轉換是由樣條曲線網格所組成的複雜變形,龐大的參數量可容許非常多種變形方式,但也會需要比較大量的計算。
#include <itkImageRegistrationMethodv4.h> #include <itkCorrelationImageToImageMetricv4.h> #include <itkTimeProbesCollectorBase.h> #include <itkMemoryProbesCollectorBase.h> #include <itkBSplineTransform.h> #include <itkLBFGSOptimizerv4.h> #include <itkImageFileReader.h> #include <itkImageFileWriter.h> #include <itkResampleImageFilter.h> #include <itkCastImageFilter.h> #include <itkSquaredDifferenceImageFilter.h> #include <itkBSplineTransformInitializer.h> #include <itkTransformToDisplacementFieldFilter.h> #include <itkCheckerBoardImageFilter.h> // LBFGSOptimizerv4 不會觸發事件,因此不用建立 Observer int main(int argc, char * argv[]) { if (argc < 3) { std::cerr << "Missing Parameters " << std::endl; std::cerr << "Usage: " << argv[0]; std::cerr << " fixedImageFile movingImageFile" << std::endl; return EXIT_FAILURE; } // 影像維度 constexpr unsigned int ImageDimension = 2; using PixelType = float; // 影像維度 using FixedImageType = itk::Image<PixelType, ImageDimension>; using MovingImageType = itk::Image<PixelType, ImageDimension>; // 轉換空間維度 const unsigned int SpaceDimension = ImageDimension; // Spline 曲線階數 constexpr unsigned int SplineOrder = 3; // 座標資料類型 using CoordinateRepType = double; // 建立 BSpline 轉換 using TransformType = itk::BSplineTransform<CoordinateRepType, SpaceDimension, SplineOrder>; TransformType::Pointer transform = TransformType::New(); // 建立 Optimizer using OptimizerType = itk::LBFGSOptimizerv4; OptimizerType::Pointer optimizer = OptimizerType::New(); // 建立 Metric using MetricType = itk::CorrelationImageToImageMetricv4<FixedImageType, MovingImageType>; MetricType::Pointer metric = MetricType::New(); // 建立 Registration using RegistrationType = itk::ImageRegistrationMethodv4<FixedImageType, MovingImageType>; RegistrationType::Pointer registration = RegistrationType::New(); // 設定 Registration 的 Metric 與 Optimizer registration->SetMetric(metric); registration->SetOptimizer(optimizer); // 讀取影像 using FixedImageReaderType = itk::ImageFileReader<FixedImageType>; using MovingImageReaderType = itk::ImageFileReader<MovingImageType>; FixedImageReaderType::Pointer fixedImageReader = FixedImageReaderType::New(); MovingImageReaderType::Pointer movingImageReader = MovingImageReaderType::New(); fixedImageReader->SetFileName(argv[1]); movingImageReader->SetFileName(argv[2]); fixedImageReader->Update(); FixedImageType::ConstPointer fixedImage = fixedImageReader->GetOutput(); // 以 BSplineTransformInitializer 設定 BSpline 轉換用的固定參數 using InitializerType = itk::BSplineTransformInitializer<TransformType, FixedImageType>; InitializerType::Pointer transformInitializer = InitializerType::New(); unsigned int numberOfGridNodesInOneDimension = 8; TransformType::MeshSizeType meshSize; meshSize.Fill(numberOfGridNodesInOneDimension - SplineOrder); transformInitializer->SetTransform(transform); transformInitializer->SetImage(fixedImage); transformInitializer->SetTransformDomainMeshSize(meshSize); transformInitializer->InitializeTransform(); // 初始化轉換參數 transform->SetIdentity(); std::cout << "Initial Parameters = " << std::endl; std::cout << transform->GetParameters() << std::endl; // 設定 Registration 的轉換 registration->SetInitialTransform(transform); // 將輸入的轉換物件直接用於輸出的轉換物件 registration->InPlaceOn(); // 設定輸入影像 registration->SetFixedImage(fixedImage); registration->SetMovingImage(movingImageReader->GetOutput()); // 自動估計參數尺度 using ScalesEstimatorType = itk::RegistrationParameterScalesFromPhysicalShift<MetricType>; ScalesEstimatorType::Pointer scalesEstimator = ScalesEstimatorType::New(); scalesEstimator->SetMetric(metric); scalesEstimator->SetTransformForward(true); scalesEstimator->SetSmallParameterVariation(1.0); optimizer->SetScalesEstimator(scalesEstimator); // 設定 Optimizer 參數 optimizer->SetGradientConvergenceTolerance(5e-2); optimizer->SetLineSearchAccuracy(1.2); optimizer->SetDefaultStepLength(1.5); optimizer->TraceOn(); optimizer->SetMaximumNumberOfFunctionEvaluations(1000); // 使用單一層級 Registration constexpr unsigned int numberOfLevels = 1; RegistrationType::ShrinkFactorsArrayType shrinkFactorsPerLevel; shrinkFactorsPerLevel.SetSize(1); shrinkFactorsPerLevel[0] = 1; RegistrationType::SmoothingSigmasArrayType smoothingSigmasPerLevel; smoothingSigmasPerLevel.SetSize(1); smoothingSigmasPerLevel[0] = 0; registration->SetNumberOfLevels(numberOfLevels); registration->SetSmoothingSigmasPerLevel(smoothingSigmasPerLevel); registration->SetShrinkFactorsPerLevel(shrinkFactorsPerLevel); // 測量時間與記憶體用量的探針 itk::TimeProbesCollectorBase chronometer; itk::MemoryProbesCollectorBase memorymeter; std::cout << std::endl << "Starting Registration" << std::endl; try { memorymeter.Start("Registration"); chronometer.Start("Registration"); registration->Update(); chronometer.Stop("Registration"); memorymeter.Stop("Registration"); std::cout << "Optimizer stop condition = " << registration->GetOptimizer()->GetStopConditionDescription() << std::endl; } catch (itk::ExceptionObject & err) { std::cerr << "ExceptionObject caught !" << std::endl; std::cerr << err << std::endl; return EXIT_FAILURE; } // 輸出時間與記憶體用量 chronometer.Report(std::cout); memorymeter.Report(std::cout); // 取得對準結果轉換參數 OptimizerType::ParametersType finalParameters = transform->GetParameters(); std::cout << "Last Transform Parameters" << std::endl; std::cout << finalParameters << std::endl; // 建立 ResampleFilter 套用影像轉換 using ResampleFilterType = itk::ResampleImageFilter<MovingImageType, FixedImageType>; ResampleFilterType::Pointer resample = ResampleFilterType::New(); // 設定轉換 resample->SetTransform(transform); // 設定輸入影像 resample->SetInput(movingImageReader->GetOutput()); // 設定輸出影像參數 resample->SetSize(fixedImage->GetLargestPossibleRegion().GetSize()); resample->SetOutputOrigin(fixedImage->GetOrigin()); resample->SetOutputSpacing(fixedImage->GetSpacing()); resample->SetOutputDirection(fixedImage->GetDirection()); resample->SetDefaultPixelValue(100); // 輸出影像類型 using OutputPixelType = unsigned char; using OutputImageType = itk::Image<OutputPixelType, ImageDimension>; // 影像轉型 Filter using CastFilterType = itk::CastImageFilter<FixedImageType, OutputImageType>; CastFilterType::Pointer caster = CastFilterType::New(); caster->SetInput(resample->GetOutput()); // 寫入影像 using WriterType = itk::ImageFileWriter<OutputImageType>; WriterType::Pointer writer = WriterType::New(); writer->SetFileName("output.png"); writer->SetInput(caster->GetOutput()); writer->Update(); // 輸出影像對準的 Deformation Field using VectorPixelType = itk::Vector<float, ImageDimension>; using DisplacementFieldImageType = itk::Image<VectorPixelType, ImageDimension>; using DisplacementFieldGeneratorType = itk::TransformToDisplacementFieldFilter<DisplacementFieldImageType, CoordinateRepType>; // 建立並設定 Displacement Field Generator DisplacementFieldGeneratorType::Pointer dispfieldGenerator = DisplacementFieldGeneratorType::New(); dispfieldGenerator->UseReferenceImageOn(); dispfieldGenerator->SetReferenceImage(fixedImage); dispfieldGenerator->SetTransform(transform); dispfieldGenerator->Update(); // 將 Displacement Field 寫入檔案 using FieldWriterType = itk::ImageFileWriter<DisplacementFieldImageType>; FieldWriterType::Pointer fieldWriter = FieldWriterType::New(); fieldWriter->SetInput(dispfieldGenerator->GetOutput()); fieldWriter->SetFileName("displacement.mha"); fieldWriter->Update(); // 產生棋盤式影像比較圖 using CheckerBoardFilterType = itk::CheckerBoardImageFilter<FixedImageType>; CheckerBoardFilterType::Pointer checker = CheckerBoardFilterType::New(); // 輸出影像對準前的棋盤式影像比較圖 checker->SetInput1(fixedImageReader->GetOutput()); checker->SetInput2(movingImageReader->GetOutput()); caster->SetInput(checker->GetOutput()); writer->SetInput(caster->GetOutput()); writer->SetFileName("before.png"); writer->Update(); // 輸出影像對準後的棋盤式影像比較圖 checker->SetInput1(fixedImageReader->GetOutput()); checker->SetInput2(resample->GetOutput()); caster->SetInput(checker->GetOutput()); writer->SetInput(caster->GetOutput()); writer->SetFileName("after.png"); writer->Update(); return EXIT_SUCCESS; }
將這個 C++ 程式儲存為 BSplineReg.cxx
之後,搭配以下 CMakeLists.txt
設定檔以 CMake 編譯。
cmake_minimum_required(VERSION 3.10.2) # 設定專案名稱 project(BSplineReg) # 尋找並引入 ITK 函式庫 find_package(ITK REQUIRED) include(${ITK_USE_FILE}) # 增加一個執行檔 add_executable(BSplineReg BSplineReg.cxx) # 定義執行檔連結方式 target_link_libraries(BSplineReg ${ITK_LIBRARIES})
編譯與執行的指令如下:
# 編譯程式 mkdir build cd build cmake .. # 執行程式 ./BSplineReg RatLungSlice1.mha RatLungSlice2.mha
這裡的 RatLungSlice1.mha
與 RatLungSlice2.mha
圖檔可以從 ITK 的 GitHub 網站取得,執行後除了可以產生對準後的圖形之外,也可以產生影像對準前後的棋盤式影像比較圖以及 displacement field。
這裡產生的 displacement.mha
可以使用以下 Python 程式繪製圖形:
import SimpleITK as sitk import matplotlib.pyplot as plt import numpy as np displacement = sitk.ReadImage("displacement.mha") movingImage = sitk.ReadImage("RatLungSlice2.mha") dispNDA = sitk.GetArrayViewFromImage(displacement) movingNDA = sitk.GetArrayViewFromImage(movingImage) ds = 4 X = np.arange(0, movingNDA.shape[0], ds) Y = np.arange(0, movingNDA.shape[1], ds) fig = plt.figure(figsize=(10, 10)) ax = fig.add_subplot() ax.imshow(movingNDA, cmap='gray') ax.quiver(X, Y, dispNDA[::ds,::ds,0], dispNDA[::ds,::ds,1], color='r', units='xy', scale=1) plt.show()