第八節、圖片分割之GrabCut算法、分水嶺算法


所謂圖像分割指的是根據灰度、顏色、紋理和形狀等特征把圖像划分成若干互不交迭的區域,並使這些特征在同一區域內呈現出相似性,而在不同區域間呈現出明顯的差異性。我們先對目前主要的圖像分割方法做個概述,后面再對個別方法做詳細的了解和學習。

一、圖像分割算法概述

1、基於閾值的分割方法

閾值法的基本思想是基於圖像的灰度特征來計算一個或多個灰度閾值,並將圖像中每個像素的灰度值與閾值相比較,最后將像素根據比較結果分到合適的類別中。因此,該類方法最為關鍵的一步就是按照某個准則函數來求解最佳灰度閾值。

2、基於邊緣的分割方法

所謂邊緣是指圖像中兩個不同區域的邊界線上連續的像素點的集合,是圖像局部特征不連續性的反映,體現了灰度、顏色、紋理等圖像特性的突變。通常情況下,基於邊緣的分割方法指的是基於灰度值的邊緣檢測,它是建立在邊緣灰度值會呈現出階躍型或屋頂型變化這一觀測基礎上的方法。

階躍型邊緣兩邊像素點的灰度值存在着明顯的差異,而屋頂型邊緣則位於灰度值上升或下降的轉折處。正是基於這一特性,可以使用微分算子進行邊緣檢測,即使用一階導數的極值與二階導數的過零點來確定邊緣,具體實現時可以使用圖像與模板進行卷積來完成。

3、基於區域的分割方法

此類方法是將圖像按照相似性准則分成不同的區域,主要包括種子區域生長法、區域分裂合並法和分水嶺法等幾種類型。

種子區域生長法是從一組代表不同生長區域的種子像素開始,接下來將種子像素鄰域里符合條件的像素合並到種子像素所代表的生長區域中,並將新添加的像素作為新的種子像素繼續合並過程,直到找不到符合條件的新像素為止。該方法的關鍵是選擇合適的初始種子像素以及合理的生長准則。

區域分裂合並法(Gonzalez,2002)的基本思想是首先將圖像任意分成若干互不相交的區域,然后再按照相關准則對這些區域進行分裂或者合並從而完成分割任務,該方法既適用於灰度圖像分割也適用於紋理圖像分割。

分水嶺法(Meyer,1990)是一種基於拓撲理論的數學形態學的分割方法,其基本思想是把圖像看作是測地學上的拓撲地貌,圖像中每一點像素的灰度值表示該點的海拔高度,每一個局部極小值及其影響區域稱為集水盆,而集水盆的邊界則形成分水嶺。該算法的實現可以模擬成洪水淹沒的過程,圖像的最低點首先被淹沒,然后水逐漸淹沒整個山谷。當水位到達一定高度的時候將會溢出,這時在水溢出的地方修建堤壩,重復這個過程直到整個圖像上的點全部被淹沒,這時所建立的一系列堤壩就成為分開各個盆地的分水嶺。分水嶺算法對微弱的邊緣有着良好的響應,但圖像中的噪聲會使分水嶺算法產生過分割的現象。

4、基於圖論的分割方法

此類方法把圖像分割問題與圖的最小割(min cut)問題相關聯。首先將圖像映射為帶權無向圖G=<V,E>,圖中每個節點N∈V對應於圖像中的每個像素,每條邊∈E連接着一對相鄰的像素,邊的權值表示了相鄰像素之間在灰度、顏色或紋理方面的非負相似度。而對圖像的一個分割s就是對圖的一個剪切,被分割的每個區域C∈S對應着圖中的一個子圖。而分割的最優原則就是使划分后的子圖在內部保持相似度最大,而子圖之間的相似度保持最小。基於圖論的分割方法的本質就是移除特定的邊,將圖划分為若干子圖從而實現分割。目前所了解到的基於圖論的方法有GraphCut,GrabCut和Random Walk等。

5、基於能量泛函的分割方法

