理论
GranCut算法是Carsten Rother, Vladimir Kolmogorov & Andrew Blake from Microsoft Research Cambridge, UK在他们的论文“GrabCut”: interactive foreground extraction using iterated graph cuts里设计的。使用最小程度的用户交互来分解前景。
从用户角度来看是怎么工作的呢?开始用户画一个矩形方块把前景图圈起来,前景区域应该完全在矩形内,然后算法反复进行分割以达到最好效果。但是有些情况下,分割的不是很好,比如把前景给标称背景了等。在这种情况下用户需要再润色,就在图像上有缺陷的点给几笔。这几笔的意思是说“嘿,这个区域应该是前景,你把它标成背景了,下次迭代改过来”或者是反过来。那么在下次迭代,结果会更好。
看下面的图像,首先球员和足球杯包在蓝色矩形框里,然后用白色笔(指出前景)和黑色笔(指出背景)来做一些润色
后台发生了什么?
·用户输入矩形,矩形外的所有东西都被确认是背景。所有矩形内的东西都是未知的,同样的任何用户输入指定前景和背景的也都被认为是硬标记,在处理过程中不会变。
·计算机会根据我们给的数据做初始标记,它会标记出前景和背景像素。
·现在回使用高斯混合模型(GMM)来为前景和背景建模
·根据我们给的数据,GMM学习和创建新的像素分布。未知像素被标为可能的前景或可能的背景(根据其他硬标记像素的颜色统计和他们之间的关系)
·根据这个像素分布创建一个图,图中的节点是像素,另外还有两个节点,源节点和汇节点,每个前景像素和源节点相连,每个背景像素和汇节点相连。
·源节点和汇节点连接的像素的边的权重由像素是前景或者背景的概率决定。像素之间的权重是由边的信息或者像素的相似度决定。如果像素颜色有很大差异,他们之间的边的权重就比较低。
·mincut算法是用来分割图的,它用最小成本函数把图切成两个分开的源点和汇点,成本函数是被切的边的权重之和。切完以后,所有连到源节点的像素称为前景,所有连到汇节点的称为背景。
·过程持续直到分类覆盖。
Demo
现在我们用OpenCV来做grabcut算法。OpenCV有个函数cv2.grabCut()来做这个,我们先看看它的参数:
·img - 输入图像
·mask - 这是掩图,我们指定哪个区域是背景,前景以及可能是背景或者前景。由下面的标志位:cv2.GC_BGD, cv2.GC_FGD, cv2.PR_BGD, cv2.GC_PR_FGD, 或者简单传入0, 1, 2, 3.
·rect - 包含前景对象的矩形的坐标,格式(x, y, w, h).
·bdgModel, fgdModel - 算法内部使用的数组, 你创建两个np.float64 类型的0数组,大小是(1, 65)
·iterCount - 算法运行的迭代次数
·mode - 应该是cv2.GC_INIT_WITH_RECT 或者 cv2.GC_INIT_WITH_MASK或者两者合并,决定我们是否画矩形或者最终的润色。
首先让我们看看矩形模式,我们加载图片,创建一个类似的掩图。我们创建fgdModel和bgdModel。 我们给矩形参数。这些都很直接。让算法运行5个迭代。Mode应该是cv2.GC_INIT_WITH_RECT因为我们用了矩形。然后运行grabcut。 它修改掩图。在新的掩图里,像素会被标记为四种标志,来指明他们是背景/前景等。所以我们修改 掩图,所有的0-像素和2-像素被置0(背景)而所有的1-像素和3-像素被置1(前景像素)。现在我们最终的掩图就绪。
import numpy as np
import cv2
from matplotlib import pyplot as pltimg = cv2.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)
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-像素确认是前景,同时有些地皮成了前景,还有logo,我们需要移除他们,我们给一些0-像素润色(确认是背景)。我们修改我们的结果掩图。
实际上我做的是,我用绘图软件打开输入图片,添加了另外一层,使用画刷工具,把丢失的前景(头发,谢,球等)用白色,不要的背景(logo,地面)用黑色画在这个新层上。然后把剩下的背景用灰色填充。然后在OpenCV里加载这个掩图,编辑原始掩图,代码:
# newmask is the mask image I manually labelled
newmask = cv2.imread('newmask.png',0)# whereever it is marked white (sure foreground), change mask=1
# whereever it is marked black (sure background), change mask=0
mask[newmask == 0] = 0
mask[newmask == 255] = 1mask, 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.imshow(img),plt.colorbar(),plt.show()
也可以不用矩形初始化而直接用掩图模式,用2-像素和3-像素(可能是背景/前景)标记矩形区域,然后把我们确认前景的标为1-像素,然后直接应用grabCut函数,用mask 模式。