天天看點

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 + z 1 , x + z 2 ) (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

的調用方法:

使用

numpy

複現:

def erod(img,kernel):
    ksize = kernel.shape
    center=(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] = 1
            else:
                new_img[i,j] = 0
    return img_pad,new_img
           

随便貼兩個結果,建議手推一遍

OpenCV學習——形态學前言理論與實踐後記
OpenCV學習——形态學前言理論與實踐後記

【注】很明顯,第一張圖的結構元對圖像的腐蝕得到的結果僅僅是将圖像向右平移一個像素,并沒有出現減小白色區域的效果。

膨脹

操作

将結構元在目标圖像上從左往右從上往下平移,平移過程中結構元中值為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.shape
    center=(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] = 1
            else:
                new_img[i,j] = 0
    return img_pad,new_img
           
OpenCV學習——形态學前言理論與實踐後記
OpenCV學習——形态學前言理論與實踐後記

【注】看第一幅圖的腐蝕結果和膨脹結果,驚不驚喜意不意外刺不刺激,竟然一模一樣,是否颠覆了自己對腐蝕和膨脹的認知。但是如果你按照公式手推一遍,會發現完全沒毛病。

開運算

操作

先進行腐蝕,再進行膨脹

公式

A ∘ B = ( A ⊖ B ) ⊕ B A\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)
           
OpenCV學習——形态學前言理論與實踐後記

可以發現,白色線條部件了,而且五角星的五個角更加平滑。此時注意雲朵并沒有任何變化。

閉運算

操作

先進行膨脹,再進行腐蝕

公式

A ∙ B = ( A ⊕ B ) ⊖ B A\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)
           
OpenCV學習——形态學前言理論與實踐後記

發現左下角圖像的内部黑線沒了,而且雲朵的輪廓被平滑了,并且尾巴連在一起了,說明能夠彌補斷裂部分。

【注意】開運算平滑的輪廓是指白色區域向黑色區域的凸出尖角,而閉運算的平滑輪廓是指黑色區域向白色區域凸出的尖角,也就是它倆的白色尖角一個凸一個凹。

擊中和不擊中

操作

如果圖像中有A、B、C三個形狀,D為其中一個形狀如B被小視窗包圍的圖像,擊中和不擊中操作就是:

  • 用D對圖像進行腐蝕
  • 用D中B的補集對D中ABC集合的補集進行腐蝕
  • 對上述兩個腐蝕操作的結果圖像進行求交集

即可利用D擊中圖像中的B。

公式

設A為某個圖像中所有形狀的集合,B為某個形狀和局部背景的集合,則利用B在A中的比對為:

A ⊛ B = ( A ⊖ B ) ∩ ( A c ⊖ B c ) 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')
           
OpenCV學習——形态學前言理論與實踐後記

因為被擊中的地方隻有一個像素,是以需要提取一下位置

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)            
           
OpenCV學習——形态學前言理論與實踐後記

邊界提取

非常簡單,就是腐蝕一下,與原圖相減即可。公示表示就是,如果A為原圖,B為結構元,則A的邊界就是

β ( A ) = A − ( A ⊖ B ) \beta(A) = A-(A\ominus B) β(A)=A−(A⊖B)

孔洞填充

操作

孔洞的定義是被前景包圍的一個背景區域,比如放在燈泡下的一個玻璃球,表面通常會有一個代表光反射的白色的點,與周圍玻璃格格不入。孔洞填充基于集合膨脹、求補和交集的算法。

若A中有一些孔洞,并且我們知道每個孔洞中某個像素位置,那麼基于目前孔洞,首先建立一個純黑色的背景圖,将此位置的像素置為1,不斷去膨脹這張圖,同時與原圖的補集與膨脹圖的交集,當此交集不變的時候,就是對目前孔洞填充完畢。

公式

設A為某個具有孔洞的圖,B為結構元, X k X_k Xk​為第 k k k次膨脹的結果

X k = ( X k − 1 ⊕ B ) ∩ A c X_k = (X_{k-1}\oplus B)\cap A^c Xk​=(Xk−1​⊕B)∩Ac

其中 k = 0 k=0 k=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 = Xcurrent
    Xcurrent = cv2.bitwise_and(cv2.dilate(Xprev,kernel),np.array(255-img_bin,dtype='uint8'))
           
OpenCV學習——形态學前言理論與實踐後記

連通分量

與孔洞填充的邏輯剛好相反,填充空洞需要對原圖取反求交集,但是提取連通分量則是直接對原圖求交集。公式如下:

X k = ( X k − 1 ⊕ B ) ∩ A X_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 (Xprev==Xcurrent).all()):
    Xprev = Xcurrent
    Xcurrent = cv2.bitwise_and(cv2.dilate(Xprev,kernel),img_bin)
           
OpenCV學習——形态學前言理論與實踐後記

凸殼

操作

如果一個形狀的任意兩個點連接配接的直線段都在該形狀内部,則稱該形狀是凸形的。任意集合S的凸殼H是包含于S的最小凸集,集合差H-S稱為S的凸缺。

書中介紹了一個簡單的擷取凸殼的形态學算法:定義結構元,然後執行擊中或不擊中操作:

X k = ( X k − 1 ⊛ B ) ∪ A X_k = (X_{k-1}\circledast B)\cup A Xk​=(Xk−1​⊛B)∪A

