⚠️ 这是一个非官方翻译网站,与 ImageMagick Studio LLC 无关。准确信息请参阅原文(https://usage.imagemagick.org/morphology/index.html)。

ImageMagick 示例——形状的形态学

ImageMagick 示例前言与索引
形态学简介

形态学以各种方式根据某像素周围其他像素的“邻域”来修改图像。这反过来可以带来种类繁多的效果,从形状的扩张与收缩(膨胀/腐蚀),到距边缘的距离,再到细化为骨架或中轴线。甚至更古老的“卷积”技术(提供模糊与锐化效果,见下一节),从某种意义上说也是一种形态学方法。本质上,形态学是用于修改、判定和发现图像中物体形状的方法。


形态学简介

形态学最初是作为一种方法而发展起来的,用以清理和研究图像中形状的结构。其工作方式是以各种方式将图像中的每个像素与其邻居进行比较,从而添加或移除、变亮或变暗该像素。将其应用于整幅图像,或许还要反复应用,就能找到并/或移除、修改特定的形状。举例来说,如果某个像素是白色的,并且完全被其他白色像素包围,那么这个像素显然不在图像的边缘上。你也许想把这个像素变成黑色,从而只留下处于边缘的像素。这种方法被称为“'EdgeIn'”(见下文)。整个过程实际上取决于“结构元素”或“核”的定义,它规定了对于每种具体的形态学方法,哪些像素应被归类为“邻居”。这个“邻域”具体应该是什么大小和形状,通常取决于你想要实现的目标,或者你在图像中具体要查找的内容。以下是一些已转换为图像的各种核的示例(使用一个名为“kernel2image"”)的特殊脚本,展示了围绕中心像素“原点”的一些“邻域”。

[IM 输出] [IM 输出] [IM 输出] [IM 输出] [IM 输出] [IM 输出] [IM 输出] [IM 输出]

这些图像已被放大,以突出显示“核”的各个元素,正如你所见,典型的核往往非常小。实际上,上面显示的“Disk”核实际上是“[原始 Disk 核图像]”,而这已经是上面展示的最大的核之一。然而“核”其实并不是真正的图像,只是一个浮点数值数组,其中指定了一个元素作为核的“原点”。这个特殊的元素是将被定义的邻域所“影响”的像素所在的位置,通常(但不总是)是对称核的中心像素。请注意,这些只是可能的邻域的部分示例。有些核可以做得更大,通常是通过增大该核特有的“半径”参数来实现,而其他一些用于特殊用途的核则是固定大小的。对于像前两个这样的简单核,可以重复(迭代)应用形态学方法来增加核的有效“大小”,从而影响距离“原点”(如图所示)更远的更多像素。然而这并不总是有效,有时还会产生意想不到的结果,不过它有时比直接使用更大的核要快,但这也并非绝对如此。“结构元素”,或者说形态学研究论文中所称的核的“SE”,其最终的大小和形状很重要,它是定位并增强或删除比该形状更大或更小的图像元素的一种手段。这正是形态学作为分拣图像中各种元素的手段而极其强大的原因所在。然而核越大,形态学方法所需的时间就越长,因此最好保持核的尺寸较小。上面展示的所有核,除最后一个外,实际上都是有形状的。透明的部分并不属于核所定义的“邻域”。也就是说,它们没有任何有效值,也不会参与任何形态学计算。请注意倒数第二个核“Corner #0”不仅有“开启”的值,还有“关闭”的值,作为其“形状”的一部分。这两种值,连同那些透明(不属于形状)的值,对于击中与错失及相关方法(见下文)都很重要。这个特定的核只是一系列用于定位图像中二值形状“角”像素的核中的第一个。上面展示的最后一个“核”则完全定义在一个较大的矩形(方形)区域上。此外,与其他仅使用值 1(白色)、0(黑色)或特殊“未定义”值的核不同,这个核的值实际上从边缘处的接近零(近乎黑色)逐渐变化到中心处的最大值(纯白色)。然而这样的核也可以使用负值,甚至是远超其他核正常范围的很大的值。请记住,核实际上只是一个数值数组,这些值可以是任意数值,而不仅限于 0 到 1 的范围。这类核在“图像卷积操作”中尤为重要,这是一种比形态学本身历史更悠久的特殊方法。因此 IM 内置了大量此类“命名”核。这将在 IM 示例的下一节“图像的卷积”中详细讨论。现在,正如我前面提到的,核其实并不是真正的图像。它们只是一个浮点数值数组。稍后我们将查看这些实际的值(上面为便于查看已将其转换为图像)。

形态学运算符

“-morphology”运算符相当复杂,因为它为用户提供了对其行为的大量控制。

  -morphology {_method_}[:{_iterations_}]   {_kernel_}[:[_k_args_}]

请注意,你至少需要提供两项内容:形态学“方法”,告诉运算符你想对图像应用哪种类型的操作;以及“核”,指定哪些“相邻”像素应该影响最终结果。两者同样重要,且都可能产生深远的影响。你可以使用“-list morphology”获取可用方法的列表。可以用“-list kernel”查看 IM 中已包含的内置核列表。我们稍后会依次介绍各种方法,以及这些方法可能使用的核。 | _“-morphology”运算符(基本方法)以及初始的一组核,是我本人在 2009 年 12 月至 2010 年 1 月于中国度假期间添加到 ImageMagick 6.5.9-0 版本中的。

不过,也可以使用更古老、密切相关的“-convolve”方法来执行简化的“方形”核形态学操作。请参阅下文的替代的基本形态学技术。

_

基本内置形状核

由于核是所有形态学方法共用的,且各种方法的结果在很大程度上取决于所选核的实际情况,我们首先来看看如何定义或选择要使用的核。已经为你预先定义好了一批不错的核,通常你并不需要超出这些范围。你可以使用“-list kernel”获取预定义内置核的列表。所有核都有特定的大小,通常是每边像素数为奇数的正方形,其中心即为核的“原点”。不过如你所见,“-morphology”运算符并不受此限制约束。内置核最常用的 k_argument(通常也是给出的第一个参数)是“半径”。它定义了核那种典型的奇数大小方形邻域将会有多大。最终的核大小通常是半径的两倍再加一(用于中心像素)。也就是说,“半径”为“2”将创建一个 5×5 像素的方形核。虽然“半径”通常定义了最终核的大小,从而决定了形态学操作在图像上的整体速度,但它可能并非最重要的因素,尤其对于卷积核而言,其数值对结果的影响要大于核的大小。如果“半径”设置为 0,或未定义,“半径”将根据所涉及的核自动默认为某个合理或最常用的值。 [IM 输出]

Unity

这是一个特殊的核,专门用于你需要一个“空操作”核的情况。大多数使用此核的形态学方法要么重新生成原始图像,要么生成空白结果。该核没有参数。这个完全相同的单元素核也可以使用“Disk:0.5”生成,它同样允许你在生成核时指定一个缩放参数。[IM 输出]

Diamond

最简约、但也许并非最简单的核是内置核“Diamond”。查看这个基本核的一种简单方式是,对一幅在黑色背景上只有单个白色像素的图像使用Dilate形态学方法。这基本上会将单个像素扩展为核邻域的“形状”。以下是使用最简约的内置核“Diamond”执行“Dilate”,并将结果放大以使其更易于查看的结果。

  magick xc: -bordercolor black -border 5x5 pixel.gif
  magick pixel.gif -scale 800% pixel_mag.gif
  magick pixel.gif -morphology Dilate Diamond \
                    -scale 800% k_diamond.gif

[IM 输出] [IM 输出]

| 请记住,本 IM 示例这一区域中显示的所有核图像结果都已被放大,以便你能看清各个像素。实际上,我们展示的所有核和结果都非常小,本来也应该如此。在这个例子中,被膨胀的图像只有 11×11 像素大小,并已放大 8 倍以供显示。
这实际上是一个用于形态学操作相当不错的核,基本定义了最简约实用的邻域:原始像素,加上与之直接接触的四个像素。这种类型的核还有一个名字叫“Z4”结构元素。它看起来相当像一个小小的“加号”。菱形只有在半径增大时才会变得明显。这个核可选的 k_arg 可以取两个值,如下所示……

     Diamond[:{_radius_}[,{_scale_}]]

对于所有的形状核而言,最重要的参数是 半径,正如前面提到的,它是一个整数,表示从中心“原点”到最近边缘的距离。因此,最终的“Diamond”核是一个包含菱形的正方形(2 倍半径加 1)。以下是使用更大的 半径 生成大型核的结果。

  for r in 1 2 3 4; do
    magick pixel.gif -morphology Dilate Diamond:$r -scale 800% k_diamond:$r.gif
  done

[IM 输出]
Diamond:1
(默认) | [IM 输出]
Diamond:2 | [IM 输出]
Diamond:3 | [IM 输出]
Diamond:4

另一个 k_argument 是 scale,其默认值为 1.0。通常用于改变核用来形成形状的实际数值。这一般只对特殊方法(例如Convolve和灰度形态学)才重要。[IM 输出]

Square

“Square”是形态学中最常用的核,因为使用其他替代技术最容易应用它。不过它并不是最简约的核(参见上面的“Diamond”)。默认情况下,“Square”核在“中心”周围使用 3x3 像素邻域。 |

  magick pixel.gif  -morphology Dilate Square -scale 800% k_square.gif

[IM 输出]
基本上这意味着原始像素的全部 8 个邻居都将被归类为该像素邻域的一部分。因此,它是一个用于像素平均,或将某个形状扩大/缩小一个像素的良好核。与所有形状核一样,它采用与上面Diamond 核所示相同的 k_arguments,其中第一个参数 半径 最为重要。

  for r in 1 2 3 4; do
    magick pixel.gif  -morphology Dilate Square:$r -scale 800% k_square:$r.gif
  done

[IM 输出]
Square:1
(默认) | [IM 输出]
Square:2 | [IM 输出]
Square:3 | [IM 输出]
Square:4

该核的默认值(半径=1)如前所述是一个 3×3 的正方形,通常被称为“Z8”结构元素(因其涉及的直接邻居数量而得名)。[IM 输出]

Octagon

'Octagon' 核是一种 8 边形状的核。它专为匹配'Octagonal Distance Metric'而设计。不要将两者混淆,因为它们是截然不同的核。下面是较小半径下生成的核……

[IM 输出] [IM 输出] [IM 输出] [IM 输出] [IM 输出]

注意在半径为 1 时,得到的核与"Diamond"核相同。正因如此,八边形的默认大小为半径'2'。 | _从此处开始我将使用专门的kernel2image脚本来生成核的图像,因为它们比使用原始的"dilate-scale"方法(如上)更清晰。不过要记住,核通常都非常小,尽管 Octagon 和 Disk 核(见下文)在特定用途下可能会变得非常大。


|
"Octagon"核是在 IM v6.6.9-4 中加入的,与"Octagonal"距离核一同引入。_
[IM 输出]

Disk

'Disk'核正如你所预期的那样,是一种圆形。常用于需要非常大的形态学核的场合。不过要注意,它是一种带锯齿的布尔圆形。然而 disk 的_半径_参数可以是浮点数,这使得在较小半径下也能生成相当多样的形状。

[IM 输出] [IM 输出] [IM 输出] [IM 输出] [IM 输出] [IM 输出]
[IM 输出] [IM 输出] [IM 输出] [IM 输出] [IM 输出]

'Disk:4.3'核是默认值,我认为这是第一个真正的圆盘形状。这种大小或更大的圆盘特别适合对图像形状进行整体的圆化和平滑处理。包含该圆盘的核的最终大小是将_半径_值向下取整后乘以 2 再加 1。因此默认的'Disk:4.3'核的核半径为 4,最终核大小为 4 乘以 2 再加 1,生成一个 9×9 的核来容纳圆盘形状。注意,小于 1(但不为 0)的值总是会生成单像素核,不过那没什么用处。除此之外,该核在多数情况下生成的核也可以用前面介绍的核类型生成。只有当半径变大时,真正的圆盘形状的核才会开始显现。最重要的一点是,带小数半径的圆盘比使用整数半径的效果要好得多。一般建议加上约 0.3 到 0.5 的小数部分,以避免在圆盘边缘生成看起来不协调的单个像素。[IM 输出]

Plus

'Plus'核实际上与其他形态学形状核略有不同,它设计用来表示一个特定的"形状",而不是像素周围简单的"邻域"。对该核使用更大的_半径_并不会简单地增大核的大小,而是会加长最终加号图形的臂长。不过臂的粗细不会增加。

[IM 输出] [IM 输出] [IM 输出] [IM 输出]

'Plus'核的默认大小为半径 2,在中心"原点"周围生成 2 个像素长的"臂"。'Plus:1'核恰好与默认的'Diamond'核相同。注意'Plus'核通常不用于常规的形态学方法,应避免将其用于此类用途。不过,如果你想要在图像中查找并突出显示单个点,它就非常有用,比如我后面在展示骨架信息时所做的那样。基本上它提供了一种绘制符号的方法,而无需确切知道各个"点"在图像中的具体位置。 [IM 输出]

Cross

'Cross'核与'Plus'核完全相同,只是旋转了 45 度。它同样只是一种适合用来扩展像素以标记各种点位置的特殊核形状。

[IM 输出] [IM 输出] [IM 输出] [IM 输出]

[IM 输出]

Ring

'Ring'核与'Plus'核一样,也是专为标记像素、在图像上生成图案而设计的特殊"形状"核。不过它不只取一个半径,而是可以取两个半径,其定义方式与Disk 核相同……

     Ring[:{_radius1_}[,{_radius2_}[,{_scale_}]]]

它的作用是将落在两个半径之间的所有像素置"开",与给定的两个半径的先后顺序无关。如果不给出半径,默认值为'2.5'和'3.5',即生成'Ring:2.5,3.5',看起来像一个中空的八边形环,非常适合圈出某个像素。通过改变两个半径的值,你可以创建任意大小和粗细的"环"。半径的微小变化会在边缘增加或减少极少量的像素,从而让你可以精细控制环的外观。如果两个半径之间相差在 1 个像素以内,你还可以生成一个由稀疏分散的点组成的环,这对于特殊用途的邻域很有用。较小的半径也会生成类似方框的核,同样很有用处。如果不给出第二个半径,它将默认为'0.5',这实际上定义了一个完整的圆盘,但不包含中心的"原点"像素。换句话说,就是排除"原点"像素的圆盘核。以下是可以生成的多种'Ring'核示例……

[IM 输出] [IM 输出] [IM 输出] [IM 输出] [IM 输出] [IM 输出] [IM 输出] [IM 输出] [IM 输出] [IM 输出] [IM 输出] [IM 输出] [IM 输出] [IM 输出] [IM 输出] [IM 输出] [IM 输出] [IM 输出] [IM 输出] [IM 输出] [IM 输出]

可以看到,通过仔细调整两个半径,你有非常多的可能性,这是在图像中标示感兴趣位置的一种好方法。[IM 输出]

Rectangle

'Rectangle'核与上文的'Square'核密切相关,默认情况下生成同样的 3×3 方形核。但它不是使用简单的半径参数,而是可以给出一个_几何_参数,以指定所需矩形核的确切大小。以下是一些规格及其生成核的图像。

[IM 输出] [IM 输出] [IM 输出] [IM 输出]
[IM 输出] [IM 输出] [IM 输出] [IM 输出]

默认情况下,核会尝试将邻域的"原点"设置在核的正"中心"。但对于偶数尺寸的矩形,它会视情况选取中心上方和/或左方紧邻的点。不过你也可以自行指定偏离中心的原点。这种核也非常适合定义长的水平和垂直线条,让你能够在图像中搜索此类对象。后文将有更多介绍。目前你还不能为矩形指定_比例_因子,其所有核值都只会被设为 1.0。

用户自定义 DIY 核

你并不局限于只使用内置核,也可以指定你自己的核,给出你想要核使用的确切数值……

   "[{_geometry_}:] {_value_}, {_value_}, {_value_},....."

'几何'规格与前面'Rectangle'核的参数基本相同。它给出核的大小,以及可选的邻域"原点"的"偏移"。如果只给出一个数字,则会将其作为方形核的尺寸。 记住几何值并不是一个"半径"参数,而是核的整体大小。
如果没有指定_几何_或':',则使用的是"旧式"规格。系统会生成一个大小足以容纳所有给定数值的奇数尺寸方形核。不建议这样做,此方式仅为向后兼容旧版本的 ImageMagick 而保留。在':'(在_几何_规格之后是必须的)之后,你需要提供以逗号和/或空白分隔的_宽度_×_高度_个浮点数值。特殊值'NaN'(意为"非数字")或单独的'-',可用于指定核中该点不属于形态学邻域。例如,下面是一个宽度为 3 的方形核规格,可用于对单像素图像进行卷积模糊。
  magick pixel.gif   -morphology Convolve \
            "3:  0.3,0.6,0.3   0.6,1.0,0.6   0.3,0.6,0.3" \
                                         -scale 800%  k_user_3.gif

[IM 输出]
对于单个像素,Convolve的效果几乎与Dilate相同,不过Convolve使用核的数值,通过扩展并累加相邻的值。而Dilate则通常使用开/关(布尔)形状,取所有邻居中的最大值。不过对单个孤立像素应用布尔形状时,会得到相同的结果。注意你可以在输入字符串中添加额外的空格(甚至换行),以分隔矩形核定义中的各行。这里我定义了一个 5×3 的矩形区域,但使用特殊的'nan'(非数字)值截去四角,做出一个椭圆形状的核…… |

  magick pixel.gif   -morphology Dilate \
            "5x3: nan,1,1,1,nan   1,1,1,1,1   nan,1,1,1,nan " \
                                    -scale 800%   k_user_5x3.gif

[IM 输出]
最后,这里是一个指定矩形邻域的示例,围绕"原点"形成一个"L"形。我使用'-'而不是'nan'来指定不属于核的部分。注意这个核的原点甚至不属于它自身的邻域,它可以位于核的矩形边界内的任何位置。 |

  magick pixel.gif   -morphology Dilate \
            "2x3+1+1:   1,-   1,-   1,1   "  -scale 800% k_lman.gif

[IM 输出]
由此可见,用户核规格非常灵活,可以让你指定几乎任何类型的核,无论是带有大量小数的卷积核,还是带有"不属于邻域"元素的形态学形状核。

将图像转换为用户核

为了更方便地生成 DIY 核,你可以使用"image2kernel"脚本来创建核。例如,这里我将一面小旗()转换为用户核数据文件("flag_kernel.dat"),然后用它对一幅带有几个像素的图像进行膨胀。

  magick -size 80x80 xc:black -fill white \
          -draw 'point 20,15 point 55,30 point 40,60'  points_pixels.gif
  image2kernel -qgm flag.gif flag_kernel.dat

  magick points_pixels.gif \
             -morphology Dilate @flag_kernel.dat \
           flagged_points.gif

[IM 输出] [IM 输出] [IM 输出]

另请参见下文的生成核的图像,它可以生成核的图像(或生成放大的美化版本)。这项技巧在绘制符号的替代方法中也有讨论。

迭代(重复)形态学运算

如你所见,你可以生成更大的核,从而在更大的邻域上应用形态学运算。然而在大多数情况下,比使用更大核更快的替代方案,是简单地重复(迭代或循环)该形态学运算符多次。这意味着该运算符的效果会被延伸得更远,与使用更大的核基本效果相同,但不会带来使用更大核所增加的计算开销。例如,这是使用'Diamond:3'核对单个像素进行的膨胀…… |

  magick pixel.gif  -morphology Dilate Diamond:3 -scale 800% k_diamond_x3.gif

[IM 输出]
但你也可以通过将较小的'Diamond'核(半径 1)使用三次来达到同样的效果…… |

  magick pixel.gif  -morphology Dilate Diamond \
                     -morphology Dilate Diamond \
                     -morphology Dilate Diamond  -scale 800% k_diamond_x3.gif

[IM 输出]
你仍然只是使用了一个非常小的 3×3 核,但通过重复基本的形态学运算三次,产生了与使用更大核相同的效果。事实上,像这样重复使用小核实际上比使用大得多的核要快不少。 | _一个大的'Diamond:3'核对图像中每个像素需要处理 81 个元素。而将较小的'Diamond'核重复 3 次,则每个像素需要处理 3×9,即 27 个核元素。在这种情况下速度快了 3 倍。

在这种情况下速度提升不算太大,但随着核尺寸的增大,节省的效果会大得多。_
由于重复形态学运算非常常见,与其多次重复该运算,你可以直接让 IM 循环或迭代该运算指定的次数。 |

  magick pixel.gif   -morphology Dilate:3 Diamond -scale 800%  k_diamond_3.gif

[IM 输出]
注意这与第一个示例的区别。这里发生的仅仅是把':3'从'Diamond'核的半径,变成了'Dilate'方法要使用的次数。使用_迭代_来增大有效邻域,对大多数"圆形"或"凸"形核(例如'Square'和'Diamond')都是有效的。但它并不适用于所有核类型。例如,对于非凸核,比如'Plus'(其形状并非"凹"形),会产生非常异常的结果。举例来说,这与从'Plus'(半径 2)变为两倍大小的'Plus:4'核并不相同…… |

  magick pixel.gif   -morphology Dilate:2 Plus  -scale 800%  k_plus_2.gif

[IM 输出]
注意,如果_迭代_次数使用'0',形态学运算将不执行任何操作。这是一种在不想让运算符执行任何操作、但又不想将其从命令行中移除时,"关闭"该运算符的实用方法。关于零迭代次数的另一种用法,参见下文的详细输出显示。使用特殊值'-1',会重复该运算直到图像中不再出现变化为止,也就是图像达到"收敛"状态。不过这样做是有风险的,在某些情况下可能导致运行时间非常长。例如对于'Dilate'这样的运算,它会简单地重复膨胀,直到整幅图像完全被白色填满。基本上会产生一种失控的"泛滥填充"(见下面的示例)。为产生更大邻域效果而对'Disk'核进行迭代,通常也不推荐。这是因为,随着半径增大,'Disk'核会变成更精确的圆盘形状,而迭代圆盘核则不仅会放大形状本身,还会放大核的误差(非圆盘形状)。因此,使用更大的半径(速度较慢)可能比迭代该运算(会产生形状更扭曲的圆盘)更好。不过,当'Disk'半径变得非常大时,结合使用半径和多次迭代,可能会产生更快、但仍可接受的结果。需要谨慎并针对具体情况进行一些实验。

变化情况的详细输出

如果你想查看迭代(重复)形态学运算的结果,可以设置"-define debug=True"选项,它会启用详细操作控制。随着形态学运算符的迭代,它会报告不断递增的迭代计数,以及每一步迭代中图像有多少像素发生了变化。输出会发送到标准错误,因此你仍然可以通过管道传递图像结果……例如,让我们用较大的'Octagon'核对单像素图像进行'Dilate',直到整幅图像被白色填满、不再有任何变化为止。记住迭代次数限制为'-1'意味着无限迭代,直到不再出现变化。 | |

  MAGICK_THREAD_LIMIT=1 \
    magick pixel.gif -define debug=true -morphology Dilate:-1 Octagon \
            -scale 800% iterate_infinite.gif

[IM 输出]
| [IM 文本]
注意每次迭代中发生变化的数量。最初有 20 个像素从黑色转变为白色。接着下一次迭代又有 48 个,依此类推。这个数字通常会随着结果形状边缘的扩大而增长,但随后随着形状触及图像边界又开始缩小。在第四次膨胀时,最后 4 个像素(位于图像的角落)被填满。在最后一次膨胀(第 5 次迭代)中,图像已经完全被填满,因此没有对任何像素进行任何更改。由于没有发生任何变化,形态学运算自动中止,给出该阶段运算的最终变化数量。使用'-1'的无限迭代确实有一个内部限制,目前设为图像宽度或高度中的最大值。这样做是为了防止 ImageMagick 陷入永不终止的循环。不过通常情况下,运算会在达到该内部限制之前很久就已完成。有些形态学方法实际上是用更简单、更基础的方法定义的。例如'Smooth'方法就是这样一种复合方法。使用该方法时生成的"-define"输出,展示了构成其处理过程的多个内部步骤。

  MAGICK_THREAD_LIMIT=1 \
    magick man.gif -define debug=true -morphology Smooth:2 Diamond null:

[IM 文本]

如果仔细观察,可以看到'Smooth'实际上迭代了 4 个更基础的方法,因此内部对图像处理了 8 次以完成所请求的运算。每一行由以下内容组成……

Smooth:_i_._s_ 这显示了应用于图像的高级形态学方法,以及 IM 正在处理的迭代计数'i'和基础"阶段"'s'。对于'Smooth'方法,第一个数字始终为'1',因为用户给出的"迭代次数"是在更底层的基础方法中应用的。在其他方法中,用户给出的迭代次数可能会在这个更高层级应用,而不是在更底层应用。第二个"阶段"数字是正在应用的基础"阶段"计数。'Smooth'本身由四个这样的阶段组成,因为它实现了'Open'和'Close'复合方法。 Dilate*:_i_._k_ 这是正在应用的基元方法。第一个数字 i 同样是用户给定的迭代次数(如果在此处应用)。第二个数字 'k ' 是该基元形态学方法所应用的核。由于只有一个核,所以此处始终为零。(参见下文的多核处理)'*' 表示该核在被形态学基元应用之前被反射(或围绕原点旋转 180 度)了。这是某些复合形态学方法所必需的,在本例中 'Close' 方法在使用 'Dilate' 和 'Erode' 基元方法时始终使用反射核。 #6 => Changed 311 Total 637 这是将形态学基元应用于图像的结果报告。'hash' 数字是图像被基元遍历次数的递增计数。这能让你很好地了解一个复合形态学运算符的计算强度有多大。接着你会得到该次遍历中以某种方式发生改变的实际像素数。如果这是针对该特定基元和核的多次迭代中的最后一次,还会输出像素修改的总计数。不过这并不反映从头到尾总共改变的像素总数,只反映该特定基元、核操作的底层迭代所造成的改变。有些像素可能会被某些形态学基元多次改变。

从上面你可以看出,IM 内部可能会应用多达四层处理来完整处理某个给定的形态学方法。不过通常大多数这些循环都只会应用一次。 | _警告:在运行于现代多核机器的多线程环境中的机器上,发生改变的像素数可能不正确!只有在单线程环境中执行时,才能保证其准确性。我将其归类为一个 bug,但不是一个致命的。

如果这成为问题,请确保在运行该特定的 ImageMagick 实例时,将环境变量 "MAGICK_THREAD_LIMIT" 设置为 '1',就像我在上面最后两个示例中所做的那样。

从 IM v6.8.4 开始,你不再需要 "MAGICK_THREAD_LIMIT" 环境设置,因为计数在多线程环境中已经能被正确处理了。

_

显示生成的核(用于调试目的)

如果你想实际查看用于定义某个已生成核的数值,可以定义一个特殊设置……

    -define morphology:showkernel=1
    -define convolve:showkernel=1

上述任一定义都会使 IM 在核被完全处理、准备好供使用之后,将该核的全部相关信息输出到「标准错误」。(参见卷积核缩放)。例如,下面是内置的 'Disk' 核的实际数值……

  magick xc: -define morphology:showkernel=1 -morphology Dilate:0 Disk null:

[IM 文本]

请注意,由于我只是想展示核本身,所以完全不关心图像处理结果。因此我将形态学的 '迭代次数 ' 设为 '0'(不做任何处理),并使用 null: 输出文件格式丢弃任何图像结果。上面出现的特殊浮点值 'nan' 与在输入用户自定义核时的含义相同。它表示「不是数字(Not A Number)」,用于标记核中不属于邻域的部分。所有形态学操作都会忽略这些值。这里再举一个例子,这次是 'Comet' 卷积核。

  magick xc: -define morphology:showkernel=1 -morphology Dilate:0 Comet:0x2  null:

[IM 文本]

这实际上是半条一维高斯曲线(sigma 为 1.0),可以提供一种从 ImageMagick 中提取这种曲线的好方法。另外请注意,这个特定核的「原点」(其影响的像素)是偏离中心的(位于 +0+0),这并不常见。输出中数值的大小和间距可以通过特殊的精度操作控制来控制。该功能是在形态学运算符添加到 IM 时大约同一时间添加的。例如,下面重复上一个示例,但使用 "-precision" 将有效数字位数从默认的 6 位限制为 3 位。

  magick xc: -define morphology:showkernel=1 -precision 3 \
          -morphology Dilate:0 Comet:0x2  null:

[IM 文本]

| "-precision" 选项是在形态学开发周期中被添加到 ImageMagick 6.5.9-1 版本的。因此,如果形态学功能可用,精度控制也应被视为可用。

生成核的图像

为了更方便地查看核,而不是在单像素图像上使用膨胀或卷积来看它会产生什么效果,我编写了一个名为 "kernel2image" 的专用脚本。该脚本提取精确的显示核输出,并将其转换为核的图像。"kernel2image" 脚本有许多选项,从输出核的原始图像(默认行为)到指定缩放比例、像素间距、蒙太奇拼接、标注乃至给生成的「核图像」上色都可以。这个脚本让查看和理解各种核变得容易得多,并被广泛用于生成这些示例页面中展示的核图像。例如,下面是我生成 "Octagon" 核图像的方法。 |

  kernel2image -10.1  -m "Octagon"  kernel_octagon.gif

[IM 输出]
特殊选项 '-10.1' 表示将所有像素缩放到 10 像素大小,同时在这些像素之间保留 1 像素的间隙。如果核被充分放大,核的「原点」会用一些绘制的圆圈标出。'-m' 则指定我要为图像创建一个蒙太奇拼接,附带提取出的 "Octagon" 核的标识标签和阴影效果。下面我为上面用过的 'L' 形用户自定义核生成一张「核图像」。 |

  kernel2image -20.2 -ml 'L-Shape'  "3: 1,-,-  1,-,-  1,1,- " kernel_lman.gif

[IM 输出]
如果你想从已有图像创建核,可以使用脚本 "image2kernel" 从图像创建核数据文件。该脚本通常接受灰度图像,但如果给定的是多色图像,图像的每个通道都会被转换为一个单独的核数据文件。下面我从一个小的旗帜图像( )创建用户核数据,然后使用 "kernel2image" 将该数据转换回放大后的「核图像」以供显示。 |

   image2kernel -qgm flag.gif  flag_kernel.dat
   kernel2image -6.1 -m  -ml "Flag"  @flag_kernel.dat  kernel_flag.gif

[IM 输出]
题外话:我本可以用另一个类似脚本 "enlarge_image" 更直接地生成小图像的「放大」版本,但那样展示的会是图像本身,而不是核数据 "flag_kernel.dat"。

多核列表处理

生成多个核

从 IM v6.6.2-0 开始,你可以指定多个核,它们会被依次应用到图像上。要指定多个核,只需将每个核定义用分号 ';' 分隔并依次追加即可。末尾的分号是可选的。例如,下面我定义一个特殊的核列表,其中包含一组可用于「模式匹配」角点像素的核。

     3: 0,0,- 0,1,1 -,1,-  ;
     3: -,0,0 1,1,1 -,1,-  ;
     3: -,1,- 1,1,0 -,0,0  ;
     3: -,1,- 0,1,1 0,0,-  ;

多余的分号(';')不影响使用,只要各核定义之间至少提供一个分号即可。任何核定义中的多余空白(包括换行符)也不影响使用。下面是该定义的显示核输出。

  magick xc: -define morphology:showkernel=1 -morphology Dilate:0 \
             " 3: 0,0,- 0,1,1 -,1,-  ;
               3: -,1,- 1,1,0 -,0,0  ;
               3: -,0,0 1,1,1 -,1,-  ;
               3: -,1,- 0,1,1 0,0,-  ; " null:

[IM 文本]

下面是使用专用的 "kernel2image" 脚本生成的这四个核的核图像。

   kernel2image -20.2 -ml '' -mt x1 \
             " 3: 0,0,- 0,1,1 -,1,-  ;
               3: -,1,- 1,1,0 -,0,0  ;
               3: -,0,0 1,1,0 -,1,-  ;
               3: -,1,- 0,1,1 0,0,-  ; "  kernel_multi.gif

[IM 文本]

现在这个定义实际上只包含一个核,它被展开成了一组绕原点每次旋转 90 度的 4 个核。题外话:这个定义几乎等同于特殊的 'Corners' 模式匹配核(见下文),区别在于它只限定于实际形状的角点,而不是任意的角点、背景或前景。

展开为旋转核列表

从 IM v6.2.2-0 开始,你可以要求 IM 使用三种特殊标志之一(无论是命名核还是用户自定义核),将单个核展开为一组旋转核列表。这三种特殊标志是……

'@ ' 以 45 度为增量循环旋转 3x3 核,生成最多 8 个旋转核组成的列表。(助记:'@' 是圆形的)
'> ' 以 90 度为增量旋转(仅限方形或线性核)。(助记:'>' 是直角形状)。
'< ' 同样生成 90 度旋转,但按「镜像」顺序排列(旋转角度为 0、180、-90、+90)。这种特殊的旋转展开形式对于诸如 'Thinning' 之类的形态学方法效果更好。(助记:'<' 是直角的镜像)。

例如,上面同样的核可以更简单地指定为……

    ' 3>:  0,0,-  0,1,1  -,1,- '

这定义了一个核,'>' 标志随后告知 IM 将其展开为一个 90 度旋转的列表。下面是生成的多核列表的图像

   kernel2image -20.2 -ml '' -mt x1 \
                '3>: 0,0,- 0,1,1 -,1,- '  kernel_rotated_list.gif

[IM 文本]

下面我将一个 3x3 核以「循环」的方式按 45 度旋转,将其展开为一个包含 8 个核的列表。

   kernel2image -20.2 -ml '' -mt x1 \
                '3@: -,1,- -,0,- 1,1,1 '  kernel_rotated_list2.gif

[IM 文本]

对于任何「单个」IM 内置命名核,你也可以在这些核的参数部分使用相同的标志来做同样的事。例如,下面我将一个对称的 'Blur' 核,使用 '>' 标志展开为一个 90 度旋转的列表。

   kernel2image -12.1 -n -ml ''   "Blur:0x1>"  blur_kernels.gif

[IM 文本]

请注意这里只生成了 2 个核,因为第三个核会与第一个核完全重复。这一点会被自动检测到,从而使旋转核的生成停止。不过,如果「原点」偏离中心,那么即便核「形状」相同,由于原点位置不同,仍会生成完整的 4 个旋转核序列。许多内置核定义会自动生成一个多核列表,因此你无需为此指定任何标志。也就是说,旋转展开也是「内置」于特定核定义之中的。这类核通常还提供原始单一核定义的「子类型」,以便你可以针对特定用途挑选特定的核。

多核结果合并:重新迭代还是合成

当你定义了多个核时,形态学方法还需要知道应如何合并由多个核生成的结果。这可以通过使用一个全局定义来控制……

    -define morphology:compose={_compose_method_}

大多数形态学方法的默认设置是 'None'。这意味着在使用给定的形态学方法应用完每个核之后,其结果图像应作为下一个核的处理源。也就是简单地「重新迭代」,或者说将应用一个核后得到的结果图像重复用于下一个核。例如,如果我使用 2 个互相旋转 90 度的 'Blur' 核进行卷积,会得到如下结果。

  magick pixel.gif  -morphology Convolve "Blur:0x1>" \
          -auto-level  blur_re-iterate.gif

[IM 输出] [IM 输出] [IM 输出] [IM 输出]

如你所见,两个核依次被应用于图像,因此每个核都基于前一个核的结果继续处理。也就是说,它以顺序方式对结果进行了'重复迭代',让下一个核继续处理上一个核的结果。这等同于分两步执行以下操作。

  magick pixel.gif -morphology Convolve "Blur:0x1" -auto-level blur_1.gif
  magick blur_1.gif -morphology Convolve "Blur:0x1+90" \
          -auto-level blur_re-iterate.gif

[IM 输出] [IM 输出] [IM 输出] [IM 输出] [IM 输出]

实际上这正是模糊运算符真正的工作原理,用以更快地生成图像模糊效果。参见高斯核与模糊核对比,其中更详尽地演示了这一用法。
通过将'{_compose_method_}'设置为除'None'以外的任何方法,该操作将不会被_重复迭代_。相反,每个核都将被应用于_原始图像_,然后使用指定的'{_compose_method_}'方法将所得到的多幅图像进行合成。例如,如果我使用'Lighten'合成方法来生成各独立结果的并集,我们会得到……

  magick pixel.gif -define morphology:compose=Lighten \
                     -morphology Convolve "Blur:0x1>" \
          -auto-level blur_union.gif

[IM 输出] [IM 输出] [IM 输出] [IM 输出]

这等同于执行……

  magick pixel.gif -morphology Convolve "Blur:0x1"  -auto-level blur_1.gif
  magick pixel.gif -morphology Convolve "Blur:0x1+90" -auto-level blur_2.gif
  magick blur_1.gif blur_2.gif -compose Lighten -composite \
          -auto-level blur_union.gif

[IM 输出] [IM 输出] [IM 输出]
[IM 输出] [IM 输出] [IM 输出]
[IM 输出] [IM 输出] [IM 输出]

如果你不确定形态学处理过程中IM实际在做什么,可以打开更改的详细输出。例如,这里是对每个核进行重复迭代时的详细输出……

  magick pixel.gif -define morphology:compose=None \
         -define debug=true -morphology Convolve "Blur:0x1>" null:

[IM 文本]

这里是各核结果的并集(Lighten合成)的详细输出……

  magick pixel.gif -define morphology:compose=Lighten \
         -define debug=true -morphology Convolve "Blur:0x1>" null:

[IM 文本]

这两者都清楚地展示了ImageMagick生成最终图像所做的处理。小数点后的数字表示每一步所应用的核编号。最后则说明了如何根据'morphology:compose'设置将各图像合成在一起。许多数学合成方法及其对应的集合论类型运算,同样也可以用于合并将各核分别应用于原始图像所得的结果。总而言之,该设置定义了多核列表中各个核将如何应用于给定图像。默认的合成值为'None',即简单地"重复迭代"结果,否则将根据给定的合成方法合并所有结果。


基本形态学方法

形态学方法是一种用于在图像内查找和分析物体形状的图像处理技术。包括扩张、收缩、定位特定形状等。它最初是针对二值(纯黑白)图像开发的,因此最常应用于包含简单黑白形状的阈值化图像。按照惯例,二值图像中的白色代表前景,黑色代表背景。方法名称正是依据这一惯例命名的。这并不是说这些运算符不能用于灰度图像,或在某些情况下用于彩色图像,只是它们最初的设计目的是处理二值形状。前文已经介绍过的基本形状核是形态学方法中最常用的定义邻域的"形状"。这类核通常被称为"结构元素",因为它们通常用于确定图像中形状的结构。

Erode(腐蚀)

正如名称所示,'**Erode**'方法会"侵蚀"白色形状,从任何背景像素处将其缩小。你也可以将其理解为扩大图像中的黑色区域。例如,下面是一个简单的二值"类人"形状,使用'Octagon'核对其进行了腐蚀处理。

  magick man.gif   -morphology Erode Octagon  erode_man.gif

[IM 输出] [IM 输出] [IM 输出]

其基本效果是使图像中的任何突起或尖点变细,或将其完全去除,但同时也会使图像中已存在的任何孔洞(例如此图像"手臂"处所造成的孔洞)变大。一般来说,核的大小决定了去除多少像素。

Dilate(膨胀)

'**Dilate**'方法是'Erode'的对偶操作。它会根据指定的核(及迭代次数)扩大白色形状,使形状变大。当然,这也意味着它会"腐蚀"图像中的黑色区域。

  magick man.gif   -morphology Dilate Octagon  dilate_man.gif

[IM 输出] [IM 输出] [IM 输出]

请注意,形状不仅变大了,其轮廓也变得更加平滑。"双腿"之间的大缺口被填补了,图像中原本存在的小的单像素"孔洞"也被填补了。核的大小和形状决定了在图像边缘附近增添了多少像素。'Dilate'和'Erode'互为对偶。也就是说(至少对于对称核而言),在应用形态学方法前后对图像取反,实际上就能执行另一种运算符的效果。例如,这里我通过对取反后的图像使用'Dilate'来实现腐蚀效果。 |

  magick man.gif -negate \
             -morphology Dilate Octagon   -negate dilate_man_neg.gif

[IM 输出]

Open(开运算)

下面是'**Open**'方法的效果,不过这次使用的是更大的'Disk'核。

  magick man.gif   -morphology Open Disk  open_man.gif

[IM 输出] [IM 输出] [IM 输出]

由此可见,'Open'通过磨圆任何尖锐的点使轮廓变得平滑,并去除任何小于所用形状的部分。它还会断开或"打开"任何细小的连接桥。但它并不会去除图像中可能存在的任何"孔洞"或间隙,例如形状"双腿"之间的间隙。同时它也不会使形状的基本"核心"大小变大或变小。实际上,它所做的是使用所提供的相同核对图像先进行'Erode'再进行'Dilate'

  magick man.gif         -morphology Erode  Disk  open_erode.gif
  magick open_erode.gif  -morphology Dilate Disk  open_man_2.gif

[IM 输出] [IM 输出] [IM 输出]

请注意,对已经使用相同核执行过'Open'的形状再次执行该操作,不会对该形状造成进一步的变化。例如……

  magick open_man.gif  -morphology Open Disk  open_man_twice.gif

[IM 输出] [IM 输出] [IM 输出]

也就是说,使用相同的核重复执行'Open'操作,对结果不会产生任何影响。正因如此,所提供的任何_迭代_次数都将分别应用于内部的膨胀和腐蚀子方法,而不是应用于整个方法本身,这样迭代次数就可以用来"扩大"有效的核,而不是无谓地重复整个复合运算。也就是说,'Open:2'这样的迭代实际上会被应用为对图像先执行'Erode:2',再执行'Dilate:2'。这样做的总体效果是使核所定义的有效"邻域"变大。 |

  magick man.gif   -morphology Open:2  Disk  open_man_x2.gif

[IM 输出]
由此可见,扩大后的邻域导致这个人形的"头部"和"脚部"末端都被去除了。形状的主体部分基本保持完整,但外观也更加平滑,而双腿间的缺口则未受影响。这与将核的尺寸加倍效果相同,不过其确切形状可能与半径加倍的核并不完全一致。

Close(闭运算)

'**Close**'方法的基本用途是减少或去除大小与核"结构元素"相当的任何"孔洞"或"间隙"。也就是"闭合"背景中大小相当的部分。

  magick man.gif    -morphology Close Disk   close_man.gif

[IM 输出] [IM 输出] [IM 输出]

该运算符的基本效果是通过填补(闭合)任何孔洞和凹陷来平滑形状的轮廓。它还会在核足以同时触及两个相近形状时,为它们形成连接"桥梁"。但它不会使形状的基本"核心"大小变大或变小。实际上,它所做的是使用所提供的相同核对图像先进行'Dilate'再进行'Erode',使图像先变大后变小。这与'Open'的执行顺序正好相反。

  magick man.gif           -morphology Dilate Disk  close_dilate.gif
  magick close_dilate.gif  -morphology Erode  Disk  close_man_2.gif

[IM 输出] [IM 输出] [IM 输出]

结果是图像外部的点将保持原样,但任何"凹湾"都会被平滑和加粗,"孔洞"和"间隙"也会被闭合。距离很近的分离物体可能会被连接在一起。与'Open'一样,使用相同的核重复执行'Close'方法不会对图像产生进一步的变化。不过,对该运算符使用'迭代'将会重复内部的子方法,从而产生更强的磨圆效果,类似于使用更大的核。而且正如'Dilate'和'Erode'方法一样,'Open'和'Close'方法也互为对偶。你可以通过在操作前后对图像进行取反,来重现另一种"对偶"方法的效果。 |

  magick man.gif   -negate -morphology Close Disk -negate   close_man_neg.gif

[IM 输出]

Smooth(平滑)

'**Smooth**'方法对形状先应用'Open'再应用'Close',先去除任何"小物体",再填补大小与核"结构元素"相当的任何"孔洞"或"间隙"。这里我们使用中等大小的'Octagon:3'核对图像进行平滑处理。 |

  magick man.gif  -morphology Smooth  Octagon:3  smooth_man.gif

[IM 输出]
如你所见,所有的"凹陷"、"间隙"、"孔洞"和"尖点"都根据核的大小和形状被平滑和磨圆了。'Smooth'运算符也经常与逐渐增大的结构元素配合重复使用,以逐步去除图像中的噪点。如果保留被去除的部分,就能得到图像的形态学"分解",可用于进一步研究。参见下文的粒度。该方法特别适合清理扫描文档。请注意,这实际上是对原始图像应用了4个独立的"基本"运算,因此其速度比单纯的'Erode'或'Dilate'慢4倍。

平坦灰度形态学

虽然这四种基本形态学方法,以及后续依据这四种方法定义的其他方法,本质上都是专门为处理二值图像而设计的,但它们也可以应用于灰度图像和彩色图像(不过彩色图像可能会产生一些奇怪的颜色效果)。此处需要灰度运算的实际示例 然而核本身始终会被视为简单的"开"或"关"邻域。任何核值只要是'nan'或小于'0.5',都将被视为不在其所定义的"邻域"之内。总而言之,上述运算符应用的是一个不具备任何"高度"或"三维"特征的"平坦"核,但仍可应用于灰度图像。

真正的灰度或三维形态学

真正的灰度或三维形态学(如某个库所述)会将核中的值实际加到或从图像中相邻像素的值上减去,然后再查找最大值/最小值作为结果。这意味着它将灰度图像视为三维形态学对象的"高度场",而核的灰度形状则是用于调整该高度场的平滑形状。虽然真正灰度形态学的实现细节已有详尽文档记录,但其在实际场景中的使用情况却鲜有记载。也就是说,除了在"平坦形状核"之外,我尚未找到任何有关使用真正灰度形态学的有用示例,只在关于其在"光度"处理中的应用方面见到过一条评论。正因如此,我尚未实现真正的三维灰度形态学。不过,如果确实有人需要这类非平坦的灰度形态学运算符,请告知我,我会实现相应的运算符。请注意,特殊的'Distance'方法(见下文)实际上与真正灰度形态学的工作方式类似,即它先将核值加到每个像素值上,再取最小的"最小"值。但该方法既不符合三维腐蚀(相减后取最小值)也不符合膨胀(相加后取最大值)形态学的定义。不过它与两者关系十分密切,或许可以用这些方法来实现。

彩色图像的强度变体

由于上述四种方法都是灰度通道方法,将它们用于彩色图像可能会产生偏色效果,即某一通道被修改而另一通道未被修改。它们实际上并非为多通道彩色图像所设计,只适用于灰度图像和二值图像。结果是对于彩色图像,颜色会失真,根据所执行的运算变得更亮或更暗。考虑到这一点,我创建了这些方法的"强度"版本:'ErodeIntensity'、'DilateIntensity'、'OpenIntensity'、'CloseIntensity'。它们会比较所定义"邻域"内的像素,并根据像素的强度替换当前像素颜色。也就是说,整个彩色像素会被复制,而不仅仅是各个通道的值。因此……

强度变体不会在图像中生成任何"新"颜色。

由于其本身的性质,强度方法会完全忽略当前的"-channel"设置。例如,这里我在内置的"rose:"图像上使用了'Dilate'形态学(扩展亮部区域)的二值和强度变体。

  magick rose: -morphology Dilate          Octagon:3  rose_dilate.gif
  magick rose: -morphology DilateIntensity Octagon:3  rose_dilate_intensity.gif

[IM 输出] [IM 输出] [IM 输出]

可以看出,普通的'Dilate'方法会在每个被膨胀的大色斑中生成不同的色调,因为每个通道都是单独处理的。而第二种强度膨胀则保留了最亮色斑的完整颜色,并按照布尔核形状进行扩展。强度方法还有一种简写命名方式,即把单词'Intensity'替换为'I'。因此这里我使用的是'CloseIntensity'方法,但采用其简写名'CloseI'。例如,下面是在内置玫瑰图像上使用四种"强度"变体的结果。 |

  magick rose: -morphology ErodeI Octagon:3 rose_erode_intensity.gif

[IM 输出]
|

  magick rose: -morphology DilateI Octagon:3 rose_dilate_intensity.gif

[IM 输出]
|

  magick rose: -morphology OpenI Octagon:3 rose_open_intensity.gif

[IM 输出]
|

  magick rose: -morphology CloseI Octagon:3 rose_close_intensity.gif

[IM 输出]
最后两种方法特别适合作为Paint 运算符的替代方案。 这些方法被归类为实验性功能 ,欢迎对其使用提出意见或反馈问题。如果没有收到任何意见,就不会再添加更多内容了!

替代的基础形态学技术

对于使用低于 v6.5.9-0 版本 IM 的用户,仍然可以实现
一些基础的形态学方法。

你可以生成一个全为 1 的核。例如,通过使用极大的 sigma 值
并指定相应的半径,借助高斯模糊生成一个 7x7 的 1 数组
(半径=3)。

因此
    -convolve 1,1,1,1,1,.....
一共 49 个 1,等价于
    -gaussian-blur 3x65535

这使你可以为二值形态学
方法生成一个简单的方形核。

因此对于 3x3 方形核(半径=1),'Dilate'(膨胀)是
    -gaussian-blur 1x65535 -threshold 0
'Erode'(腐蚀)则是
    -gaussian-blur 1x65535 -threshold 99.999%

如前所述
'Open'(开运算)是先'Dilate'(膨胀)后'Erode'(腐蚀)
'Close'(闭运算)是先'Erode'(腐蚀)后'Dilate'(膨胀)
而 Smooth(平滑)则是先'Open'(开运算)后'Close'(闭运算)

可以使用更大的半径来指定更大的方形核。

遗憾的是,若不使用卷积运算符手动定义形状,
其他内置的核形状是无法使用的。

而且这种方法实际上只适用于二值形态学。要实现
平坦灰度形态学,需要使用另一种技术:为核中的每个像素
生成一张单独的图像,并将其滚动
到该像素的位置。

阈值卷积法和滚动位移合成法这两种方法,都已在 Fred Weinhaus
编写的脚本"morphology"中实现,该脚本创建时间早于
ImageMagick 添加"-morphology"运算符之前。

可从以下地址查看并下载 Fred Weinhaus 的"Morphology"脚本
  http://www.fmwconcepts.com/imagemagick/morphology/index.php

差值形态学方法

下一层级的形态学方法,我称之为差值形态学。也就是说,这些形态学方法的结果,是某一种前述基础形态学方法与原始图像(或另一种形态学方法结果)之间的差值。本质上,它们返回的是某个简单方法对原始图像所做出的改变——给出轮廓、增加或减少的部分之间的差异。它们本质上是对图像结果进行'Difference'或'Minus'合成运算。

EdgeIn

'**EdgeIn**'方法,也称为'内部梯度',用于找出腐蚀从原图中去除的像素。因此返回的是最靠近边缘、但仍属于原始形状一部分的像素。

  magick man.gif   -morphology EdgeIn Octagon  edgein_man.gif

[IM 输出] [IM 输出] [IM 输出]

生成的边缘大约是所用核尺寸的一半,对于'Octagon'核来说会相当粗。更常见的做法是使用更小的'Diamond'或'Square'核,以生成单像素宽的形状轮廓。在将稀疏颜色作为填充运算符一节中,展示了一个使用'EdgeIn'配合 alpha 通道提取边缘像素的例子。

EdgeOut

'**EdgeOut**'方法,也称为'外部梯度',用于找出该图像经过膨胀后新增加的像素。因此返回的是紧邻形状外侧的背景像素。

  magick man.gif   -morphology EdgeOut Octagon  edgeout_man.gif

[IM 输出] [IM 输出] [IM 输出]

在轮廓或光晕透明一节中,展示了一个使用'EdgeOut'配合 alpha 通道的例子。

Edge(边缘)或形态学梯度

'**Edge**'方法返回的是"形态学梯度",可以理解为前面两种"edge"方法的叠加,或者更准确地说,是腐蚀后的形状与膨胀后的形状之间的差值。

  magick man.gif   -morphology Edge Octagon  edge_man.gif

[IM 输出] [IM 输出] [IM 输出] [IM 输出]

和之前一样,核的大小和形状决定了腐蚀图像的粗细程度。其粗细基本等于核尺寸减去中心像素。因此半径为 3 的核通常会产生 6 像素厚的'Edge'(核尺寸为 7 像素厚)。下面是使用最小的'Diamond'核得到的形状'Edge'轮廓示例。 |

  magick man.gif  -morphology Edge Diamond  man_outline.gif

该边缘是两像素厚,因为它包含了位于原始形状实际"像素边缘"两侧的像素。要让这条边缘更细,唯一的方法其实是将整幅图像沿对角线偏移半个像素。 [IM 文本]
关于以各种方式获取形状轮廓的更多细节,请参见边缘检测一节。 未来计划:使用"对角线"生成边缘。

Top-Hat(顶帽)

'**TopHat**'方法,更准确地说是"白顶帽",返回的是形状经过开运算后被移除的像素,也就是那些为了圆化尖角、断开形状之间连接桥梁而被移除的像素。

  magick man.gif   -morphology TopHat Disk  tophat_man.gif

[IM 输出] [IM 输出] [IM 输出]

可以看到,这些像素通常会形成高度分散的小岛,且没有一组像素的厚度超过所用的核。该方法名称中的"顶帽"一词,实际上是指该运算符在灰度三维形态学中的用法,而非我们这里使用的二值图像。该运算符更常用于灰度图像。 未来计划:灰度顶帽的示例

Bottom-Hat(底帽)

'**BottomHat**'方法,也称为"黑顶帽",是形状经过闭运算后新增加到图像中的像素。也就是说,是那些被用来填补"孔洞"、"缺口"和"桥接"处的像素。

  magick man.gif   -morphology BottomHat Disk  bottomhat_man.gif

[IM 输出] [IM 输出] [IM 输出]

同样可以看到,这也会产生高度分散的像素"孤岛",其厚度都不超过所用的核。不过,这些孤岛与上一种方法产生的总是完全不同的集合。 未来计划:灰度底帽的示例


使用底层形态学方法

基础形态学与通道

上述所有基础形态学方法都是通道方法,因此它们会根据当前的"-channel"设置分别应用于图像的各个通道。这意味着你可以将这些方法应用于彩色图像,只要你不太在意未定义透明区域带来的"颜色渗漏"。例如,让我们对原始"man figure"图像的 alpha 通道进行'Erode'(腐蚀),而不修改颜色通道。

  magick figure.gif -channel A  -morphology Erode Diamond:3 \
          +channel   figure_erode.gif

[IM 输出] [IM 输出] [IM 输出]

可以看到这样处理效果很好。其他示例可参见将稀疏颜色作为填充运算符,其中使用'EdgeIn'方法查找图像的边缘像素。另见轮廓或光晕透明,使用'EdgeOut'以特定颜色扩展图像的边缘。

搜索特定形状

对一个物体的认知取决于我们探测(观察)
它的方式。                    —— 乔治·马瑟隆(Georges Matheron),形态学之父

使用腐蚀从大量形状的集合中定位特定形状。将其推向极端,
就会生成[骨架](#skeletons),另见[细化骨架](#thinning_skeleton)。

使用开运算(平滑结果)或[条件膨胀](#dilate_conditional)恢复对象。

需要某种连通分量分析(分割)才能正确
统计图像中所发现物体的数量。

形状集合的粒度

通过对图像使用一系列尺寸缓慢递增的结构元素进行'Open'操作,并测量结果面积,就可以快速得到图像中能找到的此类形状数量的摘要。取该结果的导数(斜率),就能得到构成图像的形状的数量和尺寸的'谱'。这张图就是该图像针对某种特定形状的'粒度'。参见粒度分析(形态学),维基百科。从一种尺寸到下一种尺寸的差异,还能让你根据尺寸分离并统计特定元素,进而分离出含有不同尺寸和形状元素的区域。其结果就是一种纹理分割方法。演示如何确定一组形状的数量和尺寸。不过这需要一种'计数'方法(有待添加)才能完全实现。 历史小注…… 这一用法实际上正是形态学方法最初创建的原动力,源自20世纪60年代巴黎一家采矿公司。它让创建者能够开发一套自动化系统,用以分析矿物样本显微照片的晶粒结构,从而判断其是否适合开采。也就是说,定位并统计样本中矿物的尺寸和数量。举例来说:两种矿石可能含有相同数量的目标矿物(通常以晶粒或晶体形式存在于岩石中),但只有晶粒较大的矿石才能被有效开采,因为这样更容易将大块的纯矿物从周围含矿岩石中分离出来。这是一项极其耗费人力的工作,而形态学大大简化了这一过程。

非对称核的效果(基本方法测试)

让我们看看当使用不对称的核时,这些基本方法的表现如何。例如,这里我将一个用户自定义的'L'形核应用于一张特殊的形态学测试图像上(为便于观察单个像素而放大)。 |

  for method in  erode dilate open close; do
    magick test_morphology.gif \
             -morphology $method  '2x3+1+1: 1,-  1,-  1,1 '  test_$method.gif
  done

[IM 文本]
结果如下……

[IM 文本] 'Erode' 使核形状的任何精确匹配,在匹配点'原点'处变成单个白色像素。它还会将任何单个像素的'空洞'扩展为同样的形状,但相对于'原点'进行'镜像',也就是仿佛核旋转了180度。

[IM 文本] 'Dilate' 正如预期,产生了相同的结果,但是针对图像或核的'负'形式和'镜像'形式。单个白色像素扩展为核的形状,而任何匹配的'镜像'形空洞,则收缩为单个像素的'空洞'。
还请注意,测试图像正负两半之间的边界,会因应用上述基本形态学方法而移动。这是预料之中的。这就引出了关于这两种方法的一个特定要点。要把'Erode'方法转换为'Dilate'方法,或反过来,你不仅需要在处理前后对图像取负片,还需要围绕原点旋转或镜像核。通常这第二点可以忽略,因为大多数核都是'对称'的。只有在使用用户自定义的非对称核时,这一点才变得重要。 [IM 文本] 'Open' 正如前面所说,通常不会移除图像中的任何'空洞',不过完全匹配的形状会保持不变。较大的形状(例如测试图像的负半部分)也可能保留下来,但也许会有轻微改变。
[IM 文本] 'Close' 是前者的精确负结果,但其定义方式使其不需要对核进行镜像(因为其内部定义已经进行了镜像),只需要对图像取负片。


击中与错失(HMT)模式匹配

Hit-And-Miss(击中与错失)

'Hit-And-Miss'形态学方法,在计算机科学文献中也常被称为"HMT ",是一种高层次的形态学方法,专门用于在图像中查找和定位特定的模式。它通过在'原点'周围寻找'前景'和'背景'像素的特定配置来实现这一点。 从 IM v6.6.9-4 起,你可以使用方法名'HitAndMiss'、'Hit_N_Miss'或简单的'HMT'及其变体来指定这种形态学方法。在此版本之前,只能使用'HitAndMiss'这个方法名。
举例来说,我们可以寻找一个'前景'像素,其右侧紧邻着一个'背景'像素。
  magick man.gif   -morphology Hit-and-Miss '2x1:1,0'  hmt_right.gif

[IM 输出] [IM 输出] [IM 输出]

如你所见,这个只有2个元素的小核只匹配了图像右侧的像素。也就是说,该方法只返回了与给定模式匹配的特定像素。所使用的'核'或'结构元素'只能包含3种类型元素组成的模式:值为'1'表示'前景',值为'0'表示'背景',另外第三种元素可以指定为'Nan'、'-',或者值为'0.5',表示'不关心'或'任意像素'。你为'原点'设置的值非常重要,因为它决定了你只想'命中'前景形状,还是背景模式。但如果你专门将'原点'值设置为'不关心',那么你就可以匹配前景和背景中任何具有正确周边邻域的像素。例如,如果我使用这样的结构元素……

  magick man.gif   -morphology Hit-and-Miss '3x1:1,-,0'  hmt_right2.gif

[IM 输出] [IM 输出] [IM 输出]

你会得到所有右边缘的像素,无论是内侧还是外侧。这样一来,你现在标记的是形状边界的两侧,提取出的是2像素宽的边缘。不过并非所有像素都匹配该模式,所以并非每个像素都会加倍,但总体而言这就是你会得到的结果。为'原点'使用'不关心'值实际上是很常见的做法,尤其是在我们后面看加粗和细化方法时,这些方法只限于添加或移除像素。通过'不关心',同一个核定义可以用于这两种操作中的任意一种,因为操作本身定义了你感兴趣的是哪种'命中'类型。
这里还有一个例子,这次我再次将'命中'限定为形状内部、构成西北朝向拐角的像素。

  magick man.gif   -morphology HMT "3:0,0,- 0,1,1 -,1,-" hmt_nw_corner.gif

[IM 输出] [IM 输出] [IM 输出]

通过添加'>'标志,将这单个拐角扩展为一组旋转90度的拐角集合,我们就能找到形状内出现的所有拐角。 |

  magick man.gif  -morphology HMT "3>:0,0,- 0,1,1 -,1,-" hmt_corners.gif

[IM 输出] [IM 输出]
如你所见,'Hit-And-Miss'方法会定位并返回所有与所提供的任一核模式匹配的像素位置。 | _如果你去检查上述"-morphology"操作的详细输出,你会发现'Hit-And-Miss'使用'Lighten'合成方法,来创建所提供的每个模式核所匹配的所有像素的'并集'。

不幸的是,'已更改'像素计数是每个核应用所关闭的所有像素的计数。换句话说,就是形状中的像素数减去每个核所匹配到的像素数。

_
| _同理,用Hit-And-Miss 方法的结果再次反复处理它自身通常是没有意义的,因为图像已经改变得如此之多,之后很可能就不会再有匹配了。

你可以像你所看到的那样,利用这些结果去修改原始图像,从而生成一张略有不同的图像。_
你可以使用一组核,更有针对性地筛选出你特别关注的内容。例如,假设你对三条线相交的点感兴趣。那么你可以使用专为此目的设计的'LineJunctions'核集。

  magick lines.gif -morphology HMT LineJunctions hmt_junctions.gif

[IM 输出] [IM 输出]

如你所见,该核集中只有零星几处位置与之匹配。不过这样的结果可能让人很难看清原始图像中匹配位置究竟在哪里。如果处理的是灰度图像,情况会尤其糟糕。一种解决办法是使用带有某种形状核的'Dilate'(例如'Ring')来扩大匹配区域。例如…… |

  magick lines.gif \( +clone \
             -morphology HMT LineJunctions \
             -morphology Dilate Ring \
             -background red -alpha shape \
          \) -composite              hmt_junctions_rings.gif

[IM 输出]
现在你可以清楚地看到这套特定核集找到3条或更多线交汇处的位置。'LineJunctions'中的每个核可能只匹配一两个特定位置,因此以这种方式进行模式匹配可能会很慢。尽管如此,它非常精确,效果也很好。另一个类似的'Hit-And-Miss'核集是'LineEnds'核,可用于找出图像中所有线条的自由端点。 |

  magick lines.gif \( +clone \
             -morphology HMT LineEnds \
             -morphology Dilate Ring \
             -background red -alpha shape \
          \) -composite                  hmt_lineends_rings.gif

[IM 输出]
HitandMiss——仅使用前景像素 -> 腐蚀 HitandMiss——仅使用背景像素 -> 取负后膨胀

灰度图像上的击中与错失

当'Hit-And-Miss'方法应用于灰度图像时,实际返回的值将是'前景'最小值与'背景'最大值之间的差。如果出现负数结果(没有匹配),结果会被'截断为零',因为负值没有实际意义(原文作“no math”,是“no match”的误记——译者注)。换句话说,它返回的是两组像素之间数值的'最小分离度'。对于布尔形状,结果将是'0.0'(黑色)或'1.0'(白色)之一。但对于灰度图像,这相当于匹配像素的'渐变'。例如,它可以用来判断在匹配模式中特定前景与背景之间实际存在多大的对比度。如果你只想得到灰度图像中哪些像素实际匹配该模式的布尔(开/关)结果,应该在命令后添加"-threshold 0"选项。

加粗(向形状添加像素)

'Thicken'方法会在每个匹配位置向原始形状添加像素。例如,这里我寻找一个距形状右边缘两个像素远的背景像素。

  magick man.gif   -morphology Thicken '3x1+2+0:1,0,0'  thick_right.gif

[IM 输出] [IM 输出] [IM 输出]

如你所见,最终你在形状原始边界之外得到了一行像素。你可以将这个'Thicken'方法迭代几次,以延续这个序列。 |

  magick man.gif   -morphology Thicken:4 '3x1+2+0:1,0,0'  thick_right2.gif

[IM 输出]
不过,由于像素正在被添加,模式匹配核的原点不应该匹配前景像素,否则你实质上会在已经存在像素的地方再添加一个像素。在上面的例子中,将原点像素设置为背景模式,因此只有背景模式才会真正匹配。另一种方案是始终将原点设置为'不关心'的元素值。这样一来,你就能对'Thicken'使用相同的核模式,正如你之后会看到的,'Thinning'也是如此。所以更好的规则是把原点设置为'不关心'。 | _另一种生成'Thicken'操作的方法,是将该核与特殊的'Unity'核的'Hit-And-Miss'结果取并集,从而将原始图像纳入结果之中。

例如……_ |

  magick man.gif -define morphology:compose=Lighten \
                  -morphology HitAndMiss 'Unity ; 3x1+2+0:1,0,0' hmt_thicken.gif

[IM 输出] [IM 输出]

_实际上,上面示例中的多核组合设置并非必需,因为在用户未定义该设置时,'Hit-And-Miss'方法会默认专门设置这一合成方式。

_

通常,'Thicken'用于扩大诸如线条之类的形状,但不会使线条变长。一组被称为'ConvexHull'的特殊核集,能让你实现这一点。例如……

  magick -size 80x80 xc:black -fill none -stroke white \
          +antialias   -draw 'line 10,20 70,60'     man_line.gif
  magick man_line.gif   -morphology Thicken ConvexHull  thick_line.gif

[IM 输出] [IM 输出]

加粗——八边形凸包

实际的'ConvexHull'核确实是专为处理图像形状而设计的,它会将一个形状扩展为'八边形凸包'。也就是说,它会尝试填补各极值点之间的所有空隙,直到生成一个'八边形状'的对象。

  magick man.gif -morphology Close Diamond \
                  -morphology Thicken:-1 ConvexHull \
                  -morphology Close Diamond       man_hull_full.gif

[IM 输出] [IM 输出]
关于'ConvexHull'核的定义详情,以及为何需要两个'Close'方法,请参阅相关说明。

你可以通过开启详细输出设置来观察迭代过程。不过这会显示出,上述过程其实非常非常慢。每次'Thicken'迭代实际上每次只会向形状添加少量像素。因此,可能需要经过很多次迭代才能完成整个'凸包'。在这个具体案例中,该图像需要80次'Thicken'迭代,配合一个8核的'ConvexHull'。这意味着上述过程实际上需要640次基本迭代,再加上执行两次'Close'方法所需的另外4次基本迭代。这可能会耗费相当多的时间。基本上,使用击中与错失模式匹配进行迭代可能会非常非常'慢',如果能找到替代技术,就应该改用它。你可以利用这一点,通过对凸包边缘和原始形状取交集(变暗合成),来找出原始图像中哪些点导致了这个八边形形状的产生。

  magick man_hull_full.gif \
              -morphology EdgeIn Diamond man_convex_edge.gif
  magick man.gif man_convex_edge.gif \
          -compose Darken -composite man_extremities.gif

[IM 输出] [IM 输出] [IM 输出]

任何适合放入上述凸包内、但又在该凸包的每条边上至少包含一个像素的连通形状,都会生成同样的八边形凸包。

对灰度图像使用 Thicken

在处理灰度图像时,'Thicken' 会把 'Hit-And-Miss' 前景与背景分离的结果加到原点像素上。因此即使'原点'像素不属于'背景'集合,这也可以用来让匹配的像素变得更亮。举例来说,让我们用上面找角点的例子,但改用形状的 50% 灰度版本重复一次。

  magick man.gif   -evaluate multiply 0.5   man_grey.gif
  magick man_grey.gif  -morphology Thicken Corners  thick_corners.gif

[IM 输出] [IM 输出]

在使用 HDRI 版本的 ImageMagick 配合 'Thicken' 时,最好对结果做一次 "-clamp" 或 "-auto-level",以防止其超出图像像素值的范围限制。

Thinning(细化)(从形状中减去像素)

'Thinning' 方法是 'Thicken' 的对偶操作。这个方法不是添加像素,而是从原始图像中减去像素。举例来说,让我们移除所有从右边缘算起第 4 个像素处的像素。

  magick man.gif   -morphology Thinning '5x1+0+0:1,1,1,1,0' thin_right.gif

[IM 输出] [IM 输出] [IM 输出]

要让 'Thinning' 正常工作,模式匹配核的原点应该包含一个前景像素,否则该方法就没有匹配的像素可以从形状中移除。 | _生成 'Thinning' 操作的另一种方式,是对原始图像与 'Hit-And-Miss' 的结果做相对补集(使用 MinusSrc 合成方式)。你可以在核列表的开头加入该图像(作为被'减'的对象),方法是使用一个 'Unity' 核。

例如……_ |

  magick man.gif -define morphology:compose=MinusSrc \
          -morphology HMT 'Unity ; 5x1+0+0:1,1,1,1,0' hmt_thinning.gif

[IM 输出] [IM 输出]

_这是一种'交集'式的细化,在单一步骤中就去除所有核所指定的全部像素,而不是'迭代'式那样按顺序逐个核地去除像素。更多信息参见细化方式。

_

线条的连通性

未来内容:4-连通与8-连通线条的比较 参见 IM 论坛的讨论,从 8-连通到 4-连通线条。

细化边缘检测器的输出

细化最常见的用途之一,是将 边缘检测器(例如 Sobel 卷积)的阈值化输出,缩减为单像素宽度的线条,同时保留这些线条的完整长度。 使用距离梯度图像的示例。

细化到骨架

对图像做 'Thinning',实际上比 'Thicken' 更常用,因为它可以把形状简化为更易处理的形式,比如骨架。正如后面会讨论的那样,骨架的含义是形状任意两条(或多条)边缘之间的中心线。骨架很重要,因为它能对非常复杂的形状给出很好的描述。举例来说,处理图像以找出环的数目、线段的数目及其排列方式,就能告诉你许多关于该形状的信息。因此,让我们反复对 man 形状的边缘执行 'Thinning',生成一个"细化骨架",直到只剩下中心线为止。

  magick man.gif  -morphology Thinning:-1 Skeleton  man_raw_thinned.gif

[IM 输出] [IM 输出]

上面操作的 Verbose 报告会显示:共进行了 18 次迭代、使用了 8 个核,总计 144 次基本迭代。这实际上比查找其凸包(如上)要快得多,因为细化核在每次迭代中会移除整行、整列的像素,而不是每次只移除少量像素。请注意 'Skeleton' 核集未能扩展该孔洞,因此没有找到孔洞与外边缘之间的中心线。这是这个特定骨架细化核的一个严重缺陷,原因在于所有核都要求至少存在背景像素才会产生细化匹配。你可以使用多组骨架细化核来解决这个问题。更简单的解决办法,是先对图像稍作 Erode,为核提供可处理的素材。这里我也只对'红'和'绿'通道做腐蚀和细化处理,从而在蓝色通道中保留原始形状。

  magick man.gif -channel RG -morphology Erode Diamond  man_erode.gif
  magick man_erode.gif -channel RG \
          -morphology Thinning:-1 Skeleton +channel  man_skeleton.gif

[IM 输出] [IM 输出]

你还可以看到,图像中原有的孔洞现在已扩展,在其周围形成了更大的连续像素环。 下面是腐蚀后孔洞周围环的放大图。 |

  magick man_skeleton.gif -crop 22x22+47+29 +repage \
          -scale 120x120    man_skeleton_zoom.gif

[IM 输出]
注意它并没有生成孔洞与边缘之间精确的中心线。另外,由于形状先被腐蚀过,这些线条并没有一直延伸到原始形状的边缘,而是提前一个像素停止了。也就是说,线条的末端被略微'修剪'掉了。这正是'腐蚀'这一解决方案的缺点。骨架也仅限于八边形线条,这意味着它遗漏了不少细节,不过在这个例子中这种简化反而是件好事。参见下文关于骨架的章节。这是一种传统的 'Skeleton' 核,正如你所见,它会产生'粗'的对角线,因此骨架的所有部分都是'4-连通'或'菱形连通'的。'Skeleton' 核还有其他一些变体,会在最终生成的"细化骨架"中产生不同的效果。 更细的、8-连通骨架 如前所述,这种'传统'骨架的对角线较粗。但很多时候这还不够'细'。在某些场景下,你想要的是稍微更细一些的骨架,也就是想要一个'8-连通'骨架而不是'4-连通'骨架。 一种解决办法是使用另一种骨架生成变体,比如用 'Skeleton:2' 核生成(该核见于 HIPR2 图形教程网站)。举例来说…… |

  magick man.gif   -channel RG  -morphology Erode Diamond \
          -morphology Thinning:-1 Skeleton:2 +channel  man_skeleton_hipr.gif

[IM 输出]
这是环圈区域的放大图,展示了生成的骨架是如何 8-连通的,对角线更细。 |

  magick man_skeleton_hipr.gif -crop 22x22+47+29 +repage \
          -scale 120x120    man_skeleton_hipr_zoom.gif

[IM 输出]
不过我发现这类骨架不如“传统”骨架那么精确。基本上在测试用例中我发现对角线是在错误的一侧被“细化”的。这是因为对角线中被去除的那一侧完全由核集中“角”细化核的顺序决定,而不是根据形状本身的性质来判断的。
另一种做法是先得到“传统”骨架,然后对其进行细化,使对角线总是在由该对角线端点所定义的“外侧”被细化。专用的“Diagonals”细化核就是为此设计的,之后再用“Corners”核来“收尾”。那么让我们把之前那个“传统”骨架进一步细化……

  magick man_skeleton.gif -channel RG \
          -morphology Thinning:-1 Diagonals \
          -morphology Thinning Corners   man_thin_skeleton.gif

[IM 输出] [IM 输出]

这种把传统 4-连通骨架进一步细化的技术,比直接使用“Skeleton:2”变体要略慢一些。这种额外的细化需要对 8 个核进行 8 次细化迭代,即 64 次基元迭代。你也可以只使用“Corners”核,不过那样只会生成“HIPR”变体,对角线两侧中哪一侧被细化只是“随机”选择的结果。不过它只需要对全部 4 个核进行 1 遍处理,因此比使用“Diagonals”要快得多。无论如何,从“传统”4-连通骨架出发,你都可以很容易地生成(某种)8-连通版本。

骨架信息

当你得到一个骨架(也许还同时有 4-连通和 8-连通两个版本)之后,下一步通常是了解关于该骨架的更多信息。例如有多少个“自由线端”、“线交点”和“线环”存在。线端数量 这里我对之前生成的骨架(从“红色”通道中提取)使用击中与错失搜索来查找“LineEnds”。然后我把这些线端膨胀成圆环并上色,再与原骨架合并,以使它们的位置非常显眼。

  magick man_skeleton.gif -channel R -separate +channel \
          -morphology HMT LineEnds man_ends.gif
  magick man_ends.gif -morphology Dilate Ring -background Red -alpha Shape \
          man_skeleton.gif +swap -composite man_ends_marked.gif

[IM 输出] [IM 输出] [IM 输出]

注意所有线条要么彼此相连,要么连到未被找到的像素环上。只有自由线端被标示出来。如果你做一次像素计数(使用直方图输出)就会看到这个骨架产生了 12 个线端。线交点数量 你可以用“LineJunctions”核来大致统计图像中线交点的数量,前提是对一个8-连通骨架使用,最好是从用于统计线端的原始骨架细化得到的那个骨架。不要混用两种不同的骨架生成变体。

  magick man_thin_skeleton.gif -channel R -separate +channel \
            -morphology HMT LineJunctions  man_junctions.gif
  magick man_junctions.gif -morphology Dilate Ring \
          -background Red -alpha Shape \
            man_thin_skeleton.gif +swap -composite man_junctions_marked.gif

[IM 输出] [IM 输出] [IM 输出]

如果你直接对传统 4-连通骨架尝试这个核,某些“T”形交点会得到多次匹配,使统计结果非常不准确。你可以看到结果是 12 个线交点,对于这个特定形状来说是正确的。不过对某些交点而言,“LineJunctions”核并不准确。例如一个 4 线的对角“X”交点只会产生 1 次匹配,而一个正交的“+”交点则会产生 4 次匹配。这两种特殊交点都应该产生 2 次匹配,才能保持线交点计数的正确性。因此要得到准确的计数,你需要为每个“X”交点多加 1,为每个“+”交点减去 2。
对于没有环的骨架,交点数量应比线端数量少 2。但如果线端数量等于线交点数量,就说明骨架中存在一个或多个环。现在这个骨架有 12 个线端和 12 个交点,所以图像中某处至少存在一个连续的像素环。环的数量 未来功能:连通对象标注

剪除线条

既然你知道这幅图像中至少有一个环,假设你想把形状简化到只剩这些环。解决办法是反复“剪除”所有线端,直到把它们全部去掉为止。对于这样一个 4-连通骨架,你甚至可以用较小的一组“LineEnds”核,使处理速度提高约一倍。

  magick man_skeleton.gif -channel G \
          -morphology Thinning:-1 'LineEnds:1>' man_loop.gif

[IM 输出] [IM 输出]

详细报告会显示这一步用 4 个核经过 75 次迭代,共 300 次基元迭代,才把图像中所有带自由端的线条“剪除”干净。也就是说,操作次数大约是求骨架时的两倍,这说明这个操作的计算量可以有多大。如果使用完整的一组“LineEnds”核(8 个核),同样要 75 次迭代,但核数是原来的两倍,因而会达到 600 次基元迭代。

快速剪除线条

快速完整剪除技术……

  1/  找到线端和线交点。
  2/  删除线交点,使所有线段完全断开。
  3/  使用泛洪填充,或使用条件膨胀来移除“线端”线段。
  4/  恢复线交点。
  5/  将其作为原始图像上的映射,用来恢复“环”。

第一步我们已经讲过……结果是…… |

  magick man_skeleton.gif -channel R -separate +channel \
          -morphology HMT LineEnds man_ends.gif

[IM 输出]
要断开(或分离)所有线段,可以使用“LineJunctions”核。不过默认的核集并不能完全断开“T”形交点(只是定位它们)。要正确断开所有线段,你还需要在核集中加入正交的“T”核,最好也加上“+”交点核。例如: |

  magick man_skeleton.gif -channel R -separate +channel \
      -morphology HMT 'LineJunctions;LineJunctions:3>;LineJunctions:5' \
      man_disconnect.gif

[IM 输出]
使用这些匹配进行细化实际上会断开各个线段,不过你必须一步完成整个操作(参见细化方式),否则无法正确工作。 |

  magick man_skeleton.gif -channel R -separate +channel \
      -define morphology:compose=Darken \
      -morphology Thinning 'LineJunctions;LineJunctions:3>;LineJunctions:5' \
      man_line_segments.gif

[IM 输出]
这里放大展示了“环”中已断开的线段。 |

  magick man_line_segments.gif -crop 22x22+47+29 +repage \
          -scale 120x120    man_line_segments_zoom.gif

[IM 输出]
到这一步,我们就可以移除任何包含先前找到的“线端”匹配的线段了。这可以通过从这些“种子”点开始进行“泛洪填充”来删除它们。不过这只对4连通的骨架有效,而泛洪填充正是假定了这一点。此处需要示例 另外我们也可以使用条件膨胀同时找到所有这些点,并将其移除。此处需要示例——当条件膨胀或条件腐蚀可用时。 如果此时你恢复线交叉点,做一次剪枝,再移除剩下的任何单个像素,你就已经快速地移除了所有线段。是的,这看起来步骤很多,但相信我,这仍然比反复300次“剪去线的末端”来得到同样的结果要快得多。

细化方式——顺序还是同步

如果你只对线段末端做一次“剪枝”,并与原始图像比较,你会发现大多数情况下一个线段被剪去了2到4次不等,具体取决于线条的形状和方向。举例来说(结果图像已放大),下面是默认的线端细化

  magick -size 10x10 xc:black -fill white \
          +antialias  -draw 'line 1,7 8,3' line.gif
  magick line.gif -channel GB \
          -morphology Thinning LineEnds  line_seqential.gif

[IM 输出] [IM 输出]

这是因为默认情况下,每个“Thinning”核都是依次作用在前一个核处理结果之上的,即按_顺序_进行。也就是说,它会先移除一个核所选中的全部像素,然后才把结果交给下一个核处理,而下一个核可能(并且确实)会从同一个线端再选中更多像素。换句话说,默认情况下,在遍历所有提供的核完成一次完整“迭代”的过程中,线端会被多次细化。这意味着你不能依靠详细输出通过统计单次迭代中该运算符移除的像素数,来准确了解各条线原本有多长。不过你可以修改“Thinning”的工作方式,使其只移除单次遍历所有核的一次“Hit-And-Miss”迭代所能找到的那组像素。换句话说,在迭代开始时把所有核都应用于同一张图像,将结果合并,然后只移除那些像素一次,对所有核只做一次。也就是说,_同时_移除所有HMT选中的像素。基本上你需要将多核组合设置设为使用“Darken”组合方式,这样正好能实现这一点。具体来说,就是把所有选中的像素合并起来,一次性移除已选中的像素。例如…… |

  magick line.gif -channel GB -define morphology:compose=darken \
          -morphology Thinning LineEnds  line_simultaneous.gif

[IM 输出]
这里发生的是,模式匹配核中的每个核都只应用于原始图像。任何与原图匹配的像素都会被收集在一起。也就是说,我们只会移除所有核针对原图结果的“交集”,这是通过“darken”组合方式实现的。但对于任意一次遍历所有核的迭代,移除操作都是一步完成的。结果就是,一个线端无论有多少个核可以匹配它,都只会被匹配一次。因此末端只会被移除一个像素,而不会被不同的核移除2个或更多像素。总而言之,添加“Darken”多核组合设置,可以确保“Thinning”方法执行“同步细化”(所有核同时进行),而不是“顺序细化”(一次一个核——默认方式)。
不过,虽然这会让线端的剪枝行为更规整,但也会让处理变慢,并且可能改变细化的整体结果。以对一些方框同时细化左右两条边为例进行“Thinning”。

  magick -size 10x10 xc:black -fill white -draw 'rectangle 4,1 5,7' rect.gif
  magick rect.gif -channel GB -define morphology:compose=darken \
          -morphology Thinning Edges  rect_simultaneous.gif

[IM 输出] [IM 输出]

“同步细化”实际上把中间的矩形完全删除了!这是因为该形状被细化到只剩两像素厚,之后“厚”中心矩形的两侧都匹配了该模式,于是两侧同时被“细化”掉了。在对骨架进行细化时这样做也会发生同样的情况。而默认的“顺序细化”则产生了…… |

  magick rect.gif -channel GB \
          -morphology Thinning Edges rect_seqential.gif

[IM 输出]
如你所见,它保留了其中一个像素(右侧)作为骨架中心线。这是因为其中一组核先细化了“厚”中心线的一侧,而后续的核未能匹配这条已经变“薄”的线,因此它没有被移除。本质上,存在一些情况下“顺序细化”(默认方式)比特殊的“同步细化”效果更好,反之亦然。

模式匹配核

如前所述,“模式匹配”或“Hit-And-Miss”核可以包含3种不同类型的元素:前景、背景以及“无所谓”。值为“1.0”(或白色)匹配前景像素。值为“0.0”(或黑色)匹配背景像素。你可以使用值“0.5”或特殊值“Nan”或“-”来表示不属于该邻域、因而“无所谓”的像素元素。“Hit-And-Miss”只会在前景像素的最小值大于背景像素的最大值的位置匹配。然后它会返回这两个值之差,否则返回零。 [IM 输出]

Peaks

“Peaks”核是前面展示过的“Ring”核的扩展。两个半径参数会生成一圈背景像素,环绕在中心“原点”的单个前景像素周围。以下是一些较为实用的“Peak”核示例……

[IM 输出] [IM 输出] [IM 输出] [IM 输出] [IM 输出] [IM 输出]

以上这些核既可以用来明确定位一片较暗像素海洋中的单个像素“峰值”,也可以用来查找完全落在更大圆环内部的任意小形状。它们尤其适用于提升相关性模式匹配搜索的对比度。

Edges

“Edges”核集合会匹配形状平直边缘上的任意像素。它不会匹配尖锐的九十度拐角上的像素,不过它会匹配八边形上的拐角像素。

[IM 输出]

如你所见,所有90度旋转都被生成了出来,但它们是按照通常能产生更好效果的“翻转”镜像顺序排列的。通常这个核会被用作一种图像“Thinning”核,不过就目前来说,它无法细化对角线边缘,也无法生成图像的完整骨架。举例来说……

  magick man.gif -channel RG \
          -morphology Thinning:-1 Edges   thin_edges.gif

[IM 输出] [IM 输出]

参见下面的“Skeleton”核。

Corners

“Corner”核用于定位图像边缘周围任意的对角拐角像素。关于其用法示例,参见上文的“Hit-And-Miss”。

[IM 输出]

例如,这里我用它尝试细化所有对角边缘…… |

  magick man.gif -channel RG \
          -morphology Thinning:-1 Corners  thin_corners.gif

它可以与“Edges”核结合使用,以实现一种单方法的骨架细化。关于此用法示例,参见下面的“Skeleton”核。 [IM 输出]

Diagonals

“Diagonals”核是仅使用“Corners”核来将4连通对角线细化为8连通对角线的另一种方案。它可以用来细化4连通的线,方法是从拐角向中心方向逐步移除外侧的一组像素,直至完成。

[IM 输出]

注意,其结果应当再配合使用“Corners”核来完成,以便定位并细化90度拐角。用法示例参见更细的骨架。 Diagonals 子类型 通过为核提供“type[,angle]”参数,你可以选取用于构成上述核集合的特定子类型。

[IM 输出] [IM 输出]

这可以让你指定自己特定的一组核,按你想要的方式精确地细化对角线。举例来说,你可以分别细化四种对角线类型中的每一种(对上述两个核使用相同的_angle_值)。这样一来,你就可以对每种对角线类型逐一进行迭代式缩减,一旦该特定类型的对角线全部细化完成即可中止,从而减少整体执行的“基本形态学步骤”数量。需要示例 就目前给出的方式而言,默认核集合只会尝试同时且反复地细化所有对角线,直至它们全部被细化完。也就是说,所有核都会持续应用,直到所有对角线都已细化完毕,而不是只处理需要细化的那些对角线。这意味着会执行许多已经不再需要的“基本形态学步骤”,其中大多数核在每次循环中都不会对图像做出任何改动。需要完整示例 请记住,四种对角线类型中的每一种,仍然应该同时使用两个核(针对每个特定角度)来执行,这样才能使某条特定对角线的两端一起被细化,比如当该对角线是“弧”的一部分时。关于此类细化/加粗操作,IM论坛中有相关讨论,参见从8连通到4连通线。

LineEnds

“LineEnds”核集合,如上文剪去线的末端所示,用于定位线条的末端。更确切地说,它是查找尖锐端点的位置。

[IM 输出]

如你所见,它只会匹配至少有两个像素、并且匹配到的像素被背景像素“封顶”或“包围”的线条。例如,这里我们使用“Hit-And-Miss”来查找所有线端。

  magick lines.gif -morphology HMT LineEnds  hmt_lineends.gif

[IM 输出] [IM 输出] [IM 输出]

是的,这张图像中有很多线端。不过要注意,以某种“环”结尾的线条不会产生匹配。还要注意,如果你在用这个核以“迭代式细化”方式(默认方式)对图像进行“Thinning”,那么连续的多个核可能会对同一条线的末端匹配两次或更多次,从而在整个“Thinning”方法的单次迭代中使该线条被缩短多次。更多细节参见细化——顺序还是同步。 Line End 子类型 你也可以为这个核提供“type[,angle]”参数,这将返回用于生成上述“LineEnds”核集合的某一个单核定义。

[IM 输出] [IM 输出] [IM 输出] [IM 输出]

这些核可以按需要展开为一组旋转核列表,或按需要旋转到特定的“angle”。默认的“LineEnds”集合实际上使用的是这个核定义。

'LineEnds:1> ; LineEnds:2>'

“LineEnds:3”是对角线“LineEnds:2”对应的正交等价形式,它只会找出远离任何对角拐角或交叉点的线端。“LineEnds:4”是一种传统的线端核,以循环方式旋转以生成8个核(例如“LineEnds:4@”)。不过它无法定位连接到正交“T”形交叉点的线的最后一个像素。而上文定义的默认“LineEnds”集合,则能够用相同数量的核找到“T”形交叉点处的这个末端像素。

LineJunctions

“LineEnds”用于查找一组线的末端,而“LineJunctions”则用于查找由3条或更多线构成交叉点的点。

[IM 输出]

例如,这里我们使用“Hit-And-Miss”来查找所有线交叉点。

  magick lines.gif -morphology HMT LineJunctions hmt_junctions.gif

[IM 输出] [IM 输出] [IM 输出]

“LineJunctions”核通常用于两个目的。

  • 统计图像中线交叉点的数量,从而推算骨架中线段的数量。
  • 把各条线段彼此断开。

不过要注意,在上图的“T”形和“+”形交叉点处,“Y”形交叉核所匹配到的点,实际上距离真正的交点还有一个像素的偏移(原文作“one pixel one pixel away”,是“one pixel away”的误记——译者注)。因此交叉点计数可能与预期不完全一致,尤其是在“+”处,会找到四处匹配,而不是交叉计数所需要的仅仅两处。建议谨慎处理。这两方面的更多细节参见骨架信息以及快速线段剪枝。该核实际上只定义了前景像素,因此它可以直接当作“Erode”方法使用,而不必用作“Hit-and-Miss”方法。 Line Junction 子类型 这个核也可以通过指定“type[,angle]”参数来访问各种子类型。这可以用来搜索特定类型的线交叉点。

[IM 输出] [IM 输出] [IM 输出] [IM 输出] [IM 输出]

核“LineJunctions:2”也可以用“LineJunctions:3,45”来指定,类似地,“LineJunctions:5”与“LineJunctions:4,45”也是等价的。默认的“LineJunctions”核集合只使用了前两种交叉点定义(即“Y”形和对角“T”形交叉点),具体方式如下……

'LineJunctions:1@ ; LineJunctions:2>'

这适用于8连通的线交叉点。正如IM论坛“LineJunctions所使用的核”中所讨论的,如果你只想检测4连通的线交叉点,就需要查找正交的“T”形交叉点和“+”形交叉点。

'LineJunctions:3> ; LineJunctions:5'

不过由于“T”形核也会匹配到“+”,你可以把上面的写法精简为……

'LineJunctions:3>'

如果需要用于判断线段数量,可以单独用一个图像测试来专门检测4向“+”交叉点,从而将其与3向“T”交叉点区分开来。

Ridges

“Ridges”核用于定位脊线并细化像素的线条,例如在距离梯度图像中。这些核尚处于实验阶段,可能会有所变化。 默认设置旨在定位单像素宽的脊线。

[IM 输出]

Ridges:2 一种特殊的扩展子类型,用于寻找两像素粗的脊线。其复杂性源于需要定位并标记这种倾斜线,包括这些线的镜像形式。

[IM 输出]

这组核很重要,因为“形态学骨架”实际上同时包含1像素和2像素粗的线条。

ConvexHull

“ConvexHull”核集合旨在加粗形状,从而生成该形状的“八边形凸包”。也就是说,能够包含整个形状的最小八边形。

[IM 输出]

有两组旋转90度的核,一组是另一组的镜像。由于原点实际上是一个“背景”元素,因此它实际上只适合用作“Thicken”模式核。然而,对于包含水平或垂直“缝隙”的图像(例如我们的“man”形状),该核会失败。

  magick man.gif -channel R \
          -morphology Thicken:-1 ConvexHull  man_hull.gif

[IM 输出] [IM 输出]

解决方法是在使用“ConvexHull”之前先“Close”这些缝隙(以及中央的孔洞)。 |

  magick man.gif -morphology Close Diamond \
                  -morphology Thicken:-1 ConvexHull \
                  -morphology Close Diamond       man_hull_full.gif

[IM 输出]
请注意,在上面的例子中,我在使用“ConvexHull”之后又重复了一次“Close”。原因是图像中任何较大的“孔洞”也会被“Thicken”缩小为单个像素,或正交的“缝隙”。重复使用“Close”可以在不影响最终形状的情况下消除这些孔洞。这里是另一个例子,原始形状(白色)通过凸包加粗被扩展了(红色)。 |

  magick circles.gif -channel R \
          -morphology Thicken:-1 ConvexHull  circles_hull.gif

[IM 输出]
如你所见,结果是一个八边形形状,而中央的孔洞被缩小为一个两像素宽的缝隙,可以进行闭合了。

Skeleton

通过细化特定形状来生成“骨架”并非易事。即使使用相同的核集合,重新排列核的顺序也会在最终“骨架”中产生不同的变化。正因如此,我并未只实现一种“Skeleton”核集合,而是实现了多个,可通过给定“type”参数编号来选择。

Skeleton:1

第一个也是默认的集合“**Skeleton:1**”,是一种传统的细化核,也是最先使用的核。它基本上与上面的“Edges”核完全相同,只是以45度增量循环旋转。 [IM 输出] |

  magick man.gif -channel RG \
          -morphology Thinning:-1 Skeleton   thin_skeleton1.gif

[IM 输出]
结果是一个较为合理的形状“细化骨架”,尽管对角线在一侧往往仍显得有点粗。基本上生成的骨架是4连通的,这样就可以使用快速剪枝技术。另外请注意,该核集合无法正确地扩展图像中的单像素孔洞。换句话说,围绕该孔洞的骨架甚至都不接近孔洞与图像其余部分之间的中心线。更多细节请参见细化为骨架。

Skeleton:2

“**Skeleton:2**”变体与传统的“Skeleton:1”版本几乎完全相同。它见于HIPR2图像处理资源文档中。 [IM 输出] |

  magick man.gif -channel RG \
          -morphology Thinning:-1 Skeleton:2   thin_skeleton2.gif

[IM 输出]
如果将其与之前的集合进行比较,你会注意到角部的内部像素已被移除。这样一来,细化操作就能移除对角线上多余的粗度。然而这种对角线细化并不对称,且高度依赖于图像的形状以及核的应用顺序。 “Skeleton:2”变体与直接使用组合的“**Edges;Corners**”核列表非常接近。 [IM 输出] |

  magick man.gif -channel RG \
          -morphology Thinning:-1 'Edges;Corners' thin_edge-corner.gif

[IM 输出]
这与“Skeleton:2”所用的唯一区别在于列表中核的排列顺序。请注意结果骨架也随之不同,尽管使用的是同一组核。这说明通过细化生成骨架其实相当脆弱,仅仅改变顺序就可能在连通骨架中产生不同的结果。

Skeleton:3

“**Skeleton:3**”是在一项关于使用细化核(见下文ThinSE核)的正式研究中开发出来的,出自Dan S. Bloomberg于1991年发表的论文《保持连通性的形态学图像变换》。他开发了相当多这样的骨架,并将研究结果制成了表格。以下是他能够找到的、可生成4连通骨架的最佳方案。然而与之前的骨架不同,这需要使用3个旋转核(共12个)。 [IM 输出] |

  magick man.gif -channel RG \
          -morphology Thinning:-1 Skeleton:3   thin_skeleton3.gif

[IM 输出]
需要注意的是,第一组旋转核仅包含一个背景像素。这意味着该骨架能够打开“man”形状中存在的单像素孔洞,并生成以中心线为方向的骨架。它不会产生过多分支,能生成干净平滑的线条,并且也能完全细化。总而言之,这是较好的骨架细化核之一。

ThinSE

同一篇由Dan S. Bloomberg撰写的研究论文《保持连通性的形态学图像变换》,实际上从基本原理出发,开发出了一整套完整的最小3x3“细化结构元素”,它们都被设计用来保持4连通线或8连通线。“**ThinSE:**{_type_}”核集合是所有这些结构元素的列表,下面按连通性和保持强度分组列出。“{_type_}”是一个基于论文中所用上标(连通性)和下标元素编号的数字。因此核“**ThinSE:41**”是保持4连通线元素中的第一个。你还可以为给定的核定义添加旋转角度,或生成一组旋转或镜像旋转的标志。

[IM 输出] [IM 输出]
[IM 输出] [IM 输出]
[IM 输出] [IM 输出]

最后一个“通用细化核”,“**ThinSE:482**”,你可能会认出它就是定义边缘检测核集合时所用的相同核。这个通用核实际上是上面所展示的所有其他细化核由此发展而来的核心核。它是该集合的默认核。请注意,两个通用核“ThinSE:481”和“ThinSE:482”是唯一存在旋转关系的核。也就是说,“ThinSE:481x45”等价于“ThinSE:482”。许多其他内置的HMT核集合实际上是在内部以这些核为基础定义的。例如,核集合“**ThinSE:41 ; ThinSE:42 ; ThinSE:43**”及其旋转展开,将生成用于创建“Skeleton:3”集合的12个核。该骨架在论文中被列为生成良好细化骨架所发现的最佳核集合。其他生成骨架的细化核也是使用上述核定义的。
但需要提醒的是,某些核,例如“ThinSE:44”,虽然被设计用来保持“连通性”,但实际上并不能保持线的端点,因此会导致骨架被剪枝为单个点,或一组相连的环。所有核都未定义中心原点值,这意味着这些“细化核”不仅可用于“细化形状”,也同样可用于“加粗形状”,从而生成SKIZ(影响区)。如果你仔细观察,可能会注意到每个4连通核实际上也以取反并旋转180度的形式存在于8连通集合中,反之亦然。例如“ThinSE:41”和“ThinSE:84”互为取反旋转。原因是4连通性和8连通性通过细化与加粗形态学方法(使用取反图像)之间的对偶性密切相关。本质上,一个保持4连通性的“细化核”,若用于对图像进行加粗,将会在形状周围生成一个8连通的背景骨架(未剪枝的SKIZ),反之亦然。因此,通过使用取反形式(从而使加粗与细化方法互换),你可以为同一操作生成另一种形式的连通性。


距离梯度形态学

“Distance”形态学方法是众多可用的专门方法中的第一个。它的作用是使用一个专门的核来测量每个前景像素与形状“边缘”的距离。更确切地说,它测量像素与“零”或“黑”颜色值的距离。不过它只适用于纯二值(黑底白形)形状,尽管稍后你会看到,你也可以修改抗锯齿形状使其能用于distance方法。而且只能用专门设计的距离核。距离核应用于图像时,会为每个像素分配“该距离处的最小像素值加上核值”。这是同时应用于整幅图像的,使用的算法不需要多次迭代,这一点与我们之前看到的形态学方法不同。正因如此,它的速度接近单一基本形态学操作,与细化骨架形态学方法相比快得惊人。由于它是应用于整幅图像的,因此不需要“iteration”参数,因为重复(迭代)相同的核操作不会对结果产生进一步的变化。 在IM v6.6.9-4之前,需要使用“-1”迭代次数,因为该核是使用类似普通Erode的方法应用的。现在已不再需要这样做,任何给定的非零“iteration”参数都会被简单地忽略。
下面是在我们的“man”形状上使用“Distance”方法的示例。
  magick man.gif -threshold 50% \
          -morphology Distance Euclidean:4 \
          +depth  distance.png

[IM 输出] [IM 输出]

好吧,这真是“激动人心”,才怪!问题在于最终图像的颜色非常非常暗。但如果你的显示器素质不错,仔细观察,也许能看到“man”所在位置有一个非常暗的“幽灵”般的形状。发生的情况是,至少对于这张小图像来说,所有像素都“靠近”边缘,因此得到的“距离”值都不大。 | _建议在使用“Distance”方法时使用PNG图像。这是因为,与例如GIF相比,PNG能提供更大的输出值“深度”,且不会像JPEG那样造成颜色损失。

这也是为什么使用深度设置“+depth”来确保输出被重置为16位深度(对于我使用的Q16版本IM),即使我读取的是8位的GIF源图像。

对于使用Q8版本IM的用户,我建议你阅读距离核(下文)中关于“scale”距离核选项的说明,以调整所使用的“缩放值”(见下一节)。不建议在Q8版本的IM中使用非整数距离核(例如这个欧几里得距离核),虽然这样做也能得到结果,但精度较低。

关于这两个方面更深入的理解,请参见IM Examples中的质量与深度一节。


默认情况下,内置的距离核会为每个像素应用“100 × {_像素距离
}”的颜色值。如果某个像素比这个值更亮,则会将其设为该值,从而确保为每个像素分配到其到任意边缘的最小距离。结果是,形状最边缘上的像素将被赋予比背景色多100个单位的值。再往内一层的像素则会再多100个单位。具体会分配多少单位,取决于所使用的距离核。所以让我们来看看上图中设置的最大颜色值。

  magick identify -verbose distance.png | grep max:

[IM 文本]

也就是说,结果图像中的最大颜色值是“1616”,这使得图像中“最亮”的像素成为一个非常暗的2.5%灰色,其距最近边缘的距离为16.16像素。换句话说,我们看到的是一张非常暗、但并非完全黑色的图像。 让我们使用数学运算“-auto-level”来调整结果颜色值,使最亮、即距边缘最远的像素设为白色。这样我们就能真正看到所生成“距离梯度”的完整效果。 |

  magick distance.png -auto-level  distance_man.gif

| 由于我们已不再关心该图像所生成的确切“距离”值,只关心距离效果的可见结果,因此现在可以使用GIF图像文件格式保存并显示该图像。
[IM 输出]
这就是“Distance”方法的作用:根据所使用的具体距离核,在给定形状上生成一个梯度,用以表示每个像素与最近边缘的距离。 另一种使生成的“距离”图像更亮的方法是实际使用更大的距离核“scale”值,例如3000单位的值(Q8用户大概可以使用20这个值)。 |

  magick man.gif -threshold 50% +depth \
          -morphology Distance Euclidean:4,3000     distance_scaled.gif

[IM 输出]
请注意,距离梯度并未覆盖从黑到白的整个范围,而是在某个灰度值处达到峰值。既然我们已经知道峰值的“距离”,就可以计算出该最大峰值为 16.16 * 3000 => 48480,约为74%灰。你也可以使用百分比缩放系数,例如为每像素与边缘的距离使用8%的颜色范围值。 |

  magick man.gif -threshold 50% +depth \
          -morphology Distance Euclidean:4,8%    distance_scale_percent.gif

[IM 输出]
如你所见,这次我们在达到最大距离之前就已经达到了最大值上限。你可以计算出距离梯度能覆盖的最大距离为 (100% at maximum range) / (8% per pixel) => 12.5 pixel_distance。当然,如果你使用的是HDRI版本的ImageMagick,完整的距离值将会保留在内存中,至少在你对其进行钳位,或将其保存为非浮点数图像文件格式之前是如此。你也可以使用特殊的距离缩放标志“!”直接指定你所关心的最大像素距离。既然我们已经知道我们的形状距边缘的最大距离为16.16,那就请求一个18像素的限制吧。 |

  magick man.gif -threshold 50% +depth \
          -morphology Distance Euclidean:4,'18!'   distance_range.gif

[IM 输出]
“!”标志会对距离进行缩放,使其在达到颜色范围限制之前给出“n”个灰度值。因此,值为1时,只会对紧邻图像边缘的像素进行“羽化”(即变灰)。如你所见,所有的缩放方法都在很大程度上依赖于你执行“Distance”方法的实际形状大小。太小则颜色会非常暗,可能无法满足你的精度需求。太大则距离可能会被你所用ImageMagick的编译时质量所能表示的最大颜色值“裁剪”掉。有关核中“scale”系数的更多细节,请参见下文的距离核一节。关于这些示例中使用的类似man的“形状”,我想再补充最后一点说明。该形状包含一个单像素“孔洞”,它在其周围产生了一种“梯度井”。这对最终“距离梯度”图像的上半部分产生了非常强烈的影响。解决这一问题的一种方法是使用“Close”移除该孔洞,使形状变得“干净平滑”。例如……

  magick man.gif -morphology Close Diamond  man_clean.gif
  magick man_clean.gif   -morphology Distance Euclidean \
                                    -auto-level   distance_clean.gif

[IM 输出] [IM 输出] [IM 输出]

当然,这样做也会“闭合”形状“双腿”之间的间隙,从而影响最终结果的下半部分。另一种解决方法是使用泛洪填充方法提取图像外部,并将其转换为新的蒙版。这样一来,任何孔洞都被闭合了,但图像的外部边界得以保留。例如……

  magick man.gif -gamma 0,1,1 -bordercolor black -border 1x1 \
          -fill red -floodfill +0+0 black -shave 1x1 \
          -channel R -separate +channel -negate  man_floodfill.gif
  magick man_floodfill.gif    -morphology Distance Euclidean \
                                    -auto-level   distance_floodfill.gif

[IM 输出] [IM 输出] [IM 输出]

距离核(Distance Kernels)

这里给出的核非常特殊,因为它是用来定义要赋给每个像素的实际距离测量值的。例如,这里是内置"距离核"之一的Show Kernel输出。

  magick xc: -define morphology:showkernel=1 -precision 3 \
          -morphology Distance:0 Chebyshev:3     null:

[IM 文本]

需要注意的重点是"原点"(在这个例子中是核的正中心)的值为零。这一点非常重要。这个"原点"被更大的值包围着,这些值随着离"原点"距离的增加而线性增加。如果核不是以这种特定的方式定义的,可能会产生意料之外的怪异效果。核中给出的值是实际会被加到已"知"距离上的"值",只有当该值小于已赋给某像素的值时才会被赋给该像素。结果就是,"白色"像素离边缘越近就越暗,离边缘越远(在先前赋值的基础上累加)就越线性地变亮。所有提供的内置距离核都可以接受两个可选的_k参数_…

     {_distance_kernel_}[:{_radius_}[,{_scale_}[%][!]]]

第一个参数和所有Shape Kernels一样,是核的_radius_,用来定义生成的核有多大。默认情况下,内置距离核的_radius_设为'1',产生一个非常小的3x3核,这在大多数情况下效果相当好。第二个参数'scale'设置了用来表示一个像素长度的距离比例。如上例所示,它默认值为'100'。也就是说,最终像素值或灰度值为'300'的像素,应该恰好离边缘'3个像素'远。 距离缩放 如前所述,使用较大的'scale'值是为了能用"小数"距离来实现更"精确"的距离测量。然而,只有提供的'Euclidean'距离核会用到这种"小数"值。在前面的例子中,赋给的"最大距离"值是'1700',这在ImageMagick的Q8版本中会溢出(参见Quality, in memory bit depth)。IM Q8只允许颜色值达到最大值255(2的Q次方 => 2的8次方 => 256个颜色值,范围从0到255)。因此,对于使用IM Q8编译时变体的用户来说,使用较小的_scale_,比如'10'或'20'会更好用。不过当与'Euclidean'核一起使用时,精度会差很多。因此,建议使用Q8版本IM的用户只使用其他"整数"距离核,比例因子设为'1'。 你也可以在缩放因子中加入'%',将距离缩放指定为完整颜色范围的百分比。也就是说,如果你使用颜色值范围的'12.5%'作为比例,那么在距离超出你所用IM版本的颜色范围限制之前,你能得到大约8个像素的距离度量。或者你也可以使用'!',表示比例是颜色范围的除数。也就是说,如果你指定比例为'20!',距离缩放将被设置为使颜色范围限制在离图像边缘20个像素处达到。不过即使用了这些"特殊缩放标志",Q8版本的IM仍会有严重的范围精度限制。它就是没有足够的范围来容纳许多距离运算所需的数据值。当然,对于HDRI版本的IM,任何_scale_(包括完整的浮点数)都可以被准确使用,因为最终的颜色值也是以浮点数形式存储的。只是在尝试将这样的图像保存为非浮点图像文件格式之前,一定要适当地重新缩放颜色范围。
提供了多种不同的距离测量核,其中有一种可以用两种不同方式使用。每个核都能提供不同的最终"距离度量",用来指定像素离边缘的距离,并基本上定义了什么应被视为"最近的边缘"。

切比雪夫(棋盘)距离核(Chebyshev (Chessboard) Distance Kernel)

'**Chebyshev**'距离核是最简单的,它规定"原点"周围的所有像素与其邻居之间都只有1个距离单位。也就是说,所有8个邻居都彼此"相邻"。因此,不仅紧邻的四个邻居距离为1个单位,对角线上的邻居也恰好是1个单位远。这常常被比作国际象棋中"王"或"后"在棋盘上移动时的格数距离,因此也常被称为'棋盘'距离度量。不过要注意,距离核默认使用_{scale}_因子为每像素距离100个距离单位。因此,离原点每走一步的距离就是100个单位。这也是前面例子中使用的核。以下是它实际生成的核…

  magick xc: -define morphology:showkernel=1  -precision 4 \
          -morphology Distance:0 Chebyshev       null:

[IM 文本]

这个核的名字来自俄国数学家帕夫努季·切比雪夫,他首先以数学方式描述了这种形式的距离测量。你可以在Wikipedia, Chebyshev Distance上找到更多关于这种度量的资料。使用'Chebyshev'距离度量时,像素的最终距离是到最近边缘的最大X或Y值。然而,由于对角线距离只有1个单位,图像内的最大距离通常比你预期的要小。让我们用这个核的"度量"生成一个"距离渐变"。不过为了能看清发生了什么,我们用较慢的'Iterative Distance'形态学方法,并使用无限迭代次数。 | |

  magick man.gif -threshold 50% +depth \
          -define debug=true -morphology IterativeDistance:-1 Chebyshev \
          chebyshev_gradient.png


  magick identify -format 'Maximum Distance = %[max]' chebyshev_gradient.png
  magick chebyshev_gradient.png -auto-level chebyshev_gradient.gif
  rm chebyshev_gradient.png

[IM 输出]
| [IM 文本]
[IM 文本]
| _'Iterative Distance'形态学方法通过反复施加距离核,直到值不再有变化为止来计算距离。

这比更常规的'Distance'方法要慢得多,后者用两遍扫描的方法一次性设定整幅图像的距离。不过'Distance'方法的详细输出就没那么有意思了。


我打开了Verbose标志,这样命令就会在每次迭代(遍历)图像时输出有多少像素(所有白色像素)被该运算改变。然后我提取出生成的"最大"距离('1400'),再对结果进行调整(归一化),生成一幅可以看到最终渐变效果的图像。最大距离'1400'是图像中最亮像素的值(实际上是一簇4个这样的像素)。这个信息是这个距离核(度量)最重要的结果,因为它代表了能嵌入这个形状内的最大正方形的尺寸。具体来说是半径14像素,或者说边长约为(R-1)*2+1 => 27像素的正方形,以那4个最大值像素为中心。由于这个核中所有的距离单位始终是'100'的倍数,所以这个最终的距离值应该总是'100'的倍数,绝不会有小数部分。基本上这个核会产生整数距离,你可以用这个核搭配简单的'1单位'_scale
,而不会损失任何距离信息。如果你使用的是Q8版本的ImageMagick,或是应用于非常非常大的图像,推荐这样做。 这里是形状"腿部"之间渐变的放大图,突出显示了生成的距离渐变的特征。 |

  magick chebyshev_gradient.gif -crop 25x20+39+69 +repage \
          -scale 500% chebyshev_magnify.gif

[IM 输出]
如你所见,'Chebyshev'距离核产生了非常方正的渐变。这是这种简单形式的距离度量的特有表现,直接反映了距离核本身的方形性质。上图还显示出图形"腹部"靠近图像顶部的4个最大距离像素。以这4个点中的任意一个为中心画一个正方形,就能生成完全包含在该图形内的最大奇数尺寸正方形。不过要注意,可能会存在多个这样的"峰值"。

曼哈顿(出租车)距离核(Manhattan (Taxi Cab) Distance Kernel)

'**Manhattan**'距离核通过将X和Y值相加到最近边缘来测量距离。这基本上就是当你被限制只能进行网格状移动时需要行进的距离,就像纽约曼哈顿这样的大城市街道上的出租车一样。正因如此,这种度量还有其他更常见的名称,如'出租车'或'城市街区'距离度量。你可以在Wikipedia, Manhattan Distance上找到更多资料。以下是它实际生成的核…

  magick xc: -define morphology:showkernel=1  -precision 4 \
          -morphology Distance:0 Manhattan     null:

[IM 文本]

注意,现在对角线的值为'200',即离中心2个单位。也就是说,要到达一个对角线像素,你必须按前面提到的网格状移动方式经过两个像素。因此,对角线距离往往比预期的要大,结果最终的距离测量值也往往更大。让我们再次用这种"度量"提取最大距离和"距离渐变"图像。 | |

  magick man.gif -threshold 50% +depth \
          -morphology Distance Manhattan      manhattan_gradient.png


  magick identify -format 'Maximum Distance = %[max]' manhattan_gradient.png
  magick manhattan_gradient.png -auto-level manhattan_gradient.gif
  rm manhattan_gradient.png

[IM 输出]
| [IM 文本]
这次我没有使用'Iterative Distance',而且即使用了,被改变的像素总数也不会准确。只有前面的'Chebyshev'核才会将像素距离设定一次且仅一次。注意这幅图像的最终最大距离要大得多,达到'1700'个距离单位,使得形状内的最大像素离边缘有17个像素远。这个距离核也是一个"整数"核,因此你可以将_scale_设为仅'1单位'而不损失信息。 这里是渐变的放大图。 |

  magick manhattan_gradient.gif -crop 25x20+39+69 +repage \
          -scale 500% manhattan_magnify.gif

[IM 输出]
如你所见,'Manhattan'距离核生成了一种菱形状的渐变,这基本上就是这种简单距离度量所代表的含义,实际的核值也反映了这一点。

八角形距离核(Octagonal Distance Kernel)

'**Octagonal**'距离核与前面两种略有不同。它是先对最靠近边缘的像素生成Manhattan距离,然后对离边缘2个单位距离的像素使用Chebyshev。接着再对3个单位距离的像素重复使用Manhattan距离,如此类推。结果就是使用这两种更简单的核所产生的距离的"交错"或"平均化"。由于这个核是基于两个整数距离核的交错,它本身也是一个整数距离核。因此对于较低质量的ImageMagick版本,或是非常大的距离测量,可以使用'1单位'的_scale_来产生较小的值。这个距离形状也是这两种核的混合,因此产生的效果相当于'Octagon'形状的核。以下是它实际生成的核…

  magick xc: -define morphology:showkernel=1  -precision 4 \
          -morphology Distance:0 Octagonal     null:

[IM 文本]

注意这个核的最小和默认尺寸是半径2,形成一个5x5像素的核。这个稍大一点的核是生成核"交错"所必需的。整体距离通常会比真实距离略小一点。这里我们再次计算最大距离… | |

  magick man.gif -threshold 50% +depth \
          -morphology Distance Octagonal  octagonal_gradient.png


  magick identify -format 'Maximum Distance = %[max]' octagonal_gradient.png
  magick octagonal_gradient.png -auto-level octagonal_gradient.gif
  rm octagonal_gradient.png

[IM 输出]
| [IM 文本]
'1500'这个结果是一个"整数"距离,实际上介于过小的切比雪夫距离和过大的曼哈顿距离之间。不过总体来说,它应该相当接近到形状中心的真实实际距离,同时仍保持"整数"值。 这里是渐变的放大图。 |

  magick octagonal_gradient.gif -crop 25x20+39+69 +repage \
          -scale 500% octagonal_magnify.gif

[IM 输出]
你可以清楚地看到在腿部间隙顶部周围形成的"八角形"距离。你还能看到对角线是用粗细相间的对角线交错生成的。

分数八角形距离核(Fractional Octogon Distance Kernel)

目前还没有提供带名字的距离核。不过它正好可以很好地融入我们正在研究的距离核序列中。 你可以使用八角形来生成另一种类型的整数距离核。不过在这种情况下,整数距离每像素使用的单位值为2,所以生成的距离值实际上需要减半,从生成的小整数中产生一个分数值。这就是"分数八角形"这个名字的由来。为此,我们在相邻像素之间使用整数距离2,对角线使用3。

'3: 3,2,3
    2,0,2
    3,2,3'

由于可以生成"半整数",可以使用的最小比例是'2单位'。虽然不如"骑士走法"精确,但效果也不错。这个核的八角形在正交方向上有"尖点",而不是前一个核生成的"平面"。它与下一个"骑士走法"核相关,但并不完全相同,可以认为是'knights'核的一种"近似整数"形式。如果你想像之前的IM距离核一样进行缩放,可以使用这个核。

'3: 150,100,150
    100, 0 ,100
    150,100,150'

这里是一个例子 | |

  magick man.gif -threshold 50% +depth \
          -morphology Distance '3:3,2,3 2,0,2 3,2,3' \
          fractional_gradient.png


  magick identify -format 'Maximum Distance = %[max]' fractional_gradient.png
  magick fractional_gradient.png -auto-level fractional_gradient.gif
  rm fractional_gradient.png

[IM 输出]
| [IM 文本]
'34'这个结果是一个"整数"距离,但需要除以2才能得到实际的最大距离结果17。不过虽然这也是一个整数,但它也很容易得出16.5这样的分数距离。这种距离结果的分数特性正是为什么大多数核都以100为单位来定义,并且随着我们逐渐脱离纯整数距离核,这种情况在后面的核中会变得更加普遍。 这里是渐变的放大图。 |

  magick fractional_gradient.gif -crop 25x20+39+69 +repage \
          -scale 500% fractional_magnify.gif

[IM 输出]
渐变(如果你仔细研究结果)呈八边形。但很难看出哪些像素具有相同的距离值。为了更清楚地展示这种形状,我将上面的图像中一组具有相同颜色值的像素涂成了红色。 |

  magick fractional_magnify.gif -fill red -opaque gray53 \
          fractional_magnify_shape.gif

[IM 输出]
如你所见,相同数值的像素通常以“骑士移动”模式分隔开,但会形成组成八边形的线条。不过这个八边形相对于八边形距离核旋转了45度。正是这种形状差异导致最终的最大距离值更大。本质上,以这种方式旋转的一个更大的八边形能更好地契合这个形状,因而得到更大的最大距离结果。另一种可用的分数距离“整数”核如下所示,不过其距离单位是3而不是2。也就是说,需要将结果除以3才能得到以像素大小为单位的距离,这个除数用起来并不算理想。不过它仍是另一种八边形类型的距离度量。

'3: 4,3,4
    3,0,3
    4,3,4'

下面的欧几里得(骑士移动)距离核也会生成一种八边形样式的形状(所有3x3的距离核都会如此),但会尽量在对角线方向上保持精确。这未必是最好的想法,但它是这类八边形距离核中数学逻辑上最合理的一种。

倒角距离核(Chamfer Distance Kernels)

目前尚未提供具名的距离核。但在我们研究的距离核序列中,此时正好可以引入它。 “倒角(Chamfer)”距离核(尚未实现),是仅使用(通常为整数的)数字来定义的,用于填充距离矩阵。例如,你可以给出2个数字来定义前面所述的任意3x3“八边形”类型距离核。以下是前面几个整数核的定义,Chebyshev Chamfer:1,1
Manhattan Chamfer:1,2
分数八边形 Chamfer:2,3
分数八边形(备选) Chamfer:3,4
--- ---
这些核都是简单的半径1核。给出的这些值可以视为其应使用的实际“距离缩放”值。不过要注意,前面的整数八边形核需要一个包含3个数字的半径2倒角核来定义它。最广为人知的倒角核是一个半径2的核,即“Chamfer:5,7,11”,它能生成非常精确的距离,并且也会生成整数距离值,因而非常适合Q8用户使用。传统上,这个核(chamfor 5,7,11)的形式是……
'5:  -   11   -   11   -
    11    7   5   7    11
     -    5   0   5    -
    11    7   5   7    11
     -   11   -   11   -'

或者将上面的核乘以20,以得到与ImageMagick距离核通常使用的相同像素距离缩放(100)……

'5:  -   220   -   220   -
    220  140  100  140  220
     -   100   0   100   -
    220  140  100  140  220
     -   220   -   220   -'
请注意,该核实际上并没有填满所有的核距离值。这是因为那些值可以由已提供的其他值推算得出。也就是说,要完整定义一个距离核,其实不必真正填满整个二维数组,不过为了处理方便,通常还是会这样做。以下是我在研究中发现的其他已知倒角核列表(仅使用整数值)。 Chamfer:3,4 /3
Chamfer:5,7,11 /5
Chamfer:99,141,221 /100
Chamfer:987,1414,2206 /1000
Chamfer:12,17,27,38,43 /12

[diagram]
如何放置5个值以定义一个半径3的倒角核

上表中高亮显示的核是最广为人知、也最常用的倒角距离核。它只使用较小的整数值生成图像,因此可以在不损失精度的情况下存储大距离梯度图像。它也非常精确,至少足以满足几乎所有可以想到的用途。此外还有一个额外好处:归一化后的距离结果不会产生循环小数,而只是一位小数分数。这也是它常被许多图像处理软件包选用的另一个原因。 "Chamfer:5,7,11" 应被视为默认的“倒角(Chamfor)”距离核。

欧几里得(骑士移动)距离核

'**Euclidean**'核是使用精确的浮点距离数值生成的。但为了使其能在非HDRI版本的ImageMagick中运作,需要使用分数对角距离。例如对角线的值为2的平方根,约为1.4142个距离单位。为了让这一切能够运作,这些距离都按(与上述所有核相同的)100进行缩放,从而生成一个分数百分比距离。以下是它生成的默认核……

  magick xc: -define morphology:showkernel=1  -precision 4 \
          -morphology Distance:0 Euclidean    null:

[IM 文本]

现在,使用默认的_半径_1,虽然在精确度上相较前面的核有很大提升,但仍然存在一些局限。基本上,它只能以45度对角线以及正交(X和Y)方向的移动来提供距离。也就是说,这些距离在某种程度上类似于国际象棋中的“骑士的移动”。以下是使用默认的'Euclidean'(即“骑士移动”)核所生成的最大距离和“距离梯度”图像。 | |

  magick man.gif -threshold 50% +depth \
          -morphology Distance Euclidean    knight_gradient.png


  magick identify -format 'Maximum Distance = %[max]' knight_gradient.png
  magick knight_gradient.png -auto-level knight_gradient.gif
  rm knight_gradient.png

[IM 输出]
| [IM 文本]
如你所见,对于这个特定的形状,你也会得到一个'1700'距离单位的值。通常,结果会是介于较小的'Chebyshev'距离和较大的'Manhattan'距离之间的某个分数距离。这次它恰好是'100'的一个简单倍数,并且碰巧也与'Manhattan'距离相同,纯属巧合。到某个像素的实际距离,其实是对角距离加上正交(轴向)距离之和。也就是说,这并不是完全意义上的欧几里得距离,但在能使用的最小距离核(半径为1)的前提下,它是最接近的结果。 下面是形状“腿部”之间梯度的放大图。 |

  magick knight_gradient.gif -crop 25x20+39+69 +repage \
          -scale 500% knight_magnify.gif

[IM 输出]
如你所见,这个梯度的外观要圆润得多,不会出现“整数”距离核中常见的那种像洋葱一样的“分层”或“台阶”效果。这是因为每个像素更有可能被赋予各自独立的分数距离,以表示到边缘的距离。不过梯度的最终形状其实大致仍是“八边形”的,只是像指南针一样呈现出尖角,而不是像之前的'Octagonal'距离核那样上下平坦。对于一般的距离处理工作(例如“羽化”),这种默认的'Euclidean'(骑士移动)核能给出不错的结果。然而,由于得到的不是“整数”距离,你无法以'1'的距离缩放系数来使用它,这使得它对Q8版本的ImageMagick用处不大。

更大的欧几里得距离核

通过增大生成的“Euclidean”核的_radius_(半径),可以得到更精确的“毕达哥拉斯”式或真正的“欧几里得”距离度量。radius(半径)越大,结果越精确,但形态学“Distance”方法运行所需的时间也会越长,不过所需的迭代次数会更少。然而,超过半径4之后,精度提升已经不大,但速度损失却会大幅增加。关于使用超大“Euclidean”核提升精度的一些示例,请参见下方的抗锯齿形状的距离。下面是使用推荐的半径4生成的真正“Euclidean”核,它会生成一个更大的9×9核……

  magick xc: -define morphology:showkernel=1  -precision 4 \
          -morphology Distance:0 Euclidean:4     null:

[IM 文本]

使用半径4的另一个好处是,该核还包含了毕达哥拉斯三角形,其边长为3、4、5,或者按默认核缩放来说,单位为300、400、500。尽管这可以减少结果图像中小数成分的数量,但实际影响很小。不过,为了获得更高的精度,这仍是一个合理的选择。下面是它的应用…… | |

  magick man.gif -threshold 50% +depth \
          -morphology Distance Euclidean:4     euclidean_gradient.png


  magick identify -format 'Maximum Distance = %[max]' euclidean_gradient.png
  magick euclidean_gradient.png -auto-level euclidean_gradient.gif
  rm euclidean_gradient.png

[IM 输出]
| [IM 文本]
由于使用了这个更大半径的“Euclidean”核,最终得到的最大距离是迄今为止最精确的最大距离测量值。而且,除非形状非常规则,否则图像中不太可能出现多个“最亮”像素。下面是形状“腿部”之间渐变区域的放大图。 |

  magick euclidean_gradient.gif -crop 25x20+39+69 +repage \
          -scale 500% euclidean_magnify.gif

[IM 输出]
正如你所看到的,渐变在“腿部间隙”末端周围产生了近乎完美的圆形渐变。正如上文所说,使用这种核的代价是运行速度稍慢一些。

距离核的比较

这里再次给出放大图的并排比较。这清楚地展示了这四种距离度量各自生成的差异很大的渐变。

[IM 输出]
Chebyshev
(棋盘) | [IM 输出]
Manhattan
(出租车) | [IM 输出]
Octagonal
(混合) | [IM 输出]
Euclidean
(骑士走法) | [IM 输出]
Euclidean
(半径=4)

这里是另一个比较,这次是计算距离左下角附近一个黑色像素的距离,且不对像素进行任何放大。

  magick -size 100x100 xc: -draw 'point 20,80'  distance_start.png

  for kernel in chebyshev manhattan octagonal euclidean euclidean:2 euclidean:4
  do
    magick distance_start.png    -morphology Distance $kernel \
            -auto-level  point_$kernel.png
  done

[IM 输出]
Chebyshev
(棋盘) | [IM 输出]
Manhattan
(出租车) | [IM 输出]
Octagonal
(混合) | [IM 输出]
Euclidean
(骑士走法) | [IM 输出]
Euclidean
(半径=2) | [IM 输出]
Euclidean
(半径=4)

这些图像清楚地展示了这样一些线条:沿着这些线,像素被认为比你预期的更接近起始像素,同时也展示了各个核随着复杂度提高而变得更加平滑。只有最后一个核没有显示出明显的“更近像素”线条,但即便在半径为4时,这些线条依然存在,只是对这个核而言,这些线条只出现在离源点很远的地方。半径为7时能得到更好的结果,但速度代价很大,不过有时为了避免结果图像中出现伪影,确实需要这种精度。请记住,只有前三种核会产生整数距离,这样才能在Q8版本的ImageMagick上使用(需要相应调整缩放比例,详见各核的说明)。而这几种正是许多图像处理软件包通常会使用的核。计算单个点的距离将在下面的受限距离示例中再次讨论。

特殊的用户自定义距离核

你并不局限于系统已经为你提供的那些距离核。只要遵守规则——在“原点”处使用零值,并让周围的距离值递增——你就可以生成其他非常有趣的距离效果。例如,这里我使用了一个非常小的用户自定义核,它的作用只是让右侧的每个像素的值变大。 |

  magick man.gif -threshold 50% +depth \
          -morphology Distance  '2x1+0+0: 0,100 ' \
          -auto-level    distance_linear.gif

[IM 输出]
请注意两腿之间的间隙所产生的效果,它“重置”了原本缓慢递增的渐变。下面我再从两侧分别生成一个距离渐变,但两侧使用不同的缩放比例! |

  magick man.gif -threshold 50% +depth \
          -morphology Distance  '3x1: 50,0,100 ' \
          -auto-level    distance_sides.gif

[IM 输出]
以上只是距离核变体的一些示例。如果你能想到其他变体,请告诉我们。注意如果你的图像输出效果不佳,很可能是核原点设置有误,或者原点不是"零"值。距离形态学函数不会对此进行检查。

带抗锯齿形状的距离运算

距离方法的效果非常好。但检验其功能的最佳方式,是将距离函数应用于一个圆形,然后对其进行渲染阴影,从而突显出函数可能产生的哪怕最细微的误差。

  magick -size 129x129 xc: -draw 'circle 64,64 60,4' \
          -negate  circle_shape.png

  magick circle_shape.png  -morphology Distance Euclidean:4 \
          -auto-level cone_distance.png

  magick cone_distance.png -shade 135x30 -auto-level \
          +level 10,90%  cone_distance_shade.png

[IM 输出] [IM 输出] [IM 输出]

从上面可以看出,虽然确实得到了一个看起来像"圆锥"的结果,但离平滑的"圆锥"还相差甚远。它表面覆盖着一圈从边缘开始向外辐射的棱纹网络。如果仔细观察阴影圆锥的边缘,你会发现这个圆锥并没有原始形状那种平滑的圆形底面。它的边缘存在明显的"锯齿"或"阶梯状"现象,正是这些"台阶"被距离函数反映出来,从而形成了我们看到的辐射状棱纹。问题在于,距离方法并不了解圆形边缘周围那些用来实现平滑观感的细小"灰色"抗锯齿像素。事实上,任何"灰色"像素通常都会被当作一个完整的像素,而不是它们所代表的经过抗锯齿处理的部分边缘像素。我们需要做的是设法将这些灰色边缘值纳入结果中,这可以通过在应用距离方法之前进行一个预处理步骤来实现。 |

  magick circle_shape.png  -gamma 2 +level 0,100 -white-threshold 99 \
          -morphology Distance Euclidean:4   -auto-level \
          -shade 135x30 -auto-level +level 10,90%   cone_antialiased.png

[IM 输出]
这里所做的是首先将所有那些灰色像素,从表示"该像素有多大比例位于圆形边界内部"的方式,转换为表示"该像素距离圆形边缘有多远"的方式。从结果可以看出,几乎所有阶梯状边缘误差都已被修复。剩下可见的误差只出现在远离边缘的位置,并且越靠近"圆锥"中心就越明显。这是由距离函数的迭代方式造成的——每次迭代都以4个像素(欧几里得距离核的大小)为单位反复进行。因此,随着离边缘越来越远,这些细微的不精确之处会被逐渐放大。这在大多数情况下通常不是问题,但你可以通过使用更大的欧几里得核来减小甚至消除这些细微误差,从而生成更精确、更平滑的结果。不过这样做会花费更长的生成时间。 |

  magick circle_shape.png  -gamma 2 +level 0,100 -white-threshold 99 \
          -morphology Distance Euclidean:7   -auto-level \
          -shade 135x30 -auto-level +level 10,90%   cone_improved.png

[IM 输出]
这个结果对于抗锯齿或平滑形状而言,几乎是完美的距离函数效果。这里再举一个例子,这次使用上一节中提到的两种用户自定义距离核。

   magick circle_shape.png -gamma 2 +level 0,100 -white-threshold 99 \
           -morphology Distance  '2x1+0+0:0,100'  -auto-level \
           circle_gradient.png

   magick circle_shape.png -gamma 2 +level 0,100 -white-threshold 99 \
           -morphology Distance  '3x1:50,0,100'  -auto-level \
           circle_ridge.png

[IM 输出] [IM 输出]

如果没有针对抗锯齿像素的特殊处理,上面的操作将无法在图像上产生如此平滑的渐变效果。不过目前我们仍需要恢复原始形状的"形态"。

使用距离运算为形状添加羽化效果

上述技术可以应用于形状的 Alpha 通道,从而为对象实现恰当的"羽化"效果。例如,下面是围绕一个形状对象生成的10像素"平滑"羽化效果。

   magick rose_orig.png \
           \( +clone -fill black -colorize 100% \
              -fill white -draw 'circle 114,75 110,2' \
           \) -alpha off -compose CopyOpacity -composite \
           -trim +repage rose_shape.png

   magick rose_shape.png \
           \( +clone -alpha extract -virtual-pixel black \
              -gamma 2 +level 0,100 -white-threshold 99 \
              -morphology Distance Euclidean:4,10! \
              -sigmoidal-contrast 3,0% \
           \) -compose CopyOpacity -composite \
           rose_feathered.png

[IM 输出] [IM 输出]

与更简单的模糊羽化技术不同,这种方法能精确保留所有边缘。我本可以直接对 Alpha 通道应用上述操作,但问题是某些运算符并不将透明度通道视为"alpha 值",而是视为"不透明度值"(具体来说是"-white-threshold"运算符)。因此我提取出了 Alpha 通道,将其作为灰度图像处理,然后再将处理结果合并回最终图像。这个特殊的距离核会对4像素的欧几里得核进行三次迭代,从而在形状边缘附近生成足够的距离渐变。接着"-level"运算符会将其转换为一条线性渐变,从边缘('0')延伸到形状内部10像素处('1000'个单位)。此外还设置了"-virtual-pixel",以确保任何触及矩形图像容器边缘的形状,也会被视为被透明区域所环绕。在这种情况下,距离函数的结果是一种"线性坡道"或"斜面",可能会产生一些看起来比较生硬的效果。因此,加入一个较小的"-sigmoidal-contrast"调整,可以使这种从透明到不透明的过渡变得平滑。强度值越高(上例中为'3'),边缘处的羽化效果就越锐利。如果你希望羽化效果更平缓地"渐变"到透明,可以将上面代码中的'0%'替换为'50%',将 S 形曲线的"肩部"设置在10像素羽化范围的中间位置。 位图形状羽化 如果形状是位图,例如来自 GIF 图像或图像蒙版,那么上述羽化操作可以进一步简化。例如……

  magick figure.gif -channel A -virtual-pixel transparent \
          -morphology Distance Euclidean:4,3!  boolean_feathered.png

[IM 输出] [IM 输出]

关键的变化在于,距离函数所需的前处理和后处理都大大减少了,你只需用'3!'指定一个特殊的距离单位即可。这将在形状边缘周围生成一个3像素的线性渐变。另一个此类羽化(更大范围的"线性羽化")的示例可以参见缩略图,柔化边缘。你可以在上述操作中使用"-sigmoidal-contrast"运算符来平滑更大范围的羽化效果,但请注意,目前它是将透明度作为"matte"值而非 alpha 值来处理的,因此应使用'100%'而不是前一个方案中的'05'。对于位图形状,更好的做法可能是先对 Alpha 通道应用"-blur 1x0.7"使其略微平滑,然后再对结果应用前面介绍的更复杂的距离羽化处理。


条件形态学或约束形态学

这里我们来看看一些将重复形态学操作约束或限制在图像特定区域或范围内的技术。基本上这些技术可以确保你不会"溢出"或超出某个限制或感兴趣区域进行增长。这通常需要某种"蒙版"图像,通常使用写保护蒙版来限制哪些像素会被更新。

条件膨胀

你已经知道,膨胀形态学方法会根据给定的核邻域扩展特定形状。"条件膨胀"本质上是同一件事,但设定了一个限制,规定了在对图像反复应用时膨胀可以扩散多远。从某种意义上说,绘制泛洪填充是终极的"条件膨胀"。它会简单地填充任何与起点颜色相同的正交(菱形核邻域)区域。例如,我们可以在若干圆盘中选择一个点,然后条件膨胀(泛洪填充),直到该圆盘被完全重新着色,从而将它与其他形状区分开来。

  magick disks.gif -fill red -draw 'color 60,60 floodfill' \
          cond_dilate_draw.gif

[IM 输出] [IM 输出]

同样,你也可以使用泛洪填充运算符来做同样的事,但只有当起点也匹配用户提供的"条件颜色"时才会填充。 |

  magick disks.gif \
          -fill green -floodfill +10+40 white \
          -fill blue  -floodfill +30+50 white \
          cond_dilate_floodfill.gif

[IM 输出]
在这个例子中,"green"泛洪填充"命中了一个圆盘"(并将其填充),而"blue"泛洪填充操作没有匹配,因此没有圆盘被填充。这种"泛洪填充"方法的问题在于,你只能从用户提供的单一点进行膨胀。不过它非常快,并且是对整个图像进行处理的。重复或迭代的膨胀与泛洪填充相同,但可以有多个起点,只是对它所提供的填充操作没有任何限制或边界的理解。要限制其效果,我们不仅需要提供"起点",还需要提供填充的"条件边界"。为此我们要创建一个写保护蒙版(图像中哪些部分是写保护的)。

  magick disks.gif disks.gif -morphology Erode:7 Diamond disks_big_center.gif

  magick disks.gif -negate disks_mask.gif

  magick disks_big_center.gif -write-mask disks_mask.gif \
          -morphology Dilate:15 Diamond +write-mask disks_big_found.gif

[IM 输出] [IM 输出] [IM 输出] [IM 输出]

在上面的例子中,我们首先使用腐蚀方法找出任何半径大于7(直径7x2+1 => 15像素)的圆盘。然后我们对找到的这些点进行相同幅度的"条件膨胀",以便"完美地"还原找到的对象。请注意,"条件膨胀"与使用开运算方法进行对象还原是非常不同的。那种方法会生成核的内部"菱形"形状,而不是原始对象的确切形状。它也无法处理"形状不规则"或"偏离中心"的种子点。迭代次数"15"并不是关键值,但应该足够大以完全还原对象。 | _请记住,在执行基本形态学操作时,使用较小的核(例如菱形或方形)配合迭代次数,要比使用大半径的更大核(例如圆盘)更好。

在进行条件膨胀时,这一点尤其重要,因为大的核实际上可能会"跳过"分隔多个对象的间隙。

_
| _不要在使用写蒙版时使用"-1"或(接近)无限的迭代次数。IMv7形态学目前不会意识到某些像素是不可写的,因此当它看到图像没有更多变化时不会中止,因为它总是能看到变化(这些变化永远不会被写入)出现在形状边缘周围。

这个问题将在IMv7中得到修复,该版本提供了一次重大的内部重构,让运算符在处理写保护像素时能更智能一些。

_
这里是另一个展示写保护蒙版多样性的例子。找出会被图像上一条对角线穿过的圆盘。

  magick -size 80x80 xc:black -fill white \
                        -draw 'line 0,0 79,79'   disks_line.gif

  magick disks_line.gif disks.gif \
          -compose Multiply  -composite    disks_line_find.gif

  magick disks_line_find.gif -write-mask disks_mask.gif \
          -morphology Dilate:15 Diamond +write-mask disks_line_found.gif

[IM 输出] [IM 输出] [IM 输出] [IM 输出] [IM 输出]

请注意这个额外的遮蔽步骤(使用乘法合成),确保只有那条线真正穿过的对象才会被"找到"。如果没有这一步,那些虽然靠近这条线、但只是处于"种子点"核半径范围内(在上例中只是接触到)的对象,也会被"膨胀"。当然,如果你想把"附近的对象"也包括进来,完全可以用合适半径的圆盘核对这条线进行膨胀(使其变宽),然后再进行最初的遮蔽步骤,这样比单纯依赖方形核半径能提供更精细的"邻近"控制。总而言之,条件膨胀可以被视为一种多点绘制泛洪填充,虽然速度较慢,但在精确选择应该填充或"发现"什么方面更加灵活多样。

约束距离

距离形态学方法可以很容易地用来求出对象内某点到边缘的距离。但它也可以用来求出对象内每个点到另一个点的距离。例如,这里我演示了如何求出每个点到单个"种子"点的距离(直线距离,即乌鸦飞行距离)……

  magick -size 100x100 xc: -draw 'point 20,80'  distance_start.png

  magick distance_start.png -morphology Distance Euclidean \
          -auto-level  distance_point.png

  magick -font Casual -pointsize 140 label:D \
          -trim +repage -gravity center -extent 100x100 \
          -threshold 20% distance_bounds.png

  magick distance_point.png \( distance_bounds.png -negate \)  \
          -compose multiply -composite  -auto-level distance_direct.png

[IM 输出] [IM 输出] [IM 输出] [IM 输出]

到某点的距离会被对象的形状遮蔽,因为这些才是我们感兴趣的唯一点。问题在于上面的距离渐变表示的是到"起点"的"直线"或直接距离。仅仅用对象来遮蔽这个渐变并不会改变这一点。不幸的是,这通常并不是用户在"到起点的距离"方面所想要的结果。如果这个形状代表一座岛屿,而你想从岛的一端"直线"前往另一端,那你会被淋得相当湿。 你真正想要的是被限制在"对象内部"路径中的距离。也就是说,你希望这个距离被对象本身"约束",并沿着对象内部可能的最短路径行进。为此我们可以设置一个写保护蒙版,这样在计算距离渐变时就不会跨越(写入)图像的"禁止通行"区域或背景。

  magick distance_start.png -write-mask distance_bounds.png \
          -morphology IterativeDistance:150 Euclidean \
          +write-mask -fill black -opaque white -auto-level \
          distance_constrained.png

[IM 输出] [IM 输出] [IM 输出]

如你所见,结果是一个正确的"跨图像距离"渐变,清楚地显示出距离起点(白色)最远的位置是位于该孤岛另一端,恰好与缺口对面"一箭之遥"的地方。
请注意,我这里并没有简单地使用普通的距离方法,而是使用了更底层的"Iterative_Distance"形态学方法。普通的距离方法是一种特殊的两遍扫描快速距离方法,它是应用于整幅图像的。正因如此,写入蒙版无法限制其沿像素"行"方向的动作,也就是说写入蒙版几乎不起作用。结果就是"Distance"会有倾向"跳过"水平方向的缺口,而不理会写入蒙版。也就是说,普通的距离方法并不能被正确地"约束"住。而"Iterative_Distance"方法的工作方式则更像是简单的基本形态学方法,它只是逐步地作用于局部邻域。它其实更像是一种渐变形式的"膨胀",实际上与真正的灰度形态学方法关系密切。由于它是以较小的"增量步骤"来处理图像的,"Iterative_Distance"方法会受到写入蒙版的"约束"。遗憾的是,它同时也慢得多。上例执行了150遍处理,而不是普通方法的两遍,这个次数是由传给形态学方法的迭代次数给出的。最好尽量将这个迭代次数保持得尽可能小,但又要足够大,以覆盖图像中可能出现的最大距离。 | _在使用写入蒙版时,不要使用'-1'或(接近)无限的迭代次数。IMv7的形态学运算并不理解某些像素是不可写的,因此当它看到形状边缘周围"不可写像素"仍在变化时,就不会因为没有更多变化而中止运算。

希望这个问题会在IMv7中得到修复,因为IMv7提供了一次重大的内部重构,让运算符可以变得更聪明一些,能够理解某些像素是不可写的,从而避免计算它们,或者不把它们计入"已变化"之列。

_
最后你会注意到,我使用的距离核半径只有1,尽管这样的核并不是很精确。这一点很重要,因为更大的核可能会导致距离渐变跳过任何小于其半径的缺口。参见上面的条件膨胀。如果需要更高的精度,那么你需要确保没有任何缺口小于你想使用的距离核。这也包括图像边缘因急转弯而产生的缺口。


生成形状的骨架

建设中

摘自 HIPR2 Morphology
http://homepages.inf.ed.ac.uk/rbf/HIPR2/morops.htm


骨架/MAT 主要可以通过两种方式生成。第一种是使用某种
形态学细化方法,逐步从边界侵蚀掉像素
(同时保留线段端点),直到无法继续细化;
此时留下的部分就近似于骨架。

另一种方法是先计算图像的距离变换。
随后骨架位于距离变换中的奇异点(即折痕或
曲率不连续处)上。后一种方法
更适合用来计算 MAT,因为 MAT 与距离
变换相同,只是把所有不属于骨架的点都抑制为零。

注意:MAT 通常被描述为距离变换上的"局部最大值轨迹"。
严格来说,这在"局部最大值"这个短语的通常意义上
并不真正成立。如果将距离变换显示为三维曲面图,
第三维表示灰度值,那么 MAT 可以想象为
三维曲面上的脊线。



定义??

形态学骨架(通过侵蚀?)(通过细化)

骨架可以通过反复细化,或通过距离
变换并寻找三维表面上的“折痕”或脊线(分水岭
变换?)来计算。

   mat.gif -morphology HMT Ridges           -threshold 0
   mat.gif -morphology HMT LineEnds         -threshold 0
   mat.gif -morphology HMT Ridges\;LineEnds -threshold 0

   mat.gif -morphology HMT Ridges\;Ridges2  -threshold 0

   mat.gif -morphology TopHat Diamond              -threshold 0

   mat.gif -define morphology:compose=Lighten \
              -morphology TopHat '3@:-,1,- -,1,- -,-,-' -threshold 0


中轴变换(MAT)的一种定义使用每个点的强度
来表示到边界的距离。也就是说,将骨架用作
距离变换的蒙版。距离变换方法更适合
这一用途,并且计算起来可能比细化更快。

SKIZ(影响区骨架)是背景的骨架,是
该运算的负片。也就是说,把区域划分为最接近各个
前景对象的部分。(通过增厚生成)

通常,SKIZ 还会通过侵蚀线段端点
剪除为简单区域或盆地,除非端点连接到图像边缘。

通过骨架识别形状。
   distance between farthest 'end' points,
   number of 'loops' or regions in image,
   number of triple points.

到骨架的距离

从图像生成原始“形态学骨架”的一种简便做法,是把 'TopHat' 方法应用到距离梯度上。例如,这里先对形状稍作开运算,让轮廓平滑一点,然后生成它的骨架。 |

  magick man.gif \
          -morphology Open  Diamond \
          -morphology Distance  Chebyshev \
          -morphology TopHat Diamond \
          -auto-level    chebyshev_dist_skel.gif

[IM 输出]
这基本上就是一个形态学骨架。它只显示可以找到最大“方形”(更常见的说法是“最大球”)的位置,所以看起来并不完整。不过它仍然是非常原始的结果,包含许多孤立像素。令人意外的是,它确实能工作。如果没有 'Open',由于形状轮廓很粗糙,结果会非常糟。使用 Euclidean 距离度量可以得到更好的形状骨架。 |

  magick man.gif \
          -morphology Open  Diamond \
          -morphology Distance Euclidean:4 \
          -morphology TopHat Diamond \
          -auto-level    euclidean_dist_skel.gif

[IM 输出]
但如你所见,噪声也更多;这个骨架虽然更完整,却也很脏,包含许多灰度值。下面是骨架“头部”的放大图,显示它仍然是不连续的。 |

  magick euclidean_dist_skel.gif -crop 35x28+30+13 +repage \
          -scale 400%   euclidean_dist_skel_mag.gif

[IM 输出]
当然,上面的结果可以阈值化,并作为蒙版与生成它时使用的实际距离梯度一起使用。这样就能查出构成骨架的各个最大圆盘的实际大小(距离半径),从而重新创建原始形状。

使用 Autotrace 生成骨架

另一种骨架生成方法,是使用 "AutoTrace" 程序及其特殊的中心线选项。注意,由于它与印刷和字体转换有关,处理时假定为白底黑字。例如……

  magick man.gif -negate man_at_prep.pgm
  autotrace -centerline -output-format svg man_at_prep.pgm |\
      magick SVG:-  man_centerline.gif
  magick man.gif man_centerline.gif \
          -compose multiply -composite man_at_skeleton.gif

[IM 输出] [IM 输出] [IM 输出] [IM 输出]

注意,由于该过程具有矢量性质,生成的中心线是平滑曲线。同时它也是不连续的,骨架环路中有缺口,分支也会断开。不过我还没有研究 “autotrace” 实际上是如何生成这个骨架的。有关使用 "AutoTrace" 的其他示例,请参见 SVG 输出处理 和 栅格到矢量边缘。