【翻譯:OpenCV-Python教程】用GrabCut算法進行交互式的前景提取

⚠️由於自己的拖延症,3.4.3翻到一半,OpenCV發佈了4.0.0了正式版,所以接下來是按照4.0.0翻譯的。

⚠️除了版本之外,其他還是照舊,Interactive Foreground Extraction using GrabCut Algorithm,原文

目標

在本章

  • 我們會看到GrabCut(譯者注:它是graph cut算法的改進)算法來提取圖像中的前景
  • 我們將爲此創建一個交互式的應用程序

理論

GrabCut算法的設計者是來自微軟英國劍橋研究院的Carsten Rother,Vladimir Kolmogorov 和 Andrew Blake,在他們的論文 "GrabCut": interactive foreground extraction using iterated graph cuts 中提出來的。該算法需要最小的人工交互來做前景提取,這個算法被稱爲 GrabCut。

從用戶的角度來看,該算法是如何工作的呢?最初用戶在前景區域周圍繪製一個矩形(該矩形需要完全框住所有的前景區域) 。然後算法對其進行迭代分割,得到最佳結果。但在某些情況下,分割的不是那麼理想,比如說,它可能把一些前景區域標成了背景,或者反過來。如果發生了這樣的情況,用戶需要進行仔細的修正。只要在有錯誤結果的地方“劃一下”就行了。“劃一下”基本上的意思就是說,*"嘿,這個區域應該是前景,你標記它爲背景,在下一次迭代中更正它。"* 或者對背景來說,反過來。然後再下一次迭代中,你就會得到更好的結果。

看下面的圖像。第一張圖中,球員和足球被圈在一個藍色的長方形裏。然後用白色筆畫(表示前景)和黑色筆畫(表示背景)進行一些最終的修正。我們得到了一個很好的結果。

grabcut_output1.jpg

那麼到底對背景做了什麼操作呢?

  • 用戶輸入了一個矩形。所有在此矩形之外的區域都被確信爲背景(這就是我們之前提到的,你必須用此矩形框住所有前景的原因)。而在矩形內部的區域,都被認爲是未知的。類似地,任何指定前臺和後臺的用戶輸入都被認爲是硬標籤,這意味着它們不會在後續迭代的過程中改變。
  • 計算機根據我們給出的數據,做一個初始的標記。它標記出前景和背景像素(或者硬標記)。
  • 現在利用高斯混合模型(GMM)對前景和背景進行建模。
  • 根據我們給出的數據,GMM學習並且創建一個新的像素分佈。也就是說,根據未知像素與其他硬標記像素的顏色統計關係(就像聚類一樣),將未知像素標記爲可能的前景或可能的背景。
  • 由這個像素分佈構建一個圖像。圖像中的節點是像素。再添加另外兩個節點,源節點終節點。每個前景像素連接到源節點,每個背景像素連接到匯聚節點。
  • 連接像素點到 源節點/終節點 的權重值,被設置成該像素點屬於 前景/背景的概率。像素間的權重由邊緣信息或像素相似性來定義。如果像素顏色差異較大,則它們之間的邊緣就會被給出一個較低的權重。
  • 然後採用最小切割(mincut)算法對圖像進行分割。它用最小損失函數將圖分割成兩部分,源節點和終節點。順勢函數是所有被切割邊的權值之和。在分割之後,切割後,所有連接到源節點的像素都變成了前景,連接到匯聚節點的像素都變成了背景。
  • 這個過程將繼續迭代下去,直到分類收斂爲止。:

如下圖所示:(感謝提供圖像:http://www.cs.ru.ac.za/research/g02m1682/)

grabcut_scheme.jpg

示例

現在我們用OpenCV來做一個grabcut算法,OpenCV有這個函數,cv.grabCut()來完成這個工作。我們先來看看它的參數吧:

  • img - 輸入圖像
  • mask - 這是個遮罩圖層,我們在這個圖層上指定出哪裏是背景區域,前景區域,或者可能的前景/背景等等。它由以下這些標誌組成,cv.GC_BGDcv.GC_FGDcv.GC_PR_BGDcv.GC_PR_FGD,或者簡單的傳入0,1,2,3給這個圖層。
  • rect - 它表示一個矩形以及矩形所在的座標,該矩形框住了所有前景,它的格式爲(x,y,w,h)。
  • bdgModel, fgdModel - These are arrays used by the algorithm internally. You just create two np.float64 type zero arrays of size (1,65).
  • iterCount - Number of iterations the algorithm should run.
  • mode - It should be cv.GC_INIT_WITH_RECT or cv.GC_INIT_WITH_MASK or combined which decides whether we are drawing rectangle or final touchup strokes.

First let's see with rectangular mode. We load the image, create a similar mask image. We create fgdModel and bgdModel. We give the rectangle parameters. It's all straight-forward. Let the algorithm run for 5 iterations. Mode should be cv.GC_INIT_WITH_RECT since we are using rectangle. Then run the grabcut. It modifies the mask image. In the new mask image, pixels will be marked with four flags denoting background/foreground as specified above. So we modify the mask such that all 0-pixels and 2-pixels are put to 0 (ie background) and all 1-pixels and 3-pixels are put to 1(ie foreground pixels). Now our final mask is ready. Just multiply it with input image to get the segmented image.

import numpy as np

import cv2 as cv

from matplotlib import pyplot as plt

img = cv.imread('messi5.jpg')

mask = np.zeros(img.shape[:2],np.uint8)

bgdModel = np.zeros((1,65),np.float64)

fgdModel = np.zeros((1,65),np.float64)

rect = (50,50,450,290)

cv.grabCut(img,mask,rect,bgdModel,fgdModel,5,cv.GC_INIT_WITH_RECT)

mask2 = np.where((mask==2)|(mask==0),0,1).astype('uint8')

img = img*mask2[:,:,np.newaxis]

plt.imshow(img),plt.colorbar(),plt.show()

See the results below:

grabcut_rect.jpg

image

Oops, Messi's hair is gone. Who likes Messi without his hair? We need to bring it back. So we will give there a fine touchup with 1-pixel (sure foreground). At the same time, Some part of ground has come to picture which we don't want, and also some logo. We need to remove them. There we give some 0-pixel touchup (sure background). So we modify our resulting mask in previous case as we told now.

What I actually did is that, I opened input image in paint application and added another layer to the image. Using brush tool in the paint, I marked missed foreground (hair, shoes, ball etc) with white and unwanted background (like logo, ground etc) with black on this new layer. Then filled remaining background with gray. Then loaded that mask image in OpenCV, edited original mask image we got with corresponding values in newly added mask image. Check the code below:

# newmask is the mask image I manually labelled

newmask = cv.imread('newmask.png',0)

# wherever it is marked white (sure foreground), change mask=1

# wherever it is marked black (sure background), change mask=0

mask[newmask == 0] = 0

mask[newmask == 255] = 1

mask, bgdModel, fgdModel = cv.grabCut(img,mask,None,bgdModel,fgdModel,5,cv.GC_INIT_WITH_MASK)

mask = np.where((mask==2)|(mask==0),0,1).astype('uint8')

img = img*mask[:,:,np.newaxis]

plt.imshow(img),plt.colorbar(),plt.show()

See the result below:

grabcut_mask.jpg

image

So that's it. Here instead of initializing in rect mode, you can directly go into mask mode. Just mark the rectangle area in mask image with 2-pixel or 3-pixel (probable background/foreground). Then mark our sure_foreground with 1-pixel as we did in second example. Then directly apply the grabCut function with mask mode.

Additional Resources

Exercises

  • OpenCV samples contain a sample grabcut.py which is an interactive tool using grabcut. Check it. Also watch this youtube video on how to use it.
  • Here, you can make this into a interactive sample with drawing rectangle and strokes with mouse, create trackbar to adjust stroke width etc.

上篇:【翻譯:OpenCV-Python教程】用分水嶺算法分割圖像

下篇:【翻譯:OpenCV-Python教程】圖像金字塔

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章