在ITK中负责表示数据的基本类。最常见的类是itk::Image、itk::Mesh和itk::PointSet。其中,Image类遵循泛型编程的精神,其中类型与类的算法行为分离,ITK支持任何像素类型和任何空间维度的图像。
1.创建一个Image
首先,必须包含Image类的头文件。
#include "itkImage.h"
然后我们必须决定用什么类型来表示像素以及图像的尺寸。通过这两个参数,我们可以实例化image类。这里我们使用无符号的短像素数据创建了一个3D图像。
typedef itk::Image< unsigned short, 3 > ImageType;
然后可以通过从对应的图像类型调用New()操作符并将结果赋值给itk::SmartPointer来创建图像。
ImageType::Pointer image = ImageType::New();
在ITK中,区域(region)是图像的一个子集,表示图像中可以由系统中的其他类处理的部分。最常见的区域之一是 LargestPossibleRegion,它定义了整个图像。ITK中发现的其他重要区域还有BufferedRegion,它是实际保存在内存中的图像部分,此外还有RequestedRegion,它是过滤器(Filter,一种对象的处理泛型手段)或其他类在操作图像时请求的区域。
一个区域由两个类定义:itk::Index和itk::Size类。与之关联的图像区域的原点是由Index定义的。区域的范围或大小是由Size定义的。Index由一个n维数组表示,其中每个分量都是一个整数,表示图像的初始像素。当手动创建图像时,用户负责定义图像大小和图像网格开始时的索引。
图像的起始点由一个索引类定义,该索引类是一个n维数组,其中每个组件都是一个整数,表示图像初始像素的网格坐标。
ImageType::IndexType start;
start[0] = 0; // first index on X
start[1] = 0; // first index on Y
start[2] = 0; // first index on Z
区域大小由与图像尺寸相同的数组表示(使用size类)。数组的组件是无符号整数,表示图像在每个维度上的范围(以像素为单位)。
ImageType::SizeType size;
size[0] = 200; // size along X
size[1] = 200; // size along Y
size[2] = 200; // size along Z
在定义了起始索引和图像大小之后,这两个参数用于创建一个ImageRegion对象,该对象基本上封装了这两个概念。该区域由初始索引和图像大小初始化。
ImageType::RegionType region;
region.SetSize( size );
region.SetIndex( start );
最后,将该区域传递给Image对象以定义其范围和原点。SetRegions方法同时设置最大的可能存储区域、缓冲区域和请求。
image->SetRegions( region );
注意,到目前为止,所有执行的操作都没有为图像像素数据分配内存。为此必须调用Allocate()方法。分配不需要任何参数,因为内存分配所需的所有信息已经由区域提供。
image->Allocate();
在实践中,很少直接分配和初始化图像。图像通常是从一个源(如文件或数据采集硬件)读取的。下面的示例说明如何从文件中读取映像。
2.从文件中读取Image
要从文件中读取图像,首先需要包含itk::ImageFileReader类的头文件。
#include "itkImageFileReader.h"
然后,应该通过指定用于表示图像像素和尺寸的类型来定义图像类型。
typedef unsigned char PixelType;
const unsigned int Dimension = 3;
typedef itk::Image< PixelType, Dimension > ImageType;
使用image类型,现在可以实例化image reader类。图像类型用作模板参数,用于定义数据加载到内存后如何表示。此类型不必与文件中存储的类型完全对应。但是,使用了基于c风格类型转换的转换,因此选择用于表示磁盘上的数据的类型必须足以准确地描述它。除了将文件的像素类型转换为ImageFileReader的像素类型外,阅读器不会对像素数据应用任何转换。
typedef itk::ImageFileReader< ImageType > ReaderType;
现在可以使用reader类型创建一个reader对象。一个itk::SmartPointer(由::指针符号定义)用于接收对新创建的阅读器的引用。调用New()方法来创建图像阅读器的实例:
ReaderType::Pointer reader = ReaderType::New();
阅读器所需要的最小信息是要加载到内存中的图像的文件名。
这是通过SetFileName()方法提供的。这里的文件格式是从文件名扩展名推断出来的。(用户也可以使用itk::ImageIO显式地指定数据格式)
const char * filename = argv[1];
reader->SetFileName( filename );
读取类对象称为管道源对象( pipeline source objects);它们响应管道更新请求并启动管道中的数据流。管道更新机制确保读取器仅在向读取器发出数据请求且读取器尚未读取任何数据时执行。在当前示例中,我们显式地调用Update()方法,因为阅读器的输出没有连接到其他过滤器。在正常应用程序中,读取器的输出连接到图像过滤器的输入,对过滤器的更新调用触发读取器的更新:
reader->Update();
可以通过在reader中调用GetOutput()方法对图像进行新的一轮访问。也可以在更新请求发送给读取器之前调用此方法。即使图像在读取器实际执行之前是空的,但对图像的引用仍然有效。
ImageType::Pointer image = reader->GetOutput();
注意:在读取器执行之前访问图像数据的任何尝试都会生成没有像素数据的图像。很可能会导致程序崩溃,因为图像没有正确初始化。
3.访问像素数据
像素在图像中的位置由一个唯一的索引来标识。索引是一个整数数组,它定义像素在图像每个坐标维度上的位置。索引类型由图像自动定义,可以使用作用域操作符(如itk::Index)访问。数组的长度将与相关图像的尺寸匹配。
下面几行声明了索引类型的一个实例,并初始化了它的内容,以便将其与图像中的像素位置相关联(请注意,索引不使用智能指针访问它。这是因为Index是轻量级对象,不打算在对象之间共享。生成这些小对象的多个副本比使用SmartPointer机制共享它们更有效。):
ImageType::IndexType pixelIndex;
pixelIndex[0] = 27; // x position
pixelIndex[1] = 29; // y position
pixelIndex[2] = 37; // z position
用索引定义了像素位置之后,就可以访问图像中像素的内容了。GetPixel()方法允许我们获得像素的值:
ImageType::PixelType pixelValue = image->GetPixel( pixelIndex );
SetPixel()方法允许我们设置像素值:
image->SetPixel( pixelIndex, pixelValue+1 );
请注意,GetPixel()使用复制语义而不是引用语义返回像素值。因此,该方法不能用于修改图像数据值。
请记住,SetPixel()和GetPixel()都是低效的,只能用于调试或支持交互,比如通过单击鼠标查询像素值。
4.定义原点(Origin)和间距(Spacing)
在上图中,圆圈被用来代表像素的中心。假设像素的值作为位于像素中心的狄拉克函数(Dirac Delta Function)存在。
像素间距是在像素中心之间测量的,可以在每个维度上是不同的。
图像原点与图像中第一个像素的坐标相关联。
像素被认为是包含数据值的像素中心周围的矩形区域。
图像间距用一个 FixedArray 表示,其大小与图像的维数相匹配。为了手动设置图像的间距,必须创建相应类型的数组。然后,数组中的元素应该用相邻像素中心之间的间距进行初始化。下面的代码演示了图像中的方法:
ImageType::SpacingType spacing;
spacing[0] = 0.33; // spacing along X
spacing[1] = 0.33; // spacing along Y
spacing[2] = 1.20; // spacing along Z
可以使用SetSpacing()方法将数组分配给图像:
image->SetSpacing( spacing );
可以使用GetSpacing()方法从图像检索间距信息。此方法返回对FixedArray的引用。然后可以使用返回的对象读取数组的内容。注意,使用const关键字表示不会修改数组。
const ImageType::SpacingType& sp = image->GetSpacing();
std::cout << "Spacing = ";
std::cout << sp[0] << ", " << sp[1] << ", " << sp[2] << std::endl;
图像原点的管理方式与间距类似。
必须首先分配适当维度的一个点。原点的坐标可以分配给每个分量。这些坐标对应于物理空间中任意参考系统的图像第一个像素的位置。确保同一个应用程序中使用的多个图像使用一致的引用系统是用户的责任。这在图像配准应用程序中非常重要。
ImageType::PointType origin;
origin[0] = 0.0; // coordinates of the
origin[1] = 0.0; // first pixel in N-D origin[2] = 0.0;
image->SetOrigin( origin );
还可以使用GetOrigin()方法从图像中检索原点。这将返回对一个点的引用。该引用可用于读取数组的内容。再次注意,使用const关键字表示不会修改数组内容。
const ImageType::PointType& orgn = image->GetOrigin();
std::cout << "Origin = ";
std::cout << orgn[0] << ", " << orgn[1] << ", " << orgn[2] << std::endl;
一旦图像的间距和原点被初始化,图像将正确地从物理空间坐标映射像素索引。下面的代码说明了如何将物理空间中的点映射到图像索引中,以便读取最近像素的内容:
首先,必须声明itk::Point类型。点类型在用于表示坐标的类型和空间的维度上模板化。在这种特殊情况下,点的维数必须与图像的维数匹配。
typedef itk::Point< double, ImageType::ImageDimension > PointType;
与itk::Index类似,Point类是一个相对较小和简单的对象,不用智能指针。
点对象可以使用传统的数组符号来访问它的组件。特别是,[ ]操作符是可用的。出于效率考虑,对于用于访问特定点组件的索引不执行边界检查。用户的责任是确保其索引范围在{0,Dimension−1}.
PointType point;
point[0] = 1.45; // x coordinate
point[1] = 7.21; // y coordinate
point[2] = 9.28; // z coordinate
图像将使用当前间距和原点的值将点映射到索引。必须提供索引对象来接收映射的结果。可以使用图像类型中定义的IndexType实例化索引对象。
ImageType::IndexType pixelIndex;
image类的TransformPhysicalPointToIndex()方法将计算提供点的像素索引。该方法检查该索引是否包含在当前缓冲的像素数据中。该方法返回一个布尔值,指示结果索引是否位于缓冲区域内。当方法的返回值为false时,不应该使用输出索引。
下面几行说明了指向索引的映射,以及随后如何使用像素索引访问图像中的像素数据:
bool isInside = image->TransformPhysicalPointToIndex( point, pixelIndex );
if ( isInside )
{
ImageType::PixelType pixelValue = image->GetPixel( pixelIndex );
pixelValue += 5;
image->SetPixel( pixelIndex, pixelValue );
}
记住,GetPixel()和SetPixel()是访问像素数据的低效方法。当需要大量访问像素数据时,应该使用图像迭代器。