图片分割之GrabCut算法、分水岭算法

https://www.cnblogs.com/zyly/p/9392881.htmlhtml

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

https://www.cnblogs.com/zyly/p/9392881.html

1、图像分割算法概述

一、基于阈值的分割方法数组

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

二、基于边缘的分割方法dom

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

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

三、基于区域的分割方法ui

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

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

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

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

四、基于图论的分割方法

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

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

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

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

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

2、图像分割之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()
复制代码

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

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

二、建立以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)

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

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

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

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

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

五、建立窗体、并设置鼠标回调函数、而后显示源图像

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

#显示图片
cv2.imshow('img',img)
复制代码

六、鼠标回调事件代码以下

复制代码
#鼠标事件的回调函数
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
复制代码

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

复制代码
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值像素(理论上会保存全部的前景像素)。

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

分水岭算法是在分割的过程当中,它会把跟临近像素间的类似性做为重要的参考依据,从而将在空间位置上相近而且灰度值相近(求梯度)的像素点互相链接起来构成一个封闭的轮廓。分水岭算法经常使用的操做步骤:彩色图像灰度化,而后再求梯度图,最后在梯度图的基础上进行分水岭算法,求得分段图像的边缘线。

下面左边的灰度图,能够描述为右边的地形图,地形的高度是有灰度图的灰度值决定,灰度为0对应地形图的地面,灰度值最大的像素对应地形图的最高点。

对灰度图的地形图的解释,咱们考虑三类点:

  1. 局部最小点值,该点对应一个盆地的最低点,当咱们在盆地里滴一滴水的时候,因为重力做用,谁最终会汇聚到该点。注意:可能存在一个最小值面,该平面内的都是最小值点。
  2. 盆地的其余位置点,该位置滴的水会汇聚到局部最小点。
  3. 盆地的边缘点,是盆地和其余盆地交界点,在该点滴一滴水,会等几率的流向任何一个盆地。

假设咱们在盆地的最小值点,打一个洞,而后往盆地里面注水,并阻止两个盆地的水汇聚,咱们会在两个盆地的水聚集的时刻,在交界的边缘上(即分水岭线),建一个大坝,来阻止两个盆地的水汇聚成一片水域。这样图像就被分红2个像素集,一个是注水盆地像素集,一个是分水岭线像素集。

在真实图像中,因为噪声点或者其它干扰因素的存在,使得分水岭算法经常出现过分分割的现象,这主要是由于图像中可能存在不少很小的局部极小点的存在,对这些局部盆地进行分割会致使过度割。为了解决过度割的问题,学者们提出了基于标记(mark)图像的分水岭算法,就是经过先验知识,来指导分水岭算法,以便得到更好的图像分割效果。一般的mark图像,都是在某个区域定义了一些灰度层级,在这个区域的洪水淹没过程当中,水平面都是从定义的高度开始的,这样能够避免一些很小的噪声极小值区域的分割。

下面咱们来学习一下OpenCV中提供的watershed函数:

watershed(image,markers)

参数说明:

  • image:必须是一个8位 3通道彩色图像
  • markers:在执行分水岭函数watershed以前,必须对参数markers进行处理,它应该包含不一样区域的轮廓,每一个轮廓有一个本身惟一的编号,轮廓的定位能够经过Opencv中findContours方法实现,这个是执行分水岭以前的要求。

接下来执行分水岭会发生什么呢?算法会根据markers传入的轮廓做为种子(咱们把注水点由盆地的最小值点转为图像的轮廓),对图像上其余的像素点根据分水岭算法规则进行判断,并对每一个像素点的区域归属进行划定,直处处理完图像上全部像素点。而区域与区域之间的分界处的值被置为“-1”,以作区分。

简单归纳一下就是说第二个入参markers必须包含了种子点信息。Opencv官方例程中使用鼠标划线标记,其实就是在定义种子,只不过须要手动操做,而使用findContours能够自动标记种子点。而分水岭方法完成以后并不会直接生成分割后的图像,还须要进一步的显示处理,如此看来,只有两个参数的watershed其实并不简单。

分水岭算法实现图像自动分割的步骤:

  1. 图像灰度化、Canny边缘检测
  2. 查找轮廓,而且把轮廓信息按照不一样的编号绘制到watershed的第二个参数markers上,至关于标记注水点。
  3. watershed分水岭算法
  4. 绘制分割出来的区域,而后使用随机颜色填充,再跟源图像融合,以获得更好的显示效果。

代码以下:

复制代码
# -*- coding: utf-8 -*-
"""
Created on Mon Jul 30 21:38:41 2018

@author: lenovo
"""

import numpy as np
import cv2

#读入图片
img = cv2.imread('image/img22.jpg')

#转换为灰度图片
gray_img = cv2.cvtColor(img,cv2.COLOR_BGR2GRAY)

#canny边缘检测 函数返回一副二值图,其中包含检测出的边缘。
canny = cv2.Canny(gray_img,80,150)
cv2.imshow('Canny',canny)


#寻找图像轮廓 返回修改后的图像 图像的轮廓  以及它们的层次
canny,contours,hierarchy = cv2.findContours(canny,cv2.RETR_TREE,cv2.CHAIN_APPROX_SIMPLE)
#32位有符号整数类型,
marks = np.zeros(img.shape[:2],np.int32)
#findContours检测到的轮廓
imageContours = np.zeros(img.shape[:2],np.uint8)

#轮廓颜色
compCount = 0
index = 0
#绘制每个轮廓
for index in range(len(contours)):
    #对marks进行标记,对不一样区域的轮廓使用不一样的亮度绘制,至关于设置注水点,有多少个轮廓,就有多少个轮廓
    #图像上不一样线条的灰度值是不一样的,底部略暗,越往上灰度越高
    marks = cv2.drawContours(marks,contours,index,(index,index,index),1,8,hierarchy)
    #绘制轮廓,亮度同样
    imageContours = cv2.drawContours(imageContours,contours,index,(255,255,255),1,8,hierarchy)
    
