迭代器

本章介绍 image iterator 是ITK中用于图像处理的一种重要的通用编程构造。迭代器是常见的C编程语言指针的泛化,用于引用内存中的数据。ITK有各种各样的图像迭代器,其中一些是高度专门化的,以简化常见的图像处理任务。

引言

泛型编程模型定义了功能独立的组件,称为 containersalgorithms 。容器对象存储数据,算法对数据进行操作。为了访问容器中的数据,算法使用称为 iterators 。迭代器是内存指针的抽象。每种容器类型都必须定义自己的迭代器类型,但编写所有迭代器都是为了提供一个公共接口,以便算法代码能够以通用方式引用数据,并保持与容器的功能独立性。

迭代器之所以这样命名,是因为它用于 iterative 、容器值的顺序访问。迭代器出现在 forwhile 循环构造,依次访问每个数据点。例如,C指针就是一种迭代器。它可以在内存中向前(递增)和向后(递减)移动,以顺序引用数组的元素。许多迭代器实现都有一个类似于C指针的接口。

在ITK中,我们使用迭代器为使用像素类型、像素容器类型和维度的不同组合实例化的图像编写通用图像处理代码。因为ITK图像迭代器专门设计用于 image 容器、它们的接口和实现针对图像处理任务进行了优化。使用ITK迭代器,而不是通过 otb::Image 界面有很多优点。代码更紧凑,通常会自动泛化到更高的维度,算法运行得更快,迭代器简化了多线程和基于邻域的图像处理等任务。

编程接口

本节介绍标准的ITK图像迭代器编程接口。一些专门的图像迭代器可能会偏离此标准或提供其他方法。

创建迭代器

所有图像迭代器都至少有一个模板参数,该参数是它们迭代的图像类型。对图像的维度或图像的像素类型没有限制。

迭代器构造函数需要至少两个参数、一个指向要迭代的图像的智能指针和一个图像区域。图像区域,称为 iteration region 是约束迭代的直线区域。迭代区域必须完全包含在图像中。更具体地说,有效迭代区域是当前 BufferedRegion

大多数ITK图像迭代器有一个常量版本和一个非常量版本。非常数迭代器不能在非常数图像指针上实例化。常量版本的迭代器可以读取,但不能写入像素值。

下面是一个简单的示例,它定义并构造一个简单的图像迭代器 otb::Image

typedef otb::Image<float, 3> ImageType;
typedef itk::ImageRegionConstIterator<ImageType> ConstIteratorType;
typedef itk::ImageRegionIterator<ImageType>      IteratorType;

ImageType::Pointer image = SomeFilter->GetOutput();

ConstIteratorType constIterator(image, image->GetRequestedRegion());
IteratorType      iterator(image, image->GetRequestedRegion());

移动迭代器

迭代器被描述为 walking 它的迭代区域。在任何时候,迭代器都会引用或“指向”N维(ND)图像中的一个像素位置。 Forward iteration 从迭代区域的开始到迭代区域的结束。 Reverse iteration ,从刚过区域末端返回到开始处。迭代器有两个对应的起始位置,即 begin 位置和 end 位置。可以使用以下命令将迭代器直接移动到这两个位置之一:

  • GoToBegin() 将迭代器指向区域中的第一个有效数据元素。
  • GoToEnd() 将迭代器指向 one position past 区域中的最后一个有效元素。

请注意,结束位置实际上并不位于迭代区域内。记住这一点很重要,因为试图在迭代器的末尾位置取消对其的引用将产生未定义的结果。

ITK迭代器使用递减和递增运算符在其迭代中来回移动:

  • operator++() 在正方向上将迭代器递增一个位置。对于ITK图像迭代器,只定义了前缀增量运算符。
  • operator–() 将迭代器在负方向上递减一个位置。对于ITK图像迭代器,只定义了前缀减量运算符。

下图说明了图像区域上的典型迭代。大多数迭代器在图像维度增长最快的方向上递增和递减,在区域边界处包裹到下一个更高维度的第一个位置。换句话说,迭代器首先跨列移动,然后向下移动行,然后从一个片移动到另一个片,依此类推。

../_images/IteratorFigure1.png

Fig. 13 迭代器通过2D图像的法线路径。迭代区域以较深的阴影显示。箭头表示一个迭代器步骤,即一个++操作的结果。

除了对图像进行顺序迭代之外,一些迭代器还可以定义随机访问运算符。与增量操作符不同,随机访问操作符可能不会在速度上进行优化,并且需要了解图像的维度和迭代区域的范围才能正确使用。

  • operator+=(OffsetType) 将迭代器移动到当前索引加上指定位置处的像素位置 itk::Offset
  • operator-=(OffsetType) 将迭代器移动到当前索引减去指定偏移量处的像素位置。
  • SetPosition(IndexType) 将迭代器移动到给定的 itk::Index 位置。

这个 SetPosition() 对于更复杂的迭代器类型,方法可能会非常慢。通常,它应该只用于设置开始迭代位置,就像您将使用的那样 GoToBegin()GoToEnd()

一些迭代器在迭代区域中不遵循可预测的路径,并且没有固定的开始或结束像素位置。例如,条件迭代器仅在像素具有特定值或连通性时才访问像素。随机迭代器,递增和递减到随机位置,甚至可以多次访问给定的像素位置。

可以查询迭代器以确定它位于迭代区域的末尾还是开头。

  • bool IsAtEnd() 如果迭代器指向 one position past 迭代区域的末尾。
  • bool IsAtBegin() 如果迭代器指向迭代区域中的第一个位置,则为True。该方法通常用于测试反向迭代的结束。

迭代器还可以报告其当前图像索引位置。

  • IndexType GetIndex() 返回迭代器当前指向的图像像素的索引。

为了提高效率,大多数ITK图像迭代器不执行边界检查。可以将迭代器移到其有效迭代区域之外。取消引用越界迭代器将产生未定义的结果。

访问数据

ITK图像迭代器定义了读写像素值的两种基本方法。

  • PixelType Get() 返回迭代器位置的像素值。
  • void Set(PixelType) 设置迭代器位置的像素值。未为迭代器的常量版本定义。

这个 Get()Set() 方法是内联的并针对速度进行了优化,因此它们的使用等同于直接取消对图像缓冲区的引用。然而,在一些常见的情况下,使用 Get()Set() 确实会招致处罚。考虑下面的代码,该代码获取、修改一个值,然后将其写回相同的像素位置:

it.Set(it.Get() + 1);

正如所编写的那样,该代码需要比所需的多一个内存取消引用。一些迭代器定义了第三种数据访问方法来避免这种惩罚。

  • PixelType & Value() 返回对迭代器位置上的像素的引用。

这个 Value() 方法可以用作表达式中的lval或rval。它具有以下所有属性 operator* 。这个 Value() 方法可以更高效地重写我们的示例代码:

it.Value()++;

考虑使用 Value() 方法而不是 Get()Set() 当调用 operator= 对像素的操作不是微不足道的,例如在使用向量像素时,并且操作在图像中就地完成。

迭代循环

使用前面小节中描述的方法,我们现在可以编写一个简单的示例来对图像进行像素级操作。下面的代码计算输入图像中所有值的平方,并将它们写入输出图像。

ConstIteratorType in(inputImage, inputImage->GetRequestedRegion());
IteratorType      out(outputImage, inputImage->GetRequestedRegion());

for (in.GoToBegin(), out.GoToBegin(); !in.IsAtEnd(); ++in, ++out)
{
  out.Set(in.Get() * in.Get());
}

请注意,输入迭代器和输出迭代器都是在同一区域初始化的,即 RequestedRegioninputImage 。这是一个很好的实践,因为它确保输出迭代器遍历与输入迭代器完全相同的一组像素索引,但不要求输出和输入的大小相同。唯一的要求是输入图像必须包含与 RequestedRegion 输出图像的。

可以通过反向迭代图像来编写等价代码。语法稍显笨拙,因为 end 迭代区域的位置不是有效位置,只能测试迭代器是否严格 equal 回到它开始的位置。类中编写反向迭代通常更方便 while 循环播放。

in.GoToEnd();
out.GoToEnd();
while (!in.IsAtBegin())
{
  --in;
  --out;
  out.Set(in.Get() * in.Get());
}

图像迭代器

本节介绍遍历直线图像区域并一次引用一个像素的迭代器。这个 itk::ImageRegionIterator 是最基本的ITK图像迭代器,也是大多数应用程序的首选。本节中的其余迭代器是ImageRegionIterator的专门化,旨在使常见的图像处理任务更高效或更易于实现。

  • ImageRegionIterator:请参见示例 ImageRegionIterator.cxx
  • ImageRegionIteratorWithIndex:请参见示例 ImageRegionIteratorWithIndex.cxx
  • ImageLinearIteratorWithIndex:请参见示例 ImageLinearIteratorWithIndex.cxx