該類方法主要指的是活動輪廓模型(active contour model)以及在其基礎上發展出來的算法,其基本思想是使用連續曲線來表達目標邊緣,並定義一個能量泛函使得其自變量包括邊緣曲線,因此分割過程就轉變為求解能量泛函的最小值的過程,一般可通過求解函數對應的歐拉(Euler.Lagrange)方程來實現,能量達到最小時的曲線位置就是目標的輪廓所在。按照模型中曲線表達形式的不同,活動輪廓模型可以分為兩大類:參數活動輪廓模型(parametric active contour model)和幾何活動輪廓模型(geometric active contour model)。

參數活動輪廓模型是基於Lagrange框架,直接以曲線的參數化形式來表達曲線,最具代表性的是由Kasset a1(1987)所提出的Snake模型。該類模型在早期的生物圖像分割領域得到了成功的應用,但其存在着分割結果受初始輪廓的設置影響較大以及難以處理曲線拓撲結構變化等缺點,此外其能量泛函只依賴於曲線參數的選擇,與物體的幾何形狀無關,這也限制了其進一步的應用。

幾何活動輪廓模型的曲線運動過程是基於曲線的幾何度量參數而非曲線的表達參數,因此可以較好地處理拓撲結構的變化,並可以解決參數活動輪廓模型難以解決的問題。而水平集(Level Set)方法(Osher,1988)的引入,則極大地推動了幾何活動輪廓模型的發展,因此幾何活動輪廓模型一般也可被稱為水平集方法。

二、圖像分割之GrabCut算法

里不去介紹GrabCut算法的原理,感興趣的童鞋去參考博客后面的文章。該算法主要基於以下知識:

  • k均值聚類

  • 高斯混合模型建模(GMM)
  • max flow/min cut

這里介紹一些GrabCut算法的實現步驟:

  1. 在圖片中定義(一個或者多個)包含物體的矩形。
  2. 矩形外的區域被自動認為是背景。
  3. 對於用戶定義的矩形區域,可用背景中的數據來區分它里面的前景和背景區域。
  4. 用高斯混合模型(GMM)來對背景和前景建模,並將未定義的像素標記為可能的前景或者背景。
  5. 圖像中的每一個像素都被看做通過虛擬邊與周圍像素相連接,而每條邊都有一個屬於前景或者背景的概率,這是基於它與周邊像素顏色上的相似性。
  6. 每一個像素(即算法中的節點)會與一個前景或背景節點連接。
  7. 在節點完成連接后(可能與背景或前景連接),若節點之間的邊屬於不同終端(即一個節點屬於前景,另一個節點屬於背景),則會切斷他們之間的邊,這就能將圖像各部分分割出來。下圖能很好的說明該算法:

OpenCV提供了GrabCut算法相關的函數,grabCut函數:

    grabCut(img,mask,rect,bgdModel,fgdModel,iterCount,mode )

輸入:圖像、被標記好的前景、背景

輸出:分割圖像

其中輸入的前景、背景指的是一種概率,如果你已經明確某一塊區域是背景,那么它屬於背景的概率為1;當然如果你覺得它有可能背景,但是沒有百分百的肯定,這個時候你就要用到高斯模型,對其進行建模,然后估算概率。現在我以下圖為例,用戶通過交互輸入框選區域,前景位於框選區域內,也就是說矩形區域外的全部屬於背景,且概率為百分百。然后方框內可能屬於前景,概率需要用高斯混合建模求解。

參數說明:

  • img——待分割的源圖像,必須是8位3通道,在處理的過程中不會被修改
  • mask——掩碼圖像,如果使用掩碼進行初始化,那么mask保存初始化掩碼信息;在執行分割的時候,也可以將用戶交互所設定的前景與背景保存到mask中,然后再傳入grabCut函數;在處理結束之后,mask中會保存結果。mask只能取以下四種值:

GCD_BGD(=0),背景;

GCD_FGD(=1),前景;

GCD_PR_BGD(=2),可能的背景;

GCD_PR_FGD(=3),可能的前景。

              如果沒有手工標記GCD_BGD或者GCD_FGD,那么結果只會有GCD_PR_BGD或GCD_PR_FGD;

  • rect——用於限定需要進行分割的圖像范圍,只有該矩形窗口內的圖像部分才被處理;
  • bgdModel——背景模型,如果為None,函數內部會自動創建一個bgdModel;bgdModel必須是單通道浮點型圖像,且行數只能為1,列數只能為13x5;
  • fgdModel——前景模型,如果為None,函數內部會自動創建一個fgdModel;fgdModel必須是單通道浮點型圖像,且行數只能為1,列數只能為13x5;
  • iterCount——迭代次數,必須大於0;
  • mode——用於指示grabCut函數進行什么操作,可選的值有:

GC_INIT_WITH_RECT(=0),用矩形窗初始化GrabCut;

GC_INIT_WITH_MASK(=1),用掩碼圖像初始化GrabCut;

GC_EVAL(=2),執行分割。

接下來,我們就演示上圖哪個例子,把字符從圖片中摳出來:

# -*- coding: utf-8 -*-
"""
Created on Mon Jul 30 15:35:41 2018

@author: lenovo
"""

'''
基於圖論的分割方法-GraphCut
【圖像處理】圖像分割之(一~四)GraphCut,GrabCut函數使用和源碼解讀(OpenCV)
https://blog.csdn.net/kyjl888/article/details/78253829
'''

import numpy as np
import cv2
     
#鼠標事件的回調函數
def on_mouse(event,x,y,flag,param):        
    global rect
    global leftButtonDowm
    global leftButtonUp
    
    #鼠標左鍵按下
    if event == cv2.EVENT_LBUTTONDOWN:
        rect[0] = x
        rect[2] = x
        rect[1] = y
        rect[3] = y
        leftButtonDowm = True
        leftButtonUp = False
        
    #移動鼠標事件
    if event == cv2.EVENT_MOUSEMOVE:
        if leftButtonDowm and  not leftButtonUp:
            rect[2] = x
            rect[3] = y        
  
    #鼠標左鍵松開
    if event == cv2.EVENT_LBUTTONUP:
        if leftButtonDowm and  not leftButtonUp:
            x_min = min(rect[0],rect[2])
            y_min = min(rect[1],rect[3])
            
            x_max = max(rect[0],rect[2])
            y_max = max(rect[1],rect[3])
            
            rect[0] = x_min
            rect[1] = y_min
            rect[2] = x_max
            rect[3] = y_max
            leftButtonDowm = False      
            leftButtonUp = True

#讀入圖片
img = cv2.imread('image/img21.jpg')
#掩碼圖像,如果使用掩碼進行初始化,那么mask保存初始化掩碼信息;在執行分割的時候,也可以將用戶交互所設定的前景與背景保存到mask中,然后再傳入grabCut函數;在處理結束之后,mask中會保存結果
mask = np.zeros(img.shape[:2],np.uint8)

#背景模型,如果為None,函數內部會自動創建一個bgdModel;bgdModel必須是單通道浮點型圖像,且行數只能為1,列數只能為13x5;
bgdModel = np.zeros((1,65),np.float64)
#fgdModel——前景模型,如果為None,函數內部會自動創建一個fgdModel;fgdModel必須是單通道浮點型圖像,且行數只能為1,列數只能為13x5;
fgdModel = np.zeros((1,65),np.float64)

#用於限定需要進行分割的圖像范圍,只有該矩形窗口內的圖像部分才被處理;
rect = [0,0,0,0]  
    
#鼠標左鍵按下
leftButtonDowm = False
#鼠標左鍵松開
leftButtonUp = True
    
#指定窗口名來創建窗口
cv2.namedWindow('img') 
#設置鼠標事件回調函數 來獲取鼠標輸入
cv2.setMouseCallback('img',on_mouse)

#顯示圖片
cv2.imshow('img',img)


while cv2.waitKey(2) == -1:
    #左鍵按下,畫矩陣
    if leftButtonDowm and not leftButtonUp:  
        img_copy = img.copy()
        #在img圖像上,繪制矩形  線條顏色為green 線寬為2
        cv2.rectangle(img_copy,(rect[0],rect[1]),(rect[2],rect[3]),(0,255,0),2)  
        #顯示圖片
        cv2.imshow('img',img_copy)
        
    #左鍵松開,矩形畫好 
    elif not leftButtonDowm and leftButtonUp and rect[2] - rect[0] != 0 and rect[3] - rect[1] != 0:
        #轉換為寬度高度
        rect[2] = rect[2]-rect[0]
        rect[3] = rect[3]-rect[1]
        rect_copy = tuple(rect.copy())   
        rect = [0,0,0,0]
        #物體分割
        cv2.grabCut(img,mask,rect_copy,bgdModel,fgdModel,5,cv2.GC_INIT_WITH_RECT)
            
        mask2 = np.where((mask==2)|(mask==0),0,1).astype('uint8')
        img_show = img*mask2[:,:,np.newaxis]
        #顯示圖片分割后結果
        cv2.imshow('grabcut',img_show)
        #顯示原圖
        cv2.imshow('img',img)    

cv2.waitKey(0)
cv2.destroyAllWindows()

1、上面代碼比較簡單,首先加載圖片,並創建一個與所加載圖像同形狀的掩模,並用0填充。

#讀入圖片
img = cv2.imread('image/img21.jpg')
#掩碼圖像,如果使用掩碼進行初始化,那么mask保存初始化掩碼信息;在執行分割的時候,也可以將用戶交互所設定的前景與背景保存到mask中,然后再傳入grabCut函數;在處理結束之后,mask中會保存結果
mask = np.zeros(img.shape[:2],np.uint8)

2、創建以0填充的前景和背景模型。

#背景模型,如果為None,函數內部會自動創建一個bgdModel;bgdModel必須是單通道浮點型圖像,且行數只能為1,列數只能為13x5;
bgdModel = np.zeros((1,65),np.float64)
#fgdModel——前景模型,如果為None,函數內部會自動創建一個fgdModel;fgdModel必須是單通道浮點型圖像,且行數只能為1,列數只能為13x5;
fgdModel = np.zeros((1,65),np.float64)

3、可以使用數據填充這些模型,但是這里准備使用一個標識出想要隔離的對象的矩形來初始化grabCut算法。所以背景和前景模型都要基於這個初始化矩形所留下來的區域來進行,這個矩形用下面代碼來定義:

#用於限定需要進行分割的圖像范圍,只有該矩形窗口內的圖像部分才被處理;
rect = [0,0,0,0]  

后面我們使用鼠標回調事件來更新矩形框的帶下,當我們鼠標左鍵按下的時候、開始在原始圖片上繪制矩形、當鼠標左鍵松開、矩形繪制完畢。

4、定義兩個表示位、表示鼠標左鍵的狀態

#鼠標左鍵按下
leftButtonDowm = False
#鼠標左鍵松開
leftButtonUp = True

5、創建窗體、並設置鼠標回調函數、然后顯示源圖像

#指定窗口名來創建窗口
cv2.namedWindow('img') 
#設置鼠標事件回調函數 來獲取鼠標輸入
cv2.setMouseCallback('img',on_mouse)

#顯示圖片
cv2.imshow('img',img)

6、鼠標回調事件代碼如下

#鼠標事件的回調函數
def on_mouse(event,x,y,flag,param):        
    global rect
    global leftButtonDowm
    global leftButtonUp
    
    #鼠標左鍵按下
    if event == cv2.EVENT_LBUTTONDOWN:
        rect[0] = x
        rect[2] = x
        rect[1] = y
        rect[3] = y
        leftButtonDowm = True
        leftButtonUp = False
        
    #移動鼠標事件
    if event == cv2.EVENT_MOUSEMOVE:
        if leftButtonDowm and  not leftButtonUp:
            rect[2] = x
            rect[3] = y        
  
    #鼠標左鍵松開
    if event == cv2.EVENT_LBUTTONUP:
        if leftButtonDowm and  not leftButtonUp:
            x_min = min(rect[0],rect[2])
            y_min = min(rect[1],rect[3])
            
            x_max = max(rect[0],rect[2])
            y_max = max(rect[1],rect[3])
            
            rect[0] = x_min
            rect[1] = y_min
            rect[2] = x_max
            rect[3] = y_max
            leftButtonDowm = False      
            leftButtonUp = True

7、循環部分,當鼠標左鍵按下、沒有松開則實時繪制矩形框。當左鍵松開、對圖像進行分割。

while cv2.waitKey(2) == -1:
    #左鍵按下,畫矩陣
    if leftButtonDowm and not leftButtonUp:  
        img_copy = img.copy()
        #在img圖像上,繪制矩形  線條顏色為green 線寬為2
        cv2.rectangle(img_copy,(rect[0],rect[1]),(rect[0]+rect[2],rect[1]+rect[3]),(0,255,0),2)  
        #顯示圖片
        cv2.imshow('img',img_copy)
        
    #左鍵松開,矩形畫好 
    elif not leftButtonDowm and leftButtonUp and rect[0] != 0 and rect[1] != 0:
        rect_copy = tuple(rect.copy())   
        print(rect_copy)
        rect = [0,0,0,0]
        #物體分割
        cv2.grabCut(img,mask,rect_copy,bgdModel,fgdModel,5,cv2.GC_INIT_WITH_RECT)
            
        mask2 = np.where((mask==2)|(mask==0),0,1).astype('uint8')
        img_show = img*mask2[:,:,np.newaxis]
        #顯示圖片分割后結果
        cv2.imshow('grabcut',img_show)
        #顯示原圖
        cv2.imshow('img',img)    

cv2.waitKey(0)
cv2.destroyAllWindows()

調用完grabCut函數之后,掩模圖像mask元素值已經變成了0~3之間的值。值為0和2的將轉為0,值為1和3的將轉為1,然后保存在mask2中,這樣就可以用mask2過濾出所有的0值像素(理論上會保存所有的前景像素)。

 三、圖像分割之分水嶺算法

分水嶺算法是在分割的過程中,它會把跟臨近像素間的相性作為重要的參考依據,從而將在空間位置上相近並且灰度值相近求梯度)的像素點互相連接起來構成一個封閉的輪廓。分水嶺算法常用的操作步驟:彩色圖像灰度化,然后再求梯度圖,最后在梯度圖的基礎上進行分水嶺算法,求得分段圖像的邊緣線。

下面左邊的灰度圖,可以描述為右邊的地形圖,地形的高度是有灰度圖的灰度值決定,灰度為0對應地形圖的地面,灰度值最大的像素對應地形圖的最高點。

對灰度圖的地形圖的解釋,我們考慮三類點:

  1. 局部最小點值,該點對應一個盆地的最低點,當我們在盆地里滴一滴水的時候,由於重力作用,誰最終會匯聚到該點。注意:可能存在一個最小值面,該平面內的都是最小值點。
  2. 盆地的其他位置點,該位置滴的水會匯聚到局部最小點。
  3. 盆地的邊緣點,是盆地和其他盆地交界點,在該點滴一滴水,會等概率的流向任何一個盆地。

假設我們在盆地的最小值點,打一個洞,然后往盆地里面注水,並阻止兩個盆地的水匯聚,我們會在兩個盆地的水匯集的時刻,在交界的邊緣上(即分水嶺線),建一個大壩,來阻止兩個盆地的水匯聚成一片水域。這樣圖像就被分成2個像素集,一個是注水盆地像素集,一個是分水嶺線像素集。

在真實圖像中,由於噪聲點或者其它干擾因素的存在,使得分水嶺算法常常出現過度分割的現象,這主要是因為圖像中可能存在很多很小的局部極小點的存在,對這些局部盆地進行分割會導致過分割。為了解決過分割的問題,學者們提出了基於標記(mark)圖像的分水嶺算法,就是通過先驗知識,來指導分水嶺算法,以便獲得更好的圖像分割效果。通常的mark圖像,都是在某個區域定義了一些灰度層級,在這個區域的洪水淹沒過程中,水平面都是從定義的高度開始的,這樣可以避免一些很小的噪聲極小值區域的分割。

下面我們來學習一下OpenCV中提供的watershed函數:

watershed(image,markers)

參數說明:

  • image:必須是一個8位 3通道彩色圖像
  • markers:在執行分水嶺函數watershed之前,必須對參數markers進行處理,它應該包含不同區域的輪廓,每個輪廓有一個自己唯一的編號,輪廓的定位可以通過Opencv中findContours方法實現,這個是執行分水嶺之前的要求。

接下來執行分水嶺會發生什么呢?算法會根據markers傳入的輪廓作為種子(我們把注水點由盆地的最小值點轉為圖像的輪廓),對圖像上其他的像素點根據分水嶺算法規則進行判斷,並對每個像素點的區域歸屬進行划定,直到處理完圖像上所有像素點。而區域與區域之間的分界處的值被置為“-1”,以做區分。

簡單概括一下就是說第二個入參markers必須包含了種子點信息。Opencv官方例程中使用鼠標划線標記,其實就是在定義種子,只不過需要手動操作,而使用findContours可以自動標記種子點。而分水嶺方法完成之后並不會直接生成分割后的圖像,還需要進一步的顯示處理,如此看來,只有兩個參數的watershed其實並不簡單。

分水嶺算法實現圖像自動分割的步驟:

  1. 圖像灰度化、Canny邊緣檢測
  2. 查找輪廓,並且把輪廓信息按照不同的編號繪制到watershed的第二個參數markers上,相當於標記注水點。
  3. watershed分水嶺算法
  4. 繪制分割出來的區域,然后使用隨機顏色填充,再跟源圖像融合,以得到更好的顯示效果。

代碼如下:

# -*- coding: utf-8 -*-
"""
Created on Mon Jul 30 21:38:41 2018

@author: lenovo
"""

import numpy as np
import cv2

#讀入圖片
img = cv2.imread('image/img22.jpg')

#轉換為灰度圖片
gray_img = cv2.cvtColor(img,cv2.COLOR_BGR2GRAY)

#canny邊緣檢測 函數返回一副二值圖,其中包含檢測出的邊緣。
canny = cv2.Canny(gray_img,80,150)
cv2.imshow('Canny',canny)


#尋找圖像輪廓 返回修改后的圖像 圖像的輪廓  以及它們的層次
canny,contours,hierarchy = cv2.findContours(canny,cv2.RETR_TREE,cv2.CHAIN_APPROX_SIMPLE)
#32位有符號整數類型,
marks = np.zeros(img.shape[:2],np.int32)
#findContours檢測到的輪廓
imageContours = np.zeros(img.shape[:2],np.uint8)

#輪廓顏色
compCount = 0
index = 0
#繪制每一個輪廓
for index in range(len(contours)):
    #對marks進行標記,對不同區域的輪廓使用不同的亮度繪制,相當於設置注水點,有多少個輪廓,就有多少個輪廓
    #圖像上不同線條的灰度值是不同的,底部略暗,越往上灰度越高
    marks = cv2.drawContours(marks,contours,index,(index,index,index),1,8,hierarchy)
    #繪制輪廓,亮度一樣
    imageContours = cv2.drawContours(imageContours,contours,index,(255,255,255),1,8,hierarchy)
    
#查看 使用線性變換轉換輸入數組元素成8位無符號整型。
markerShows = cv2.convertScaleAbs(marks)    
cv2.imshow('markerShows',markerShows)
#cv2.imshow('imageContours',imageContours)

#使用分水嶺算法
marks = cv2.watershed(img,marks)
afterWatershed = cv2.convertScaleAbs(marks)  
cv2.imshow('afterWatershed',afterWatershed)

#生成隨機顏色
colorTab = np.zeros((np.max(marks)+1,3))
#生成0~255之間的隨機數
for i in range(len(colorTab)):
    aa = np.random.uniform(0,255)
    bb = np.random.uniform(0,255)
    cc = np.random.uniform(0,255)
    colorTab[i] = np.array([aa,bb,cc],np.uint8)
    
bgrImage = np.zeros(img.shape,np.uint8)

#遍歷marks每一個元素值,對每一個區域進行顏色填充
for i in range(marks.shape[0]):
    for j in range(marks.shape[1]):
        #index值一樣的像素表示在一個區域
        index = marks[i][j]
        #判斷是不是區域與區域之間的分界,如果是邊界(-1),則使用白色顯示
        if index == -1:
            bgrImage[i][j] = np.array([255,255,255])
        else:                        
            bgrImage[i][j]  = colorTab[index]
cv2.imshow('After ColorFill',bgrImage)            

#填充后與原始圖像融合
result = cv2.addWeighted(img,0.6,bgrImage,0.4,0)
cv2.imshow('addWeighted',result)     

cv2.waitKey(0)
cv2.destroyAllWindows()

