OpenCV 4.12.0
开源计算机视觉
加载中...
搜索中...
无匹配项
使用 GrabCut 算法进行交互式前景提取

目标

在本章中

  • 我们将学习 GrabCut 算法,用于提取图像中的前景
  • 我们将为此创建一个交互式应用程序。

理论

GrabCut 算法由英国微软研究院剑桥分部的 Carsten Rother、Vladimir Kolmogorov 和 Andrew Blake 在他们的论文 "GrabCut": 使用迭代图割的交互式前景提取 中设计。需要一种用户交互最少的前景提取算法,GrabCut 应运而生。

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

请参见下面的图像。首先,球员和足球被包含在一个蓝色矩形中。然后进行一些白色笔画(表示前景)和黑色笔画(表示背景)的最终修饰。我们得到了一个很好的结果。

image

那么后台发生了什么?

  • 用户输入矩形。矩形外的所有内容都将被视为确定的背景(这就是之前提到你的矩形应该包含所有对象的原因)。矩形内的所有内容都是未知的。类似地,任何指定前景和背景的用户输入都被认为是硬标签,这意味着它们在过程中不会改变。
  • 计算机根据我们提供的数据进行初始标记。它标记前景和背景像素(或硬标记)。
  • 现在使用高斯混合模型 (GMM) 来对前景和背景进行建模。
  • 根据我们提供的数据,GMM 学习并创建新的像素分布。也就是说,未知像素被标记为可能的背景或可能的前景,这取决于它与其他硬标记像素在颜色统计方面的关系(这就像聚类)。
  • 从该像素分布构建一个图。图中的节点是像素。添加了另外两个节点,源节点汇节点。每个前景像素都连接到源节点,每个背景像素都连接到汇节点。
  • 将像素连接到源节点/终端节点的边的权重由像素作为前景/背景的概率定义。像素之间的权重由边缘信息或像素相似度定义。如果像素颜色存在很大差异,则它们之间的边缘将获得较低的权重。
  • 然后使用最小割算法来分割图。它将图切割成两个分离的源节点和汇节点,具有最小的成本函数。成本函数是被切割的边的所有权重的总和。切割后,所有连接到源节点的像素都变为前景,而连接到汇节点的像素都变为背景。
  • 该过程一直持续到分类收敛。

它在下图(图片来源:http://www.cs.ru.ac.za/research/g02m1682/)中进行了说明

image

演示

现在我们使用 OpenCV 进行 grabcut 算法。 OpenCV 具有函数 cv.grabCut() 为此。我们将首先看到它的参数

  • img - 输入图像
  • mask - 这是一个掩码图像,我们在其中指定哪些区域是背景、前景或可能的背景/前景等。这是通过以下标志完成的,cv.GC_BGD, cv.GC_FGD, cv.GC_PR_BGD, cv.GC_PR_FGD,或者只是将 0,1,2,3 传递给图像。
  • rect - 它是包含前景对象的矩形的坐标,格式为 (x,y,w,h)
  • bdgModel, fgdModel - 这些是算法内部使用的数组。你只需创建两个大小为 (1,65) 的 np.float64 类型零数组。
  • iterCount - 算法应运行的迭代次数。
  • mode - 应该是 cv.GC_INIT_WITH_RECTcv.GC_INIT_WITH_MASK 或组合,它决定我们是绘制矩形还是最终修饰笔画。

首先让我们看看矩形模式。我们加载图像,创建一个类似的掩码图像。我们创建 fgdModelbgdModel。我们给出矩形参数。一切都很简单。让算法运行 5 次迭代。模式应该是 cv.GC_INIT_WITH_RECT 因为我们正在使用矩形。然后运行 grabcut。它修改掩码图像。在新的掩码图像中,像素将被标记为四个标志,表示如上所述的背景/前景。因此,我们修改掩码,使所有 0 像素和 2 像素都设置为 0(即背景),所有 1 像素和 3 像素都设置为 1(即前景像素)。现在我们的最终掩码已准备好。只需将它与输入图像相乘即可获得分割的图像。

import numpy as np
import cv2 as cv
from matplotlib import pyplot as plt
img = cv.imread('messi5.jpg')
assert img is not None, "文件无法读取,请检查 os.path.exists()"
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()
CV_EXPORTS_W Mat imread(const String &filename, int flags=IMREAD_COLOR_BGR)
从文件加载图像。
void grabCut(InputArray img, InputOutputArray mask, Rect rect, InputOutputArray bgdModel, InputOutputArray fgdModel, int iterCount, int mode=GC_EVAL)
运行 GrabCut 算法。

查看下面的结果

image

哎呀,梅西的头发不见了。谁喜欢没有头发的梅西?我们需要把它找回来。所以我们将用 1 像素(确定的前景)在那里进行精细的修饰。与此同时,地面的某些部分也进入了画面,我们不想要,还有一些标志。我们需要删除它们。在那里,我们给出一些 0 像素修饰(确定的背景)。所以我们按照我们现在告诉的方式修改了我们在前一个案例中的结果掩码。

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

# newmask 是我手动标记的掩码图像
newmask = cv.imread('newmask.png', cv.IMREAD_GRAYSCALE)
assert newmask is not None, "文件无法读取,请检查 os.path.exists()"
# 只要它被标记为白色(确定的前景),就将 mask=1
# 只要它被标记为黑色(确定的背景),就将 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()

请参阅下面的结果

image

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

练习

  1. OpenCV 示例包含一个示例 grabcut.py,它是一个使用 grabcut 的交互式工具。检查一下。还可以观看此 youtube 视频 了解如何使用它。
  2. 在这里,您可以将其制作成一个交互式示例,用鼠标绘制矩形和笔画,创建轨迹栏来调整笔画宽度等。