opencv-python 图像分割

发布时间 2023-08-07 23:39:57作者: 寒水浮云

本章节介绍图像分割方面的算法:分水岭算法,grabcut算法,meanshift算法等知识。

图像分割:将前景物体从背景中提取出来。

图像分割分为传统图像分割和基于深度学习的图像分割。

传统图像分割有:分水岭算法,grabcut算法,meanshift算法,背景抠出等。

1 分水岭算法

分水岭算法是基于图像形态学和图像结构的来实现的一种分割方法。

在没有背景模板可以用的情况下,分水岭算法首先计算图像的梯度(如查找轮廓),形成的线条组成了山脉或岭,没有纹理的地方形成盆地或山谷;然后从指定的点向盆地灌水,当图像被灌满时,所有有标记的区域就被分割开了。

分水岭算法中涉及到的api:

dist_fg = distanceTransform(img,distanceType,maskSize)

计算img中非0值距离到它最近的0值之间的距离(该函数用来确定前景),返回的和img大小一样的矩阵,矩阵的元素是距离(浮点数)。

img:要处理的图像。 distanceType:计算距离的方式,DIST_L1,DIST_L2。 maskSize:进行扫描时的kernel的大小,L1用3,L2用5。

_, markers = connectedComponents(img,[labels,[connectivity]])

求连通域(img通常是前景图片,该函数用来计算markers),输入图片要求是8位的单通道图片,单通道的值是0-255的整型。用0标记图像的背景,用大于0的整数标记其他的对象。

connectivity:连通方式,4,8(默认)。

markers = watershed(image,markers)

执行分水岭算法(找出图像边界)。返回的maskers做了修改,大于1是前景,1是背景,-1表示边界区域。

markers:它是一个与原始图像大小相同的矩阵,int32数据类型,表示哪些是前景,哪些是背景。分水岭算法将标记为0的区域视为未知区域,标记为1是背景,标记大于1是前景。

分水岭算法练习,提取下面图片中的硬币:

如果直接用形态学方面的知识处理:

#用形态学和与运算提取感兴趣前景
import cv2
import numpy as np

img = cv2.imread('./coins.png')
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
_,thresh = cv2.threshold(gray,0,255,cv2.THRESH_BINARY_INV | cv2.THRESH_OTSU)  #全局二值化,大津算法自动找阈值

kernel = cv2.getStructuringElement(cv2.MORPH_RECT,(3,3)) #结构元
erode = cv2.erode(thresh,kernel) #抠出的图略大了一点,腐蚀一下

bit_img = cv2.bitwise_and(img,img,mask=erode) #用与运算提取前景


cv2.imshow('coins',img)
cv2.imshow('erode',erode)
cv2.imshow('bit_img',bit_img)
cv2.waitKey(0)
cv2.destroyAllWindows()

 结果如下:

可以基本实现前景硬币提取,但是硬币连接处边缘过度不平滑,用分水岭算法进行处理。

分水岭算法的关键是标记出前景,背景和未知区域,利用形态学的知识找出前景,背景和未知区域:

import cv2
import numpy as np
import matplotlib.pyplot as plt
%matplotlib inline

img = cv2.imread('./coins.png')
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)

plt.hist(gray.ravel(),bins=256,range=[0,255])  #用plt画直方图,ravel()函数是把多维变一维
plt.show()

#img的图是典型的双峰结构,用大津算法进行二值化处理
_,thresh = cv2.threshold(gray,0,255,cv2.THRESH_BINARY_INV | cv2.THRESH_OTSU)  #全局二值化,大津算法自动找阈值

#二值化后的图存在毛边,有小噪点,做一下开运算
kernel = cv2.getStructuringElement(cv2.MORPH_RECT,(3,3)) #结构元
img_open= cv2.morphologyEx(thresh,cv2.MORPH_OPEN,kernel,iterations=2)
cv2.imshow('img_open',img_open)

#想办法找到前景和背景
#对img_open进行膨胀操作,找背景
bg = cv2.dilate(img_open,kernel,iterations=2)
cv2.imshow('bg',bg)

#对img_open进行腐蚀操作,找前景,但是从前景图上来看,效果不太好,因为硬币与硬币之间有明显的通道,跟实际(相切)不一样
fg = cv2.erode(img_open,kernel,iterations=2)
cv2.imshow('fg',fg)

#可以通过膨胀减去腐蚀,就是硬币的边界,即未知区域
unkown = cv2.subtract(bg,fg)
cv2.imshow('unkown',unkown)

cv2.waitKey(0)
cv2.destroyAllWindows()

背景没有什么问题,黑色区域全部属于背景,但是前景部分有些白色区域(硬币边缘)不应该被认作前景,因此通过腐蚀来确定前景不太合适,用distanceTransform()来确定前景,然后重新确定前景和未知区域。

import cv2
import numpy as np
import matplotlib.pyplot as plt

img = cv2.imread('./coins.png')
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)

plt.hist(gray.ravel(),bins=256,range=[0,255])  #用plt画直方图,ravel()函数是把多维变一维
plt.show()

#img的图是典型的双峰结构,用大津算法进行二值化处理
_,thresh = cv2.threshold(gray,0,255,cv2.THRESH_BINARY_INV | cv2.THRESH_OTSU)  #全局二值化,大津算法自动找阈值

#二值化后的图存在毛边,有小噪点,做一下开运算
kernel = cv2.getStructuringElement(cv2.MORPH_RECT,(3,3)) #结构元
img_open= cv2.morphologyEx(thresh,cv2.MORPH_OPEN,kernel,iterations=2)
# cv2.imshow('img_open',img_open)

#想办法找到前景和背景
#对img_open进行膨胀操作,找背景
bg = cv2.dilate(img_open,kernel,iterations=2)
cv2.imshow('bg',bg)

#对img_open进行腐蚀操作,找前景,但是从前景图上来看,效果不太好,因为硬币与硬币之间有明显的通道,跟实际(相切)不一样
# fg = cv2.erode(img_open,kernel,iterations=2)
# cv2.imshow('fg',fg)

#可以通过膨胀减去腐蚀,就是硬币的边界,即未知区域
# unkown = cv2.subtract(bg,fg)
# cv2.imshow('unkown',unkown)

#通过腐蚀来确定前景不合适,用distanceTransform()来确定前景
dist_fg = cv2.distanceTransform(img_open,cv2.DIST_L2, 5)
#对dist_fg做归一化方便展示结果
dist_fg = cv2.normalize(dist_fg,None,0,1.0,cv2.NORM_MINMAX)
# print('dist_fg.max:',dist_fg.max())
cv2.imshow('dist_fg',dist_fg)

#对dist_fg做二值化处理
_,fg = cv2.threshold(dist_fg,0.6*dist_fg.max(),255,cv2.THRESH_BINARY)
cv2.imshow('fg',fg)
fg = np.uint8(fg)  #把fg的数据类型转换位uint8的整型
# print(fg)
unkown = cv2.subtract(bg,fg)  #计算未知区域,硬币边缘
cv2.imshow('unkown',unkown)

cv2.waitKey(0)
cv2.destroyAllWindows()

 结果如下:

 

然后把前景,背景和未知区域写进markers中(用connectedComponents函数):

#connectedComponents要求输入的图片是8位的单通道图片,单通道的值是0-255的整型。这个函数可以计算出标志区域(0标记背景,大于0的整数标记前景)
_,markers = cv2.connectedComponents(fg)
print('markers_max:',markers.max(),'markers_min:',markers.min())  #marks大小和输入图片一样

#因为分水岭算法watershed中是:0是未知区域,1是背景,大于1是前景,markers +1的话,把原来的0变为1即可。
markers += 1
#从markers中筛选出未知区域,然后赋值位0
markers[unkown == 255] = 0  #此时watershed需要的markers已经完成
print(markers.max()) 

 最后执行分水岭算法,抠出硬币:

#执行分水岭算法
markers = cv2.watershed(img,markers)  #返回的markers做了修改,边界区域标记为了-1
print('markers:',markers.max(),markers.min())

# img[markers == -1] = [0,0,255]  #标记边缘
# # cv2.imshow('img',img)

# img[markers > 1] = [0,255,0]  #标记前景
# cv2.imshow('img',img)