我們對下面的圖像采用分水嶺算法進行分割:

然后我們分析代碼:

左側是使用Canny邊緣檢測后得到的二值化圖像,然后我們對二值化圖像進行查找輪廓,並進行處理得到符合要求的marks:

#尋找圖像輪廓 返回修改后的圖像 圖像的輪廓  以及它們的層次
canny,contours,hierarchy = cv2.findContours(canny,cv2.RETR_TREE,cv2.CHAIN_APPROX_SIMPLE)
#32位有符號整數類型,
marks = np.zeros(img.shape[:2],np.int32)
#findContours檢測到的輪廓
imageContours = np.zeros(img.shape[:2],np.uint8)

#輪廓顏色
compCount = 0
index = 0
#繪制每一個輪廓
for index in range(len(contours)):
    #對marks進行標記,對不同區域的輪廓使用不同的亮度繪制,相當於設置注水點,有多少個輪廓,就有多少個輪廓
    #圖像上不同線條的灰度值是不同的,底部略暗,越往上灰度越高
    marks = cv2.drawContours(marks,contours,index,(index,index,index),1,8,hierarchy)
    #繪制輪廓,亮度一樣
    imageContours = cv2.drawContours(imageContours,contours,index,(255,255,255),1,8,hierarchy)

然后我們把marks轉換為8位單通道灰度圖顯示,得到上面的右圖,可以看到圖像上不同輪廓的灰度值是不同的,底部略暗,越往上灰度越高。這些輪廓和不同的灰度值說明了什么?

每一個輪廓代表一個種子,輪廓的不同灰度值其實代表了對不同注水種子的編號,有多少不同灰度值的輪廓,就有多少個種子,圖像最后分割后就有多少個區域。

#查看 使用線性變換轉換輸入數組元素成8位無符號整型。
markerShows = cv2.convertScaleAbs(marks)    
cv2.imshow('markerShows',markerShows)
#cv2.imshow('imageContours',imageContours)

再來看一下執行完分水嶺算法之后的marks(下面左圖)。

上面左圖為分割出來的區域,我們可以看到,源圖像空間上臨近並且灰度值上相近的區域被划分為一個區域(同一區域的灰度值是一樣的),不同區域間被划開。

#使用分水嶺算法
marks = cv2.watershed(img,marks)
afterWatershed = cv2.convertScaleAbs(marks)  
cv2.imshow('afterWatershed',afterWatershed)

然后我們使用顏色填充分割出來的區域,得到上圖右邊的效果。

#生成隨機顏色
colorTab = np.zeros((np.max(marks)+1,3))
#生成0~255之間的隨機數
for i in range(len(colorTab)):
    aa = np.random.uniform(0,255)
    bb = np.random.uniform(0,255)
    cc = np.random.uniform(0,255)
    colorTab[i] = np.array([aa,bb,cc],np.uint8)
    
bgrImage = np.zeros(img.shape,np.uint8)

#遍歷marks每一個元素值,對每一個區域進行顏色填充
for i in range(marks.shape[0]):
    for j in range(marks.shape[1]):
        #index值一樣的像素表示在一個區域
        index = marks[i][j]
        #判斷是不是區域與區域之間的分界,如果是邊界(-1),則使用白色顯示
        if index == -1:
            bgrImage[i][j] = np.array([255,255,255])
        else:                        
            bgrImage[i][j]  = colorTab[index]
cv2.imshow('After ColorFill',bgrImage)            

我們再把填充后的圖像與源圖像進行融合,得到下面的效果:

#填充后與原始圖像融合
result = cv2.addWeighted(img,0.6,bgrImage,0.4,0)
cv2.imshow('addWeighted',result)     

cv2.waitKey(0)
cv2.destroyAllWindows()

參考文章:

[1]【圖像處理】圖像分割之(一~四)GraphCut,GrabCut函數使用和源碼解讀(OpenCV)

[2]圖像處理(十四)圖像分割(4)grab cut的圖割實現-Siggraph 2004

[3]Opencv學習——圖像分割之分水嶺算法

[4]圖像處理——分水嶺算法

[5]OpenCV庫中watershed函數(分水嶺算法)的詳細使用例程

[6]分水嶺算法及案例


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM