OpenCV 的 GrabCut 是一种经典的交互式图像分割算法,能让你用“画个框”或“涂几笔”的方式,轻松把前景从背景中抠出来。下面从原理、接口到完整交互式代码一步步讲解。
输入:彩色图像 + 用户提供的初始信息(矩形框/前景-背景涂鸦)。
模型:前景和背景分别用 高斯混合模型(GMM) 建模颜色分布。
优化:将图像视为一个图(Graph),像素为节点,通过 图割(Graph Cut) 最小化一个能量函数,同时考虑:
区域项:像素属于前景/背景的概率(由GMM给出)。
边界项:相邻像素颜色差异,鼓励边界落在梯度大的地方。
迭代:交替进行“根据当前分割更新GMM”和“根据新GMM重新图割”,逐步逼近最佳分割。
cv2.grabCutcv2.grabCut(img, mask, rect, bgdModel, fgdModel, iterCount, mode)
img:8位3通道图像。
mask:输入/输出的掩码图,单通道8位,形状与img相同。像素值含义:
cv2.GC_BGD (0) – 明确背景
cv2.GC_FGD (1) – 明确前景
cv2.GC_PR_BGD (2) – 可能背景
cv2.GC_PR_FGD (3) – 可能前景
rect:矩形 (x, y, w, h),当 mode 包含 GC_INIT_WITH_RECT 时使用,矩形外为明确背景,内为可能前景。
bgdModel, fgdModel:内部使用的 GMM 参数数组,大小 (1, 65) 浮点型,只需创建后传入,函数会自动更新。
iterCount:迭代次数。
mode:
cv2.GC_INIT_WITH_RECT:基于矩形初始化。
cv2.GC_INIT_WITH_MASK:基于现有 mask 初始化(用户已涂鸦前景/背景)。
cv2.GC_EVAL:仅执行一次图割(前提是模型已训练好)。
可组合:GC_INIT_WITH_RECT | GC_EVAL 等。
#GrabCut交互式分割
import cv2
import numpy as np
# img = cv2.imread('image.jpg')
img = cv2.imread('./images/water_coins.png')
mask = np.zeros(img.shape[:2], np.uint8) # 全0 = 全背景
bgdModel = np.zeros((1,65), np.float64)
fgdModel = np.zeros((1,65), np.float64)
rect = (50, 50, 400, 300) # 包含前景的矩形
cv2.grabCut(img, mask, rect, bgdModel, fgdModel, 5, cv2.GC_INIT_WITH_RECT)
# 提取前景区域(明确前景 + 可能前景)
mask2 = np.where((mask==2)|(mask==0), 0, 1).astype('uint8')
result = img * mask2[:,:,np.newaxis]
cv2.imshow('result', result)
cv2.waitKey()
此时 mask 中的 0,2 视为背景,1,3 视为前景。
用户可以在分割结果不完美的地方,用鼠标画出前景笔画(白色)或背景笔画(黑色),然后以 GC_INIT_WITH_MASK 模式再次运行 GrabCut,并增加少许迭代(如2次),使分割更准确。
创建一张同样大小的涂鸦图 drawing,用于记录用户笔触。
鼠标事件:左键画前景(255),右键画背景(128或其他值)。
将涂鸦合并到 mask 中:
遇到255 → 设为 GC_FGD
遇到128 → 设为 GC_BGD
调用 grabCut(img, mask, None, bgdModel, fgdModel, iterCount, cv2.GC_INIT_WITH_MASK)
重复直至满意。
按 r 重置,左键涂前景,右键涂背景,滚轮调整笔刷大小。
import cv2
import numpy as np
# 全局变量
drawing = False # 是否正在绘画
mode = 'fg' # 'fg' 或 'bg'
brush_size = 3
img = None
mask = None
bgdModel = np.zeros((1,65), np.float64)
fgdModel = np.zeros((1,65), np.float64)
rect = (0,0,1,1) # 若不使用矩形初始化,需预先设置mask
def reset_mask():
"""全图设为可能背景,或根据矩形初始化"""
global mask, bgdModel, fgdModel
mask = np.zeros(img.shape[:2], np.uint8)
bgdModel = np.zeros((1,65), np.float64)
fgdModel = np.zeros((1,65), np.float64)
# 若已有矩形,可在此调用 GC_INIT_WITH_RECT
# 否则让用户直接涂鸦
def apply_grabcut():
global mask, bgdModel, fgdModel
# 至少有一次初始化后才能用 MASK 模式
if np.count_nonzero(mask) == 0: # 若 mask 全0,先做一次矩形初始化
cv2.grabCut(img, mask, rect, bgdModel, fgdModel, 5, cv2.GC_INIT_WITH_RECT)
else:
cv2.grabCut(img, mask, None, bgdModel, fgdModel, 2, cv2.GC_INIT_WITH_MASK)
def mouse_callback(event, x, y, flags, param):
global drawing, mode, mask, brush_size
if event == cv2.EVENT_LBUTTONDOWN:
drawing = True
mode = 'fg'
elif event == cv2.EVENT_RBUTTONDOWN:
drawing = True
mode = 'bg'
elif event == cv2.EVENT_LBUTTONUP or event == cv2.EVENT_RBUTTONUP:
drawing = False
apply_grabcut() # 松开鼠标后自动更新
elif event == cv2.EVENT_MOUSEWHEEL:
if flags > 0:
brush_size = min(20, brush_size+1)
else:
brush_size = max(1, brush_size-1)
if drawing:
# 在mask上直接设置标记
val = cv2.GC_FGD if mode == 'fg' else cv2.GC_BGD
cv2.circle(mask, (x, y), brush_size, val, -1)
def main():
global img, mask, rect
img = cv2.imread('image.jpg')
if img is None:
print("Image not found")
return
img = cv2.resize(img, (800, 600))
mask = np.zeros(img.shape[:2], np.uint8)
# 可选:先用矩形初始化
rect = cv2.selectROI('GrabCut', img, False)
cv2.destroyWindow('GrabCut')
cv2.grabCut(img, mask, rect, bgdModel, fgdModel, 5, cv2.GC_INIT_WITH_RECT)
cv2.namedWindow('GrabCut')
cv2.setMouseCallback('GrabCut', mouse_callback)
print("左键画前景 | 右键画背景 | 滚轮调整笔刷 | 按 'r' 重置 | ESC退出")
while True:
# 显示分割结果
mask_show = np.where((mask==2)|(mask==0), 0, 1).astype('uint8')
res = img * mask_show[:,:,np.newaxis]
# 半透明叠加掩码显示涂鸦区域
display = res.copy()
cv2.imshow('GrabCut', display)
k = cv2.waitKey(1) & 0xFF
if k == 27: # ESC
break
elif k == ord('r'):
reset_mask()
# 如果最初没有矩形,此时mask全0;可重新selectROI
rect = cv2.selectROI('GrabCut', img, False)
cv2.destroyWindow('GrabCut')
cv2.grabCut(img, mask, rect, bgdModel, fgdModel, 5, cv2.GC_INIT_WITH_RECT)
cv2.destroyAllWindows()
if __name__ == '__main__':
main()
运行后先用鼠标框选一个大致的矩形,程序会进行初步分割。
在分割结果上,按住左键涂抹前景区域,右键涂抹背景区域,松开鼠标自动更新。
滚轮调整笔刷粗细,按 r 可重新框选重置。
矩形框的质量:框越紧贴前景边界,初始分割越好。若框太大,背景颜色与前景区分不明显时容易混淆。
笔触颜色:标记前景用 GC_FGD,标记背景用 GC_BGD,尽量只涂在明确区域,避免涂在模糊边界。
迭代次数:矩形初始化建议 5 次左右;用涂鸦精炼时 2~3 次即可,太多可能过拟合细微噪声。
速度:GrabCut 计算较慢,对高分辨率图像可先缩放处理,最后将 mask 放大回原图。
显存/内存:bgdModel 和 fgdModel 必须保持为参数传入,每次调用后模型会被更新,不能重新清零,否则丢失学习结果。

全部评论