#抠出硬币
#mask把要抠图的地方赋值为255,其他位置赋值为0
mask = np.zeros(shape=img.shape[:2],dtype=np.uint8)
mask[markers > 1] = 255
img_coins = cv2.bitwise_and(img,img,mask=mask)
cv2.imshow('img_coins',img_coins)

 结果如下:

最终的分水岭算法整体代码如下:

#分水岭算法
import cv2
import numpy as np
import matplotlib.pyplot as plt

img = cv2.imread('./coins.png')
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)

plt.hist(gray.ravel(),bins=256,range=[0,255])  #用plt画直方图,ravel()函数是把多维变一维
plt.show()

#img的图是典型的双峰结构,用大津算法进行二值化处理
_,thresh = cv2.threshold(gray,0,255,cv2.THRESH_BINARY_INV | cv2.THRESH_OTSU)  #全局二值化,大津算法自动找阈值

#二值化后的图存在毛边,有小噪点,做一下开运算
kernel = cv2.getStructuringElement(cv2.MORPH_RECT,(3,3)) #结构元
img_open= cv2.morphologyEx(thresh,cv2.MORPH_OPEN,kernel,iterations=2)
# cv2.imshow('img_open',img_open)

#想办法找到前景和背景
#对img_open进行膨胀操作,找背景
bg = cv2.dilate(img_open,kernel,iterations=2)
cv2.imshow('bg',bg)

#对img_open进行腐蚀操作,找前景,但是从前景图上来看,效果不太好,因为硬币与硬币之间有明显的通道,跟实际(相切)不一样
# fg = cv2.erode(img_open,kernel,iterations=2)
# cv2.imshow('fg',fg)

#可以通过膨胀减去腐蚀,就是硬币的边界,即未知区域
# unkown = cv2.subtract(bg,fg)
# cv2.imshow('unkown',unkown)

#通过腐蚀来确定前景不合适,用distanceTransform()来确定前景
dist_fg = cv2.distanceTransform(img_open,cv2.DIST_L2, 5)
#对dist_fg做归一化方便展示结果
dist_fg = cv2.normalize(dist_fg,None,0,1.0,cv2.NORM_MINMAX)
# print('dist_fg.max:',dist_fg.max())
cv2.imshow('dist_fg',dist_fg)

#对dist_fg做二值化处理
_,fg = cv2.threshold(dist_fg,0.6*dist_fg.max(),255,cv2.THRESH_BINARY)
cv2.imshow('fg',fg)
fg = np.uint8(fg)  #把fg的数据类型转换位uint8的整型
# print(fg)
unkown = cv2.subtract(bg,fg)  #计算未知区域,硬币边缘
cv2.imshow('unkown',unkown)

#connectedComponents要求输入的图片是8位的单通道图片,单通道的值是0-255的整型。这个函数可以计算出标志区域(0标记背景,大于0的整数标记前景)
_,markers = cv2.connectedComponents(fg)
print('markers_max:',markers.max(),'markers_min:',markers.min())  #marks大小和输入图片一样

#因为分水岭算法watershed中是:0是未知区域,1是背景,大于1是前景,markers +1的话,把原来的0变为1即可。
markers += 1
#从markers中筛选出未知区域,然后赋值位0
markers[unkown == 255] = 0  #此时watershed需要的markers已经完成
print(markers.max())

#展示一下markers
markers_copy = markers.copy()
markers_copy[markers == 0] =127 #位置区域
markers_copy[markers >1] = 255  #前景
markers_copy = markers_copy.astype('uint8')  #要注意需要把其类型转换位uint8才能展示图片
cv2.imshow('markers_copy',markers_copy)

#执行分水岭算法
markers = cv2.watershed(img,markers)  #返回的markers做了修改,边界区域标记为了-1
print('markers:',markers.max(),markers.min())

# img[markers == -1] = [0,0,255]  #标记边缘
# # cv2.imshow('img',img)

# img[markers > 1] = [0,255,0]  #标记前景
# cv2.imshow('img',img)

#抠出硬币
#mask把要抠图的地方赋值为255,其他位置赋值为0
mask = np.zeros(shape=img.shape[:2],dtype=np.uint8)
mask[markers > 1] = 255
img_coins = cv2.bitwise_and(img,img,mask=mask)
cv2.imshow('img_coins',img_coins)

cv2.waitKey(0)
cv2.destroyAllWindows() 

 和canny对比:

# 和canny对比
import cv2
import numpy as np

img = cv2.imread('./coins.png')
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)

img = cv2.Canny(img,100,150)


cv2.imshow('img',img)

cv2.waitKey(0)
cv2.destroyAllWindows()

  canny只能检测出边缘,不能进行前景提取分割。

和findContours对比(也只能进行轮廓查找和标记):

#和findContours对比

import cv2
import numpy as np

img = cv2.imread('./coins.png')
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
ret,thresh = cv2.threshold(gray,0,255,cv2.THRESH_BINARY | cv2.THRESH_OTSU) #二值化

#findContours要求是单通道,0-255的整数的图片,最好是二值化的图片。
imges,contours,_ = cv2.findContours(thresh,cv2.RETR_TREE,cv2.CHAIN_APPROX_SIMPLE)

cv2.drawContours(img,contours,-1,[0,0,255],2)

cv2.imshow('gray',gray)
cv2.imshow('thresh',thresh)
cv2.imshow('img',img)

cv2.waitKey(0)
cv2.destroyAllWindows()

 

2 GrabCut 分割

通过交互的方式获取前景物体。

1.用户指定前景的大体区域,剩下的为背景区域。

2.用户可以明确指定某些地方为前景或背景。

3.gradcut采用分段迭代的方法分析前景物体,形成模型树。

4.最后根据权重决定某个像素是前景还是背景。

GradCut主要用到: k均值聚类;高斯混合模型建模(GMM);max flow/min cut。

GradCut算法的实现基本步骤:

1.在图片中定义(一个或多个)包含物体的矩形。

2.矩形外的区域被自动认为是背景。

3.对于用户定义的矩形区域,可以用背景中的数据来区分它里面的前景和背景区域。

4.用高斯混合模型(GMM)来对前景和背景建模,并将未定义的像素标记为可能的前景或背景。

5.图像中的每一个像素都被看做通过虚拟边与周围像素相连接,而每条边都有一个属于前景或者背景的概率,这是基于它与周边像素颜色上的相似性。

6.每个像素(即算法中的节点)会与一个前景或背景节点连接。

7.在节点完成连接后(可能与背景或前景连接),若节点之间的边属于不同的终端(即一个节点属于属于前景,另一个节点属于背景),则会切断他们之间的边,这样就能将图像各部分分割出来。

 opencv中提供的grabcut函数是:gradCut(img,mask,rect,bgdModel,fgdModel,iterCount,[mode]) -> mask,bgdModel,fgdModel

参数说明:

img --待分割的图像,必须是8位3通道,在处理的过程中不会被修改
mask --掩码图像,如果使用掩码进行初始化,那么mask保存初始化掩码信息;在执行分割的时候,也可以将用户交互所设定的前景和背景保存到mask中,
然后再传入gradCut函数;在处理结束之后,mask会保存结果,mask只能取以下四种值: GCD_BGD(=0),背景;GCD_FGD(=1),前景;
GCD_PR_BGD(=2),可能的背景;GCD_PR_FGD(=3),可能的前景。
rect --用于限定需要进行分割的图像范围,只有该矩形窗口内的图像部分才被处理。
bgdModel --背景模型,如果为None,函数内部会自动创建一个bgdModel;bgdModel必须是单通道浮点型图像,且行数只能为1,列数只能为13*5.
fgdModel --前景模型,如果为None,函数内部会自动创建一个fgdModel;fgdModel必须是单通道浮点型图像,且行数只能为1,列数只能为13*5.
iterCount --迭代次数,必须大于0
mode --用于指示gradCut函数进行什么操作,可选的有:GC_INIT_WITH_RECT(=0),用矩形窗初始化gradCut;GC_INIT_WITH_MASK(=1),用掩码图像初始化gradCut;
GC_EVAL(=2),执行分割。

利用grabcut提取lena图片的人脸区域,python实现如下:

import cv2
import numpy as np
import matplotlib.pyplot as plt
%matplotlib inline


img = cv2.imread('./lena2.jpg')
img_gray = cv2.cvtColor(img,cv2.COLOR_BGR2GRAY)
print(img.shape)

plt.imshow(img_gray,cmap ='gray')
plt.show()

