1. 程式人生 > > OpenCv學習筆記4--影象分割之GrabCut演算法

OpenCv學習筆記4--影象分割之GrabCut演算法

說明: 本文章是opencv學習筆記系列的第四篇小結,可能前幾篇內容太多,排版也不甚合理,所以為了更好的觀看體驗,這次的內容會稍微少那麼一點點,再次重申 

歡迎star,不定時更新...

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

一、影象分割演算法概述

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函式:

cv2.grabCut(img, mask, rect, bgdModel, fgdModel, iterCount[, mode]) → None

輸入:影象、被標記好的前景、背景

輸出:分割影象

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

引數:
  • img - 輸入8位3通道影象。處理過程中不會被修改
  • mask-

    輸入/輸出8位單通道掩碼。如果使用掩碼進行初始化,那麼mask儲存初始化掩碼資訊;在執行分割的時候,也可以將使用者互動所設定的前景與背景儲存到mask中,然後再傳入grabCut函式;在處理結束之後,mask中會儲存結果。

  • mode設定為GC_INIT_WITH_RECT時,該功能初始化掩碼 GC_INIT_WITH_RECT。其元素可能具有以下值之一:

    • GC_BGD定義了明顯的背景畫素。
    • GC_FGD定義了一個明顯的前景(物件)畫素。
    • GC_PR_BGD定義了可能的背景畫素。
    • GC_PR_FGD定義了可能的前景畫素。
  • rect - 包含分段物件的ROI。ROI外部的畫素標記為“明顯的背景”。該引數僅在以下時使用 mode==GC_INIT_WITH_RECT
  • bgdModel - 背景模型的臨時陣列。處理相同影象時請勿修改它。
  • fgdModel - 前景模型的臨時陣列。處理相同影象時請勿修改它。
  • iterCount - 返回結果之前演算法應該進行的迭代次數。必須大於0, 請注意,可以使用mode==GC_INIT_WITH_MASK 或 進一步呼叫結果mode==GC_EVAL
  • mode-
    • GC_INIT_WITH_RECT 該函式使用提供的矩形初始化狀態和掩碼。之後,它執行 iterCount 演算法的迭代。
    • GC_INIT_WITH_MASK 該函式使用提供的掩碼初始化狀態。請注意, GC_INIT_WITH_RECT 並且 GC_INIT_WITH_MASK 可以組合使用。然後,ROI外部的所有畫素都會自動初始化 GC_BGD
    • GC_EVAL 該值表示演算法應該恢復

上面是屬於opencv官方文件的講解,不過個人感覺大神給出的解釋更加通俗透徹,這裡放上來,可以對比看下.

引數說明:

  • 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),執行分割。

OK, 下面給出第一個簡單的例子,這是關於grabcut演算法的第一個初級程式碼,直接給定rect[ ],指定ROI.,和往常一樣,程式碼可以直接copy執行.

完整程式碼如下:

# -*- coding:utf-8 -*-
import cv2
import numpy as np

'''
       created on  08:10:27 2018-11-15
       @author:ren_dong

            Grabcut 影象分割  
                
            直接給定矩形區域作為ROI
                
            GrabCut演算法的實現步驟:
            
                1 在圖片中定義(一個或者多個)包含物體的矩形。
                2 矩形外的區域被自動認為是背景。
                3 對於使用者定義的矩形區域,可用背景中的資料來區分它裡面的前景和背景區域。
                4 用高斯混合模型(GMM)來對背景和前景建模,並將未定義的畫素標記為可能的前景或者背景。
                5 影象中的每一個畫素都被看做通過虛擬邊與周圍畫素相連線,而每條邊都有一個屬於前景或者背景的概率,這是基於它與周邊畫素顏色上的相似性。
                6 每一個畫素(即演算法中的節點)會與一個前景或背景節點連線。
                7 在節點完成連線後(可能與背景或前景連線),若節點之間的邊屬於不同終端(即一個節點屬於前景,另一個節點屬於背景),則會切斷他們之間的邊,這就能將影象各部分分割出來。

                
            cv2.grabCut(img, mask, rect, bgdModel, fgdModel, iterCount[, mode]) → None


'''
#載入影象img
fname = 'test.jpg'
img = cv2.imread(fname)

#設定矩形區域  作為ROI         矩形區域外作為背景
rect = (275, 120, 170, 320)

