opencv-python 图像分割
本章节介绍图像分割方面的算法:分水岭算法,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之后再进行边缘检测的结果会好很多:
或者进行花园中大面积的不同花草分割效果也会好些: