第八节、图片分割之GrabCut算法、分水岭算法

所谓图像分割指的是根据灰度、颜色、纹理和形状等特征把图像划分成若干互不交迭的区域,并使这些特征在同一区域内呈现出相似性,而在不同区域间呈现出明显的差异性。我们先对目前主要的图像分割方法做个概述,后面再对个别方法做详细的了解和学习。

一、图像分割算法概述

1、基于阈值的分割方法

阈值法的基本思想是基于图像的灰度特征来计算一个或多个灰度阈值,并将图像中每个像素的灰度值与阈值相比较,最后将像素根据比较结果分到合适的类别中。因此,该类方法最为关键的一步就是按照某个准则函数来求解最佳灰度阈值。

2、基于边缘的分割方法

所谓边缘是指图像中两个不同区域的边界线上连续的像素点的集合,是图像局部特征不连续性的反映,体现了灰度、颜色、纹理等图像特性的突变。通常情况下,基于边缘的分割方法指的是基于灰度值的边缘检测,它是建立在边缘灰度值会呈现出阶跃型或屋顶型变化这一观测基础上的方法。

阶跃型边缘两边像素点的灰度值存在着明显的差异,而屋顶型边缘则位于灰度值上升或下降的转折处。正是基于这一特性,可以使用微分算子进行边缘检测,即使用一阶导数的极值与二阶导数的过零点来确定边缘,具体实现时可以使用图像与模板进行卷积来完成。

3、基于区域的分割方法

此类方法是将图像按照相似性准则分成不同的区域,主要包括种子区域生长法、区域分裂合并法和分水岭法等几种类型。

种子区域生长法是从一组代表不同生长区域的种子像素开始,接下来将种子像素邻域里符合条件的像素合并到种子像素所代表的生长区域中,并将新添加的像素作为新的种子像素继续合并过程,直到找不到符合条件的新像素为止。该方法的关键是选择合适的初始种子像素以及合理的生长准则。

区域分裂合并法(Gonzalez,2002)的基本思想是首先将图像任意分成若干互不相交的区域,然后再按照相关准则对这些区域进行分裂或者合并从而完成分割任务,该方法既适用于灰度图像分割也适用于纹理图像分割。

分水岭法(Meyer,1990)是一种基于拓扑理论的数学形态学的分割方法,其基本思想是把图像看作是测地学上的拓扑地貌,图像中每一点像素的灰度值表示该点的海拔高度,每一个局部极小值及其影响区域称为集水盆,而集水盆的边界则形成分水岭。该算法的实现可以模拟成洪水淹没的过程,图像的最低点首先被淹没,然后水逐渐淹没整个山谷。当水位到达一定高度的时候将会溢出,这时在水溢出的地方修建堤坝,重复这个过程直到整个图像上的点全部被淹没,这时所建立的一系列堤坝就成为分开各个盆地的分水岭。分水岭算法对微弱的边缘有着良好的响应,但图像中的噪声会使分水岭算法产生过分割的现象。

4、基于图论的分割方法

此类方法把图像分割问题与图的最小割(min cut)问题相关联。首先将图像映射为带权无向图G=<V,E>,图中每个节点N∈V对应于图像中的每个像素,每条边∈E连接着一对相邻的像素,边的权值表示了相邻像素之间在灰度、颜色或纹理方面的非负相似度。而对图像的一个分割s就是对图的一个剪切,被分割的每个区域C∈S对应着图中的一个子图。而分割的最优原则就是使划分后的子图在内部保持相似度最大,而子图之间的相似度保持最小。基于图论的分割方法的本质就是移除特定的边,将图划分为若干子图从而实现分割。目前所了解到的基于图论的方法有GraphCut,GrabCut和Random Walk等。

5、基于能量泛函的分割方法

该类方法主要指的是活动轮廓模型(active contour model)以及在其基础上发展出来的算法,其基本思想是使用连续曲线来表达目标边缘,并定义一个能量泛函使得其自变量包括边缘曲线,因此分割过程就转变为求解能量泛函的最小值的过程,一般可通过求解函数对应的欧拉(Euler.Lagrange)方程来实现,能量达到最小时的曲线位置就是目标的轮廓所在。按照模型中曲线表达形式的不同,活动轮廓模型可以分为两大类:参数活动轮廓模型(parametric active contour model)和几何活动轮廓模型(geometric active contour model)。

