南京网站建设润洽,网站关键词排名快速提升,越秀区网站建设,官方网站下载派的app前言
继续学习图像里面的形态学知识——结构元、腐蚀、膨胀、开运算、闭运算、击中/不击中变换。以及部分基本形态学算法#xff0c;包括边界提取、空洞填充、连通分量的提取、凸壳、细化、粗化、骨架、裁剪、形态学重建。
其实就是对冈萨雷斯的《数字图像处理》中第9章节《…前言
继续学习图像里面的形态学知识——结构元、腐蚀、膨胀、开运算、闭运算、击中/不击中变换。以及部分基本形态学算法包括边界提取、空洞填充、连通分量的提取、凸壳、细化、粗化、骨架、裁剪、形态学重建。
其实就是对冈萨雷斯的《数字图像处理》中第9章节《形态学处理》的简要理解。
如果你认为腐蚀是减小白色区域膨胀是扩充白色区域请务必看本博客注意不同结构元的结果。
参考博客 OpenCV官方的形态学运算文档
冈萨雷斯的《数字图像处理》第9章
某位大佬的形态学总结
理论与实践
结构元
结构元实际就是一个自定义的矩阵在书中通常称为集合是研究一幅图像中感兴趣特性所用的小集合或者子图像。结构元通常有反射和平移两个操作。假设一个集合(结构元)定义为B那么
反射定义为B^\hat{B}B^是B中的坐标(x,y)(x,y)(x,y)被(−x,−y)(-x,-y)(−x,−y)替代。平移定义为(B)z(B)_z(B)z是B中的坐标(x,y)(x,y)(x,y)被(xz1,xz2)(xz_1,xz_2)(xz1,xz2)替代。
同时结构元还有一个原点这在opencv中叫anchor后面腐蚀膨胀的操作都是更改原点对应的原图像素。
【注】不要小看结构元其设计直接影响到最终效果这也是为什么开头说“腐蚀减小白色区域膨胀扩充白色区域”是错误观点因为一切以公式和结构元为准。依据不同的任务设计不同的结构元才是我们关注的点比如垂直方向的细节需要细化或者粗化应该用什么结构元采用什么操作。
腐蚀
操作
将结构元在目标图像上从左往右从上往下平移平移过程中结构元中值为1的位置对应的图像像素都是1则结构元原点对应位置的像素为1否则为0。注意平移的起点以结构元原点(中心)为准所以一般来说需要对图像做padding这样才能保证平移的起始位置让结构元原点对齐图像的左上角第一个像素。
公式
若结构元为E图像为A那么腐蚀的公式表示就是 A⊖E{z∣(E)z⊆A}A\ominus E\{z|(E)_z\subseteq A\} A⊖E{z∣(E)z⊆A} 作用
将小于结构元的图像细节从图像中滤除了腐蚀缩小或者细化了二值图像中的物体。禁止说消除或减小白色区域说的时候可以加个可能因为结构元对结果会有很大的影响。
实现
代码表示就是
opencv的调用方法
result cv2.erode(src,kernel,iterations1,borderTypecv2.BORDER_CONSTANT,borderValue1)使用numpy复现
def erod(img,kernel):ksize kernel.shapecenter(int(ksize[0]/2),int(ksize[1]/2))img_pad cv2.copyMakeBorder(src,center[0],center[0],center[1],center[1],borderTypecv2.BORDER_CONSTANT,value0)new_img np.zeros_like(img)ele_idx np.argwhere(kernel1)for i in range(img.shape[0]):for j in range(img.shape[1]):block img_pad[i:iksize[0],j:jksize[1]]if(block[ele_idx[...,0],ele_idx[...,1]].all()1):new_img[i,j] 1else:new_img[i,j] 0return img_pad,new_img随便贴两个结果建议手推一遍 【注】很明显第一张图的结构元对图像的腐蚀得到的结果仅仅是将图像向右平移一个像素并没有出现减小白色区域的效果。
膨胀
操作
将结构元在目标图像上从左往右从上往下平移平移过程中结构元中值为1的位置对应的图像像素至少有一个为1则结构元原点对应位置的像素为1否则为0。
公式
若结构元为E图像为A那么膨胀的公式表示就是 A⊕E{z∣[(E)z∩A≠∅]}A\oplus E \{z|[(E)_z\cap A\neq \varnothing]\} A⊕E{z∣[(E)z∩A∅]} 作用
增长或粗化二值图像中的物体通常可以用于桥接裂缝。
实现
def dilate(img,kernel): ksize kernel.shapecenter(int(ksize[0]/2),int(ksize[1]/2))img_pad cv2.copyMakeBorder(src,center[0],center[0],center[1],center[1],borderTypecv2.BORDER_CONSTANT,value0)new_img np.zeros_like(img)ele_idx np.argwhere(kernel1)for i in range(img.shape[0]):for j in range(img.shape[1]):block img_pad[i:iksize[0],j:jksize[1]]if(block[ele_idx[...,0],ele_idx[...,1]].any()1):new_img[i,j] 1else:new_img[i,j] 0return img_pad,new_img【注】看第一幅图的腐蚀结果和膨胀结果惊不惊喜意不意外刺不刺激竟然一模一样是否颠覆了自己对腐蚀和膨胀的认知。但是如果你按照公式手推一遍会发现完全没毛病。
开运算
操作
先进行腐蚀再进行膨胀
公式 A∘B(A⊖B)⊕BA\circ B(A\ominus B)\oplus B A∘B(A⊖B)⊕B 作用
平滑物体轮廓断开较窄的狭颈并消除细的突出物。
实现
kernel np.ones((7,7),np.uint8)
# 自带的
img_open1 cv2.morphologyEx(img_bin, cv2.MORPH_OPEN, kernel)
# 先腐蚀后膨胀
open_tmp cv2.erode(img_bin,kernel)
img_open2 cv2.dilate(open_tmp,kernel)可以发现白色线条部件了而且五角星的五个角更加平滑。此时注意云朵并没有任何变化。
闭运算
操作
先进行膨胀再进行腐蚀
公式 A∙B(A⊕B)⊖BA\bullet B(A\oplus B)\ominus B A∙B(A⊕B)⊖B 作用
同样能够平滑轮廓弥合较窄的间断和细长的沟壑消除小孔洞填补轮廓线中的断裂。
实现
## 闭运算
kernel np.ones((7,7),np.uint8)
#自带
img_close1 cv2.morphologyEx(img_bin, cv2.MORPH_CLOSE, kernel)
close_tmp cv2.dilate(img_bin,kernel)
img_close2 cv2.erode(close_tmp,kernel)发现左下角图像的内部黑线没了而且云朵的轮廓被平滑了并且尾巴连在一起了说明能够弥补断裂部分。
【注意】开运算平滑的轮廓是指白色区域向黑色区域的凸出尖角而闭运算的平滑轮廓是指黑色区域向白色区域凸出的尖角也就是它俩的白色尖角一个凸一个凹。
击中和不击中
操作
如果图像中有A、B、C三个形状D为其中一个形状如B被小窗口包围的图像击中和不击中操作就是
用D对图像进行腐蚀用D中B的补集对D中ABC集合的补集进行腐蚀对上述两个腐蚀操作的结果图像进行求交集
即可利用D击中图像中的B。
公式
设A为某个图像中所有形状的集合B为某个形状和局部背景的集合则利用B在A中的匹配为 A⊛B(A⊖B)∩(Ac⊖Bc)A\circledast B (A\ominus B)\cap (A^c\ominus B^c) A⊛B(A⊖B)∩(Ac⊖Bc) 这样就可以用B中的形状命中A中的某个形状。
作用
一般作为形状检测的基本工具但是测试的时候感觉局限性太大了形状大小稍微有变动就有可能击不中。书中也有讲使用与物体有关的结构元和与北京有关的结构元基于一个假设定义——仅当两个或多个物体形成相脱离(断开)的集合时物体才是可分得。所以要求每个物体(形状)至少被一个像素宽的背景围绕。当不关心背景只关注由0和1组成的某些模式感兴趣的时候击中或不击中就变成了腐蚀操作腐蚀是匹配的集合。
实现
还是上面的那张图但是我们想击中五角星
## 按步骤实现
tmp1 cv2.erode(img_bin,kernel)
tmp2 255.0 - cv2.erode(255.0-img_bin,255.0-kernel)
result cv2.bitwise_and(np.asarray(tmp1,dtypenp.uint8),np.asarray(tmp2,dtypenp.uint8))
plt.figure(figsize(16,16))
plt.subplot(131)
plt.imshow(tmp1,cmapgray)
plt.subplot(132)
plt.imshow(tmp2,cmapgray)
plt.subplot(133)
plt.imshow(result,cmapgray)因为被击中的地方只有一个像素所以需要提取一下位置
pos[]
for i in range(result.shape[0]):for j in range(result.shape[1]):if(result[i,j]255 and np.sum(result[i-1:i2,j-1:j2])255):pos.append([i,j])
for i in range(len(pos)):cv2.circle(img,(pos[i][1],pos[i][0]),5,(0,255,0),-1)
plt.imshow(img) 边界提取
非常简单就是腐蚀一下与原图相减即可。公示表示就是如果A为原图B为结构元则A的边界就是 β(A)A−(A⊖B)\beta(A) A-(A\ominus B) β(A)A−(A⊖B)
孔洞填充
操作
孔洞的定义是被前景包围的一个背景区域比如放在灯泡下的一个玻璃球表面通常会有一个代表光反射的白色的点与周围玻璃格格不入。孔洞填充基于集合膨胀、求补和交集的算法。
若A中有一些孔洞并且我们知道每个孔洞中某个像素位置那么基于当前孔洞首先建立一个纯黑色的背景图将此位置的像素置为1不断去膨胀这张图同时与原图的补集与膨胀图的交集当此交集不变的时候就是对当前孔洞填充完毕。
公式
设A为某个具有孔洞的图B为结构元XkX_kXk为第kkk次膨胀的结果 Xk(Xk−1⊕B)∩AcX_k (X_{k-1}\oplus B)\cap A^c Xk(Xk−1⊕B)∩Ac 其中k0k0k0时即初始的时候膨胀图为只有当前孔洞某个位置为1其它均为0的图片。
作用
能够填充图中指定位置的孔洞
实现
hole_pos (72,82)
kernel cv2.getStructuringElement(cv2.MORPH_RECT, (3, 3))
Xprev np.zeros_like(img_bin)
Xprev[hole_pos[1],hole_pos[0]]255
Xcurrent cv2.bitwise_and(cv2.dilate(Xprev,kernel),np.array(255-img_bin,dtypeuint8))
while(not (XprevXcurrent).all()):Xprev XcurrentXcurrent cv2.bitwise_and(cv2.dilate(Xprev,kernel),np.array(255-img_bin,dtypeuint8))连通分量
与孔洞填充的逻辑刚好相反填充空洞需要对原图取反求交集但是提取连通分量则是直接对原图求交集。公式如下 Xk(Xk−1⊕B)∩AX_k (X_{k-1}\oplus B)\cap A Xk(Xk−1⊕B)∩A 代码实现
pos (47,68)
kernel cv2.getStructuringElement(cv2.MORPH_RECT, (3, 3))
Xprev np.zeros_like(img_bin)
Xprev[pos[1],pos[0]]255
Xcurrent cv2.bitwise_and(cv2.dilate(Xprev,kernel),img_bin)
while(not (XprevXcurrent).all()):Xprev XcurrentXcurrent cv2.bitwise_and(cv2.dilate(Xprev,kernel),img_bin)凸壳
操作
如果一个形状的任意两个点连接的直线段都在该形状内部则称该形状是凸形的。任意集合S的凸壳H是包含于S的最小凸集集合差H-S称为S的凸缺。
书中介绍了一个简单的获取凸壳的形态学算法定义结构元然后执行击中或不击中操作 Xk(Xk−1⊛B)∪AX_k (X_{k-1}\circledast B)\cup A Xk(Xk−1⊛B)∪A 其中X0AX_0AX0A收敛即为xkxk−1x_kx_{k-1}xkxk−1。
使用四个结构元执行上述四个操作得到四个收敛图最后求并集就得到了A的凸壳。
这个操作其实可以直接用轮廓检测中的凸包函数convexHull得到就不做实现了。
细化
结构元B对图像A的细化可利用击中或不击中变换表示为 A⊗BA−(A⊛B)A\otimes B A-(A\circledast B) A⊗BA−(A⊛B)
粗化
粗化是细化的形态学对偶直接定义 A⋅BA∪(A⊛B)A\cdot B A\cup(A\circledast B) A⋅BA∪(A⊛B)
骨架提取
图形A的骨架可以用腐蚀和开操作来表达: S(A)⋃k0KSk(A)S(A) \bigcup\limits_{k0}^K S_k(A) S(A)k0⋃KSk(A) 其中 Sk(A)(A⊖kB)−(A⊖kB)∘BS_k(A) (A\ominus kB) - (A\ominus kB)\circ B Sk(A)(A⊖kB)−(A⊖kB)∘B 式中B是一个结构元而(A⊖kB)(A\ominus kB)(A⊖kB)表示对A的连续k次腐蚀 (A⊖kB)(((⋯(A⊖B)⊖B)⊖⋯)⊖B)(A\ominus kB)(((\cdots(A\ominus B)\ominus B)\ominus\cdots)\ominus B) (A⊖kB)(((⋯(A⊖B)⊖B)⊖⋯)⊖B) K是A被腐蚀为空集前的最后一次迭代步骤也就是 Kmax{k∣(A⊖kB)≠∅}K \max \{k|(A\ominus kB)\neq \varnothing\} Kmax{k∣(A⊖kB)∅} 实现
#https://theailearner.com/tag/thinning-opencv/
kernel cv2.getStructuringElement(cv2.MORPH_CROSS,(3,3))
thin np.zeros(img_bin.shape,dtypeuint8)img1 img_bin.copy()
while (cv2.countNonZero(img1)!0):erode cv2.erode(img1,kernel)opening cv2.morphologyEx(erode,cv2.MORPH_OPEN,kernel)subset erode - openingthin cv2.bitwise_or(subset,thin)img1 erode.copy()也可以使用opencv-contrib实现的Zhang-Suen:A Fast Parallel Algorithm for Thinning Digital Patterns的细化算法
thinned cv2.ximgproc.thinning(img_bin,cv2.ximgproc.THINNING_ZHANGSUEN)代码实现步骤和理论详解可以看论文或者一个大佬的实现或者看我的本篇博客对应的github即可。 形态学重建
上面的形态学操作都是只涉及一幅图像和一个结构元而形态学重建则是非常强力的形态学变换涉及两幅图像和一个结构元。一幅图像是标记表示变换的起点而另一幅图像是模板约束改变换。
令FFF表示标记图像GGG表示模板图像书中定义一个前提F⊆GF\subseteq GF⊆G那么形态学重建涉及到的概念有 测地膨胀 DG(n){F,n0(F⊕B)∩G,n1DG(1)[DG(n−1)(F)],n≥1D_G^{(n)}\begin{cases} F,\quad n0\\ (F\oplus B)\cap G,\quad n1\\ D^{(1)}_G\left[D^{(n-1)}_G(F) \right],\quad n\geq 1 \end{cases} DG(n)⎩⎪⎪⎨⎪⎪⎧F,n0(F⊕B)∩G,n1DG(1)[DG(n−1)(F)],n≥1 这个交集能够保证模板GGG限制FFF的膨胀也就是说对传统的膨胀加了约束。 ## 测地膨胀
def D(n,F,B,G):if(n0):return Fif(n1):return cv2.bitwise_and(cv2.dilate(F,B),G)#cv2.bitwise_andreturn D(1,D(n-1,F,B,G),B,G)测地腐蚀 EG(n){F,n0(F⊖B)∪G,n1EG(1)[EG(n−1)(F)],n≥1E_G^{(n)}\begin{cases} F,\quad n0\\ (F\ominus B)\cup G,\quad n1\\ E^{(1)}_G\left[E^{(n-1)}_G(F) \right],\quad n\geq 1 \end{cases} EG(n)⎩⎪⎪⎨⎪⎪⎧F,n0(F⊖B)∪G,n1EG(1)[EG(n−1)(F)],n≥1 这个并集能够保证测地腐蚀始终大于或者等于模板图像也就是对传统的腐蚀加入了约束。 ## 测地腐蚀
def E(n,F,B,G):if(n0):return Fif(n1):return cv2.bitwise_or(cv2.erode(F,B),G)return E(1,E(n-1,F,B,G),B,G)由于约束的存在上述两个操作一定会有收敛的时候。
对应的形态学重建也就有两种 使用膨胀的重建 RDG(F)DG(k)(F)R_D^G(F)D^{(k)}_G(F) RDG(F)DG(k)(F) 迭代k次直到收敛条件达到DG(k)(F)DG(k1)(F)D_G^{(k)}(F)D_G^{(k1)}(F)DG(k)(F)DG(k1)(F) ## 膨胀重建
def RD(input_img,kernel,template):prevD D(1,input_img,kernel,template)i2while(1):currD D(i,input_img,kernel,template)if((prevDcurrD).all()):return currDelse:prevD currDii1使用腐蚀的重建 RGE(F)EGk(F)R_G^E(F) E_G^k(F) RGE(F)EGk(F) 同样是迭代k此直到收敛EG(k)EG(k1)(F)E_G^{(k)}E_G^{(k1)}(F)EG(k)EG(k1)(F)
书中有一个例子是重建开操作可正确恢复腐蚀后所保留的物体形状。一般重建开操作的定义是先对图像进行nnn此腐蚀再进行膨胀重建公式表示就是 OR(n)(F)RFD[F⊖nB]O_R^{(n)}(F) R_F^D\left[F\ominus nB\right] OR(n)(F)RFD[F⊖nB] 利用重建开操作提取图中的长垂直的字符注意这里实现的时候有个坑腐蚀的时候书中指明使用(51,1)(51,1)(51,1)的结构元但是重建开操作的时候结构元不要用这么细长的一个。
kernel_erode cv2.getStructuringElement(cv2.MORPH_RECT,(1,51))
kernel_rec cv2.getStructuringElement(cv2.MORPH_RECT,(3,3))
img_erode cv2.erode(img_bin,kernel_erode)
img_rec RD(img_erode,kernel_rec,img_bin)最后一行的两幅图分别是开运算和重建开运算的结果可以发现重建开运算很好的保留了竖长的字符。
后记
本片博客最重要的结论就是腐蚀和膨胀的结果并非和网上说的单纯的减小或者增加白色区域的面积实际上应该是结构元的设计对最终腐蚀和膨胀的结果有很大的影响有些结构元可能导致腐蚀操作中图像某些局部区域被膨胀反之亦然也可能有些结构元对你的图像并无得任何效果。
博客会更新到微信公众号中对应的图像基础知识列表中代码也在公众号简介的github中(CSDN博客右侧也有github地址)有兴趣点一波关注啵~~