OpenCV 29: 使用Grabcut算法的交互式前景提取

3,134 阅读6分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 31 天,点击查看活动详情

目标

在这一章当中

  • 看到 GrabCut算法来提取图像中的前景
  • 为此创建一个交互式应用程序

理论

GrabCut 算法由英国剑桥微软研究院的 Carsten Rother、Vladimir Kolmogorov 和 Andrew Blake 设计。在他们的论文“GrabCut”中:使用迭代图切割的交互式前景提取。需要一种算法以最少的用户交互进行前景提取,结果是 GrabCut。

从用户的角度来看它是如何工作的?最初用户在前景区域周围绘制一个矩形(前景区域应该完全在矩形内)。然后算法对其进行迭代分割以获得最佳结果。完毕。但是在某些情况下,分割不会很好。例如,它可能将某些前景区域标记为背景,反之亦然。在这种情况下,用户需要进行精细的修饰。只需在存在一些错误结果的图像上进行一些描边即可。Strokes 基本上是说 “嘿,这个区域应该是前景,你把它标记为背景,在下一次迭代中纠正它” 或者它的相反背景。然后在下一次迭代中,将获得更好的结果。

见下图。第一位球员和足球被封闭在一个蓝色矩形中。然后用 白色笔触(表示前景)和黑色笔触(表示背景) 进行一些最终修饰,最终得到了一个不错的结果。

在这里插入图片描述

这背后会发生什么?

  • 用户输入矩形。此矩形之外的所有内容都将被视为确定的背景(这就是之前提到的矩形应包含所有对象的原因)。矩形内的一切都是未知的。类似地,任何指定前景和背景的用户输入都被视为硬标签,这意味着它们不会在过程中发生变化。
  • 计算机根据所提供的数据进行初始标记。标记前景和背景像素(或硬标记)
  • 现在使用高斯混合模型(GMM)对前景和背景进行建模
  • 根据提供的数据,GMM 学习并创建新的像素分布。即,未知像素被标记为可能的前景或可能的背景,这取决于它与其他硬标记像素在颜色统计方面的关系(就像聚类一样)。
  • 图形是根据此像素分布构建的。图中的节点是像素。添加了另外两个节点**,Source node和Sink node**。每个前景像素都连接到 Source 节点,每个背景像素都连接到 Sink 节点。
  • 将像素连接到源节点/结束节点的边的权重由像素为前景/背景的概率定义。像素之间的权重由边缘信息或像素相似度定义。如果像素颜色存在较大差异,则它们之间的边缘将获得较低的权重。
  • 然后使用mincut算法对图进行分割。它以最小的代价函数将图切割成两个分离的源节点和汇节点。成本函数是被切割的边的所有权重的总和。剪切后,所有连接到源节点的像素成为前景,连接到接收节点的像素成为背景。
  • 该过程一直持续到分类收敛。 示意图如下所示(图片提供:[http : //www.cs.ru.ac.za/research/g0… : //www.cs.ru.ac.za/research/g0…

在这里插入图片描述

演示

现在使用 OpenCV 实现grabcut算法。OpenCV 有函数cv2.grabCut() 。我们将首先看到它的参数:

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

  • img - 输入图像
  • mask - 遮罩图像,指定哪些区域是背景、前景或可能的背景/前景等。由以下标志完成,cv2.GC_BGDcv2.GC_FGDcv2.GC_PR_BGDcv2.GC_PR_FGD,或简单地通过0,1,2,3
  • rect - 它是包含格式为 (x,y,w,h) 的前景对象的矩形的坐标
  • bdgModel , fgdModel - 这些是算法内部使用的数组。只需创建两个大小为 (1,65) 的 np.float64 类型零数组
  • iterCount - 算法应该运行的迭代次数
  • mode 应该是cv2.GC_INIT_WITH_RECTcv2.GC_INIT_WITH_MASK或组合,这决定了是绘制矩形还是最终的修饰笔触。

首先,看看矩形模式( rectangular mode)。加载图像,然后创建一个类似的蒙版图像。创建fgdModelbgdModel。并给出矩形参数。这一切都是直截了当的。让算法迭代运行 5 次。模式应该是cv2.GC_INIT_WITH_RECT,这是因为使用的是矩形。然后运行grabcut。它修改蒙版图像。在新的蒙版图像中,像素将被标记为四个标志,表示上面指定的背景/前景。所以修改掩码,使得所有 0 像素和 2 像素都置为 0(即背景),所有 1 像素和 3 像素均置为 1(即前景像素)。现在最终的mask准备好了。只需将其与输入图像相乘即可得到分割图像。

import cv2
import numpy as np
from matplotlib import pyplot as plt

img = cv2.imread('messi2.jpg')
mask = np.zeros(img.shape[:2], np.uint8)
cv2.imwrite('dd.jpg', mask)

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

rect = (50, 50, 450, 290)
cv2.grabCut(img, mask, rect, bgdModel, fgdModel, 5, cv2.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()

查看以下结果:

在这里插入图片描述

哎呀,梅西的头发不见了。谁喜欢没有头发的梅西?我们需要把它弄回来。因此,将使用 1 像素(当然是前景) 进行精细修饰。同时,一些地面出现了我们不想要的图片,还有一些标志, 也需要移除它们。在那里,提供了一些 0 像素的修饰(当然是背景)。因此,正如现在所说的那样,修改了之前案例中的结果掩码。

实际做的是,在绘画应用程序中打开输入图像并为图像添加了另一个图层。在油漆中使用画笔工具,在这个新图层上用白色标记错过的前景(头发、鞋子、球等)和用黑色标记不需要的背景(如标志、地面等)。然后用灰色填充剩余的背景。 然后在 OpenCV 中加载该蒙版图像,编辑我们获得的原始蒙版图像,并在新添加的蒙版图像中使用相应的值。检查下面的代码:

# newmask is the mask image by manually labelled
newmask = cv2.imread('messi-new-mask.jpg', 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 = cv2.grabCut(img,mask,None,bgdModel,fgdModel,5,cv2.GC_INIT_WITH_MASK)
mask = np.where((mask==2)|(mask==0),0,1).astype('uint8')
img = img*mask[:,:,np.newaxis]
plt.subplot(121)
plt.imshow(mask)

plt.subplot(122)
plt.imshow(img)
plt.colorbar()
plt.show()

在这里插入图片描述

在这里,可以直接进入掩码模式,而不是在 rect 模式下初始化。只需用 2 像素或 3 像素(可能的背景/前景)标记蒙版图像中的矩形区域。然后像我们在第二个示例中所做的那样用 1 像素标记我们的 sure_foreground。然后直接应用带有mask模式的grabCut函数。

附加资源