#取出前景目标矩形框(x,y,w,h)
rect = (200,200,180,190)


mask = np.zeros(img.shape[:2],np.uint8)  #生成mask
cv2.grabCut(img,mask,rect,None,None,5,mode=cv2.GC_INIT_WITH_RECT)  #使用rect矩形的前景进行计算,mode
print(mask.dtype)

#返回的mask已经做了修改,GCD_BGD(=0),背景;GCD_FGD(=1),前景;GCD_PR_BGD(=2),可能的背景;GCD_PR_FGD(=3),可能的前景。
mask1 = np.where((mask ==3) | (mask==1),255,0).astype(np.uint8) #mask生成的数据是int32的,转换成uint8后续的与操作才能进行。
# print('mask1',mask1.dtype)
cv2.imshow('mask1',mask1)

output = cv2.bitwise_and(img,img,mask=mask1)  #使用与运算
print(output.dtype)
cv2.rectangle(img,(200,200),(380,390),[0,255,0],2)

cv2.imshow('img',img)
cv2.imshow('output',output)
cv2.waitKey(0)
cv2.destroyAllWindows()

左边眼睛没有提取出来,可以第二次使用gradcut,对mask进行修改:

import cv2
import numpy as np
import matplotlib.pyplot as plt
%matplotlib inline

img = cv2.imread('./lena2.jpg')
img_gray = cv2.cvtColor(img,cv2.COLOR_BGR2GRAY)
print(img.shape)

plt.imshow(img_gray,cmap ='gray')
plt.show()

#取出前景目标矩形框(x,y,w,h)
rect = (200,200,180,190)

mask = np.zeros(img.shape[:2],np.uint8)  #生成mask
cv2.grabCut(img,mask,rect,None,None,5,mode=cv2.GC_INIT_WITH_RECT)  #使用rect矩形的前景进行计算,mode
print(mask.dtype)

#返回的mask已经做了修改,GCD_BGD(=0),背景;GCD_FGD(=1),前景;GCD_PR_BGD(=2),可能的背景;GCD_PR_FGD(=3),可能的前景。
mask1 = np.where((mask ==3) | (mask==1),255,0).astype(np.uint8) #mask生成的数据是int32的,转换成uint8后续的与操作才能进行。
# print('mask1',mask1.dtype)
cv2.imshow('mask1',mask1)

output = cv2.bitwise_and(img,img,mask=mask1)  #使用与运算
print(output.dtype)
# cv2.rectangle(img,(200,200),(380,390),[0,255,0],2)

#第二次使用gradcut,对mask进行修改
mask[240:290,300:344] = 1  #指定该区域为前景(眼睛区域被认为是背景了)

cv2.grabCut(img,mask,None,None,None,5,mode=cv2.GC_INIT_WITH_MASK) #注意要使用mask的mode来计算
mask2 = np.where((mask ==3) | (mask==1),255,0).astype(np.uint8)
cv2.imshow('mask2',mask2)
output2 = cv2.bitwise_and(img,img,mask=mask2)  #使用与运算

# cv2.imshow('img',img)
cv2.imshow('output',output)
cv2.imshow('output2',output2)
cv2.waitKey(0)
cv2.destroyAllWindows()

  结果如下:

 基本上效果挺好,但是每次需要在代码中指定需要提取的前景矩形框不太方便,可以选择把grabcut封装成一个类,然后通过鼠标操作来选择提取区域的矩形框。

利用鼠标左键移动画框来标记需要提取的区域,然后用grabcut来进行提取(按g键显示提取结果)。

import cv2
import numpy as np