#查看 使用线性变换转换输入数组元素成8位无符号整型。
markerShows = cv2.convertScaleAbs(marks)    
cv2.imshow('markerShows',markerShows)
#cv2.imshow('imageContours',imageContours)

#使用分水岭算法
marks = cv2.watershed(img,marks)
afterWatershed = cv2.convertScaleAbs(marks)  
cv2.imshow('afterWatershed',afterWatershed)

#生成随机颜色
colorTab = np.zeros((np.max(marks)+1,3))
#生成0~255之间的随机数
for i in range(len(colorTab)):
    aa = np.random.uniform(0,255)
    bb = np.random.uniform(0,255)
    cc = np.random.uniform(0,255)
    colorTab[i] = np.array([aa,bb,cc],np.uint8)
    
bgrImage = np.zeros(img.shape,np.uint8)

#遍历marks每个元素值,对每个区域进行颜色填充
for i in range(marks.shape[0]):
    for j in range(marks.shape[1]):
        #index值同样的像素表示在一个区域
        index = marks[i][j]
        #判断是否是区域与区域之间的分界,若是是边界(-1),则使用白色显示
        if index == -1:
            bgrImage[i][j] = np.array([255,255,255])
        else:                        
            bgrImage[i][j]  = colorTab[index]
cv2.imshow('After ColorFill',bgrImage)            

#填充后与原始图像融合
result = cv2.addWeighted(img,0.6,bgrImage,0.4,0)
cv2.imshow('addWeighted',result)     

cv2.waitKey(0)
cv2.destroyAllWindows()
复制代码

咱们对下面的图像采用分水岭算法进行分割:

而后咱们分析代码:

左侧是使用Canny边缘检测后获得的二值化图像,而后咱们对二值化图像进行查找轮廓,并进行处理获得符合要求的marks:

复制代码
#寻找图像轮廓 返回修改后的图像 图像的轮廓  以及它们的层次
canny,contours,hierarchy = cv2.findContours(canny,cv2.RETR_TREE,cv2.CHAIN_APPROX_SIMPLE)
#32位有符号整数类型,
marks = np.zeros(img.shape[:2],np.int32)
#findContours检测到的轮廓
imageContours = np.zeros(img.shape[:2],np.uint8)

#轮廓颜色
compCount = 0
index = 0
#绘制每个轮廓
for index in range(len(contours)):
    #对marks进行标记,对不一样区域的轮廓使用不一样的亮度绘制,至关于设置注水点,有多少个轮廓,就有多少个轮廓
    #图像上不一样线条的灰度值是不一样的,底部略暗,越往上灰度越高
    marks = cv2.drawContours(marks,contours,index,(index,index,index),1,8,hierarchy)
    #绘制轮廓,亮度同样
    imageContours = cv2.drawContours(imageContours,contours,index,(255,255,255),1,8,hierarchy)
复制代码

而后咱们把marks转换为8位单通道灰度图显示,获得上面的右图,能够看到图像上不一样轮廓的灰度值是不一样的,底部略暗,越往上灰度越高。这些轮廓和不一样的灰度值说明了什么?

每个轮廓表明一个种子,轮廓的不一样灰度值其实表明了对不一样注水种子的编号,有多少不一样灰度值的轮廓,就有多少个种子,图像最后分割后就有多少个区域。

#查看 使用线性变换转换输入数组元素成8位无符号整型。
markerShows = cv2.convertScaleAbs(marks)    
cv2.imshow('markerShows',markerShows)
#cv2.imshow('imageContours',imageContours)

再来看一下执行完分水岭算法以后的marks(下面左图)。

上面左图为分割出来的区域,咱们能够看到,源图像空间上临近而且灰度值上相近的区域被划分为一个区域(同一区域的灰度值是同样的),不一样区域间被划开。

#使用分水岭算法
marks = cv2.watershed(img,marks)
afterWatershed = cv2.convertScaleAbs(marks)  
cv2.imshow('afterWatershed',afterWatershed)

而后咱们使用颜色填充分割出来的区域,获得上图右边的效果。

复制代码
#生成随机颜色
colorTab = np.zeros((np.max(marks)+1,3))
#生成0~255之间的随机数
for i in range(len(colorTab)):
    aa = np.random.uniform(0,255)
    bb = np.random.uniform(0,255)
    cc = np.random.uniform(0,255)
    colorTab[i] = np.array([aa,bb,cc],np.uint8)
    
bgrImage = np.zeros(img.shape,np.uint8)

#遍历marks每个元素值,对每个区域进行颜色填充
for i in range(marks.shape[0]):
    for j in range(marks.shape[1]):
        #index值同样的像素表示在一个区域
        index = marks[i][j]
        #判断是否是区域与区域之间的分界,若是是边界(-1),则使用白色显示
        if index == -1:
            bgrImage[i][j] = np.array([255,255,255])
        else:                        
            bgrImage[i][j]  = colorTab[index]
cv2.imshow('After ColorFill',bgrImage)
复制代码

咱们再把填充后的图像与源图像进行融合,获得下面的效果:

#填充后与原始图像融合
result = cv2.addWeighted(img,0.6,bgrImage,0.4,0)
cv2.imshow('addWeighted',result)     

cv2.waitKey(0)
cv2.destroyAllWindows()

参考文章:

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

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

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

[4]图像处理——分水岭算法

[5]OpenCV库中watershed函数(分水岭算法)的详细使用例程

[6]分水岭算法及案例

相关文章
相关标签/搜索