天天看点

ITK 配准框架示例

图像配准的基本过程如下:

1.指定用于评估配准效果的相似度或误差测度;

2.指定一个变换模型,如刚体变换、仿射变换、弹性变换(elastic)、流体变换或B-样条等;

3.指定插值策略,如最邻近插值(nearst neighbour)、三线性插值(trilinear)、sinc插值等;

4.寻找变换参数以最大化相似性测度。

如下图所示:

ITK 配准框架示例

配准框架的基本流程如下:

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<Self> 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.重采样, 将变换后的浮动图像映射到固定图像空间中,保存配准结果

各个配准组件组成的管道结构如下所示:

ITK 配准框架示例

Command/Observer 观察者模式的执行过程如下图所示:

ITK 配准框架示例

本例的 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)

程序所使用的两幅示例图像,固定图像,浮动图像分别如下:

ITK 配准框架示例
ITK 配准框架示例

程序执行后的过程及结果为:

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 配准框架示例
ITK 配准框架示例

可以看到,配准后的两幅图像仍然有稍许没有对齐的部分。可以通过更复杂的测度,优化方法进行配准。

ITK 提供了大量不同的几何变换、测度方法、插值算法以及优化算法,主要的不同在于不同组件的参数设置。比较复杂的方法参数设置是件困难的事情,通常需要大量实验才能确定合适的参数,而且不同的实际问题参数设置也不尽相同,比较重要的参数通常需要在其它参数设置好以后再进行微调,才能得到合适的值。ITK 中还提供了使用多分辨策略的配准框架,弹性配准框架等等,现在刚刚开始研究,其它的慢慢研究。

继续阅读