class APP:
    def __init__(self,image):
        self.image = image
        self.img = cv2.imread(self.image)
        self.img2 = self.img.copy() #拷贝一份img,用来确保鼠标移动画矩形的时候不干扰原图
        
        self.start_x = 0 #绘制矩形起始点
        self.start_y = 0
        self.flag_rect =False  #是否需要绘制矩形的标志
        self.rect = (0,0,0,0)
        self.mask = np.zeros(self.img.shape[:2],np.uint8)
        self.output = np.zeros(self.img.shape[:2],np.uint8)
    
    def onMouse(self,event,x,y,flags,params):
        #按下左键,开始框选前景区域
        if event == cv2.EVENT_LBUTTONDOWN:
            self.start_x =x
            self.start_y =y
            self.flag_rect = True
        elif event == cv2.EVENT_MOUSEMOVE and self.flag_rect:
            self.img = self.img2.copy() #鼠标每次移动的时候再拷贝一份img2到img用于显示(img会一直刷新,所以之前鼠标移动画的不会影响后面)
            cv2.rectangle(self.img,(self.start_x,self.start_y),(x,y),[0,0,0],2)

        elif event == cv2.EVENT_LBUTTONUP:
            cv2.rectangle(self.img,(self.start_x,self.start_y),(x,y),[0,0,255],2)
            self.flag_rect = False
            #记录绘制矩形的起始点和宽高,后面grabcut用
            self.rect = (min(self.start_x,x),min(self.start_y,y),abs(x-self.start_x),abs(y-self.start_y))
    
    #核心逻辑:窗口,回调函数,图片
    def run(self):
               
        #绑定鼠标事件
        cv2.namedWindow('img')
        cv2.setMouseCallback('img',self.onMouse)
        
        while True:
            cv2.imshow('img',self.img)
            cv2.imshow('output',self.output)
            key = cv2.waitKey(1)
            if key == 27:
                break
            elif key ==ord('g'):
                
                cv2.grabCut(self.img2,self.mask,self.rect,None,None,5,mode=cv2.GC_INIT_WITH_RECT)
                mask1 = np.where((self.mask ==3) | (self.mask==1),255,0).astype(np.uint8)
                self.output = cv2.bitwise_and(self.img2,self.img2,mask=mask1)
                
        cv2.destroyAllWindows()
    
app =APP('./lena2.jpg')
app.run()

但是这样只能进行初次提取,不能根据结果继续优化,再添加一些其他功能:用grabcut交互提取目标前景,可以用右键单击框定前景区域,然后按g键执行grabcut算法提取roi区域。如果对提取的区域不满意,可以按一下f键,按住鼠标左键拖动,再次添加前景区域;按一下b键,按住鼠标左键拖动,添加背景区域,然后,通过点击enter键或者空格键重新提取roi区域。最后,通过按w键保存提取roi区域的图片,完成后通过q键或者esc键退出。python代码实现如下:

import cv2
import numpy as np

DRAW_BG = {'color': [0,0,0], 'val': 0}    # 背景,标记为0
DRAW_FG = {'color': [255,255,255], 'val': 1}    # 前景,标记为1 

