- 本章将介绍使用GrabCut算法提取图像前景的方法,并创建一个交互式应用来实现这一功能。
理论基础
GrabCut算法由微软剑桥研究院的Carsten Rother、Vladimir Kolmogorov和Andrew Blake在论文《GrabCut:基于迭代图割的交互式前景提取》 中提出, 旨在通过最小化用户交互实现前景提取。
用户只需在目标前景区域(需完全包含在矩形内)绘制外接矩形,算法通过迭代分割获得最佳结果。 若出现误判(如将前景误标为背景或反之),用户可通过白色笔触标记应属前景的区域或黑色笔触标记应属背景的区域进行修正,算法会在下次迭代中优化结果。 例如图中足球运动员被蓝色矩形框选后,经白/黑笔触局部修正,最终获得精确分割效果。
那么,背景中发生了什么?
用户划定矩形框后,框外区域被直接视为确定背景(因此要求目标物体必须全部包含在矩形内), 框内区域则作为待识别区域。用户手动标记的前景/背景区域会被固定为硬标签。 系统首先根据输入数据进行初始标记,随后通过高斯混合模型(GMM)建立前景和背景的颜色分布模型, 将未标记像素归类为可能前景或可能背景(类似聚类过程)。
接着构建像素关系图,图中节点包含所有像素及额外添加的源节点(代表前景)和汇节点(代表背景)。 像素与源/汇节点的连接权重由其归属概率决定,像素间的边权重则由颜色相似度决定(差异越大权重越低)。 最后通过最小割算法分割图结构,以最小代价函数(被切割边权重总和)将图划分为连接源节点的前景区域和连接汇节点的背景区域, 该迭代过程持续至分类结果收敛(示意图可参考原论文)。
如下图所示:
演示
现在将使用OpenCV实现GrabCut算法。OpenCV提供了cv2.grabCut()
函数,其参数如下:
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)。bgdModel
、fgdModel
:算法内部使用的数组,需创建两个大小为(1, 65)的np.float64
类型零数组。iterCount
:算法运行的迭代次数。mode
:模式选择,如cv2.GC_INIT_WITH_RECT
(矩形模式)或cv2.GC_INIT_WITH_MASK
(掩模模式)。
首先让我们看看矩形模式操作步骤。
加载图像并创建掩模图像,
初始化fgdModel
和bgdModel
,设置矩形参数。
以cv2.GC_INIT_WITH_RECT
模式运行算法(例如迭代5次)。
算法会修改掩模图像,其中像素被标记为上述四种标志。
将掩模中0(背景)和2(可能背景)的像素置为0,1(前景)和3(可能前景)的像素置为1。
最终掩模与输入图像相乘,即可得到分割结果。
import numpy as np
import cv2
from matplotlib import pyplot as plt
img = cv2.imread('/data/cvdata/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)
(array([[0, 0, 0, ..., 0, 0, 0], [0, 0, 0, ..., 0, 0, 0], [0, 0, 0, ..., 0, 0, 0], ..., [0, 0, 0, ..., 0, 0, 0], [0, 0, 0, ..., 0, 0, 0], [0, 0, 0, ..., 0, 0, 0]], shape=(342, 548), dtype=uint8), array([[1.36743970e-01, 2.62816234e-01, 1.82774925e-01, 2.09057251e-01, 2.08607620e-01, 1.29887741e+02, 1.27731761e+02, 1.16250360e+02, 3.05598653e+01, 2.74510947e+01, 2.42415996e+01, 8.42414668e+01, 8.51126614e+01, 9.08009302e+01, 5.12152771e+01, 1.40154384e+02, 9.98021642e+01, 5.52254067e+01, 5.20718351e+01, 5.69090358e+01, 1.18914507e+03, 8.64872580e+02, 9.01217943e+02, 8.64872580e+02, 8.50517495e+02, 8.21224575e+02, 9.01217943e+02, 8.21224575e+02, 8.32437885e+02, 1.10664230e+02, 1.02798959e+02, 1.02382099e+02, 1.02798959e+02, 9.97580469e+01, 1.01014141e+02, 1.02382099e+02, 1.01014141e+02, 1.08882098e+02, 2.17588296e+02, 1.50402570e+02, 8.09600844e+01, 1.50402570e+02, 1.61811915e+02, 1.65302755e+02, 8.09600844e+01, 1.65302755e+02, 2.89399524e+02, 1.55149206e+02, 7.08301918e+01, 9.17048202e+01, 7.08301918e+01, 1.87939872e+02, 1.26847191e+02, 9.17048202e+01, 1.26847191e+02, 2.57053411e+02, 1.40330325e+02, 1.04195541e+02, 1.14986018e+02, 1.04195541e+02, 1.08848475e+02, 1.04174790e+02, 1.14986018e+02, 1.04174790e+02, 2.27982127e+02]]), array([[1.14248952e-01, 2.34798234e-01, 1.59837611e-01, 3.56279255e-01, 1.34835947e-01, 4.65796117e+01, 4.26980583e+01, 4.44250485e+01, 6.27980915e+01, 4.42496221e+01, 1.32169501e+02, 5.01812630e+01, 1.47759889e+02, 1.01042609e+02, 1.50383313e+02, 1.61622790e+02, 1.63979141e+02, 1.41876440e+02, 5.49666009e+01, 3.60383350e+01, 1.14228211e+03, 9.85596951e+02, 9.20088200e+02, 9.85596951e+02, 9.35035239e+02, 8.44509893e+02, 9.20088200e+02, 8.44509893e+02, 8.73974868e+02, 5.05300219e+02, 3.17115499e+02, 5.94884280e+02, 3.17115499e+02, 5.43852088e+02, 1.11398165e+02, 5.94884280e+02, 1.11398165e+02, 1.59736035e+03, 7.48906691e+01, 1.99058411e+01, 3.38262807e+01, 1.99058411e+01, 4.14360316e+01, 3.66749083e+01, 3.38262807e+01, 3.66749083e+01, 3.99630699e+01, 2.29041235e+03, 6.08459596e+02, 4.27040872e+02, 6.08459596e+02, 1.73741711e+03, 1.59212476e+03, 4.27040872e+02, 1.59212476e+03, 1.94219116e+03, 1.61679240e+03, 7.15931378e+02, 4.30556234e+02, 7.15931378e+02, 3.78751270e+02, 2.52619740e+02, 4.30556234e+02, 2.52619740e+02, 2.22409685e+02]]))
mask2 = np.where((mask==2)|(mask==0),0,1).astype('uint8')
img = img*mask2[:,:,np.newaxis]
plt.imshow(img),plt.colorbar(),plt.show()
(<matplotlib.image.AxesImage at 0x7f423917bdd0>, <matplotlib.colorbar.Colorbar at 0x7f428215dac0>, None)
结果如下所示:
糟糕,梅西的头发不见了!谁喜欢没有头发的梅西呢?我们需要把它找回来。 于是用1像素的白色笔触(明确前景)在头发区域进行了精细标记。 同时,画面中出现了不需要的地面和logo部分, 用0像素的黑色笔触(明确背景)将其标记为需要去除的区域。
实际操作流程:
- 图层标记:在绘图软件中打开原图,新建透明图层
- 前景修复:用白色画笔标记遗漏的前景(头发、球鞋、足球等)
- 背景清理:用黑色画笔涂抹需要去除的背景(logo、多余地面等)
- 中性填充:其余背景区域用灰色填充
- 掩模合成:在OpenCV中加载这个手工修正的掩模图层,将其对应数值更新到原始掩模图像中
查看如下代码:
newmask
是我手动标注的掩膜图像。
newmask = cv2.imread('/data/cvdata/newmask.png',0)
[ WARN:0@176.600] global loadsave.cpp:268 findDecoder imread_('/data/cvdata/newmask.png'): can't open/read file: check file path/integrity
将标注图像中的白色区域(确定前景)设为1,黑色区域(确定背景)设为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.imshow(img),plt.colorbar(),plt.show()
(<matplotlib.image.AxesImage at 0x7f42377a7ce0>, <matplotlib.colorbar.Colorbar at 0x7f42391544d0>, None)