OpenCV学习——形态学
前言
繼續學習圖像里面的形態學知識——結構元、腐蝕、膨脹、開運算、閉運算、擊中/不擊中變換。以及部分基本形態學算法,包括邊界提取、空洞填充、連通分量的提取、凸殼、細化、粗化、骨架、裁剪、形態學重建。
其實就是對岡薩雷斯的《數字圖像處理》中第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)被(x+z1,x+z2)(x+z_1,x+z_2)(x+z1?,x+z2?)替代。
同時結構元還有一個原點,這在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,iterations=1,borderType=cv2.BORDER_CONSTANT,borderValue=1)使用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],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隨便貼兩個結果,建議手推一遍
【注】很明顯,第一張圖的結構元對圖像的腐蝕得到的結果僅僅是將圖像向右平移一個像素,并沒有出現減小白色區域的效果。
膨脹
操作
將結構元在目標圖像上從左往右從上往下平移,平移過程中結構元中值為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],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
【注】看第一幅圖的腐蝕結果和膨脹結果,驚不驚喜意不意外刺不刺激,竟然一模一樣,是否顛覆了自己對腐蝕和膨脹的認知。但是如果你按照公式手推一遍,會發現完全沒毛病。
開運算
操作
先進行腐蝕,再進行膨脹
公式
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,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')因為被擊中的地方只有一個像素,所以需要提取一下位置
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)邊界提取
非常簡單,就是腐蝕一下,與原圖相減即可。公示表示就是,如果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
其中k=0k=0k=0時,即初始的時候,膨脹圖為只有當前孔洞某個位置為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,dtype='uint8')) while(not (Xprev==Xcurrent).all()):Xprev = XcurrentXcurrent = cv2.bitwise_and(cv2.dilate(Xprev,kernel),np.array(255-img_bin,dtype='uint8'))連通分量
與孔洞填充的邏輯剛好相反,填充空洞需要對原圖取反求交集,但是提取連通分量則是直接對原圖求交集。公式如下:
Xk=(Xk?1⊕B)∩AX_k = (X_{k-1}\oplus B)\cap A Xk?=(Xk?1?⊕B)∩A
代碼實現
凸殼
操作
如果一個形狀的任意兩個點連接的直線段都在該形狀內部,則稱該形狀是凸形的。任意集合S的凸殼H是包含于S的最小凸集,集合差H-S稱為S的凸缺。
書中介紹了一個簡單的獲取凸殼的形態學算法:定義結構元,然后執行擊中或不擊中操作:
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?。
使用四個結構元執行上述四個操作,得到四個收斂圖,最后求并集,就得到了A的凸殼。
這個操作其實可以直接用輪廓檢測中的凸包函數convexHull得到,就不做實現了。
細化
結構元B對圖像A的細化可利用擊中或不擊中變換表示為:
A?B=A?(A?B)A\otimes B = A-(A\circledast B) A?B=A?(A?B)
粗化
粗化是細化的形態學對偶,直接定義:
A?B=A∪(A?B)A\cdot B = A\cup(A\circledast B) A?B=A∪(A?B)
骨架提取
圖形A的骨架可以用腐蝕和開操作來表達:
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是一個結構元,而(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被腐蝕為空集前的最后一次迭代步驟,也就是:
K=max?{k∣(A?kB)≠?}K = \max \{k|(A\ominus kB)\neq \varnothing\} K=max{k∣(A?kB)?=?}
實現
也可以使用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,那么形態學重建涉及到的概念有:
-
測地膨脹
## 測地膨脹 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?
這個交集,能夠保證模板GGG限制FFF的膨脹,也就是說對傳統的膨脹加了約束。 -
測地腐蝕
## 測地腐蝕 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?
這個并集能夠保證測地腐蝕始終大于或者等于模板圖像,也就是對傳統的腐蝕加入了約束。
由于約束的存在,上述兩個操作一定會有收斂的時候。
對應的形態學重建也就有兩種:
-
使用膨脹的重建
## 膨脹重建 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次,直到收斂條件達到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)
書中有一個例子是重建開操作:可正確恢復腐蝕后所保留的物體形狀。一般重建開操作的定義是先對圖像進行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)的結構元,但是重建開操作的時候,結構元不要用這么細長的一個。
最后一行的兩幅圖分別是開運算和重建開運算的結果,可以發現重建開運算很好的保留了豎長的字符。
后記
本片博客最重要的結論就是:腐蝕和膨脹的結果并非和網上說的單純的減小或者增加白色區域的面積,實際上應該是結構元的設計對最終腐蝕和膨脹的結果有很大的影響,有些結構元可能導致腐蝕操作中,圖像某些局部區域被膨脹,反之亦然,也可能有些結構元對你的圖像并無得任何效果。
博客會更新到微信公眾號中對應的圖像基礎知識列表中,代碼也在公眾號簡介的github中(CSDN博客右側也有github地址),有興趣點一波關注啵~~
總結
以上是生活随笔為你收集整理的OpenCV学习——形态学的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Power BI 报表服务器中的行级别安
- 下一篇: 径向基函数RBF三维网格变形