其中 X 0 = A X_0=A X0​=A,收斂即為 x k = x k − 1 x_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 = 0 K S k ( A ) S(A) = \bigcup\limits_{k=0}^K S_k(A) S(A)=k=0⋃K​Sk​(A)

其中,

S k ( A ) = ( A ⊖ k B ) − ( A ⊖ k B ) ∘ B S_k(A) = (A\ominus kB) - (A\ominus kB)\circ B Sk​(A)=(A⊖kB)−(A⊖kB)∘B

式中,B是一個結構元,而 ( A ⊖ k B ) (A\ominus kB) (A⊖kB)表示對A的連續k次腐蝕

( A ⊖ k B ) = ( ( ( ⋯ ( 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 ⊖ k B ) ≠ ∅ } K = \max \{k|(A\ominus kB)\neq \varnothing\} K=max{k∣(A⊖kB)​=∅}

實作

#https://theailearner.com/tag/thinning-opencv/
kernel = cv2.getStructuringElement(cv2.MORPH_CROSS,(3,3))
thin = np.zeros(img_bin.shape,dtype='uint8')

img1 = img_bin.copy()
while (cv2.countNonZero(img1)!=0):
    erode = cv2.erode(img1,kernel)
    opening = cv2.morphologyEx(erode,cv2.MORPH_OPEN,kernel)
    subset = erode - opening
    thin = 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即可。

OpenCV學習——形态學前言理論與實踐後記

形态學重建

上面的形态學操作都是隻涉及一幅圖像和一個結構元;而形态學重建則是非常強力的形态學變換,涉及兩幅圖像和一個結構元。一幅圖像是标記,表示變換的起點,而另一幅圖像是模闆,限制改變換。

令 F F F表示标記圖像, G G G表示模闆圖像,書中定義一個前提 F ⊆ G F\subseteq G F⊆G,那麼形态學重建涉及到的概念有:

  • 測地膨脹

    D G ( n ) = { F , n = 0 ( F ⊕ B ) ∩ G , n = 1 D G ( 1 ) [ D G ( n − 1 ) ( F ) ] , n ≥ 1 D_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 G G限制 F F F的膨脹,也就是說對傳統的膨脹加了限制。

    ## 測地膨脹
    def D(n,F,B,G):
        if(n==0):
            return F
        if(n==1):
            return cv2.bitwise_and(cv2.dilate(F,B),G)#cv2.bitwise_and
        return D(1,D(n-1,F,B,G),B,G)
               
  • 測地腐蝕

    E G ( n ) = { F , n = 0 ( F ⊖ B ) ∪ G , n = 1 E G ( 1 ) [ E G ( n − 1 ) ( F ) ] , n ≥ 1 E_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 E(n,F,B,G):
        if(n==0):
            return F
        if(n==1):
            return cv2.bitwise_or(cv2.erode(F,B),G)
        return E(1,E(n-1,F,B,G),B,G)
               

由于限制的存在,上述兩個操作一定會有收斂的時候。

對應的形态學重建也就有兩種:

  • 使用膨脹的重建

    R D G ( F ) = D G ( k ) ( F ) R_D^G(F)=D^{(k)}_G(F) RDG​(F)=DG(k)​(F)

    疊代k次,直到收斂條件達到 D G ( k ) ( F ) = D G ( k + 1 ) ( F ) D_G^{(k)}(F)=D_G^{(k+1)}(F) DG(k)​(F)=DG(k+1)​(F)

    ## 膨脹重建
    def RD(input_img,kernel,template):
        prevD = D(1,input_img,kernel,template)
        i=2
        while(1):
            currD = D(i,input_img,kernel,template)
            if((prevD==currD).all()):
                return currD
            else:
                prevD = currD
                i=i+1
               
  • 使用腐蝕的重建

    R G E ( F ) = E G k ( F ) R_G^E(F) = E_G^k(F) RGE​(F)=EGk​(F)

    同樣是疊代k此,直到收斂 E G ( k ) = E G ( k + 1 ) ( F ) E_G^{(k)}=E_G^{(k+1)}(F) EG(k)​=EG(k+1)​(F)

書中有一個例子是重建開操作:可正确恢複腐蝕後所保留的物體形狀。一般重建開操作的定義是先對圖像進行 n n n此腐蝕,再進行膨脹重建,公式表示就是

O R ( n ) ( F ) = R F D [ F ⊖ n B ] 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)
           
OpenCV學習——形态學前言理論與實踐後記

最後一行的兩幅圖分别是開運算和重建開運算的結果,可以發現重建開運算很好的保留了豎長的字元。

後記

本片部落格最重要的結論就是:腐蝕和膨脹的結果并非和網上說的單純的減小或者增加白色區域的面積,實際上應該是結構元的設計對最終腐蝕和膨脹的結果有很大的影響,有些結構元可能導緻腐蝕操作中,圖像某些局部區域被膨脹,反之亦然,也可能有些結構元對你的圖像并無得任何效果。

部落格會更新到微信公衆号中對應的圖像基礎知識清單中,代碼也在公衆号簡介的

github

中(

CSDN

部落格右側也有

github

位址),有興趣點一波關注啵~~

OpenCV學習——形态學前言理論與實踐後記

繼續閱讀