OpenCV-Python系列之GrabCut演算法
常情況下,我們需要影象進行前景後景進行分離,有的時候也許我們僅僅是需要前景。本次教程我們將介紹GrabCut演算法進行互動式前景提取。
GrabCut是一種基於圖切割的影象分割方法。GrabCut演算法是基於Graph Cut演算法的改進。
基於要被分割物件的指定邊界框開始,使用高斯混合模型估計被分割物件和背景的顏色分佈(注意,這裡將影象分為被分割物件和背景兩部分)。簡而言之,就是隻需確認前景和背景輸入,該演算法就可以完成前景和背景的最優分割。
該演算法利用影象中紋理(顏色)資訊和邊界(反差)資訊,只要少量的使用者互動操作就可得到較好的分割效果,和分水嶺演算法比較相似,但計算速度比較慢,得到的結果比較精確。若從靜態影象中提取前景物體(例如從一個影象剪下到另外一個影象),採用GrabCut演算法是最好的選擇。
原理
我們採用RGB顏色空間,分別用一個K個高斯分量(一取般K=5)的全協方差GMM(混合高斯模型)來對目標和背景進行建模。於是就存在一個額外的向量k = {k1, . . ., kn, . . ., kN},其中kn就是第n個畫素對應於哪個高斯分量,kn∈ {1, . . . K}。對於每個畫素,要不來自於目標GMM的某個高斯分量,要不就來自於背景GMM的某個高斯分量。
所以用於整個影象的Gibbs能量為(式7):
其中,U就是區域項,和上一文說的一樣,你表示一個畫素被歸類為目標或者背景的懲罰,也就是某個畫素屬於目標或者背景的概率的負對數。我們知道混合高斯密度模型是如下形式:
所以取負對數之後就變成式(9)那樣的形式了,其中GMM的引數θ就有三個:每一個高斯分量的權重π、每個高斯分量的均值向量u(因為有RGB三個通道,故為三個元素向量)和協方差矩陣∑(因為有RGB三個通道,故為3x3矩陣)。如式(10)。也就是說描述目標的GMM和描述背景的GMM的這三個引數都需要學習確定。一旦確定了這三個引數,那麼我們知道一個畫素的RGB顏色值之後,就可以代入目標的GMM和背景的GMM,就可以得到該畫素分別屬於目標和背景的概率了,也就是Gibbs能量的區域能量項就可以確定了,即圖的t-link的權值我們就可以求出。那麼n-link的權值怎麼求呢?也就是邊界能量項V怎麼求?
邊界項和之前說的Graph Cut的差不多,體現鄰域畫素m和n之間不連續的懲罰,如果兩鄰域畫素差別很小,那麼它屬於同一個目標或者同一背景的可能性就很大,如果他們的差別很大,那說明這兩個畫素很有可能處於目標和背景的邊緣部分,則被分割開的可能性比較大,所以當兩鄰域畫素差別越大,能量越小。而在RGB空間中,衡量兩畫素的相似性,我們採用歐式距離(二範數)。這裡面的引數β由影象的對比度決定,可以想象,如果影象的對比度較低,也就是說本身有差別的畫素m和n,它們的差||zm-zn||還是比較低,那麼我們需要乘以一個比較大的β來放大這種差別,而對於對比度高的影象,那麼也許本身屬於同一目標的畫素m和n的差||zm-zn||還是比較高,那麼我們就需要乘以一個比較小的β來縮小這種差別,使得V項能在對比度高或者低的情況下都可以正常工作。這時候我們想要的圖就可以得到了,我們就可以對其進行分割了。
我們來看看具體的實現原理:
(1)通過直接框選目標來得到一個初始的trimap T,即方框外的畫素全部作為背景畫素TB,而方框內TU的畫素全部作為“可能是目標”的畫素。
(2)對TB內的每一畫素n,初始化畫素n的標籤αn=0,即為背景畫素;而對TU內的每個畫素n,初始化畫素n的標籤αn=1,即作為“可能是目標”的畫素。
(3)經過上面兩個步驟,我們就可以分別得到屬於目標(αn=1)的一些畫素,剩下的為屬於背景(αn=0)的畫素,這時候,我們就可以通過這個畫素來估計目標和背景的GMM了。我們可以通過k-mean演算法分別把屬於目標和背景的畫素聚類為K類,即GMM中的K個高斯模型,這時候GMM中每個高斯模型就具有了一些畫素樣本集,這時候它的引數均值和協方差就可以通過他們的RGB值估計得到,而該高斯分量的權值可以通過屬於該高斯分量的畫素個數與總的畫素個數的比值來確定。
OpenCV中的使用
實現步驟:
1.在圖片中定義含有(一個或者多個)物體的矩形
2.矩形外的區域被自動認為是背景
3.對於使用者定義的矩形區域,可用背景中的資料來區別它裡面的前景和背景區域
4.用高斯混合模型來對背景和前景建模,並將未定義的畫素標記為可能的前景或背景
5.影象中歐冠的每一個畫素都被看作通過虛擬邊與周圍畫素相連線,而每條邊都有一個屬於前景或背景的概率,這基於它與周圍顏色上的相似性
6.每一個畫素(即演算法中的節點)會與一個前景或背景節點連結
7.在節點完成連結後,若節點之間的邊屬於不同終端,則會切斷它們之間的邊,這就能將影象各部分分割出來
我們先來了解相關的函式API:
mask, bgdModel, fgdModel = cv2.grabCut(img, mask, rect, bgdModel, fgdModel, iterCount[, mode])
img:輸入影象
mask :蒙版影象,指定哪些區域是背景,前景或可能的背景/前景等.它是由下面的標誌,cv2.GC_BGD,cv2.GC_FGD,cv2.GC_PR_BGD,cv2.GC_PR_FGD,或簡單地將0,1,2,3傳遞給影象。
rect :矩形的座標,包含了前景物件的格式(x,y,w,h)
bdgModel, fgdModel :演算法內部使用的陣列,只需要建立兩個大小為(1,65)的np.float64型別的0陣列.
iterCount :演算法執行的迭代次數.
mode :cv2.GC_INIT_WITH_RECT或cv2.GC_INIT_WITH_MASK,或者組合起來決定我們是畫矩形還是最後的觸點.
我們來看程式碼:
import numpy as np import cv2 from matplotlib import pyplot as plt import warnings warnings.filterwarnings("ignore", module="matplotlib") imgpath = "temp.jpg" img = cv2.imread(imgpath) Coords1x, Coords1y = 'NA', 'NA' Coords2x, Coords2y = 'NA', 'NA' def OnClick(event): # 獲取當滑鼠 " 按下 " 的時候,滑鼠的位置 global Coords1x, Coords1y if event.button == 1: try: Coords1x = int(event.xdata) Coords1y = int(event.ydata) except: Coords1x = event.xdata Coords1y = event.ydata print("#### 左上角座標 : ", Coords1x, Coords1y) def OnMouseMotion(event): # 獲取當滑鼠 " 移動 " 的時候,滑鼠的位置 global Coords2x, Coords2y if event.button == 3: try: Coords2x = int(event.xdata) Coords2y = int(event.ydata) except: Coords2x = event.xdata Coords2y = event.ydata print("#### 右下角座標 : ", Coords2x, Coords2x) def OnMouseRelease(event): if event.button == 2: fig = plt.gca() img = cv2.imread(imgpath) # 建立一個與所載入影象同形狀的 Mask mask = np.zeros(img.shape[:2], np.uint8) # 演算法內部使用的陣列 , 你必須建立兩個 np.float64 型別的 0 陣列 , 大小是 (1, 65) bgdModel = np.zeros((1, 65), np.float64) fgdModel = np.zeros((1, 65), np.float64) # 計算人工前景的矩形區域 (rect.x,rect.y,rect.width,rect.height) if (Coords2x - Coords1x) > 0 and (Coords2y - Coords1y) > 0: try: rect = (Coords1x, Coords1y, Coords2x - Coords1x, Coords2y - Coords1y) print('#### 分割區域: ', rect) print('#### 等會兒 有點慢 ...') iterCount = 5 cv2.grabCut(img, mask, rect, bgdModel, fgdModel, iterCount, cv2.GC_INIT_WITH_RECT) mask2 = np.where((mask == 2) | (mask == 0), 0, 1).astype('uint8') img = img * mask2[:, :, np.newaxis] plt.subplot(121), plt.imshow(cv2.cvtColor(img, cv2.COLOR_BGR2RGB)) plt.subplot(122), plt.imshow(cv2.cvtColor(cv2.imread(imgpath), cv2.COLOR_BGR2RGB)) fig.figure.canvas.draw() print('May the force be with me!') except: print('#### 先左鍵 後右鍵 ') else: print('#### 左下角座標值必須大於右上角座標 ') # 預先繪製圖片 fig = plt.figure() plt.imshow(cv2.cvtColor(img, cv2.COLOR_BGR2RGB)) # 滑鼠左鍵,選取分割區域(長方形)的左上角點 fig.canvas.mpl_connect('button_press_event', OnClick) # 滑鼠右鍵,選取分割區域(長方形)的右下角點 fig.canvas.mpl_connect('button_press_event', OnMouseMotion) # 滑鼠中鍵,在所選區域執行分割操作 fig.canvas.mpl_connect('button_press_event', OnMouseRelease) plt.show()
顯示一張圖片:
這個時候我們用滑鼠左鍵點選左上角,右鍵點選右下角,之後點選滑鼠中鍵進行生成:
可以看到,前景後景都已經被分離出來了。
天道酬勤 循序漸進 技壓群雄