本篇博客介绍的形态学图像处理操作。形态学在数学语言里是集合论。在图像处理中,形态学是很重要的分支,但是它又不像傅里叶变换等内容,包含详细的公式推导。通常形态学操作的数学表达都比较抽象,不太容易理解。因此,本篇博客打算以文字与实践相结合的方式进行阐述。
本篇文章内容中,所有具有处理结果图像的形态学操作均是本人根据教材自己编写的代码,由于代码较多,之后会放在个人github中,如在公布之前有需要的读者,可以留言说明。
本篇内容主要知识和相关操作图片来自冈萨雷斯第三版的《数字图像处理》,如对细节内容感兴趣的读者可以查阅相关书籍进行参考。本篇博文的内容与原书内容的顺序有所出入,如有不足欢迎留言指正,谢谢。
其实结构元(SE,Structure Element)最直接的理解就是卷积操作中的卷积核,或者是空间域滤波中提到的滤波器。虽然形态学操作中结构元的形状可以是任意的,但是由于在图像操作中,为了方便计算,通常要求结构元是矩形的阵列,对于任意形状的结构元,如果不满足矩形的要求,则用0将其填充为矩形即可。
另外,结构元内部的有效元素不像滤波器那样有权值,通常结构元中只分为两种元素,就是0和1,不会出现其他数值的系数。(当然对有些算法来说也有例外)。结构元对图像进行的操作也和卷积非常类似,就是由结构元的中心依次滑过图像,然后进行设计好的操作即可。接下来就开始介绍图像处理中常见的形态学操作。
形态学基本操作是形态学最重要的基础,几乎所有复杂的形态学操作都由基本形态学操作组合而成。在本章节中,主要介绍的是二值图像的形态学操作,因此本章中默认图像都是二值图像,且由0表示背景像素,1表示前景像素。
下面结合集合论的数学表达以及实际使用案例对各种操作进行介绍。
1).腐蚀(erode)
A ⊖ B = { z ∣ ( B ) z ⊆ A } ⇔ A ⊖ B = { z ∣ ( B ) z ∩ A c = ∅ } A\ominus B=\{z|(B)_z\subseteq A\} \Leftrightarrow A\ominus B=\{z|(B)_z\cap A^c=\empty\} A⊖B={z∣(B)z⊆A}⇔A⊖B={z∣(B)z∩Ac=∅}
其中, A c A^c Ac是图像A的补集,即1-A。上式表达的意思就是,在z的移动下,如果结构元B能够完全包含在图像A中(完全包含意味着结构元B中的有效元素对应于图像A中的位置全都是前景像素。z的移动就是针对于图像A的所有像素范围的移动。
上图中,左边实线围住的9x9方格就是结构元B,红色代表其有效区域,白色代表其无效区域(0填充)。右侧实现代表图像A,浅蓝色部分代表A的前景像素,白色代表背景像素。当结构元滑动到1区域时,所有有效区域对应的像素都是前景像素(紫色区域,红+蓝),因此称此时 B ⊆ A B\subseteq A B⊆A,此时,1号位置在结果图像中应该置1。反之,如果只有部分区域重合,如结构元处于2号位置,那么说明此时的2号点不满足腐蚀条件,应该在结果图像中置0。
下面是腐蚀操作对实际图像的处理效果。
2).膨胀(dilate)
A ⊕ B = { z ∣ ( B ^ ) z ∩ A ≠ ∅ } ⇔ A ⊕ B = { z ∣ [ ( B ^ ) z ∩ A ] ⊆ A } A\oplus B=\{z|(\hat B)_z\cap A\ne\empty\} \Leftrightarrow A\oplus B=\{z|[(\hat B)_z\cap A]\subseteq A\} A⊕B={z∣(B^)z∩A=∅}⇔A⊕B={z∣[(B^)z∩A]⊆A}
其中, B ^ \hat B B^称为结构元B的反射,指的是,结构元B绕结构元的中心(通常设置在SE矩阵中心)旋转180°后的新结构元。对于对称的结构元,就有 B = B ^ B=\hat B B=B^。
膨胀的集合含义就是,SE和图像的前景像素至少有一个交点,那么当前移动到的像素就置1,否则置0。在膨胀的条件下,之前图示中的1,2号区域均会被置1。(腐蚀时,2号区域是被置0的)。
下面是膨胀操作对实际图像的处理效果。
3).腐蚀和膨胀的对偶性
开操作和闭操作都是基于腐蚀和膨胀操作的。
1).开操作(opening)
A ∘ B = ( A ⊖ B ) ⊕ B A\circ B=(A\ominus B)\oplus B A∘B=(A⊖B)⊕B
开操作就是先用SE腐蚀图像A,再对腐蚀后的结果进行膨胀。第一反应或许会觉得,这操作不是很多余吗?其实细想后发现,其中自有妙处。假设图像中存在很多细小琐碎的颗粒或噪声,那么经过第一步腐蚀后,这些细小的噪声就会被腐蚀而消失,尽管紧接着进行了膨胀操作,但是由于细小颗粒已经完全消失,因此即使膨胀也无法恢复。相反,那些图像中的主体,经过腐蚀后仍然存在,再经过膨胀就可以恢复原样。这么一说,开操作是不是非常有用呢?仔细理解形态学操作的含义非常重要,虽然形态学操作单独拆开看都不是很复杂,但是要在应用中发挥最巧妙的效果,需要使用者对形态学各种操作能得到的效果进行深入理解。否则只会弄巧成拙或完全无法发挥形态学图像处理的威力。
开操作的性质:
以下是开操作的实际操作效果:
2).闭操作(closing)
A ∙ B = ( A ⊕ B ) ⊖ B A\bullet B=(A\oplus B)\ominus B A∙B=(A⊕B)⊖B
闭操作和开操作正好相反,是先膨胀再腐蚀。如果图像中存在细小的孔洞或者缝隙,通过膨胀后就可以填充细小孔洞和缝隙,使其和主体连接起来,而再腐蚀的时候,只会腐蚀图像主体,而不会再恢复缝隙等瑕疵。因此,闭操作很适合拿来修补图像瑕疵的。
闭操作的性质:
这个基础形态学变换就没有之前几个那么直观了。理解起来比较抽象。首先,假设有个结构元 B 1 B_1 B1, B 1 B_1 B1的有效元素共同组成了特定的形状。其次,设W是一个比 B 1 B_1 B1尺寸更大的全1模板,记 B 2 = W − B 1 B_2=W-B_1 B2=W−B1,即从模板W中将 B 1 B_1 B1挖去。然后,击中和击不中变换就可以表达为:
A ⊛ B = ( A ⊖ D ) ∩ [ A c ⊖ ( W − D ) ] = ( A ⊖ B 1 ) ∩ ( A c ⊖ B 2 ) A\circledast B=(A\ominus D)\cap[A^c\ominus(W-D)]=(A\ominus B_1)\cap (A^c\ominus B_2) A⊛B=(A⊖D)∩[Ac⊖(W−D)]=(A⊖B1)∩(Ac⊖B2)
击中和击不中变换的直观理解:
假设有图像A(如下图1), A = C ∪ D ∪ E A=C\cup D\cup E A=C∪D∪E,表示A图像中有C,D,E三个前景子区域。如果我们想要在图像A中寻找是否存在形状D(图1中的45x45的正方形),怎么办?这时候,我们就可以将结构元 B 1 = D B_1=D B1=D,然后选择合适的W求出 B 2 = W − D B_2=W-D B2=W−D,对图像进行击中和击不中变换,变换所得到的结果,如果A中存在和D一样的形状,则会留下一个点,该点是D形状所在位置的中心;如果不存在D,那么得到的图像就是空。
其中,击中和击不中变换中的 A ⊖ B 1 A\ominus B_1 A⊖B1是在图像中找到与 B 1 B_1 B1匹配的结果,即击中结果,如下图2。注意,击中并不代表说A图像中有与 B 1 B_1 B1一模一样的形状,比如左侧长方形,其中包含形状D,但是并不与D完全相同。所以才需要进一步进行击不中变换。而 A c ⊖ B 2 A^c\ominus B_2 Ac⊖B2中的 B 2 B_2 B2可以理解为一个框架的中间掏出一个与D形状一模一样的空缺(见下图5,黑色区域为45x45,白色区域为47x47),框架的白色区域通常设置为1个像素宽,图中为了显示而故意夸大了。然后将该框架试图通过图像A,白色区域表示实体,框架的实体和图像中的实体实会相互阻隔而无法通过的。因此可以看到,图像A中只有比形状D小的实体可以穿过 B 2 B_2 B2,这些 B 2 B_2 B2能穿过的地方的集合,就叫做击不中区域,如下图3中的白色区域,就代表框架 B 2 B_2 B2的中心对准白色区域任意点时,框架可以穿过图像(或者说图像A中的实体击不中框架)。
综上所述,将击中结果和击不中结果相交,最终所得到的结果为下图4。相交的结果直观点可以理解为,既可以被D形状完全击中,又可以被以D形状为空的框架完全穿过的实体,那就肯定只能是和D形状一模一样的实体。图4中可以看到一个白点,这就说明图像A中存在与形状D一模一样的区域,且该区域中心的位置就是图4白点所在位置。
击中和击不中变换更常用的场景——击中变换
在某些场合中,我们可能对检测某个集合内由1和0组成的特定模式感兴趣(这种情况下,要求结构元中的1对于图像的前景元素,结构元中的0对应图像的背景元素,结构元中的x则表示对该区域不感兴趣,可以为任意元素),此时,击中与击不中变换就不需要击不中变换(因为前景和背景都属于我们感兴趣的范围),而简单地转换成了特定模式的腐蚀操作。(和原始的腐蚀不同,原始的腐蚀的结构元中,结构元中为0的部分相当于这里的x,它不要求对于的图像像素一定是背景元素,而可以是任意元素)。
以下图为例,假设此时的结构元为下图左的样子,其中红色为1区域,灰色为0区域,x为不感兴趣区域。那么,此时图像要匹配,则需要满足SE的红色区域对应的图像都为前景像素,灰色区域对应的图像均为背景像素,而x区域对应的图像则不感兴趣,不做要求。如果满足上述条件,则将当前位置的结果图像置1,或者说是结构元SE击中图像/在图像中找到了匹配。下图中间的实线区域代表图像。下图右侧的实线区域中,1号位置表示在图像中找到了SE的匹配;而2号区域,中间正上方和正下方不满足需要为背景图像的要求,因此该位置并未找到匹配。
有了形态学的基础操作,就可以将其进行组合得到基本的形态学算法,以下一一介绍。同样,下述算法都是基于二值图像的操作。基于灰度图像的形态学算法之后还会介绍。
β ( A ) = A − ( A ⊖ B ) \beta(A)=A-(A\ominus B) β(A)=A−(A⊖B)
其中, β ( A ) \beta(A) β(A)是提取出来的A图像的边缘图像;B是适当的结构元,如果B尺寸比较大,那么通常提取到的边缘就会比较粗。稍微观察一下就会理解,二值图像的边缘其实就是图像A被腐蚀掉的部分。因此只要腐蚀前和腐蚀后的图像相减,就可以得到边缘。具体效果见下图:
算法步骤:
为什么迭代公式中要存在" ∩ A c \cap A^c ∩Ac "这一步?这称为“条件膨胀”,有兴趣的读者可以试试去掉这一步,那么最终的孔洞膨胀的结果将会填满整个图像,得到一张全白的图。通过与 A c A^c Ac进行交集的操作,保证我们膨胀的过程中, X k X_k Xk图像始终不会将原图像的前景图像覆盖,也就是用原图像的前景像素限制了膨胀的空间,所以才叫条件膨胀。
下面是对图像进行孔洞填充的示意图,左图为原图,右图为填充后的结果。中间的动图是填充过程。我只取了上半部分的孔洞进行填充,所以最终并没有把所有孔洞都填满(初始化标记孔洞位置太麻烦了)。另外,可以看到,其中有个孔洞的中间是有个白点的,而在填充时这个白点也没有被填(动图中的小黑点),所以可以从中理解条件膨胀的含义。
其实算法和孔洞填充一样,只是第一步初始化的时候,选择的标记像素点是想要提取的连通分量中的任意一点。第三步迭代的时候,条件膨胀从A的补集替换为A本身。这是很直观的,因为我们现在想要提取的是图像前景,因此需要将膨胀的元素限制在图像前景范围内。
算法步骤:
如果你只想要提取图中某个连通区域,不希望提取其他区域,并且可以确认该连通区域一定会覆盖图像中某一点,那么这种算法可以很好地完成任务,提取想要的连通部分。
连通分量的提取实践如下,使用的原图像是孔洞填充的原图像,只提取了其中两个较小的圆环:
定义:如果集合A内任意两个点的直线段都在A的内部,则称集合A是凸形的。任意集合S的凸壳H是包含S的最小凸集。差集H-S称为S的凸缺。
算法步骤:
上述过程有两点说明,均很重要!
1). 算法中的 ⊛ \circledast ⊛ 是仅包含击中变换的击中和击不中变换,关于什么是仅包含击中变换,可以参考2.3节最后的解释;
2). 原《数字图像处理》教材中,迭代公式是 “ X k i = ( X k − 1 ⊛ B i ) ∪ A X_k^i=(X_{k-1}\circledast B^i)\cup A Xki=(Xk−1⊛Bi)∪A” ,经验证后发现是错的,迭代公式应该是 “ X k i = ( X k − 1 ⊛ B i ) ∪ X k − 1 X_k^i=(X_{k-1}\circledast B^i)\cup X_{k-1} Xki=(Xk−1⊛Bi)∪Xk−1” 。读者也可自行验证。
上述算法有一个明显的缺点,就是求出的凸壳可能不是包含原图像的最小凸集。优化最终结果有个常用的办法,就是让求出的凸壳不超过初始点集在水平和垂直方向上的尺寸。当然,也可以针对求出的结果进行进一步的限制生长,但是通常会损失算法效率,需要慎重考虑。
凸壳算法的具体实践如下图,图1是原始图像,图2动图是整个求解凸壳的过程,动图可按2行3列观看,其中第一行第一列是 B 1 B^1 B1的迭代过程,第二行第一列是 B 2 B^2 B2的迭代过程,第一行第二列是 B 3 B^3 B3的迭代过程,第二行第二列是 B 4 B^4 B4的迭代过程,第一行第三列是每次迭代结束后叠加在一起的 C ( A ) C(A) C(A)。