【翻译: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教程】图像金字塔

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