#img.shape[:2]得到img的row 和 col ,
# 得到和img尺寸一樣的掩模即mask ,然後用0填充
mask = np.zeros(img.shape[:2], np.uint8)

#建立以0填充的前景和背景模型,  輸入必須是單通道的浮點型影象, 1行, 13x5 = 65的列 即(1,65)
bgModel = np.zeros((1,65), np.float64)
fgModel = np.zeros((1,65), np.float64)

#呼叫grabcut函式進行分割,輸入影象img, mask,  mode為 cv2.GC_INIT_WITH-RECT
cv2.grabCut(img, mask, rect, bgModel, fgModel, 5, cv2.GC_INIT_WITH_RECT)

##呼叫grabcut得到rect[0,1,2,3],將0,2合併為0,   1,3合併為1  存放於mask2中
mask2 = np.where((mask == 2) | (mask == 0), 0, 1).astype(np.uint8)

#得到輸出影象
out = img * mask2[:, :, np.newaxis]

cv2.imshow('origin', img)
cv2.imshow('grabcut', out)
cv2.waitKey()
cv2.destroyAllWindows()

下面是程式碼執行結果展示:

左圖為原始影象,右圖為進行grabcut演算法處理之後的影象,我們完成了摳圖,  [斜眼] !

可能對於某些圖片不能準確的指定任務或者ROI所在的區域,所以我們對上述程式進行了改進,加入了滑鼠回撥函式,可以進行互動式滑鼠選取ROI,然後進行grabcut處理,可以說是完整版的grabcut演算法.

完整程式碼如下:

# -*- coding:utf-8 -*-
import cv2
import numpy as np

'''
       created on  08:10:27 2018-11-15
       @author:ren_dong

                Grabcut 影象分割  
                
                加入滑鼠回撥函式,可以進行互動式操作,滑鼠選擇矩形區域作為ROI
                
               cv2.grabCut(img, mask, rect, bgdModel, fgdModel, iterCount[, mode]) → None
               on_mouse()


'''

# 滑鼠事件的回撥函式
def on_mouse(event, x, y, flag, param):
    global rect
    global leftButtonDown
    global leftButtonUp

    # 滑鼠左鍵按下
    if event == cv2.EVENT_LBUTTONDOWN:
        rect[0] = x
        rect[2] = x
        rect[1] = y
        rect[3] = y
        leftButtonDown = True
        leftButtonUp = False

    # 移動滑鼠事件
    if event == cv2.EVENT_MOUSEMOVE:
        if leftButtonDown and not leftButtonUp:
            rect[2] = x
            rect[3] = y

            # 滑鼠左鍵鬆開
    if event == cv2.EVENT_LBUTTONUP:
        if leftButtonDown 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
            leftButtonDown = False
            leftButtonUp = True


# 讀入圖片
img = cv2.imread('ouc.jpg')
'''
    
     掩碼影象,如果使用掩碼進行初始化,那麼mask儲存初始化掩碼資訊;
     在執行分割的時候,也可以將使用者互動所設定的前景與背景儲存到mask中,
     然後再傳入grabCut函式;
     在處理結束之後,mask中會儲存結果

'''
#img.shape[:2]=img.shape[0:2] 代表取彩色影象的長和寬  img.shape[:3] 代表取長+寬+通道
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 初始化
rect = [0, 0, 0, 0]

# 滑鼠左鍵按下
leftButtonDown = False
# 滑鼠左鍵鬆開
leftButtonUp = True

# 指定視窗名來建立視窗
cv2.namedWindow('img')
# 設定滑鼠事件回撥函式 來獲取滑鼠輸入
cv2.setMouseCallback('img', on_mouse)

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


##設定迴圈,進行互動式操作
while cv2.waitKey(2) == -1:
    # 左鍵按下,畫矩陣
    if leftButtonDown 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 leftButtonDown 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_copy = tuple(rect)
        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()
cv2.destroyAllWindows()

下面是結果展示區:

OK,上面展示了我的在讀院校,我們使用滑鼠選取了標誌所在的區域作為ROI,右圖就是grabcut演算法處理之後的結果,效果還不錯. 

這是說明一下,其實大部分內容是參考大神部落格進行編寫的,所以這裡一直使用筆記作為記錄標誌,我只是在理解別人的思路,後來發現我寫出的不如大神的理解深刻,所以這裡給出大神的部落格地址,非常感謝這些博主.