图像配准的基本过程如下:
1.指定用于评估配准效果的相似度或误差测度;
2.指定一个变换模型,如刚体变换、仿射变换、弹性变换(elastic)、流体变换或B-样条等;
3.指定插值策略,如最邻近插值(nearst neighbour)、三线性插值(trilinear)、sinc插值等;
4.寻找变换参数以最大化相似性测度。
如下图所示:
配准框架的基本流程如下:
1.输入待配准的两幅图像,参考图 Fixed Image,浮动图 Moving Image.
2.对参考图的指定区域进行几何坐标变换(Transform) 得到新的区域 ;
3.通过插值方法(Image Interpolator)得到浮动图在上一步新区域的坐标;
4.相似性测度模块计算参考图和插值图之间的相似度,是一个关于几何变换参数的函数;
5.相似度函数输入到优化模块中进行最优化计算得到最终变换参数,这个过程一般通过
迭代来实现,即重复2~4步直到取得最大值;
6.整个配准算法模块输出浮动图在最优变换下的插值图像。
配准过程是一个优化问题,配准过程每进行一次迭代,得到一测度值,将该测度值与
我们所设定的值进行比较,如果达到预期的效果则停止迭代,得到最终配准结果。当然,
迭代可能无限制进行,所以我们还需要设置一迭代上限。
下面以一个简单例子对 ITK 中的配准框架进行说明:
ITK 的配准框架由如下几个组件组成:变换组件、相似性测度组件、插值组件和优化组件;各个组件通过一个称为“配准方法”的组件连接到一起,形成一个一个管道结构。
//代码:
#if defined(_MSC_VER) #pragma warning ( disable : 4786 ) #endif //本例演示了使用刚体变换进行 2D 图像配准, 使用的变换为 itk::CenteredRigid2DTransform #include "itkImageRegistrationMethod.h" #include "itkMeanSquaresImageToImageMetric.h" #include "itkLinearInterpolateImageFunction.h" #include "itkRegularStepGradientDescentOptimizer.h" #include "itkImage.h" #include "itkCenteredRigid2DTransform.h" #include "itkImageFileReader.h" #include "itkImageFileWriter.h" #include "itkResampleImageFilter.h" #include "itkSubtractImageFilter.h" #include "itkRescaleIntensityImageFilter.h" #include "itkCommand.h" /******************************************************************** *类 CommandIterationUpdate:创建 Command/Observer 设计模式的简单方式 *本例中它的主要功能为监视配准过程的执行,每迭代一次,调用一次Execute() 方法,输出相应参数 *此处涉及三个类: itk::Object, itk::Command,itk::EventObject。 *Object 是大多数类的基类,该类维护一个着一个指针链表,指针指向事件观察者(event observer). *Observer 的角色由 Command 类扮演, Observers 通过一个 Object 注册自己,声明当一特定事件发 *生时. 配准过程通过一 itk::Optimizer 执行一个迭代过程来控制. 大多数 optimizer 在每次迭代 *后都会调用一 itk::IterationEvent. 当一个事件发生时, 该对象遍历 registred observers(Command) *链表,并检查哪一个对该事件感兴趣, 当找到一个对该事件感兴趣的 observer 时,则调用与其关联的 *Execute() 方法. 在这方面, Execute() 应该看做是一个 callback,所以它应该 遵循一般 callback *的原则,如不应该执行计算机量大的任务,应该只完成一些快速且简单的任务. ********************************************************************/ class CommandIterationUpdate : public itk::Command { public: typedef CommandIterationUpdate Self; typedef itk::Command Superclass; typedef itk::SmartPointer Pointer; //itkNewMacro: 该宏包装了 New() 方法的所有代码 itkNewMacro( Self ); protected: CommandIterationUpdate() {}; public: typedef itk::RegularStepGradientDescentOptimizer OptimizerType; typedef const OptimizerType * OptimizerPointer; void Execute(itk::Object *caller, const itk::EventObject & event) { Execute( (const itk::Object *)caller, event); } //object: 指向激活事件的对象;event: 需要被激活的事件 void Execute(const itk::Object * object, const itk::EventObject & event) { OptimizerPointer optimizer = dynamic_cast< OptimizerPointer >( object ); //CheckEvent() 检测是否是观察的事件 if( ! itk::IterationEvent().CheckEvent( &event ) ) { return; } std::cout << optimizer->GetCurrentIteration() << " "; std::cout << optimizer->GetValue() << " "; std::cout << optimizer->GetCurrentPosition() << std::endl; } }; int main( int argc, char *argv[] ) { if( argc < 4 ) { std::cerr << "Missing Parameters " << std::endl; std::cerr << "Usage: " << argv[0]; std::cerr << " fixedImageFile movingImageFile "; std::cerr << " outputImagefile [differenceAfterRegistration] "; std::cerr << " [differenceBeforeRegistration] "; std::cerr << " [initialStepLength] "<< std::endl; return EXIT_FAILURE; } //1.定义待配准图像类型: 维数, 像素类型 const unsigned int Dimension = 2; typedef unsigned char PixelType; typedef itk::Image< PixelType, Dimension > FixedImageType; typedef itk::Image< PixelType, Dimension > MovingImageType; //2.定义配准框架的基本组件: 变换, 优化, 测度, 插值, 配准组件 //用于 2D 图像的一个刚体变换,该变换的唯一参数为: 空间坐标的类型 typedef itk::CenteredRigid2DTransform< double > TransformType; typedef itk::RegularStepGradientDescentOptimizer OptimizerType; typedef itk::MeanSquaresImageToImageMetric< FixedImageType, MovingImageType > MetricType; typedef itk::LinearInterpolateImageFunction< MovingImageType, double > InterpolatorType; //配准组件: 该组件用于连接其它各组件 typedef itk::ImageRegistrationMethod< FixedImageType, MovingImageType > RegistrationType; MetricType::Pointer metric = MetricType::New(); OptimizerType::Pointer optimizer = OptimizerType::New(); InterpolatorType::Pointer interpolator = InterpolatorType::New(); RegistrationType::Pointer registration = RegistrationType::New(); //3.使用配准组件将:变换, 优化, 测度, 插值 四个基本组件连接至一起 registration->SetMetric( metric ); registration->SetOptimizer( optimizer ); registration->SetInterpolator( interpolator ); TransformType::Pointer transform = TransformType::New(); registration->SetTransform( transform ); //4.设置待配准图像及变换区域 //定义待配准两幅输入图像的 reader typedef itk::ImageFileReader< FixedImageType > FixedImageReaderType; typedef itk::ImageFileReader< MovingImageType > MovingImageReaderType; FixedImageReaderType::Pointer fixedImageReader = FixedImageReaderType::New(); MovingImageReaderType::Pointer movingImageReader = MovingImageReaderType::New(); fixedImageReader->SetFileName( argv[1] ); movingImageReader->SetFileName( argv[2] ); //本例, 固定图像与浮动图像都来自文件 //需要 itk::ImageRegistrationMethod 从 file readers 的输出获取输入 registration->SetFixedImage( fixedImageReader->GetOutput() ); registration->SetMovingImage( movingImageReader->GetOutput() ); //更新 readers,确保在初始化变换时图像参数(size,origin,spacing)有效. fixedImageReader->Update(); //可以将配准限制在固定图像的一特定区域,通过将其作为测度(metric)计算的输入. //该区域通过方法SetFixedImageRegion() 定义. 通过使用这个特征, 可以避免图像 //中某些不需要的的对象影响配准输出,或者减少计算时间,本例中使用整幅图像. //此区域通过 BufferedRegion 标识. registration->SetFixedImageRegion( fixedImageReader->GetOutput()->GetBufferedRegion() ); //更新固定,浮动图像 reader, 使其 origin, size, spacing 有效, fixedImageReader->Update(); movingImageReader->Update(); //5.设置各组件的参数,主要是变换、优化组件的参数 //本例使用的为变换为: 先进行一个旋转变换,再进行一平移变换. typedef FixedImageType::SpacingType SpacingType; typedef FixedImageType::PointType OriginType; typedef FixedImageType::RegionType RegionType; typedef FixedImageType::SizeType SizeType; FixedImageType::Pointer fixedImage = fixedImageReader->GetOutput(); const SpacingType fixedSpacing = fixedImage->GetSpacing(); const OriginType fixedOrigin = fixedImage->GetOrigin(); const RegionType fixedRegion = fixedImage->GetLargestPossibleRegion(); const SizeType fixedSize = fixedRegion.GetSize(); //本例使用固定图像的中心点做为旋转中心 //使用固定图与浮动图中心之间距离向量作为平移量 //首先计算固定图像中心点 TransformType::InputPointType centerFixed; centerFixed[0] = fixedOrigin[0] + fixedSpacing[0] * fixedSize[0] / 2.0; centerFixed[1] = fixedOrigin[1] + fixedSpacing[1] * fixedSize[1] / 2.0; MovingImageType::Pointer movingImage = movingImageReader->GetOutput(); const SpacingType movingSpacing = movingImage->GetSpacing(); const OriginType movingOrigin = movingImage->GetOrigin(); const RegionType movingRegion = movingImage->GetLargestPossibleRegion(); const SizeType movingSize = movingRegion.GetSize(); //然后计算浮动图像中心点 TransformType::InputPointType centerMoving; centerMoving[0] = movingOrigin[0] + movingSpacing[0] * movingSize[0] / 2.0; centerMoving[1] = movingOrigin[1] + movingSpacing[1] * movingSize[1] / 2.0; //设置旋转中心 transform->SetCenter( centerFixed ); //设置平移量 transform->SetTranslation( centerMoving - centerFixed ); //设置初始旋转角度 transform->SetAngle( 0.0 ); //然后将当前的变换参数作为初始参数, 传递给配准过程 registration->SetInitialTransformParameters( transform->GetParameters() ); //设置优化组件参数. //注意: 旋转和变换的"单位刻度" 是不同的,旋转用弧度度量, 平移以毫米度量 typedef OptimizerType::ScalesType OptimizerScalesType; OptimizerScalesType optimizerScales( transform->GetNumberOfParameters() ); const double translationScale = 1.0 / 1000.0; optimizerScales[0] = 1.0; optimizerScales[1] = translationScale; optimizerScales[2] = translationScale; optimizerScales[3] = translationScale; optimizerScales[4] = translationScale; optimizer->SetScales( optimizerScales ); //本例使用的优化器是 itk::RegularStepGradientDescentOptimizer, //梯度下降法的一种,下面定义优化参数: //relaxation fator, initial step length, minimal step length, number of iterations double initialStepLength = 0.1; //初始步长 if( argc > 6 ) { initialStepLength = atof( argv[6] ); } optimizer->SetRelaxationFactor( 0.6 ); //松驰因子 optimizer->SetMaximumStepLength( initialStepLength ); //最大步长 //最小步长和迭代最大次数, 用来限制优化过程迭代的执行 optimizer->SetMinimumStepLength( 0.001 ); //最小步长 optimizer->SetNumberOfIterations( 200 ); //最大迭代次数,以防止无限次迭代 /* *应该注意: 配准过程是一个优化的过程, 优化过程每迭代一次, *就与相似性测度进行一次比较 *当相似性测度值达到我们设定的预期值,或者达到设定的迭代上限时 *就停止迭代,得到最终结果. */ //6.实例化一 Command/Observer 对象, 监视配准过程的执行, 并触发配准过程的执行. //实例化一个 Command/Observer 对象, 将其注册为 optimizer 的观察者 CommandIterationUpdate::Pointer observer = CommandIterationUpdate::New(); optimizer->AddObserver( itk::IterationEvent(), observer ); try { registration->StartRegistration(); //触发配准过程的执行 } catch( itk::ExceptionObject & err ) { std::cerr << "ExceptionObject caught !" << std::endl; std::cerr << err << std::endl; return EXIT_FAILURE; } //取得配准执行完毕的最终参数 OptimizerType::ParametersType finalParameters = registration->GetLastTransformParameters(); const double finalAngle = finalParameters[0]; const double finalRotationCenterX = finalParameters[1]; const double finalRotationCenterY = finalParameters[2]; const double finalTranslationX = finalParameters[3]; const double finalTranslationY = finalParameters[4]; const unsigned int numberOfIterations = optimizer->GetCurrentIteration(); const double bestValue = optimizer->GetValue(); const double finalAngleInDegrees = finalAngle * 180.0 / vnl_math::pi; std::cout << "Result = " << std::endl; std::cout << " Angle (radians) = " << finalAngle << std::endl; std::cout << " Angle (degrees) = " << finalAngleInDegrees << std::endl; std::cout << " Center X = " << finalRotationCenterX << std::endl; std::cout << " Center Y = " << finalRotationCenterY << std::endl; std::cout << " Translation X = " << finalTranslationX << std::endl; std::cout << " Translation Y = " << finalTranslationY << std::endl; std::cout << " Iterations = " << numberOfIterations << std::endl; std::cout << " Metric value = " << bestValue << std::endl; //7.重采样, 将变换后的浮动图像映射到固定图像空间中,保存配准结果 //一般情况,最后一步为: 利用最终的变换结果将浮动图像映射到固定图像空间 //可通过 itk::ResampleImageFilter 完成 typedef itk::ResampleImageFilter< MovingImageType, FixedImageType > ResampleFilterType; TransformType::Pointer finalTransform = TransformType::New(); finalTransform->SetParameters( finalParameters ); finalTransform->SetFixedParameters( transform->GetFixedParameters() ); //设置重采样过滤器的相应参数 ResampleFilterType::Pointer resample = ResampleFilterType::New(); resample->SetTransform( finalTransform ); //将浮动图连接至重采样过滤器的输入端 resample->SetInput( movingImageReader->GetOutput() ); resample->SetSize( fixedImage->GetLargestPossibleRegion().GetSize() ); resample->SetOutputOrigin( fixedImage->GetOrigin() ); resample->SetOutputSpacing( fixedImage->GetSpacing() ); resample->SetOutputDirection( fixedImage->GetDirection() ); //设置默认灰度值,用来 "突出" 显示映射后在浮动图像之外的区域 resample->SetDefaultPixelValue( 100 ); //保存配准后图像 typedef itk::ImageFileWriter< FixedImageType > WriterFixedType; WriterFixedType::Pointer writer = WriterFixedType::New(); writer->SetFileName( argv[3] ); writer->SetInput( resample->GetOutput() ); //重采样过滤器的输出 try { writer->Update(); } catch( itk::ExceptionObject & excp ) { std::cerr << "ExceptionObject while writing the resampled image !" << std::endl; std::cerr << excp << std::endl; return EXIT_FAILURE; } //通过 itk::SubtractImageFilter 比较"经过变换的浮动图像"与"固定图像"之间的差异 typedef itk::Image< float, Dimension > DifferenceImageType; typedef itk::SubtractImageFilter< FixedImageType, FixedImageType, DifferenceImageType > DifferenceFilterType; DifferenceFilterType::Pointer difference = DifferenceFilterType::New(); typedef unsigned char OutputPixelType; typedef itk::Image< OutputPixelType, Dimension > OutputImageType; //由于两幅图像之间的差异可能对应非常小的亮度值 //使用 itk::RescaleIntensityImageFilter 重新调节亮度值,使其更加明显 typedef itk::RescaleIntensityImageFilter< DifferenceImageType, OutputImageType > RescalerType; RescalerType::Pointer intensityRescaler = RescalerType::New(); intensityRescaler->SetOutputMinimum( 0 ); intensityRescaler->SetOutputMaximum( 255 ); difference->SetInput1( fixedImageReader->GetOutput() ); difference->SetInput2( resample->GetOutput() ); resample->SetDefaultPixelValue( 1 ); intensityRescaler->SetInput( difference->GetOutput() ); typedef itk::ImageFileWriter< OutputImageType > WriterType; WriterType::Pointer writer2 = WriterType::New(); writer2->SetInput( intensityRescaler->GetOutput() ); try { if( argc > 4 ) { //保存配准后, 浮动图与固定图之间差异的对比 writer2->SetFileName( argv[4] ); writer2->Update(); } //保存配准前, 浮动图与固定图之间差异的对比, 这里仍然需要重采样 //因为浮动图与固定图不一定有相同的 origin, size, spacing. //可以过一个恒等变换(Identity Transform)完成 TransformType::Pointer identityTransform = TransformType::New(); identityTransform->SetIdentity(); resample->SetTransform( identityTransform ); if( argc > 5 ) { writer2->SetFileName( argv[5] ); writer2->Update(); } } catch( itk::ExceptionObject & excp ) { std::cerr << "Error while writing difference images" << std::endl; std::cerr << excp << std::endl; return EXIT_FAILURE; } return EXIT_SUCCESS; }
除去细节,可以看出,使用 ITK 的配准框架进行医学图像配准主要遵循以下步骤:
1.定义待配准图像类型: 维数, 像素类型
2.定义配准框架的基本组件: 变换, 优化, 测度, 插值, 配准组件
3.使用配准组件将:变换, 优化, 测度, 插值 四个基本组件连接至一起
4.设置待配准图像及变换区域,有需要时进行适当处理
5.设置各组件的参数,变换、优化、测度,不同方法设置不同
6.实例化一个 Command/Observer 对象, 监视配准过程的执行, 并触发配准过程的执行.
7.重采样, 将变换后的浮动图像映射到固定图像空间中,保存配准结果
各个配准组件组成的管道结构如下所示:
Command/Observer 观察者模式的执行过程如下图所示:
本例的 CMakeLists.txt 内容为:
CMAKE_MINIMUM_REQUIRED(VERSION 2.4)
PROJECT(ImageRegistration5)
FIND_PACKAGE(ITK)
IF(ITK_FOUND)
INCLUDE(${ITK_USE_FILE})
ELSE(ITK_FOUND)
MESSAGE(FATAL_ERROR
"ITK not found. Please set ITK_DIR.")
ENDIF(ITK_FOUND)
ADD_EXECUTABLE(ImageRegistration5 ImageRegistration5.cxx )
TARGET_LINK_LIBRARIES(ImageRegistration5 ITKIO ITKNumerics)
程序所使用的两幅示例图像,固定图像,浮动图像分别如下:
程序执行后的过程及结果为:
Debug>ImageRegistration5.exe BrainProtonDensitySliceBorder20.png BrainProtonDensitySliceRotated10.png output.png after.png befor.png
0 2098.33 [0.0252835, 110.5, 128.5, 0.0912944, -0.0320326]
1 1766.73 [0.0618533, 110.501, 128.502, 0.177712, -0.0665167]
2 1380.97 [0.104328, 110.501, 128.5, 0.135417, -0.0669017]
3 877.601 [0.122006, 110.501, 128.494, 0.0784302, -0.0690638]
4 647.837 [0.140307, 110.5, 128.487, 0.0219416, -0.0639346]
5 410.321 [0.162961, 110.497, 128.479, -0.0313332, -0.0501843]
6 153.093 [0.189839, 110.49, 128.483, -0.0120674, -0.00086769]
7 115.912 [0.177587, 110.487, 128.494, 0.0427617, 0.0171322]
8 61.0641 [0.177564, 110.489, 128.488, 0.0095898, 0.00463817]
9 60.3813 [0.17645, 110.495, 128.489, 0.0217426, -0.0286424]
10 61.411 [0.177373, 110.491, 128.488, 0.0108792, -0.0103758]
11 60.4302 [0.177628, 110.488, 128.488, 0.00999985, 0.0108734]
12 60.5575 [0.177367, 110.49, 128.488, 0.00930347, -0.00186576]
13 60.3963 [0.177692, 110.489, 128.488, 0.011102, 0.00556995]
14 60.3989 [0.177429, 110.489, 128.488, 0.00980777, 0.0011698]
15 60.3822 [0.177691, 110.489, 128.488, 0.0109154, 0.00368068]
16 60.3942 [0.177487, 110.489, 128.488, 0.0103087, 0.00215544]
17 60.3781 [0.177817, 110.489, 128.488, 0.00869253, 0.00202342]
18 60.4265 [0.17767, 110.489, 128.488, 0.00967173, 0.00209358]
19 60.3867 [0.177458, 110.489, 128.488, 0.0106296, 0.00194103]
//配准完毕后的最终参数
Result =
Angle (radians) = 0.177458
Angle (degrees) = 10.1676
Center X = 110.489
Center Y = 128.488
Translation X = 0.0106296
Translation Y = 0.00194103
Iterations = 21
Metric value = 60.3809
配准后的图像,配准前两幅图像的差异,配准后的差异:
可以看到,配准后的两幅图像仍然有稍许没有对齐的部分。可以通过更复杂的测度,优化方法进行配准。
ITK 提供了大量不同的几何变换、测度方法、插值算法以及优化算法,主要的不同在于不同组件的参数设置。比较复杂的方法参数设置是件困难的事情,通常需要大量实验才能确定合适的参数,而且不同的实际问题参数设置也不尽相同,比较重要的参数通常需要在其它参数设置好以后再进行微调,才能得到合适的值。ITK 中还提供了使用多分辨策略的配准框架,弹性配准框架等等,现在刚刚开始研究,其它的慢慢研究。