OpenCV中GrabCut交互式分割图像

22人浏览 / 0人评论 / 添加收藏

OpenCV 的 GrabCut 是一种经典的交互式图像分割算法,能让你用“画个框”或“涂几笔”的方式,轻松把前景从背景中抠出来。下面从原理、接口到完整交互式代码一步步讲解。

1. GrabCut 核心思想

输入:彩色图像 + 用户提供的初始信息(矩形框/前景-背景涂鸦)。

模型:前景和背景分别用 高斯混合模型(GMM) 建模颜色分布。

优化:将图像视为一个图(Graph),像素为节点,通过 图割(Graph Cut) 最小化一个能量函数,同时考虑:

区域项:像素属于前景/背景的概率(由GMM给出)。

边界项:相邻像素颜色差异,鼓励边界落在梯度大的地方。

迭代:交替进行“根据当前分割更新GMM”和“根据新GMM重新图割”,逐步逼近最佳分割。

2. OpenCV 函数:cv2.grabCut

cv2.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 等。

3、代码实例:

#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 视为前景。

4. 交互式精炼:用画笔修正

用户可以在分割结果不完美的地方,用鼠标画出前景笔画(白色)或背景笔画(黑色),然后以 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)

重复直至满意。

5. 完整交互式代码示例(Python)

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 可重新框选重置。

6. 注意事项与技巧

矩形框的质量:框越紧贴前景边界,初始分割越好。若框太大,背景颜色与前景区分不明显时容易混淆。

笔触颜色:标记前景用 GC_FGD,标记背景用 GC_BGD,尽量只涂在明确区域,避免涂在模糊边界。

迭代次数:矩形初始化建议 5 次左右;用涂鸦精炼时 2~3 次即可,太多可能过拟合细微噪声。

速度:GrabCut 计算较慢,对高分辨率图像可先缩放处理,最后将 mask 放大回原图。

显存/内存:bgdModel 和 fgdModel 必须保持为参数传入,每次调用后模型会被更新,不能重新清零,否则丢失学习结果。

全部评论