参数活动轮廓模型是基于Lagrange框架,直接以曲线的参数化形式来表达曲线,最具代表性的是由Kasset a1(1987)所提出的Snake模型。该类模型在早期的生物图像分割领域得到了成功的应用,但其存在着分割结果受初始轮廓的设置影响较大以及难以处理曲线拓扑结构变化等缺点,此外其能量泛函只依赖于曲线参数的选择,与物体的几何形状无关,这也限制了其进一步的应用。

几何活动轮廓模型的曲线运动过程是基于曲线的几何度量参数而非曲线的表达参数,因此可以较好地处理拓扑结构的变化,并可以解决参数活动轮廓模型难以解决的问题。而水平集(Level Set)方法(Osher,1988)的引入,则极大地推动了几何活动轮廓模型的发展,因此几何活动轮廓模型一般也可被称为水平集方法。

二、图像分割之GrabCut算法

里不去介绍GrabCut算法的原理,感兴趣的童鞋去参考博客后面的文章。该算法主要基于以下知识:

  • k均值聚类

  • 高斯混合模型建模(GMM)
  • max flow/min cut

这里介绍一些GrabCut算法的实现步骤:

  1. 在图片中定义(一个或者多个)包含物体的矩形。
  2. 矩形外的区域被自动认为是背景。
  3. 对于用户定义的矩形区域,可用背景中的数据来区分它里面的前景和背景区域。
  4. 用高斯混合模型(GMM)来对背景和前景建模,并将未定义的像素标记为可能的前景或者背景。
  5. 图像中的每一个像素都被看做通过虚拟边与周围像素相连接,而每条边都有一个属于前景或者背景的概率,这是基于它与周边像素颜色上的相似性。
  6. 每一个像素(即算法中的节点)会与一个前景或背景节点连接。
  7. 在节点完成连接后(可能与背景或前景连接),若节点之间的边属于不同终端(即一个节点属于前景,另一个节点属于背景),则会切断他们之间的边,这就能将图像各部分分割出来。下图能很好的说明该算法:

OpenCV提供了GrabCut算法相关的函数,grabCut函数:

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

输入:图像、被标记好的前景、背景

输出:分割图像

其中输入的前景、背景指的是一种概率,如果你已经明确某一块区域是背景,那么它属于背景的概率为1;当然如果你觉得它有可能背景,但是没有百分百的肯定,这个时候你就要用到高斯模型,对其进行建模,然后估算概率。现在我以下图为例,用户通过交互输入框选区域,前景位于框选区域内,也就是说矩形区域外的全部属于背景,且概率为百分百。然后方框内可能属于前景,概率需要用高斯混合建模求解。

参数说明:

  • 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),执行分割。

接下来,我们就演示上图哪个例子,把字符从图片中抠出来:

# -*- coding: utf-8 -*-
"""
Created on Mon Jul 30 15:35:41 2018

@author: lenovo
"""

'''
基于图论的分割方法-GraphCut
【图像处理】图像分割之(一~四)GraphCut,GrabCut函数使用和源码解读(OpenCV)
https://blog.csdn.net/kyjl888/article/details/78253829
'''

import numpy as np
import cv2
     
#鼠标事件的回调函数
def on_mouse(event,x,y,flag,param):        
    global rect
    global leftButtonDowm
    global leftButtonUp
    
    #鼠标左键按下
    if event == cv2.EVENT_LBUTTONDOWN:
        rect[0] = x
        rect[2] = x
        rect[1] = y
        rect[3] = y
        leftButtonDowm = True
        leftButtonUp = False
        
    #移动鼠标事件
    if event == cv2.EVENT_MOUSEMOVE:
        if leftButtonDowm and  not leftButtonUp:
            rect[2] = x
            rect[3] = y        
  
    #鼠标左键松开
    if event == cv2.EVENT_LBUTTONUP:
        if leftButtonDowm and  not leftButtonUp:
            x_min = min(rect[0],rect[2])
            y_min = min(rect[1],rect[3])
            
            x_max = max(rect[0],rect[2])
            y_max = max(rect[1],rect[3])
            
            rect[0] = x_min
            rect[1] = y_min
            rect[2] = x_max
            rect[3] = y_max
            leftButtonDowm = False      
            leftButtonUp = True