邻域迭代器

在ITK中,像素邻域被粗略地定义为图像中局部相邻的一小部分像素。邻域的大小和形状以及邻域中像素之间的连接性可能会因应用程序而异。

许多图像处理算法都是基于邻域的,也就是说,在一个像素上的结果 i 是从ND邻域中的像素值计算得出的 i 。考虑2D中的有限差分运算。像素索引处的导数 i = (j, k) 例如,被视为以下值的加权差 (j+1, k)(j-1, k) 。邻域运算的其他常见示例包括卷积滤波和图像形态。

本节介绍一类为处理像素邻域而设计的ITK图像迭代器。ITK邻域迭代器就像普通图像迭代器一样遍历图像区域,但它不是在每一步只引用一个像素,而是同时指向整个ND个像素邻域。对标准迭代器接口的扩展提供了对所有邻域像素和信息(如邻域的大小、范围和位置)的读写访问。

邻域迭代器使用第节中定义的相同运算符 [sec:IteratorsInterface] 并且与用于循环遍历图像的普通迭代器相同的代码构造。插图 [fig:NeighborhoodIteratorFig1] 显示了在迭代区域中移动的邻域迭代器。此迭代器定义了一个 3x3 它所访问的每个像素周围的邻域。这个 center 邻域迭代器的位置始终位于其当前索引之上,并且所有其他邻域像素索引被引用为距中心索引的偏移量。邻域迭代器中心下的像素和阴影区域下的所有像素,或 extent 可以取消对迭代器的引用。

../_images/NeighborhoodIteratorFig1.png

Fig. 14 A的路径 3x3 通过2D图像区域的邻域迭代器。邻域的范围由围绕迭代器位置的散列来指示。位于此范围内的像素可通过迭代器访问。箭头表示一个迭代器步骤,即一个++操作的结果。

除了标准图像指针和迭代区域(第节 [sec:IteratorsInterface] ),邻域迭代器构造函数需要一个参数来指定要覆盖的邻域范围。邻域范围在每个轴上沿其中心对称,并以 N 这些距离统称为 radius 。每个元素 d 半径的范围,其中 0 < d < NN 是邻域的维度,以像素为单位给出邻域的范围 N 。生成的ND超立方体的每个面的长度为 2d + 1 像素,距离为 d 在相邻中心的单个像素的两侧。插图 [fig:NeighborhoodIteratorFig2] 显示了各种2D迭代器形状的迭代器半径和邻域大小之间的关系。

邻域迭代器在构造后通过调用 GetRadius() 方法。其他一些方法提供了有关迭代器及其底层图像的一些有用信息。

../_images/NeighborhoodIteratorFig2.png

Fig. 15 显示了几种可能的2D邻域迭代器形状以及它们的半径和大小。邻域像素可以通过其整数索引(顶部)或距中心的偏移量(底部)来取消参考。每个迭代器的中心像素都有阴影。

  • SizeType GetRadius() 将邻域的ND半径返回为 itk::Size
  • const ImageType *GetImagePointer() 返回迭代器引用的图像的指针。
  • unsigned long Size() 返回邻域的像素数大小。

邻域迭代器接口扩展了常规的ITK迭代器接口,用于设置和获取像素值。取消引用像素的一种方法是将邻域视为一个线性数组,其中每个像素都有一个唯一的整数索引。数组中像素的索引是通过从邻域的左上角沿图像维度增加最快的方向递增来确定的:第一列,然后是行,然后是切片,依此类推。如图所示 [fig:NeighborhoodIteratorFig2] ,则在每个像素的顶部显示唯一的整数索引。中心像素始终位于合适的位置 n/2 ,在哪里 n 是数组的大小。

  • PixelType GetPixel(const unsigned int i) 返回邻域位置的像素值 i
  • void SetPixel(const unsigned int i, PixelType p) 设置位置的像素值 ip

考虑邻域中的像素位置的另一种方法是将其视为距邻域中心的ND偏移量。控件的左上角 3x3x3 例如,邻域可以用偏移量来描述 (-1, -1, -1) 。同一邻域的右下角和右后角的偏移量 (1, 1, 1) 。如图所示 [fig:NeighborhoodIteratorFig2] 距中心的偏移量显示在每个邻域像素的底部。

  • PixelType GetPixel(const OffsetType &o) 获取位置偏移处的像素值 o 从社区中心来的。
  • void SetPixel(const OffsetType &o, PixelType p) 设置位置偏移处的值 o 从邻里中心到价值 p