class APP:
    def __init__(self,image):
        self.image = image
        self.img = cv2.imread(self.image)
        self.img2 = self.img.copy() #拷贝一份img,用来确保鼠标移动画矩形的时候不干扰原图
        self.value = DRAW_FG   #默认添加前景区域
        
        self.start_x = 0 #绘制矩形起始点
        self.start_y = 0
        self.flag_rect =False  #是否需要绘制矩形的标志
        self.flag_line =False  #是否需要添加前景或背景的线

        self.rect = (0,0,0,0)
        self.mask = np.zeros(self.img.shape[:2],np.uint8)  #grabcut的mask
        self.output = np.zeros(self.img.shape[:2],np.uint8)

    def onMouse(self,event,x,y,flags,params):
        
        #按下右键,开始框选前景区域
        if event == cv2.EVENT_RBUTTONDOWN:
            self.start_x =x
            self.start_y =y
            self.flag_rect = True
            
        elif event == cv2.EVENT_MOUSEMOVE and self.flag_rect:
            self.img = self.img2.copy() #鼠标每次移动的时候再拷贝一份img2到img用于显示(img会一直刷新,所以之前鼠标移动画的不会影响后面)
            cv2.rectangle(self.img,(self.start_x,self.start_y),(x,y),[0,0,0],2)
            
        elif event == cv2.EVENT_RBUTTONUP:
            cv2.rectangle(self.img,(self.start_x,self.start_y),(x,y),[0,0,255],2)
            self.flag_rect = False
            #记录绘制矩形的起始点和宽高,后面grabcut用
            self.rect = (min(self.start_x,x),min(self.start_y,y),abs(x-self.start_x),abs(y-self.start_y))
            
        #按下左键,开始添加前景或背景区域
        elif event == cv2.EVENT_LBUTTONDOWN:
            self.start_x =x
            self.start_y =y
            self.flag_line = True
            
        elif event == cv2.EVENT_MOUSEMOVE and self.flag_line:

            cv2.line(self.img,(self.start_x,self.start_y),(x,y),self.value['color'],6)
            cv2.line(self.mask,(self.start_x,self.start_y),(x,y),self.value['val'],6)
            self.start_x =x
            self.start_y =y
            
        elif event == cv2.EVENT_LBUTTONUP:
            cv2.line(self.img,(self.start_x,self.start_y),(x,y),self.value['color'],6)
            cv2.line(self.mask,(self.start_x,self.start_y),(x,y),self.value['val'],6)
            self.flag_line = False
        

    #核心逻辑:窗口,回调函数,图片
    def run(self):
               
        #绑定鼠标事件
        cv2.namedWindow('img')
        cv2.setMouseCallback('img',self.onMouse)
        
        while True:
            cv2.imshow('img',self.img)
            cv2.imshow('output',self.output)
            key = cv2.waitKey(1)
            if key == 27 or key == ord('q'):
                break
                
            #按下g键,进行grabcut分割    
            elif key ==ord('g'): 
                cv2.grabCut(self.img2,self.mask,self.rect,None,None,5,mode=cv2.GC_INIT_WITH_RECT)
                mask1 = np.where((self.mask ==3) | (self.mask==1),255,0).astype(np.uint8)
                self.output = cv2.bitwise_and(self.img2,self.img2,mask=mask1)
                
            #按下f键,在已经分割的图像上标记前景区域
            elif key == ord('f'):
                self.value = DRAW_FG

            #按下b键,在已经分割的图像上标记背景区域   
            elif key == ord('b'):
                self.value = DRAW_BG

            elif key == 13 or key == 32:   #按下enter键,显示重新提取的分割结果
                cv2.grabCut(self.img2,self.mask,None,None,None,5,mode=cv2.GC_INIT_WITH_MASK)
                mask2 = np.where((self.mask ==3) | (self.mask==1),255,0).astype(np.uint8)
#                 cv2.imshow('mask2',mask2)
                self.output = cv2.bitwise_and(self.img2,self.img2,mask=mask2)
    
            elif key == ord('w'):
                cv2.imwrite('./roi.jpg',self.output)
                print('save success')
            
        cv2.destroyAllWindows()
    
        
app =APP('./lena2.jpg')
app.run()

 初次提取roi区域:

多次添加修改前景和背景,进行人脸区域的提取:

3 meanshift图像分割

 meanshift严格来说并不是对图像进行分割的,而是在色彩层面进行平滑滤波的,它会中和色彩分布相近的颜色,平滑色彩细节,侵蚀掉面积较小的颜色区域。效果上看可以形成油画风,它以图像上任一点p为圆心,半径为sp,色彩幅值为sr进行不断迭代,经过迭代,将收敛点的像素值代替原来的像素值,从而去除局部相似的纹理,同时保留了边缘等差异较大的特征。

import cv2
import numpy as np

img = cv2.imread('./keys.jpg')
gray = cv2.cvtColor(img,cv2.COLOR_BGR2GRAY)
_,thresh = cv2.threshold(gray,0,255,cv2.THRESH_BINARY | cv2.THRESH_OTSU)
img_canny0 = cv2.Canny(thresh,150,300)


img_meanshift = cv2.pyrMeanShiftFiltering(img,60,60) #空间半径sp和色彩半径sr需要根据实际情况调整(应该是0-100),返回三通道图片
img_canny = cv2.Canny(img_meanshift,150,200)  #canny阈值越小,边缘检测越细致

#findContours的输入图片必须是单通道的图
_,contours,_ = cv2.findContours(img_canny,cv2.RETR_EXTERNAL,cv2.CHAIN_APPROX_SIMPLE)
cv2.drawContours(img,contours,-1,[0,0,255],2)
# cv2.findContours()

cv2.imshow('canny',np.hstack((img_canny0,img_canny)))
cv2.imshow('img',np.hstack((img,img_meanshift)))

cv2.waitKey(0)
cv2.destroyAllWindows()

 进行meanshift之后再进行边缘检测的结果会好很多:

 或者进行花园中大面积的不同花草分割效果也会好些: