常情况下,我们需要图像进行前景后景进行分离,有的时候也许我们仅仅是需要前景。本次教程我们将介绍GrabCut算法进行交互式前景提取。
GrabCut是一种基于图切割的图像分割方法。GrabCut算法是基于Graph Cut算法的改进。
基于要被分割对象的指定边界框开始,使用高斯混合模型估计被分割对象和背景的颜色分布(注意,这里将图像分为被分割对象和背景两部分)。简而言之,就是只需确认前景和背景输入,该算法就可以完成前景和背景的最优分割。
该算法利用图像中纹理(颜色)信息和边界(反差)信息,只要少量的用户交互操作就可得到较好的分割效果,和分水岭算法比较相似,但计算速度比较慢,得到的结果比较精确。若从静态图像中提取前景物体(例如从一个图像剪切到另外一个图像),采用GrabCut算法是最好的选择。
原理
我们采用RGB颜色空间,分别用一个K个高斯分量(一取般K=5)的全协方差GMM(混合高斯模型)来对目标和背景进行建模。于是就存在一个额外的向量k = {k1, . . ., kn, . . ., kN},其中kn就是第n个像素对应于哪个高斯分量,kn∈ {1, . . . K}。对于每个像素,要不来自于目标GMM的某个高斯分量,要不就来自于背景GMM的某个高斯分量。
所以用于整个图像的Gibbs能量为(式7):
其中,U就是区域项,和上一文说的一样,你表示一个像素被归类为目标或者背景的惩罚,也就是某个像素属于目标或者背景的概率的负对数。我们知道混合高斯密度模型是如下形式:
所以取负对数之后就变成式(9)那样的形式了,其中GMM的参数θ就有三个:每一个高斯分量的权重π、每个高斯分量的均值向量u(因为有RGB三个通道,故为三个元素向量)和协方差矩阵∑(因为有RGB三个通道,故为3x3矩阵)。如式(10)。也就是说描述目标的GMM和描述背景的GMM的这三个参数都需要学习确定。一旦确定了这三个参数,那么我们知道一个像素的RGB颜色值之后,就可以代入目标的GMM和背景的GMM,就可以得到该像素分别属于目标和背景的概率了,也就是Gibbs能量的区域能量项就可以确定了,即图的t-link的权值我们就可以求出。那么n-link的权值怎么求呢?也就是边界能量项V怎么求?
边界项和之前说的Graph Cut的差不多,体现邻域像素m和n之间不连续的惩罚,如果两邻域像素差别很小,那么它属于同一个目标或者同一背景的可能性就很大,如果他们的差别很大,那说明这两个像素很有可能处于目标和背景的边缘部分,则被分割开的可能性比较大,所以当两邻域像素差别越大,能量越小。而在RGB空间中,衡量两像素的相似性,我们采用欧式距离(二范数)。这里面的参数β由图像的对比度决定,可以想象,如果图像的对比度较低,也就是说本身有差别的像素m和n,它们的差||zm-zn||还是比较低,那么我们需要乘以一个比较大的β来放大这种差别,而对于对比度高的图像,那么也许本身属于同一目标的像素m和n的差||zm-zn||还是比较高,那么我们就需要乘以一个比较小的β来缩小这种差别,使得V项能在对比度高或者低的情况下都可以正常工作。这时候我们想要的图就可以得到了,我们就可以对其进行分割了。
我们来看看具体的实现原理:
(1)通过直接框选目标来得到一个初始的trimap T,即方框外的像素全部作为背景像素TB,而方框内TU的像素全部作为“可能是目标”的像素。
(2)对TB内的每一像素n,初始化像素n的标签αn=0,即为背景像素;而对TU内的每个像素n,初始化像素n的标签αn=1,即作为“可能是目标”的像素。
(3)经过上面两个步骤,我们就可以分别得到属于目标(αn=1)的一些像素,剩下的为属于背景(αn=0)的像素,这时候,我们就可以通过这个像素来估计目标和背景的GMM了。我们可以通过k-mean算法分别把属于目标和背景的像素聚类为K类,即GMM中的K个高斯模型,这时候GMM中每个高斯模型就具有了一些像素样本集,这时候它的参数均值和协方差就可以通过他们的RGB值估计得到,而该高斯分量的权值可以通过属于该高斯分量的像素个数与总的像素个数的比值来确定。
OpenCV中的使用
实现步骤:
1.在图片中定义含有(一个或者多个)物体的矩形
2.矩形外的区域被自动认为是背景
3.对于用户定义的矩形区域,可用背景中的数据来区别它里面的前景和背景区域
4.用高斯混合模型来对背景和前景建模,并将未定义的像素标记为可能的前景或背景
5.图像中欧冠的每一个像素都被看作通过虚拟边与周围像素相连接,而每条边都有一个属于前景或背景的概率,这基于它与周围颜色上的相似性
6.每一个像素(即算法中的节点)会与一个前景或背景节点链接
7.在节点完成链接后,若节点之间的边属于不同终端,则会切断它们之间的边,这就能将图像各部分分割出来
我们先来了解相关的函数API:
mask, bgdModel, fgdModel = cv2.grabCut(img, mask, rect, bgdModel, fgdModel, iterCount[, mode])
img:输入图像
mask :蒙版图像,指定哪些区域是背景,前景或可能的背景/前景等.它是由下面的标志,cv2.GC_BGD,cv2.GC_FGD,cv2.GC_PR_BGD,cv2.GC_PR_FGD,或简单地将0,1,2,3传递给图像。
rect :矩形的坐标,包含了前景对象的格式(x,y,w,h)
bdgModel, fgdModel :算法内部使用的数组,只需要创建两个大小为(1,65)的np.float64类型的0数组.
iterCount :算法运行的迭代次数.
mode :cv2.GC_INIT_WITH_RECT或cv2.GC_INIT_WITH_MASK,或者组合起来决定我们是画矩形还是最后的触点.
我们来看代码:
import numpy as np import cv2 from matplotlib import pyplot as plt import warnings warnings.filterwarnings("ignore", module="matplotlib") imgpath = "temp.jpg" img = cv2.imread(imgpath) Coords1x, Coords1y = 'NA', 'NA' Coords2x, Coords2y = 'NA', 'NA' def OnClick(event): # 获取当鼠标 " 按下 " 的时候,鼠标的位置 global Coords1x, Coords1y if event.button == 1: try: Coords1x = int(event.xdata) Coords1y = int(event.ydata) except: Coords1x = event.xdata Coords1y = event.ydata print("#### 左上角坐标 : ", Coords1x, Coords1y) def onm ouseMotion(event): # 获取当鼠标 " 移动 " 的时候,鼠标的位置 global Coords2x, Coords2y if event.button == 3: try: Coords2x = int(event.xdata) Coords2y = int(event.ydata) except: Coords2x = event.xdata Coords2y = event.ydata print("#### 右下角坐标 : ", Coords2x, Coords2x) def onm ouseRelease(event): if event.button == 2: fig = plt.gca() img = cv2.imread(imgpath) # 创建一个与所加载图像同形状的 Mask mask = np.zeros(img.shape[:2], np.uint8) # 算法内部使用的数组 , 你必须创建两个 np.float64 类型的 0 数组 , 大小是 (1, 65) bgdModel = np.zeros((1, 65), np.float64) fgdModel = np.zeros((1, 65), np.float64) # 计算人工前景的矩形区域 (rect.x,rect.y,rect.width,rect.height) if (Coords2x - Coords1x) > 0 and (Coords2y - Coords1y) > 0: try: rect = (Coords1x, Coords1y, Coords2x - Coords1x, Coords2y - Coords1y) print('#### 分割区域: ', rect) print('#### 等会儿 有点慢 ...') iterCount = 5 cv2.grabCut(img, mask, rect, bgdModel, fgdModel, iterCount, cv2.GC_INIT_WITH_RECT) mask2 = np.where((mask == 2) | (mask == 0), 0, 1).astype('uint8') img = img * mask2[:, :, np.newaxis] plt.subplot(121), plt.imshow(cv2.cvtColor(img, cv2.COLOR_BGR2RGB)) plt.subplot(122), plt.imshow(cv2.cvtColor(cv2.imread(imgpath), cv2.COLOR_BGR2RGB)) fig.figure.canvas.draw() print('May the force be with me!') except: print('#### 先左键 后右键 ') else: print('#### 左下角坐标值必须大于右上角坐标 ') # 预先绘制图片 fig = plt.figure() plt.imshow(cv2.cvtColor(img, cv2.COLOR_BGR2RGB)) # 鼠标左键,选取分割区域(长方形)的左上角点 fig.canvas.mpl_connect('button_press_event', OnClick) # 鼠标右键,选取分割区域(长方形)的右下角点 fig.canvas.mpl_connect('button_press_event', onm ouseMotion) # 鼠标中键,在所选区域执行分割操作 fig.canvas.mpl_connect('button_press_event', onm ouseRelease) plt.show()
显示一张图片:
这个时候我们用鼠标左键点击左上角,右键点击右下角,之后点击鼠标中键进行生成:
可以看到,前景后景都已经被分离出来了。