邻域迭代器还提供了设置和获取邻域中心的值的快捷方式。

  • PixelType GetCenterPixel() 获取位于邻域中心的值。
  • void SetCenterPixel(PixelType p) Sets the value at the center of the neighborhood to the value p

还有另一种设置和获取像素值的快捷方式,这些像素值位于沿其中一个图像轴距离邻域中心一定整数距离的位置。

  • PixelType GetNext(unsigned int d) 获取正方向上与邻域中心直接相邻的值 d 轴心。
  • void SetNext(unsigned int d, PixelType p) 沿正方向设置与邻域中心紧邻的值 d 将轴设置为值 p
  • PixelType GetPrevious(unsigned int d) 获取在负方向上与邻域中心直接相邻的值 d 轴心。
  • void SetPrevious(unsigned int d, PixelType p) 沿负向设置与邻域中心紧邻的值 d 将轴设置为值 p
  • PixelType GetNext(unsigned int d, unsigned int s) 获取定位的像素值 s 从邻域中心沿 d 轴心。
  • void SetNext(unsigned int d, unsigned int s, PixelType p) 设置定位的像素值 s 从邻域中心沿 d 轴到值 p
  • PixelType GetPrevious(unsigned int d, unsigned int s) 获取定位的像素值 s 从邻域中心沿 d 轴心。
  • void SetPrevious(unsigned int d, unsigned int s, PixelType p) 设置定位的像素值 s 从邻域中心沿 d 轴到值 p

还可以使用常规的ITK邻域对象一次从迭代器中提取或设置所有邻域值。这在算法中可能很有用,该算法在邻域中执行特别大量的计算,否则将需要多次取消对相同像素的引用。

  • NeighborhoodType GetNeighborhood() 返回一个 itk::Neighborhood 具有与邻域迭代器相同的大小和形状,并包含迭代器位置的所有值。
  • void SetNeighborhood(NeighborhoodType &N) 将邻域中迭代器位置的所有值设置为包含在邻域中的值 N ,它的大小和形状必须与迭代器相同。

定义了几种方法来提供有关邻居的信息。

  • IndexType GetIndex() 返回邻域迭代器中心像素的图像索引。
  • IndexType GetIndex(OffsetType o) 返回偏移量处像素的图像索引 o 从社区中心来的。
  • IndexType GetIndex(unsigned int i) 返回数组位置的像素的图像索引 i
  • OffsetType GetOffset(unsigned int i) 返回距数组位置像素邻域中心的偏移量 i
  • unsigned long GetNeighborhoodIndex(OffsetType o) 返回偏移量处像素的数组位置 o 从社区中心来的。
  • std::slice GetSlice(unsigned int n) 返回一个 std::slice 沿轴线通过迭代器邻域 n

在靠近图像边界的邻域中进行基于邻域的计算可能需要落入该边界之外的数据。图中的迭代器 [fig:NeighborhoodIteratorFig1] 例如,位于边界像素的中心,因此它的三个邻居实际上不存在于图像中。当邻域的范围落在图像之外时,根据规则提供缺失邻域的像素值,通常选择该规则来满足算法的数值要求。用于提供越界值的规则称为 boundary condition

ITK邻域迭代器自动检测边界外的解引用,并根据边界条件返回值。边界条件类型由迭代器的第二个可选模板参数指定。默认情况下,邻域迭代器使用诺伊曼条件,其中跨越边界的一阶导数为零。Neumann规则只是将最近的边界内像素值返回到所请求的边界外位置。在ITK工具包中可以找到其他几个常见的边界条件。它们包括从数据集的另一侧返回像素值的周期条件,在处理周期数据(如傅里叶变换)时非常有用,以及返回设置值的常量值条件 v 对于所有超出边界的像素取消引用。常量值条件等效于用值填充图像 v

