第七章 开发者指南
7.1 与ITK的关系
elastix代码的很大一部分是基于ITK Ib´a˜nez et al[2005]。 ITK的使用意味着低级功能(图像类,内存分配等)经过彻底测试。 当然,由ITK支持的所有图像格式elastix也支持。 可以使用各种编译器(MS Visual Studio,GCC)在多个操作系统(Windows XP,Linux,Mac OS X)上编译C ++源代码,并支持32位和64位系统。
除了现有的ITK图像配准类之外,elastix还实现了新的功能。 最重要的增强功能列在表7.1中。 请注意,从第4版起,ITK还具有变换级联,并支持空间导数(但不是其导数再次为μ)。
7.2 elastix代码概述
elastix源代码大致分为两层,分别以C ++编写:A)实现图像配准功能的ITK风格的类,以及B)elastix包装器,它兼容阅读和设置参数,实例化和连接组件,保存(中间) 结果和类似的“行政”任务。 模块化设计可以添加新的组件,而无需更改elastix core。 添加一个新组件首先创建一个层A类,可以独立于B层进行编译和测试。接下来,需要写一个小B层包装,将A类与elastix的其他部分连接起来。
例如,图像采样器被实现为所有从基类itk :: ImageSamplerBase继承的ITK类。 这些可以在src / Common / ImageSamplers中找到。 这是elastix的“层A”。 对于每个采样器(随机,网格,完整...),写入一个包装器,位于src / Components / ImageSamplers中,它在配准过程的每个新分辨率之前都需要配置采样器。 这是elastix的“B层”。
7.2.1 目录结构
基本目录结构如下:
- dox
- src/Common: ITK类,Layer A stuff。 该目录还包含一些与ITK无关的外部库,如xout(由我们编写)和ANNlib。
- src/Core: 这是主要的elasticix内核,负责执行流程,连接类,读取参数等。
- 抽样策略的模块化框架。See for more details Staring and
Klein [2010b]. - 几个新的优化者:Kiefer-Wolfowitz,Robbins-Monro,自适应随机梯度下降,进化策略。 对现有ITK优化器进行完整的返工,增加用户控制和更好的错误处理:准牛顿,非线性共轭梯度。
- 几个新的或更灵活的成本函数:(标准化)互信息,用Parzen窗口实现,类似于Th'evenaz and Unser [2000],多重α相互信息,弯曲能量惩罚项,刚性惩罚项。
- 连接任意数量几何变换的能力。
- 转换不仅支持∂T/∂μ的计算,而且还支持空间导数∂T/∂x和∂2T/∂x2的计算,并且它们的导数为μ的计算,用于计算正则化项时经常需要。 另外,更一般地集成了某些转换的紧凑支持。 更多详情查看Staring and Klein [2010a]。
- 成本函数的线性组合,而不是单一的成本函数。
- 抽样策略的模块化框架。See for more details Staring and
表7.1:与ITK相比,elastix的最重要的增强和增加
- src/Components: 此目录包含组件及其elastix包装器(B层)。 非常特定的组件A层代码也可以在这里找到。
在elastix 4.4和更高版本中,还可以添加您自己的组件目录。 这些可以位于elastix源树之外的任何地方。 有关详细信息,请参见第7.4节。
7.3 在自己的软件中使用elastix
在自己的软件中有(至少)三种使用弹性的方式:
- 编译elastix可执行文件,并直接用适当的参数调用它。 这是最简单的方法,Matlab和MeVisLab代码存在。
- 将elastix源代码包含在您自己的项目中,请参见第7.3.1节。
- 将elastix编译为库并链接到该库,请参见第7.3.2节。 此功能从版本4.7(2014年2月)起可用。
7.3.1 在您自己的软件中包括elasticix代码
您可能会发现一些elastix类有助于集成到您自己的项目中。 例如,如果您正在开发一个新的elasticix组件,并且首先要在elasticix之外测试(参见第7.4节),在这种情况下,您当然可以将所需的elastix文件复制到您自己的项目中,或者手动设置包含路径,但这不会很方便。
为了更容易,在elastix二进制目录中生成一个UseElastix.cmake文件。 您可以将其包含在您自己的项目的CMakeLists.txt文件中,并且CMake将确保设置了所有必需的包含目录。 此外,您可以链接到elastix库,如elxCommon,以避免重新编译代码。
一个例子可以在elastix source distribution的目录dox / externalproject中找到。
7.3.2 使用elastix作为库
简介
elastix还提供了用作动态或静态链接库的可能性。 这提供了将其功能集成到您自己的软件中的可能性,而无需调用外部的elasticix可执行文件。 后者的缺点是,您的软件(可能已经在内存中已经有固定和运动的图像)必须将这些图像存储到磁盘。 然后,elastix将再次加载它们(因此它们将在内存中两次),执行配准,并将结果写入磁盘。 然后,您的软件需要将结果从磁盘加载到内存。 这种方法显然导致内存使用的增加,由于读/写开销而导致的性能下降,并不是非常优雅。 当使用elastix作为库时,您的软件只需要将存储器指针传递给库接口,因此不需要读/写或内存映像复制。 配准之后,elastix将会将指针返回到您的程序。
库功能还在相当的深入开发中,但是以下基本功能已经可用:
- 使用elastix组件的任何组合配准任何一对图像。
- 配准掩码(masks)的使用
- 连续使用多个参数文件(类似于使用elastix可执行文件的-p选项多次)。
- 使用transformix来转换图像。
将elastix作为静态或动态库构建
要构建elastix库作为库,您必须禁用CMake中的ELASTIX_BUILD_ EXECUTABLE选项。 使用此选项禁用一个构建项目,将创建一个静态库。 如果要创建动态库(测试不够好),则必须启用ELASTIX_BUILD_SHARED_ LIBS选项。
与elastix库连接
在构建自己的软件项目时,需要将elastix连接,并将elastix源目录作为包含目录提供给编译器。 您可以这样做,例如,通过将以下代码添加到您的CMakeLists.txt文件中:
set( ELASTIX_BUILD_DIR "" CACHE PATH "Path to elastix build folder" )
set( ELASTIX_USE_FILE ${ELASTIX_BUILD_DIR}/UseElastix.cmake )
if( EXISTS ${ELASTIX_USE_FILE} )
include( ${ELASTIX_USE_FILE} )
link_libraries( param )
link_libraries( elastix )
link_libraries( transformix )
endif()
这将为CMake添加一个参数,ELASTIX_BUILD_DIR,需要在运行CMake时由用户提供。 您应该提供您编译elastix源代码的目录(具有UseElastix.cmake文件的目录)。 如果要更好地控制链接elastix的二进制文件,请使用CMaketarget_link_libraries指令。
准备配准参数设置
要能够运行elastix,您需要首先准备参数设置。 例如你可以通过从文件中读取它们来做到这一点:
#include "elastixlib.h"
#include "itkParameterFileParser.h"
using namespace elastix;
typedef ELASTIX::ParameterMapType RegistrationParametersType;
typedef itk::ParameterFileParser ParserType;
// Create parser for transform parameters text file.
ParserType::Pointer file_parser = ParserType::New();
// Try parsing transform parameters text file.
file_parser->SetParameterFileName( "par_registration.txt" );
try
{
file_parser->ReadParameterFile();
}
catch( itk::ExceptionObject & e )
{
std::cout << e.what() << std::endl;
// Do some error handling!
}
// Retrieve parameter settings as map.
RegistrationParametersType parameters = file_parser->GetParameterMap();
如果要连续使用多个参数文件,请逐个加载它们,并将它们添加到向量中:
typedef std::vector<RegistrationParametersType> RegistrationParametersContainerType;
然后在下面的代码中使用此向量,而不是单个参数映射。 您还可以在C ++代码中设置参数映射。 检查ELASTIX :: ParameterMapType的typedef的确切格式。
运行elastix
加载参数设置后,使用例如以下代码运行elastix:
ELASTIX* elastix = new ELASTIX();
int error = 0;
try
{
error = elastix->RegisterImages(
static_cast<typename itk::DataObject::Pointer>( fixed_image.GetPointer() ),
static_cast<typename itk::DataObject::Pointer>( moving_image.GetPointer() ),
parameters, // Parameter map read in previous code
output_directory, // Directory where output is written, if enabled
write_log_file, // Enable/disable writing of elastix.log
output_to_console, // Enable/disable output to console
0, // Provide fixed image mask (optional, 0 = no mask)
0 // Provide moving image mask (optional, 0 = no mask)
);
}
catch( itk::ExceptionObject &err )
{
// Do some error handling.
}
if( error == 0 )
{
if( elastix->GetResultImage().IsNotNull() )
{
// Typedef the ITKImageType first...
ITKImageType * output_image = static_cast<ITKImageType *>(
elastix->GetResultImage().GetPointer() );
}
else
{
// Registration failure. Do some error handling.
}
// Get transform parameters of all registration steps.
RegistrationParametersContainerType transform_parameters = elastix->GetTransformParameterMapList();
// Clean up memory.
delete elastix;
运行transformix
由ELASTIX类提供的转换参数,您可以运行transformix:
TRANSFORMIX* transformix = new TRANSFORMIX();
int error = 0;
try
{
error = transformix->TransformImage(
static_cast<typename itk::DataObject::Pointer>(
input_image_adapter.GetPointer() ),
transform_parameters, // Parameters resulting from elastix run
write_log_file, // Enable/disable writing of transformix.log
output_to_console); // Enable/disable output to console
}
catch( itk::ExceptionObject &err )
{
// Do some error handling.
}
if( error == 0 )
{
// Typedef the ITKImageType first...
ITKImageType * output_image = static_cast<ITKImageType *>(
transformix->GetResultImage().GetPointer() );
}
else
{
// Do some error handling.
}
// Clean up memory.
delete transformix;
或者,您可以使用参数文件解析器从文件(例如,从TransformParameters.0.txt)读取转换参数,方法与上述配准参数所示相同。
7.4 创建新组件
如果要创建自己的组件,开始编写A层类是很自然的,而不用担心elastix。 A层过滤器应该实现所有基本功能,并且可以在单独的ITK程序中进行测试,如果它执行了应该做的事情。 一旦你获得了这个ITK类的工作,在elastix文件(从现有组件复制粘贴开始)中编写B层包装很简单。
使用CMake,您可以通过使用“ELASTIX_USER_COMPONENT_DIRS”选项告诉elastix你的新组件的源代码在哪些目录中,来了解新组件的源代码所在目录的弹性。 elastix将搜索包含ADD ELXCOMPONENT(<name> ...)命令的CMakeLists.txt文件的这些目录的所有子目录。 伴随elastix组件的CMakeLists.txt文件通常如下所示:
ADD_ELXCOMPONENT( AdvancedMeanSquaresMetric
elxAdvancedMeanSquaresMetric.h
elxAdvancedMeanSquaresMetric.hxx
elxAdvancedMeanSquaresMetric.cxx
itkAdvancedMeanSquaresImageToImageMetric.h
itkAdvancedMeanSquaresImageToImageMetric.hxx )
ADD_ELXCOMPONENT命令是在src / Components / CMakeLists.txt中定义的宏。 第一个参数是B层包装类的名称,它在“elxAdvancedMeanSquaresMetric.h”中声明。 之后,您可以指定组件所依赖的源文件。 在上面的例子中,以“itk”开头的文件形成了A层代码。 以“elx”开头的文件是B层代码。 文件“elxAdvancedMeanSquaresMetric.cxx”特别简单。 它只包括两行:
#include "elxAdvancedMeanSquaresMetric.h"
elxInstallMacro( AdvancedMeanSquaresMetric );
elxInstallMacro在src / Core / Install / elxMacro.h中定义。
文件elxAdvancedMeanSquaresMetric.h / hxx一起定义了B层包装类。 该类从相应的层A继承,也可以从elx :: BaseComponent继承。 这使我们有机会向所有elastix组件添加通用接口,而不管这些ITK类继承自哪里。 此接口的示例如下:
void BeforeAll(void)
void BeforeRegistration(void)
void BeforeEachResolution(void)
void AfterEachResolution(void)
void AfterEachIteration(void)
void AfterRegistration(void)
这些方法会在函数名称所示的时刻自动调用。 这让你有机会阅读/设置一些参数,打印一些输出,保存一些结果等。
7.5 编程风格
为了提高代码的可读性和一致性,对可维护性有积极的影响,我们采用了编码风格。 自4.7版以来,elastix提供了一个粗略的uncrustify配置文件。
-
White spacing 良好的间距提高了代码的可读性。 因此,
- 不要使用tabs。 Tabs取决于tab大小,这将使代码显示取决于查看器。 我们每个标签使用2个空格。 在Visual Studio中,可以设置为首选项:转到工具→选项→文本编辑器→所有语言→制表符,然后tab size=缩进大小= 2并标记“插入空格”。 在vim中,您可以调整您的.vimrc以包含set ts = 2; set sw = 2;
set expandtab。 - 行末没有空格,就像ITK一样。 这只是丑陋的。为了使它们(非常)引人注目的在.vimrc中添加以下内容:
- 不要使用tabs。 Tabs取决于tab大小,这将使代码显示取决于查看器。 我们每个标签使用2个空格。 在Visual Studio中,可以设置为首选项:转到工具→选项→文本编辑器→所有语言→制表符,然后tab size=缩进大小= 2并标记“插入空格”。 在vim中,您可以调整您的.vimrc以包含set ts = 2; set sw = 2;
:highlight ExtraWhitespace ctermbg=red guibg=red
:match ExtraWhitespace /\s+$/
- 在函数,循环,索引等中使用空格。所以,
FunctionName(.void.);
for(.i.=.0;.i.<.10;.++i.)
vector[.i.].=.3;
- **缩进**
- 不要太多,不要太长线(too long lines)
namespace itk
{ ^ ^
/**
.* ********************* Function ******************************
./
^
template <class TTemplate1, class TTemplate2>
void
ClassName<TTemplate1, TTemplate2>
::Function(.void.)
{
..//Function body
..this->OtherMemberFunction(.arguments.);
..for(.i.=.0;.i.<.10;.++i.)
..{
....x.+=.i..i;
..}
^
} // end Function()
^
}.//.end.namespace.itk
- 类
namespace itk
{ ^
/.\class.ClassName
..\brief.Brief.description
.
..Detailed.description
.
..\ingroup.Group
./
^
template < templateArguments >
class.ClassName:
public.SuperclassName
{
public:
^
../*.Standard.class.typedefs../
..typedef.ClassName..................Self;
..typedef.SuperclassName.............Superclass;
- **变量和函数命名** 如果从变量的名称,你知道它是本地或一个类成员,那很好。 因此,
- 成员变量以m_为前缀,后跟大写。 在实现中使用this->引用它们。 所以,这个 this->m_MemberVariable是正确的。
- 局部变量应以小写字符开头。
- 函数名从大写开始
- 使用这个 - >来调用成员函数
- **更好的代码** 看一些简单的事情:
- 随时随地使用const
- 对于浮点数不要使用0,但是0.0可以避免可能出现的错误。
- 在派生类中覆盖虚拟函数时使用virtual关键字。 这在C ++中不是严格需要的,但是当您使用virtual关键字,覆盖或意图被覆盖的函数会很清楚。
- 始终使用开启和关闭括号。 尽管C ++并不总是需要这样做的
if(.condition.)
{
..valid = true;
}
而不是
if(.condition.)
..valid = true;
``
对于循环也应该这样。
- 注释 代码是由别人阅读,或者是你在几年的时间里阅读。 所以,
- 多注释
- 以} // end FunctionName()结束函数