【35】图像分割:GrabCut算法原理与Python实现
操作简单:只需框选目标,无需手动标记。精度高:GMM模型能精准捕捉颜色分布。通用性强:适用于大多数自然图像(如人像、物体分割)。局限性:对颜色相似的前景/背景(如绿幕前的绿色衣服)分割效果不佳,需手动调整掩码(用模式)。
一、引言
图像分割是计算机视觉的核心任务之一,目标是将图像划分为“前景”(感兴趣区域)和“背景”。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则是**“分割-模型更新”的迭代循环**:
- 初始化:用户框选目标(框外为背景,框内为“可能前景”)。
- 模型学习:用当前标记的像素训练前景/背景GMM。
- 图构建与分割:基于GMM计算t-links(像素属于前景/背景的概率)和n-links(邻域相似性),求解最小割得到新标记。
- 重复:直到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分割的交互功能,步骤如下:
- 读取图像并初始化窗口。
- 鼠标回调函数:处理矩形绘制。
- 按键响应:空格执行分割,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就业等免费获取。

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



所有评论(0)