边界检查是一项计算开销很大的操作,因为每次递增迭代器时都会进行边界检查。为了提高效率,当邻域迭代器检测到不需要边界检查时,它会自动禁用边界检查。用户还可以显式禁用或启用边界检查。大多数基于邻域的算法可以通过巧妙地定义迭代区域来最小化边界检查的需要。这些技术在一节中进行了探讨 [sec:NeighborhoodExample3] 。

  • void NeedToUseBoundaryConditionOn() 显式打开边界检查。应谨慎使用此方法,因为不必要地启用边界检查可能会导致性能显著下降。通常,您应该允许迭代器自动确定此设置。
  • void NeedToUseBoundaryConditionOff() 显式禁用边界检查。应谨慎使用此方法,因为在需要时禁用边界检查将导致超出边界的读取和未定义的结果。
  • void OverrideBoundaryCondition(BoundaryConditionType *b) 使用边界条件对象覆盖模板化的边界条件 b 取而代之的是。客体 b 在迭代器释放它之前不应将其删除。此方法可用于在运行时更改迭代器行为。
  • void ResetBoundaryCondition() 停止使用任何运行时指定的边界条件,并返回到使用模板参数中指定的条件。
  • void SetPixel(unsigned int i, PixelType p, bool status) 设置邻域数组位置的值 i 至价值 p 。如果该职位 i 是越界的, status 设为 false ,否则 status 设为 true

以下各节描述了两个ITK邻域迭代器类, itk::NeighborhoodIteratoritk::ShapedNeighborhoodIterator 。每个都有常量版本和非常量版本。赋形迭代器是对标准NeighborhoodIterator的改进,它支持任意形状(非直线)的邻域。

NeighborhoodIterator

ITK中的标准邻域迭代器类是 itk::NeighborhoodIterator 。连同它的 const 版本, itk::ConstNeighborhoodIterator ,它实现了上述完整的API。本节提供几个示例来说明NeighborhoodIterator的用法。

  • 基本邻域技术:边缘检测。请参见示例 NeighborhoodIterators1.cxx
  • 卷积滤波:Sobel算子。请参见示例 NeighborhoodIterators2.cxx
  • 优化迭代速度。请参见示例 NeighborhoodIterators3.cxx
  • 可分离卷积:高斯滤波。请参见示例 NeighborhoodIterators4.cxx
  • 随机访问迭代:请参阅示例 NeighborhoodIterators6.cxx

ShapedNeighborhoodIterator

本节介绍邻域迭代器的一个变体,称为 shaped 邻域迭代器。形状邻域的定义类似于位掩码,或者 stencil ,关闭或打开法线邻域迭代器的直线邻域中的不同偏移量以创建图案。非活动位置(不在模板中的位置)在迭代期间不会更新,并且它们的值无法读取或写入。成形迭代器是在类中实现的 itk::ShapedNeighborhoodIterator ,它是的子类 itk::NeighborhoodIterator 。一个常量版本, itk::ConstShapedNeighborhoodIterator ,也是可用的。

与常规邻域迭代器一样,成形邻域迭代器必须使用ND RADIUS对象进行初始化,但成形迭代器的邻域半径仅定义 possible 邻里。然后可以激活或停用任意数量的可能邻居。整形邻域迭代器定义了用于激活邻居的API。当相对于邻域中心定义的邻域位置被激活时,它被放置在 active list 然后成为模板的一部分。迭代器可以在任何时候通过在活动列表中添加或移除偏移量来“重塑”。

  • void ActivateOffset(OffsetType &o) 包括偏移量 o 在活跃的邻居位置的模板中。偏移量相对于邻域中心。
  • void DeactivateOffset(OffsetType &o) 删除偏移量 o 从活跃的邻里位置的模板。偏移量相对于邻域中心。
  • void ClearActiveList() 通过清除活动列表来停用迭代器模板中的所有位置。
  • unsigned int GetActiveIndexListSize() 返回赋形迭代器模具中当前处于活动状态的像素位置数。

由于在赋形迭代器中对邻域的定义不那么严格,因此像素访问方法集受到限制。只有 GetPixel()SetPixel() 方法是可用的,在非活动的邻域偏移上调用这些方法将返回未定义的结果。

对于遍历邻域中所有像素偏移量的常见情况,Shape迭代器类提供了遍历其模板中的活动偏移量的迭代器。这 stencil iterator 可以递增或递减,并定义 Get()Set() 用来读写附近的价值观。

  • ShapedNeighborhoodIterator::Iterator Begin() 通过指向模板中第一个有效位置的定形迭代器模具返回常量或非常数迭代器。
  • ShapedNeighborhoodIterator::Iterator End() 通过指向的定形迭代器模板返回常量或非常量迭代器 one position past 模具中的最后一个有效位置。

成形邻域迭代器的功能和接口最好用例子来描述。我们将使用ShapeNeighborhoodIterator来实现一些二值图像形态算法。下面的例子实现了侵蚀和膨胀。

有关成形邻域的形态运算,另请参阅示例 ShapedNeighborhoodIterators1.cxxShapedNeighborhoodIterators2.cxx