一、引言

图像分割是计算机视觉的核心任务之一,目标是将图像划分为“前景”(感兴趣区域)和“背景”。Graph Cuts(图割)是经典的能量优化分割算法,但需要手动标记前景/背景种子点,操作繁琐。GrabCut作为Graph Cuts的改进版,只需框选目标区域即可实现高质量分割,是OpenCV中最实用的分割工具之一。

二、Graph Cuts:图论视角下的能量优化分割

Graph Cuts的核心思想是将图像分割转化为图的最小割问题——通过构建像素级的图结构,用“最小割”划分前景与背景。

1. 图的构建

构造无向图 (G = (V, E)),其中:

  • 顶点集 (V):包含图像中每个像素的“普通顶点”,以及两个终端顶点——源点S(代表前景)汇点T(代表背景)
  • 边集 (E):分为两类:
    • n-links(邻域边):连接相邻像素的顶点,权值反映像素间的颜色相似性(相似性越高,权值越大,鼓励相邻像素同属一类)。
    • t-links(终端边):连接每个普通顶点与S/T,权值反映该像素属于前景/背景的概率(如灰度直方图匹配度)。

2. 能量函数与最小割

图像分割可视为“像素标记问题”——给每个像素分配标签 (l_i \in {0,1})(0=背景,1=前景)。Graph Cuts通过最小化能量函数实现最优分割:
E(L)=αR(L)+B(L) E(L) = \alpha R(L) + B(L) E(L)=αR(L)+B(L)
其中:

  • (R(L)):区域项,衡量标记的“合理性”——像素属于前景/背景的概率(如灰度直方图匹配度)。
  • (B(L)):边界项,衡量标记的“连续性”——相邻像素标记不同时的惩罚(颜色差异越大,惩罚越大)。
  • (\alpha):平衡区域项与边界项的权重((\alpha=0)时仅考虑边界)。

3. 最小割求解

根据福特-富克森定理,图的最大流(从S到T的最大流量)等于最小割(断开S与T的最小权值边集)。Boykov-Kolmogorov算法可高效求解s-t图的最小割,将顶点划分为两个子集:

  • (S’)(包含S):前景像素。
  • (T’)(包含T):背景像素。

三、GrabCut:迭代优化的交互式分割革新

GrabCut是Graph Cuts的改进版,解决了Graph Cuts“需要手动标记种子点”的痛点,核心改进有三点:

1. 用GMM替代灰度直方图

Graph Cuts用灰度直方图建模前景/背景,无法捕捉颜色的多峰分布(如肤色的暖色调变化)。GrabCut则用RGB三通道混合高斯模型(GMM)——每个类(前景/背景)由多个高斯分量组成,能更精准地拟合颜色分布。

2. 迭代优化流程

Graph Cuts是“一次性分割”,GrabCut则是**“分割-模型更新”的迭代循环**:

  1. 初始化:用户框选目标(框外为背景,框内为“可能前景”)。
  2. 模型学习:用当前标记的像素训练前景/背景GMM。
  3. 图构建与分割:基于GMM计算t-links(像素属于前景/背景的概率)和n-links(邻域相似性),求解最小割得到新标记。
  4. 重复:直到GMM参数收敛(通常5-10次迭代)。

3. 支持不完全标注

用户只需框选目标(框外为背景),无需标记前景种子点——算法自动学习前景的颜色分布,这是GrabCut最实用的改进。

四、OpenCV中GrabCut函数详解

OpenCV通过cv2.grabCut函数实现GrabCut算法,函数原型如下:

cv2.grabCut(img, mask, rect, bgdModel, fgdModel, iterCount, mode)

参数说明

参数 含义
`img` 输入图像,必须是**8位3通道(BGR格式)**。
`mask` 掩码图像(输入/输出),值为以下四种:
- `cv2.GC_BGD`(0):确定背景;
- `cv2.GC_FGD`(1):确定前景;
- `cv2.GC_PR_BGD`(2):可能背景;
- `cv2.GC_PR_FGD`(3):可能前景。
`rect` 包含前景的矩形,格式为`(x, y, w, h)`(x/y为左上角坐标,w/h为宽高)。
`bgdModel` 背景GMM的参数数组,需初始化为`np.zeros((1, 65), np.float64)`。
`fgdModel` 前景GMM的参数数组,同`bgdModel`。
`iterCount` 迭代次数(建议5-10次)。
`mode` 初始化方式:
- `cv2.GC_INIT_WITH_RECT`(0):用矩形初始化(首次分割);
- `cv2.GC_INIT_WITH_MASK`(1):用掩码初始化(后续调整)。

五、Python实战:交互式GrabCut分割

以下代码实现鼠标框选+GrabCut分割的交互功能,步骤如下:

  1. 读取图像并初始化窗口。
  2. 鼠标回调函数:处理矩形绘制。
  3. 按键响应:空格执行分割,ESC退出。

完整代码

import cv2
import numpy as np

# 全局变量:记录交互状态
drawing = False  # 是否正在绘制矩形
start_point = (-1, -1)  # 矩形起点
rect = (0, 0, 0, 0)  # 矩形(x, y, w, h)
img = None  # 输入图像
mask = None  # 掩码
bgd_model = np.zeros((1, 65), np.float64)  # 背景GMM参数
fgd_model = np.zeros((1, 65), np.float64)  # 前景GMM参数
iter_num = 0  # 迭代次数


def mouse_callback(event, x, y, flags, param):
    """鼠标回调函数:处理矩形绘制"""
    global drawing, start_point, rect, img, mask

    if event == cv2.EVENT_LBUTTONDOWN:
        # 左键按下:开始绘制
        drawing = True
        start_point = (x, y)
        # 重置掩码:初始全为背景
        mask = np.zeros(img.shape[:2], dtype=np.uint8)
        mask[:] = cv2.GC_BGD

    elif event == cv2.EVENT_MOUSEMOVE:
        # 鼠标移动:更新矩形
        if drawing:
            end_point = (x, y)
            # 计算矩形(确保宽高为正)
            rect_x = min(start_point[0], end_point[0])
            rect_y = min(start_point[1], end_point[1])
            rect_w = abs(start_point[0] - end_point[0])
            rect_h = abs(start_point[1] - end_point[1])
            rect = (rect_x, rect_y, rect_w, rect_h)
            # 显示矩形
            temp_img = img.copy()
            cv2.rectangle(temp_img, start_point, end_point, (0, 0, 255), 2)
            cv2.imshow("GrabCut Segmentation", temp_img)

    elif event == cv2.EVENT_LBUTTONUP:
        # 左键抬起:结束绘制
        drawing = False
        # 标记框内为“可能前景”
        mask[rect[1]:rect[1]+rect[3], rect[0]:rect[0]+rect[2]] = cv2.GC_PR_FGD
        # 显示最终矩形
        temp_img = img.copy()
        cv2.rectangle(temp_img, (rect[0], rect[1]), (rect[0]+rect[2], rect[1]+rect[3]), (0, 0, 255), 2)
        cv2.imshow("GrabCut Segmentation", temp_img)


def main():
    """主函数:处理图像和交互"""
    global img, mask, bgd_model, fgd_model, iter_num, rect

    # 读取图像(替换为你的图像路径)
    img = cv2.imread("test_image.png")
    if img is None:
        print("无法读取图像,请检查路径!")
        return

    # 初始化窗口与鼠标回调
    cv2.namedWindow("GrabCut Segmentation")
    cv2.setMouseCallback("GrabCut Segmentation", mouse_callback)
    cv2.imshow("GrabCut Segmentation", img)

    while True:
        key = cv2.waitKey(1) & 0xFF
        if key == 27:  # ESC键:退出
            break
        elif key == ord(' '):  # 空格键:执行分割
            if rect[2] == 0 or rect[3] == 0:
                print("请先框选目标区域!")
                continue
            # 调用GrabCut(迭代5次,用矩形初始化)
            cv2.grabCut(img, mask, rect, bgd_model, fgd_model, 5, cv2.GC_INIT_WITH_RECT)
            iter_num += 1
            # 提取前景掩码(确定前景+可能前景)
            foreground_mask = np.where((mask == cv2.GC_FGD) | (mask == cv2.GC_PR_FGD), 1, 0).astype(np.uint8)
            # 应用掩码:提取前景
            result = cv2.bitwise_and(img, img, mask=foreground_mask)
            # 显示结果(叠加矩形)
            cv2.rectangle(result, (rect[0], rect[1]), (rect[0]+rect[2], rect[1]+rect[3]), (0, 0, 255), 2)
            cv2.imshow("GrabCut Segmentation", result)
            print(f"当前迭代次数:{iter_num}")

    cv2.destroyAllWindows()


if __name__ == "__main__":
    main()

五、总结

GrabCut是OpenCV中最实用的分割工具之一,优势如下:

  • 操作简单:只需框选目标,无需手动标记。
  • 精度高:GMM模型能精准捕捉颜色分布。
  • 通用性强:适用于大多数自然图像(如人像、物体分割)。

局限性:对颜色相似的前景/背景(如绿幕前的绿色衣服)分割效果不佳,需手动调整掩码(用cv2.GC_INIT_WITH_MASK模式)。

获取更多资料

我给大家整理了一套全网最全的人工智能学习资料(1.5T),包括:机器学习,深度学习,大模型,CV方向,NLP方向,kaggle大赛,实战项目、自动驾驶,AI就业等免费获取
请添加图片描述
请添加图片描述

Logo

魔乐社区(Modelers.cn) 是一个中立、公益的人工智能社区,提供人工智能工具、模型、数据的托管、展示与应用协同服务,为人工智能开发及爱好者搭建开放的学习交流平台。社区通过理事会方式运作,由全产业链共同建设、共同运营、共同享有,推动国产AI生态繁荣发展。

更多推荐