#读入图片
img = cv2.imread('image/img21.jpg')
#掩码图像,如果使用掩码进行初始化,那么mask保存初始化掩码信息;在执行分割的时候,也可以将用户交互所设定的前景与背景保存到mask中,然后再传入grabCut函数;在处理结束之后,mask中会保存结果
mask = np.zeros(img.shape[:2],np.uint8)

#背景模型,如果为None,函数内部会自动创建一个bgdModel;bgdModel必须是单通道浮点型图像,且行数只能为1,列数只能为13x5;
bgdModel = np.zeros((1,65),np.float64)
#fgdModel——前景模型,如果为None,函数内部会自动创建一个fgdModel;fgdModel必须是单通道浮点型图像,且行数只能为1,列数只能为13x5;
fgdModel = np.zeros((1,65),np.float64)

#用于限定需要进行分割的图像范围,只有该矩形窗口内的图像部分才被处理;
rect = [0,0,0,0]  
    
#鼠标左键按下
leftButtonDowm = False
#鼠标左键松开
leftButtonUp = True
    
#指定窗口名来创建窗口
cv2.namedWindow('img') 
#设置鼠标事件回调函数 来获取鼠标输入
cv2.setMouseCallback('img',on_mouse)

#显示图片
cv2.imshow('img',img)


while cv2.waitKey(2) == -1:
    #左键按下,画矩阵
    if leftButtonDowm and not leftButtonUp:  
        img_copy = img.copy()
        #在img图像上,绘制矩形  线条颜色为green 线宽为2
        cv2.rectangle(img_copy,(rect[0],rect[1]),(rect[2],rect[3]),(0,255,0),2)  
        #显示图片
        cv2.imshow('img',img_copy)
        
    #左键松开,矩形画好 
    elif not leftButtonDowm and leftButtonUp and rect[2] - rect[0] != 0 and rect[3] - rect[1] != 0:
        #转换为宽度高度
        rect[2] = rect[2]-rect[0]
        rect[3] = rect[3]-rect[1]
        rect_copy = tuple(rect.copy())   
        rect = [0,0,0,0]
        #物体分割
        cv2.grabCut(img,mask,rect_copy,bgdModel,fgdModel,5,cv2.GC_INIT_WITH_RECT)
            
        mask2 = np.where((mask==2)|(mask==0),0,1).astype('uint8')
        img_show = img*mask2[:,:,np.newaxis]
        #显示图片分割后结果
        cv2.imshow('grabcut',img_show)
        #显示原图
        cv2.imshow('img',img)    

cv2.waitKey(0)
cv2.destroyAllWindows()

1、上面代码比较简单,首先加载图片,并创建一个与所加载图像同形状的掩模,并用0填充。

#读入图片
img = cv2.imread('image/img21.jpg')
#掩码图像,如果使用掩码进行初始化,那么mask保存初始化掩码信息;在执行分割的时候,也可以将用户交互所设定的前景与背景保存到mask中,然后再传入grabCut函数;在处理结束之后,mask中会保存结果
mask = np.zeros(img.shape[:2],np.uint8)

2、创建以0填充的前景和背景模型。

#背景模型,如果为None,函数内部会自动创建一个bgdModel;bgdModel必须是单通道浮点型图像,且行数只能为1,列数只能为13x5;
bgdModel = np.zeros((1,65),np.float64)
#fgdModel——前景模型,如果为None,函数内部会自动创建一个fgdModel;fgdModel必须是单通道浮点型图像,且行数只能为1,列数只能为13x5;
fgdModel = np.zeros((1,65),np.float64)

3、可以使用数据填充这些模型,但是这里准备使用一个标识出想要隔离的对象的矩形来初始化grabCut算法。所以背景和前景模型都要基于这个初始化矩形所留下来的区域来进行,这个矩形用下面代码来定义:

#用于限定需要进行分割的图像范围,只有该矩形窗口内的图像部分才被处理;
rect = [0,0,0,0]  

