一、图像分割
图像分割: 将前景物体从背景中分离出来,分为传统图像分割和基于深度学习的图像分割方法。
传统图像分割就是使用OpenCV进行的图像分割。分割方法有分水岭法、GrabCut法、MeanShift法、背景扣除等。
图像分割算法的选择取决于特定的应用场景、所需精度、计算资源和图像本身的特性。各算法特点:
二 、GrabCut
算法的实现步骤
- 在图片中定义(一个或者多个)包含物体的矩形。
- 矩形外的区域被自动认为是背景。
- 对于用户定义的矩形区域,可用背景中的数据来区分它里面的前景和背景区域。
- 用高斯混合模型(GMM)来对背景和前景建模,并将未定义的像素标记为可能的前景或者背景。
- 图像中的每一个像素都被看做通过虚拟边与周围像素相连接,而每条边都有一个属于前景或者背景的概率,这是基于它与周边像素颜色上的相似性。
- 每一个像素(即算法中的节点)会与一个前景或背景节点连接。
- 在节点完成连接后(可能与背景或前景连接),若节点之间的边属于不同终端(即一个节点属于前景,另一个节点属于背景),则会切断他们之间的边,这就能将图像各部分分割出来。下图能很好的说明该算法:
函数参数
-
grabCut(img, mask, rect, bgdModel, fgdModel, iterCount[, mode]) -> mask, bgdModel, fgdModel
-
img——待分割的源图像,必须是8位3通道,在处理的过程中不会被修改
-
mask——掩码图像,如果使用掩码进行初始化,那么mask保存初始化掩码信息;在执行分割的时候,也可以将用户交互所设定的前景与背景保存到mask中,然后再传入grabCut函数;在处理结束之后,mask中会保存结果。mask只能取以下四种值:
GCD_BGD(=0),背景;
GCD_FGD(=1),前景;
GCD_PR_BGD(=2),可能的背景;
GCD_PR_FGD(=3),可能的前景。如果没有手工标记GCD_BGD或者GCD_FGD,那么结果只会有GCD_PR_BGD或GCD_PR_FGD;
-
-
rect——用于限定需要进行分割的图像范围,只有该矩形窗口内的图像部分才被处理;
-
bgdModel——背景模型,如果为None,函数内部会自动创建一个bgdModel;bgdModel必须是单通道浮点型图像,且行数只能为1,列数只能为13x5;
-
fgdModel——前景模型,如果为None,函数内部会自动创建一个fgdModel;fgdModel必须是单通道浮点型图像,且行数只能为1,列数只能为13x5;
-
iterCount——迭代次数,必须大于0;
-
mode——用于指示grabCut函数进行什么操作,可选的值有:
GC_INIT_WITH_RECT(=0),用矩形窗初始化GrabCut;
GC_INIT_WITH_MASK(=1),用掩码图像初始化GrabCut;
GC_EVAL(=2),执行分割。
小案例
实现功能:鼠标左键框选目标区域,按下 g 键即可完成抠图功能。如果在扣图的基础上想多扣或少扣继续完善的话,可继续按 q 键或者 w 键选择前景或背景进行分离。
import cv2
import numpy as np
# 面向对象 : 把grabcut进行交互式抠图的功能封装成一个类.
class App:
def __init__(self, image):
self.image = image
self.img = cv2.imread(self.image)
self.img2 = self.img.copy()
self.start_x = 0
self.start_y = 0
# 是否需要绘制矩形的标志
self.rect_flag = False
self.rect = (0, 0, 0, 0)
self.mask = np.zeros(shape=self.img.shape[:2], dtype=np.uint8)
# 输出
self.output = np.zeros(shape=self.img.shape[:2], dtype=np.uint8)
# 实例方法, 第一个参数一定是self
# staticmethod默认类和实例对象不会自动传参数(self, cls)
# @staticmethod 静态方法 @classmethod
def on_mouse(self, event, x, y, flags, param):
# 按下左键, 开始框选前景区域
if event == cv2.EVENT_LBUTTONDOWN:
# 记录起始的坐标
self.start_x = x
self.start_y = y
self.rect_flag = True
elif event == cv2.EVENT_LBUTTONUP:
self.rect_flag = False
# 记录用户的矩形大小
self.rect = (min(self.start_x, x), min(self.start_y, y),
abs(self.start_x - x), abs(self.start_y - y))
cv2.rectangle(self.img, (self.start_x, self.start_y), (x, y), (0, 0, 255), 2)
elif event == cv2.EVENT_MOUSEMOVE and self.rect_flag:
# 画矩形
self.img = self.img2.copy()
cv2.rectangle(self.img, (self.start_x, self.start_y), (x, y), (0, 255, 0), 2)
# 编辑模式
# 核心逻辑: 窗口 回调函数 图片
def run(self):
cv2.namedWindow('img')
# 绑定鼠标事件
cv2.setMouseCallback('img', self.on_mouse)
while True:
cv2.imshow('img', self.img)
cv2.imshow('output', self.output)
key = cv2.waitKey(1)
if key == 27:
break
elif key == ord('g'):
# 进行切图
self.mask = np.zeros(shape=self.img.shape[:2], dtype=np.uint8)
# 输出清空
self.output = np.zeros(shape=self.img.shape[:2], dtype=np.uint8)
cv2.grabCut(self.img2, self.mask, self.rect, None, None, 5, mode=cv2.GC_INIT_WITH_RECT)
# cv2.grabCut(self.img2, self.mask, None, None, None, 5, mode=cv2.GC_INIT_WITH_MASK)
elif key == ord('q'):
# 框选目标区域改为前景
self.mask[self.rect[1]: (self.rect[1] + self.rect[3]), self.rect[0]: (self.rect[0] + self.rect[2])] = 1
cv2.grabCut(self.img2, self.mask, None, None, None, 5, mode=cv2.GC_INIT_WITH_MASK)
elif key == ord('w'):
# 框选目标区域改为背景
self.mask[self.rect[1]: (self.rect[1] + self.rect[3]), self.rect[0]: (self.rect[0] + self.rect[2])] = 0
cv2.grabCut(self.img2, self.mask, None, None, None, 5, mode=cv2.GC_INIT_WITH_MASK)
# 把前景或者可能是前景的位置设置为255,
mask2 = np.where((self.mask == 1) | (self.mask == 3), 255, 0).astype(np.uint8)
# 使用与运算.
self.output = cv2.bitwise_and(self.img2, self.img2, mask=mask2)
cv2.destroyAllWindows()
app = App('./lena.png')
app.run()
效果如下: