opencv-python 4.16. 基于GrabCut算法的交互式前景提取

发布时间 2023-04-10 17:15:44作者: 一枚码农

理论

GrabCut算法由英国剑桥微软研究院的Carsten Rother,Vladimir Kolmogorov和Andrew Blake设计。在他们的论文:"GrabCut": interactive foreground extraction using iterated graph cuts中提出了一种基于最小用户交互的前景提取算法,其结果为GrabCut。

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

见下图。第一名球员和足球被包围在一个蓝色矩形中。然后进行一些具有白色笔划(表示前景)和黑色笔划(表示背景)的最终修饰。我们得到了一个很好的结果。

image

那么背景会发生什么?

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

如下图所示(图片提供:http://www.cs.ru.ac.za/research/g02m1682/)
image

示例

OpenCV函数cv.grabCut具有此功能。
cv.grabCut(img, mask, rect, bgdModel, fgdModel, iterCount, mode) -> mask, bgdModel, fgdModel
参数说明

  • img:输入图像,GrabCut假设它是一个8位的3通道图像(即, BGR信道顺序中的无符号8位整数)。
  • mask:输入/输出掩膜。假设该掩膜是一个单通道图像,数据类型为无符号8位整数。如果你使用边界框初始化(第七个参数mode设为cv2.GC_INIT_WITH_RECT),这个掩膜会自动初始化。否则,GrabCut假设正在执行掩码初始化(第七个参数设为cv2.GC_INIT_WITH_MASK)。
  • rect:包含我们要分割的区域的边框矩形。此参数仅在将模式设置为cv2.GC_INIT_WITH_MASK时使用)。
  • bgdModel: GrabCut内部建模背景时使用的临时数组。
  • fgdModel: GrabCut在建模前景时使用的临时数组。
  • iterCount:GrabCut在对前景和背景建模时执行的迭代次数。迭代次数越多,GrabCut运行的时间越长,理想情况下,结果会更好。
  • model:要么cv2.GC_INIT_WITH_RECT或cv2.GC_INIT_WITH_MASK,这分别取决于你是用一个边框还是一个掩码初始化GrabCut。

OpenCV的GrabCut实现返回一个3元组:

  • mask:应用GrabCut后的输出掩模
  • fgdMode:用于建模背景的临时数组(可以忽略此值)
  • fgdModel:用于建模前景的临时数组(同样可以忽略此值)

首先让我们看看矩形模式。我们加载图像,创建一个类似的掩膜图像。我们创建了fgdModel和bgdModel。我们给出矩形参数。这一切都是直截了当的。让算法运行5次迭代。模式应该是cv.GC_INIT_WITH_RECT,因为我们使用矩形。然后运行抓取。它修改了掩膜图像。在新的掩模图像中,像素将被标记为表示背景/前景的四个标记,如上所述。因此,我们修改掩模,使得所有0像素和2像素都被置为0(即背景),并且所有1像素和3像素被置为1(即前景像素)。现在我们的最后面具准备好了。只需将其与输入图像相乘即可得到分割后的图像。

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

img = cv.imread(r'C:\Users\yuyalong\Pictures\Saved Pictures\mess.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.rectangle(img, (50, 50), (450, 290), (0, 255, 0), 3)
# cv.imshow('img', img)


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]

cv.imshow('img', img)
cv.waitKey(0)

image
结果梅西的头发不见了。因此,我们将为其提供1像素(确定前景)的精细修饰。 与此同时,有些地方已经出现了我们不想要的图片,还有一些标识。 我们需要删除它们。 在那里我们提供一些0像素的修饰(确定背景)。 因此,正如我们现在所说的那样,我们在之前的案

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

用画笔标记图像

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


def draw_circle(event, x, y, flags, param):
    # 当左键按下并移动时绘制图形,event可以查看移动,flag查看是否按下
    if event == cv.EVENT_MOUSEMOVE and flags == cv.EVENT_FLAG_LBUTTON:
        cv.circle(img, (x, y), 5, (255, 255, 255), -1)
    elif event == cv.EVENT_MOUSEMOVE and flags == cv.EVENT_FLAG_RBUTTON:
        cv.circle(img, (x, y), 5, (0, 0, 0), -1)


img = cv.imread(r'C:\Users\yuyalong\Pictures\Saved Pictures\mess.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.rectangle(img, (50, 50), (450, 290), (0, 255, 0), 3)

cv.namedWindow('img')
cv.setMouseCallback('img', draw_circle)
while True:
    cv.imshow('img', img)
    k = cv.waitKey(20)
    if k == 27:  # wait for ESC key to exit
        cv.destroyAllWindows()
        break
    elif k == ord('s'):  # wait for 's' key to save and exit
        cv.imwrite('messigray.png', img)
        break
cv.destroyAllWindows()

image

提取前景

这里没懂是怎么做。
看链接 https://opencv-python-tutorials.readthedocs.io/zh/latest/4. OpenCV中的图像处理/4.16. 基于GrabCut算法的交互式前景提取/

示例2

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

img = cv.imread(r'C:\Users\yuyalong\Pictures\Saved Pictures\kids.jpg')

# 创建掩膜
mask = np.zeros(img.shape[:2], np.uint8)

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

# 创建矩形框
rect = (120, 10, 510, 620)
# cv.rectangle(img, (120, 10), (510, 620), (0, 255, 0), 3)
# cv.imshow('img', img)


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]

cv.imshow('img', img)
cv.waitKey(0)

image
image
image