后面我们使用鼠标回调事件来更新矩形框的带下,当我们鼠标左键按下的时候、开始在原始图片上绘制矩形、当鼠标左键松开、矩形绘制完毕。

4、定义两个表示位、表示鼠标左键的状态

#鼠标左键按下
leftButtonDowm = False
#鼠标左键松开
leftButtonUp = True

5、创建窗体、并设置鼠标回调函数、然后显示源图像

#指定窗口名来创建窗口
cv2.namedWindow('img') 
#设置鼠标事件回调函数 来获取鼠标输入
cv2.setMouseCallback('img',on_mouse)

#显示图片
cv2.imshow('img',img)

6、鼠标回调事件代码如下

#鼠标事件的回调函数
def on_mouse(event,x,y,flag,param):        
    global rect
    global leftButtonDowm
    global leftButtonUp
    
    #鼠标左键按下
    if event == cv2.EVENT_LBUTTONDOWN:
        rect[0] = x
        rect[2] = x
        rect[1] = y
        rect[3] = y
        leftButtonDowm = True
        leftButtonUp = False
        
    #移动鼠标事件
    if event == cv2.EVENT_MOUSEMOVE:
        if leftButtonDowm and  not leftButtonUp:
            rect[2] = x
            rect[3] = y        
  
    #鼠标左键松开
    if event == cv2.EVENT_LBUTTONUP:
        if leftButtonDowm and  not leftButtonUp:
            x_min = min(rect[0],rect[2])
            y_min = min(rect[1],rect[3])
            
            x_max = max(rect[0],rect[2])
            y_max = max(rect[1],rect[3])
            
            rect[0] = x_min
            rect[1] = y_min
            rect[2] = x_max
            rect[3] = y_max
            leftButtonDowm = False      
            leftButtonUp = True

7、循环部分,当鼠标左键按下、没有松开则实时绘制矩形框。当左键松开、对图像进行分割。

while cv2.waitKey(2) == -1:
    #左键按下,画矩阵
    if leftButtonDowm and not leftButtonUp:  
        img_copy = img.copy()
        #在img图像上,绘制矩形  线条颜色为green 线宽为2
        cv2.rectangle(img_copy,(rect[0],rect[1]),(rect[0]+rect[2],rect[1]+rect[3]),(0,255,0),2)  
        #显示图片
        cv2.imshow('img',img_copy)
        
    #左键松开,矩形画好 
    elif not leftButtonDowm and leftButtonUp and rect[0] != 0 and rect[1] != 0:
        rect_copy = tuple(rect.copy())   
        print(rect_copy)
        rect = [0,0,0,0]
        #物体分割
        cv2.grabCut(img,mask,rect_copy,bgdModel,fgdModel,5,cv2.GC_INIT_WITH_RECT)
            
        mask2 = np.where((mask==2)|(mask==0),0,1).astype('uint8')
        img_show = img*mask2[:,:,np.newaxis]
        #显示图片分割后结果
        cv2.imshow('grabcut',img_show)
        #显示原图
        cv2.imshow('img',img)    

cv2.waitKey(0)
cv2.destroyAllWindows()

调用完grabCut函数之后,掩模图像mask元素值已经变成了0~3之间的值。值为0和2的将转为0,值为1和3的将转为1,然后保存在mask2中,这样就可以用mask2过滤出所有的0值像素(理论上会保存所有的前景像素)。

 三、图像分割之分水岭算法

算法叫做分水岭是因为它里面有水的概念。把图像中低密度的区域(变化很少)想象成山谷,图像中高密度的区域(变化很多)想象成山谷。开始向山谷中注入水直到不同的山谷的水开始汇聚。为了阻止不同山谷的水汇聚、可以设置一些栅栏,最后得到的栅栏就是图像分割。

还以下面这张图为例,我们想把江南大学的logo从北京中分离出来。

参考文章:

【图像处理】图像分割之(一~四)GraphCut,GrabCut函数使用和源码解读(OpenCV)

图像处理(十四)图像分割(4)grab cut的图割实现-Siggraph 2004

Opencv学习——图像分割之分水岭算法

猜你喜欢

转载自www.cnblogs.com/zyly/p/9392881.html