OpenCV学习——形态学
前言
繼續(xù)學(xué)習(xí)圖像里面的形態(tài)學(xué)知識(shí)——結(jié)構(gòu)元、腐蝕、膨脹、開(kāi)運(yùn)算、閉運(yùn)算、擊中/不擊中變換。以及部分基本形態(tài)學(xué)算法,包括邊界提取、空洞填充、連通分量的提取、凸殼、細(xì)化、粗化、骨架、裁剪、形態(tài)學(xué)重建。
其實(shí)就是對(duì)岡薩雷斯的《數(shù)字圖像處理》中第9章節(jié)《形態(tài)學(xué)處理》的簡(jiǎn)要理解。
如果你認(rèn)為腐蝕是減小白色區(qū)域,膨脹是擴(kuò)充白色區(qū)域,請(qǐng)務(wù)必看本博客,注意不同結(jié)構(gòu)元的結(jié)果。
參考博客:
OpenCV官方的形態(tài)學(xué)運(yùn)算文檔
岡薩雷斯的《數(shù)字圖像處理》第9章
某位大佬的形態(tài)學(xué)總結(jié)
理論與實(shí)踐
結(jié)構(gòu)元
結(jié)構(gòu)元實(shí)際就是一個(gè)自定義的矩陣,在書(shū)中通常稱(chēng)為集合,是研究一幅圖像中感興趣特性所用的小集合或者子圖像。結(jié)構(gòu)元通常有反射和平移兩個(gè)操作。假設(shè)一個(gè)集合(結(jié)構(gòu)元)定義為B,那么:
- 反射:定義為B^\hat{B}B^,是B中的坐標(biāo)(x,y)(x,y)(x,y)被(?x,?y)(-x,-y)(?x,?y)替代。
- 平移:定義為(B)z(B)_z(B)z?,是B中的坐標(biāo)(x,y)(x,y)(x,y)被(x+z1,x+z2)(x+z_1,x+z_2)(x+z1?,x+z2?)替代。
同時(shí)結(jié)構(gòu)元還有一個(gè)原點(diǎn),這在opencv中叫anchor,后面腐蝕膨脹的操作都是更改原點(diǎn)對(duì)應(yīng)的原圖像素。
【注】不要小看結(jié)構(gòu)元,其設(shè)計(jì)直接影響到最終效果,這也是為什么開(kāi)頭說(shuō)“腐蝕減小白色區(qū)域,膨脹擴(kuò)充白色區(qū)域”是錯(cuò)誤觀點(diǎn),因?yàn)橐磺幸怨胶徒Y(jié)構(gòu)元為準(zhǔn)。依據(jù)不同的任務(wù)設(shè)計(jì)不同的結(jié)構(gòu)元才是我們關(guān)注的點(diǎn),比如垂直方向的細(xì)節(jié)需要細(xì)化或者粗化,應(yīng)該用什么結(jié)構(gòu)元采用什么操作。
腐蝕
操作
將結(jié)構(gòu)元在目標(biāo)圖像上從左往右從上往下平移,平移過(guò)程中結(jié)構(gòu)元中值為1的位置對(duì)應(yīng)的圖像像素都是1,則結(jié)構(gòu)元原點(diǎn)對(duì)應(yīng)位置的像素為1,否則為0。注意,平移的起點(diǎn)以結(jié)構(gòu)元原點(diǎn)(中心)為準(zhǔn),所以一般來(lái)說(shuō)需要對(duì)圖像做padding,這樣才能保證平移的起始位置讓結(jié)構(gòu)元原點(diǎn)對(duì)齊圖像的左上角第一個(gè)像素。
公式
若結(jié)構(gòu)元為E,圖像為A,那么腐蝕的公式表示就是
A?E={z∣(E)z?A}A\ominus E=\{z|(E)_z\subseteq A\} A?E={z∣(E)z??A}
作用
將小于結(jié)構(gòu)元的圖像細(xì)節(jié)從圖像中濾除了,腐蝕縮小或者細(xì)化了二值圖像中的物體。禁止說(shuō)消除或減小白色區(qū)域,說(shuō)的時(shí)候可以加個(gè)可能,因?yàn)榻Y(jié)構(gòu)元對(duì)結(jié)果會(huì)有很大的影響。
實(shí)現(xiàn)
代碼表示就是:
opencv的調(diào)用方法:
result = cv2.erode(src,kernel,iterations=1,borderType=cv2.BORDER_CONSTANT,borderValue=1)使用numpy復(fù)現(xiàn):
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],borderType=cv2.BORDER_CONSTANT,value=0)new_img = np.zeros_like(img)ele_idx = np.argwhere(kernel==1)for i in range(img.shape[0]):for j in range(img.shape[1]):block = img_pad[i:i+ksize[0],j:j+ksize[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隨便貼兩個(gè)結(jié)果,建議手推一遍
【注】很明顯,第一張圖的結(jié)構(gòu)元對(duì)圖像的腐蝕得到的結(jié)果僅僅是將圖像向右平移一個(gè)像素,并沒(méi)有出現(xiàn)減小白色區(qū)域的效果。
膨脹
操作
將結(jié)構(gòu)元在目標(biāo)圖像上從左往右從上往下平移,平移過(guò)程中結(jié)構(gòu)元中值為1的位置對(duì)應(yīng)的圖像像素至少有一個(gè)為1,則結(jié)構(gòu)元原點(diǎn)對(duì)應(yīng)位置的像素為1,否則為0。
公式
若結(jié)構(gòu)元為E,圖像為A,那么膨脹的公式表示就是
A⊕E={z∣[(E)z∩A≠?]}A\oplus E = \{z|[(E)_z\cap A\neq \varnothing]\} A⊕E={z∣[(E)z?∩A?=?]}
作用
增長(zhǎng)或粗化二值圖像中的物體,通常可以用于橋接裂縫。
實(shí)現(xiàn)
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],borderType=cv2.BORDER_CONSTANT,value=0)new_img = np.zeros_like(img)ele_idx = np.argwhere(kernel==1)for i in range(img.shape[0]):for j in range(img.shape[1]):block = img_pad[i:i+ksize[0],j:j+ksize[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
【注】看第一幅圖的腐蝕結(jié)果和膨脹結(jié)果,驚不驚喜意不意外刺不刺激,竟然一模一樣,是否顛覆了自己對(duì)腐蝕和膨脹的認(rèn)知。但是如果你按照公式手推一遍,會(huì)發(fā)現(xiàn)完全沒(méi)毛病。
開(kāi)運(yùn)算
操作
先進(jìn)行腐蝕,再進(jìn)行膨脹
公式
A°B=(A?B)⊕BA\circ B=(A\ominus B)\oplus B A°B=(A?B)⊕B
作用
平滑物體輪廓,斷開(kāi)較窄的狹頸并消除細(xì)的突出物。
實(shí)現(xiàn)
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)可以發(fā)現(xiàn),白色線條部件了,而且五角星的五個(gè)角更加平滑。此時(shí)注意云朵并沒(méi)有任何變化。
閉運(yùn)算
操作
先進(jìn)行膨脹,再進(jìn)行腐蝕
公式
A?B=(A⊕B)?BA\bullet B=(A\oplus B)\ominus B A?B=(A⊕B)?B
作用
同樣能夠平滑輪廓,彌合較窄的間斷和細(xì)長(zhǎng)的溝壑,消除小孔洞,填補(bǔ)輪廓線中的斷裂。
實(shí)現(xiàn)
## 閉運(yùn)算 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)發(fā)現(xiàn)左下角圖像的內(nèi)部黑線沒(méi)了,而且云朵的輪廓被平滑了,并且尾巴連在一起了,說(shuō)明能夠彌補(bǔ)斷裂部分。
【注意】開(kāi)運(yùn)算平滑的輪廓是指白色區(qū)域向黑色區(qū)域的凸出尖角,而閉運(yùn)算的平滑輪廓是指黑色區(qū)域向白色區(qū)域凸出的尖角,也就是它倆的白色尖角一個(gè)凸一個(gè)凹。
擊中和不擊中
操作
如果圖像中有A、B、C三個(gè)形狀,D為其中一個(gè)形狀如B被小窗口包圍的圖像,擊中和不擊中操作就是:
- 用D對(duì)圖像進(jìn)行腐蝕
- 用D中B的補(bǔ)集對(duì)D中ABC集合的補(bǔ)集進(jìn)行腐蝕
- 對(duì)上述兩個(gè)腐蝕操作的結(jié)果圖像進(jìn)行求交集
即可利用D擊中圖像中的B。
公式
設(shè)A為某個(gè)圖像中所有形狀的集合,B為某個(gè)形狀和局部背景的集合,則利用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中的某個(gè)形狀。
作用
一般作為形狀檢測(cè)的基本工具,但是測(cè)試的時(shí)候感覺(jué)局限性太大了,形狀大小稍微有變動(dòng)就有可能擊不中。書(shū)中也有講,使用與物體有關(guān)的結(jié)構(gòu)元和與北京有關(guān)的結(jié)構(gòu)元基于一個(gè)假設(shè)定義——僅當(dāng)兩個(gè)或多個(gè)物體形成相脫離(斷開(kāi))的集合時(shí),物體才是可分得。所以要求每個(gè)物體(形狀)至少被一個(gè)像素寬的背景圍繞。當(dāng)不關(guān)心背景,只關(guān)注由0和1組成的某些模式感興趣的時(shí)候,擊中或不擊中就變成了腐蝕操作;腐蝕是匹配的集合。
實(shí)現(xiàn)
還是上面的那張圖,但是我們想擊中五角星
## 按步驟實(shí)現(xiàn) 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,dtype=np.uint8),np.asarray(tmp2,dtype=np.uint8)) plt.figure(figsize=(16,16)) plt.subplot(131) plt.imshow(tmp1,cmap='gray') plt.subplot(132) plt.imshow(tmp2,cmap='gray') plt.subplot(133) plt.imshow(result,cmap='gray')因?yàn)楸粨糁械牡胤街挥幸粋€(gè)像素,所以需要提取一下位置
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:i+2,j-1:j+2])==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)邊界提取
非常簡(jiǎn)單,就是腐蝕一下,與原圖相減即可。公示表示就是,如果A為原圖,B為結(jié)構(gòu)元,則A的邊界就是
β(A)=A?(A?B)\beta(A) = A-(A\ominus B) β(A)=A?(A?B)
孔洞填充
操作
孔洞的定義是被前景包圍的一個(gè)背景區(qū)域,比如放在燈泡下的一個(gè)玻璃球,表面通常會(huì)有一個(gè)代表光反射的白色的點(diǎn),與周?chē)AЦ窀癫蝗搿?锥刺畛浠诩吓蛎洝⑶笱a(bǔ)和交集的算法。
若A中有一些孔洞,并且我們知道每個(gè)孔洞中某個(gè)像素位置,那么基于當(dāng)前孔洞,首先建立一個(gè)純黑色的背景圖,將此位置的像素置為1,不斷去膨脹這張圖,同時(shí)與原圖的補(bǔ)集與膨脹圖的交集,當(dāng)此交集不變的時(shí)候,就是對(duì)當(dāng)前孔洞填充完畢。
公式
設(shè)A為某個(gè)具有孔洞的圖,B為結(jié)構(gòu)元,XkX_kXk?為第kkk次膨脹的結(jié)果
Xk=(Xk?1⊕B)∩AcX_k = (X_{k-1}\oplus B)\cap A^c Xk?=(Xk?1?⊕B)∩Ac
其中k=0k=0k=0時(shí),即初始的時(shí)候,膨脹圖為只有當(dāng)前孔洞某個(gè)位置為1,其它均為0的圖片。
作用
能夠填充圖中指定位置的孔洞
實(shí)現(xiàn)
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,dtype='uint8')) while(not (Xprev==Xcurrent).all()):Xprev = XcurrentXcurrent = cv2.bitwise_and(cv2.dilate(Xprev,kernel),np.array(255-img_bin,dtype='uint8'))連通分量
與孔洞填充的邏輯剛好相反,填充空洞需要對(duì)原圖取反求交集,但是提取連通分量則是直接對(duì)原圖求交集。公式如下:
Xk=(Xk?1⊕B)∩AX_k = (X_{k-1}\oplus B)\cap A Xk?=(Xk?1?⊕B)∩A
代碼實(shí)現(xiàn)
凸殼
操作
如果一個(gè)形狀的任意兩個(gè)點(diǎn)連接的直線段都在該形狀內(nèi)部,則稱(chēng)該形狀是凸形的。任意集合S的凸殼H是包含于S的最小凸集,集合差H-S稱(chēng)為S的凸缺。
書(shū)中介紹了一個(gè)簡(jiǎn)單的獲取凸殼的形態(tài)學(xué)算法:定義結(jié)構(gòu)元,然后執(zhí)行擊中或不擊中操作:
Xk=(Xk?1?B)∪AX_k = (X_{k-1}\circledast B)\cup A Xk?=(Xk?1??B)∪A
其中X0=AX_0=AX0?=A,收斂即為xk=xk?1x_k=x_{k-1}xk?=xk?1?。
使用四個(gè)結(jié)構(gòu)元執(zhí)行上述四個(gè)操作,得到四個(gè)收斂圖,最后求并集,就得到了A的凸殼。
這個(gè)操作其實(shí)可以直接用輪廓檢測(cè)中的凸包函數(shù)convexHull得到,就不做實(shí)現(xiàn)了。
細(xì)化
結(jié)構(gòu)元B對(duì)圖像A的細(xì)化可利用擊中或不擊中變換表示為:
A?B=A?(A?B)A\otimes B = A-(A\circledast B) A?B=A?(A?B)
粗化
粗化是細(xì)化的形態(tài)學(xué)對(duì)偶,直接定義:
A?B=A∪(A?B)A\cdot B = A\cup(A\circledast B) A?B=A∪(A?B)
骨架提取
圖形A的骨架可以用腐蝕和開(kāi)操作來(lái)表達(dá):
S(A)=?k=0KSk(A)S(A) = \bigcup\limits_{k=0}^K S_k(A) S(A)=k=0?K?Sk?(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是一個(gè)結(jié)構(gòu)元,而(A?kB)(A\ominus kB)(A?kB)表示對(duì)A的連續(xù)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被腐蝕為空集前的最后一次迭代步驟,也就是:
K=max?{k∣(A?kB)≠?}K = \max \{k|(A\ominus kB)\neq \varnothing\} K=max{k∣(A?kB)?=?}
實(shí)現(xiàn)
也可以使用opencv-contrib實(shí)現(xiàn)的Zhang-Suen:A Fast Parallel Algorithm for Thinning Digital Patterns的細(xì)化算法:
thinned = cv2.ximgproc.thinning(img_bin,cv2.ximgproc.THINNING_ZHANGSUEN)代碼實(shí)現(xiàn)步驟和理論詳解可以看論文或者一個(gè)大佬的實(shí)現(xiàn),或者看我的本篇博客對(duì)應(yīng)的github即可。
形態(tài)學(xué)重建
上面的形態(tài)學(xué)操作都是只涉及一幅圖像和一個(gè)結(jié)構(gòu)元;而形態(tài)學(xué)重建則是非常強(qiáng)力的形態(tài)學(xué)變換,涉及兩幅圖像和一個(gè)結(jié)構(gòu)元。一幅圖像是標(biāo)記,表示變換的起點(diǎn),而另一幅圖像是模板,約束改變換。
令FFF表示標(biāo)記圖像,GGG表示模板圖像,書(shū)中定義一個(gè)前提F?GF\subseteq GF?G,那么形態(tài)學(xué)重建涉及到的概念有:
-
測(cè)地膨脹
## 測(cè)地膨脹 def D(n,F,B,G):if(n==0):return Fif(n==1):return cv2.bitwise_and(cv2.dilate(F,B),G)#cv2.bitwise_andreturn D(1,D(n-1,F,B,G),B,G)
DG(n)={F,n=0(F⊕B)∩G,n=1DG(1)[DG(n?1)(F)],n≥1D_G^{(n)}=\begin{cases} F,\quad n=0\\ (F\oplus B)\cap G,\quad n=1\\ D^{(1)}_G\left[D^{(n-1)}_G(F) \right],\quad n\geq 1 \end{cases} DG(n)?=????????F,n=0(F⊕B)∩G,n=1DG(1)?[DG(n?1)?(F)],n≥1?
這個(gè)交集,能夠保證模板GGG限制FFF的膨脹,也就是說(shuō)對(duì)傳統(tǒng)的膨脹加了約束。 -
測(cè)地腐蝕
## 測(cè)地腐蝕 def E(n,F,B,G):if(n==0):return Fif(n==1):return cv2.bitwise_or(cv2.erode(F,B),G)return E(1,E(n-1,F,B,G),B,G)
EG(n)={F,n=0(F?B)∪G,n=1EG(1)[EG(n?1)(F)],n≥1E_G^{(n)}=\begin{cases} F,\quad n=0\\ (F\ominus B)\cup G,\quad n=1\\ E^{(1)}_G\left[E^{(n-1)}_G(F) \right],\quad n\geq 1 \end{cases} EG(n)?=????????F,n=0(F?B)∪G,n=1EG(1)?[EG(n?1)?(F)],n≥1?
這個(gè)并集能夠保證測(cè)地腐蝕始終大于或者等于模板圖像,也就是對(duì)傳統(tǒng)的腐蝕加入了約束。
由于約束的存在,上述兩個(gè)操作一定會(huì)有收斂的時(shí)候。
對(duì)應(yīng)的形態(tài)學(xué)重建也就有兩種:
-
使用膨脹的重建
## 膨脹重建 def RD(input_img,kernel,template):prevD = D(1,input_img,kernel,template)i=2while(1):currD = D(i,input_img,kernel,template)if((prevD==currD).all()):return currDelse:prevD = currDi=i+1
RDG(F)=DG(k)(F)R_D^G(F)=D^{(k)}_G(F) RDG?(F)=DG(k)?(F)
迭代k次,直到收斂條件達(dá)到DG(k)(F)=DG(k+1)(F)D_G^{(k)}(F)=D_G^{(k+1)}(F)DG(k)?(F)=DG(k+1)?(F) -
使用腐蝕的重建
RGE(F)=EGk(F)R_G^E(F) = E_G^k(F) RGE?(F)=EGk?(F)
同樣是迭代k此,直到收斂EG(k)=EG(k+1)(F)E_G^{(k)}=E_G^{(k+1)}(F)EG(k)?=EG(k+1)?(F)
書(shū)中有一個(gè)例子是重建開(kāi)操作:可正確恢復(fù)腐蝕后所保留的物體形狀。一般重建開(kāi)操作的定義是先對(duì)圖像進(jìn)行nnn此腐蝕,再進(jìn)行膨脹重建,公式表示就是
OR(n)(F)=RFD[F?nB]O_R^{(n)}(F) = R_F^D\left[F\ominus nB\right] OR(n)?(F)=RFD?[F?nB]
利用重建開(kāi)操作,提取圖中的長(zhǎng)垂直的字符,注意這里實(shí)現(xiàn)的時(shí)候有個(gè)坑,腐蝕的時(shí)候書(shū)中指明使用(51,1)(51,1)(51,1)的結(jié)構(gòu)元,但是重建開(kāi)操作的時(shí)候,結(jié)構(gòu)元不要用這么細(xì)長(zhǎng)的一個(gè)。
最后一行的兩幅圖分別是開(kāi)運(yùn)算和重建開(kāi)運(yùn)算的結(jié)果,可以發(fā)現(xiàn)重建開(kāi)運(yùn)算很好的保留了豎長(zhǎng)的字符。
后記
本片博客最重要的結(jié)論就是:腐蝕和膨脹的結(jié)果并非和網(wǎng)上說(shuō)的單純的減小或者增加白色區(qū)域的面積,實(shí)際上應(yīng)該是結(jié)構(gòu)元的設(shè)計(jì)對(duì)最終腐蝕和膨脹的結(jié)果有很大的影響,有些結(jié)構(gòu)元可能導(dǎo)致腐蝕操作中,圖像某些局部區(qū)域被膨脹,反之亦然,也可能有些結(jié)構(gòu)元對(duì)你的圖像并無(wú)得任何效果。
博客會(huì)更新到微信公眾號(hào)中對(duì)應(yīng)的圖像基礎(chǔ)知識(shí)列表中,代碼也在公眾號(hào)簡(jiǎn)介的github中(CSDN博客右側(cè)也有g(shù)ithub地址),有興趣點(diǎn)一波關(guān)注啵~~
總結(jié)
以上是生活随笔為你收集整理的OpenCV学习——形态学的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: Power BI 报表服务器中的行级别安
- 下一篇: 径向基函数RBF三维网格变形