精华内容
下载资源
问答
  • 图像分割分水岭算法

    2016-03-08 08:55:49
    图像分割分水岭算法,内附有详细解释和源图像。
  • 主要为大家详细介绍了Opencv实现用于图像分割分水岭算法,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
  • 图像分割分水岭算法MATLAB源代码

    热门讨论 2010-06-07 23:21:49
    数字图像处理 图像分割分水岭算法 源代码 matlab
  • 目标• 使用分水岭算法基于掩模的图像分割• 学习函数: cv2.watershed()原理任何一幅灰度图像都可以被看成拓扑平面,灰度值高的区域可以被看成是山峰,灰度值低的区域可以被看成是山谷。我们向每一个山谷中灌不同...

    目标

    • 使用分水岭算法基于掩模的图像分割

    • 学习函数: cv2.watershed()

    原理

    任何一幅灰度图像都可以被看成拓扑平面,灰度值高的区域可以被看成是山峰,灰度值低的区域可以被看成是山谷。我们向每一个山谷中灌不同颜色的水,随着水的位的升高,不同山谷的水就会相遇汇合,为了防止不同山谷的水汇合,我们需要在水汇合的地方构建起堤坝。不停的灌水,不停的构建堤坝直到所有的山峰都被水淹没。我们构建好的堤坝就是对图像的分割。这就是分水岭算法的背后哲理。

    但是这种方法通常都会得到过度分割的结果,这是由噪声或者图像中其他不规律的因素造成的。为了减少这种影响, OpenCV 采用了基于掩模的分水岭算法,在这种算法中我们要设置哪些山谷点会汇合,哪些不会,这是一种交互式的图像分割。我们要做的就是给我们已知的对象打上不同的标签。如果某个

    区域肯定是前景或对象,就使用某个颜色(或灰度值)标签标记它。如果某个区域肯定不是对象而是背景就使用另外一个颜色标签标记。而剩下的不能确定是前景还是背景的区域就用 0 标记。这就是我们的标签。然后实施分水岭算法。每一次灌水,我们的标签就会被更新,当两个不同颜色的标签相遇时就构建堤

    坝,直到将所有山峰淹没,最后我们得到的边界对象(堤坝)的值为 -1。

    代码

    下面的例子中我们将就和距离变换和分水岭算法对紧挨在一起的对象进行分割。

    如下图所示,这些硬币紧挨在一起。就算你使用阈值操作,它们任然是紧挨着的。

    我们从找到这些硬币的近似估计值开始,我们使用Otsu's二值化。

    import cv2

    import numpy as np

    from matplotlib import pyplot as plt

    img = cv2.imread('image/coins.png')

    gray = cv2.cvtColor(img,cv2.COLOR_BGR2GRAY)

    ret,thresh = cv2.threshold(gray,255,cv2.THRESH_BINARY_INV+cv2.THRESH_OTSU)

    结果图:

    现在我们要去除图像中的所有的白噪声,这就需要使用形态学中的开运算。为了去除对象上小的空洞我们需要使用形态学闭运算。所以我们现在知道靠近对象中心的区域肯定是前景,而远离对象中心的区域肯定是背景。而不能确定的区域就是硬币之间的边界。

    所以我们要提取肯定是硬币的区域。腐蚀操作可以去除边缘像素。剩下就可以肯定是硬币了。当硬币之间没有接触时,这种操作是有效的。但是由于硬币之间是相互接触的,我们就有了另外一个更好的选择:距离变换再加上合适的阈值。接下来我们要找到肯定不是硬币的区域。这是就需要进行膨胀操作了。膨胀可以将对象的边界延伸到背景中去。这样由于边界区域被去处理,我们就可以知道那些区域肯定是前景,那些肯定是背景。

    剩下的区域就是我们不知道该如何区分的了。这就是分水岭算法要做的。这些区域通常是前景与背景的交界处(或者两个前景的交界)。我们称之为边界。从肯定是不是背景的区域中减去肯定是前景的区域就得到了边界区域。

    kernel = np.ones((3,3),np.uint8)

    opening = cv2.morphologyEx(thresh,cv2.MORPH_OPEN,kernel,iterations= 2)

    sure_bg = cv2.dilate(opening,iterations=3)

    dist_transform =cv2.distanceTransform(opening,1,5)

    ret,sure_fg = cv2.threshold(dist_transform,0.7*dist_transform.max(),0)

    sure_fg = np.uint8(sure_fg)

    unknown = cv2.subtract(sure_bg,sure_fg)

    如结果所示,在阈值化之后的图像中,我们得到了肯定是硬币的区域,而且硬币之间也被分割开了。(有些情况下你可能只需要对前景进行分割,而不需要将紧挨在一起的对象分开,此时就没有必要使用距离变换了,腐蚀就足够了当然腐蚀也可以用来提取肯定是前景的区域。)

    现在知道了哪些是背景哪些是硬币了,那我们就可以创建标签(一个与原图像大小相同,数据类型为 in32 的数组),并标记其中的区域了。对我们已经确定分类的区域(无论是前景还是背景)使用不同的正整数标记,对我们不确定的区域使用 0 标记。我们可以使用函数 cv2.connectedComponents()来做这件事。它会把将背景标记为 0,其他的对象使用从 1 开始的正整数标记。

    但是,我们知道如果背景标记为 0,那分水岭算法就会把它当成未知区域了。所以我们想使用不同的整数标记它们。而对不确定的区域(函数cv2.connectedComponents 输出的结果中使用 unknown 定义未知区域)标记为 0。

    ret,markers1 = cv2.connectedComponents(sure_fg)

    markers = markers1 + 1

    markers[unknown == 255] = 0

    结果使用 JET 颜色地图表示。深蓝色区域为未知区域。肯定是硬币的区域使用不同的颜色标记。其余区域就是用浅蓝色标记的背景了。现在标签准备好了。

    到最后一步:实施分水岭算法了。标签图像将会被修改,边界区域的标记将变为 -1.

    markers3 = cv2.watershed(img,markers)

    img[markers3 == -1] = [255,0]

    结果如下,有些硬币的边界被分割的很好,也有一些硬币之间的边界分割的不好。

    参考:Opencv官方教程中文版(For Python)

    以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持编程小技巧。

    展开全文
  • 分水岭(Watershed)是基于地理形态的分析的图像分割算法,模仿地理结构(比如山川、沟壑,盆地)来实现对不同物体的分类。分水岭算法中会用到一个重要的概念——测地线距离。测地线距离(Geodesic Distance)测地线...

    分水岭(Watershed)是基于地理形态的分析的图像分割算法,模仿地理结构(比如山川、沟壑,盆地)来实现对不同物体的分类。

    分水岭算法中会用到一个重要的概念——测地线距离。

    测地线距离(Geodesic Distance)

    测地线距离就是地球表面两点之间的最短路径(可执行路径)的距离,在图论中,Geodesic Distance 就是图中两节点的最短路径的距离,这与平时在几何空间通常用到的 Euclidean Distance(欧氏距离),即两点之间的最短距离有所区别。

    在下图中,两个黑点的 Euclidean Distance 是用虚线所表示的线段的长度

    ,而 Geodesic Distance 作为实际路径的最短距离,其距离应为沿途实线段距离之和的最小值,即

    5243e49c3eac21e64974226f8ba8e943.png
    图1

    在三维曲面空间中两点间的测地距离就是两点间沿着三维曲面的表面走的最短路径。

    分水岭算法

    图像的灰度空间很像地球表面的整个地理结构,每个像素的灰度值代表高度。其中的灰度值较大的像素连成的线可以看做山脊,也就是分水岭。其中的水就是用于二值化的gray threshold level,二值化阈值可以理解为水平面,比水平面低的区域会被淹没,刚开始用水填充每个孤立的山谷(局部最小值)。

    当水平面上升到一定高度时,水就会溢出当前山谷,可以通过在分水岭上修大坝,从而避免两个山谷的水汇集,这样图像就被分成2个像素集,一个是被水淹没的山谷像素集,一个是分水岭线像素集。最终这些大坝形成的线就对整个图像进行了分区,实现对图像的分割。

    c18182669d74a1f8708bb100c8d9b59d.png
    图2

    在该算法中,空间上相邻并且灰度值相近的像素被划分为一个区域。

    分水岭算法的整个过程:

    1. 把梯度图像中的所有像素按照灰度值进行分类,并设定一个测地距离阈值。
    2. 找到灰度值最小的像素点(默认标记为灰度值最低点),让threshold从最小值开始增长,这些点为起始点。
    3. 水平面在增长的过程中,会碰到周围的邻域像素,测量这些像素到起始点(灰度值最低点)的测地距离,如果小于设定阈值,则将这些像素淹没,否则在这些像素上设置大坝,这样就对这些邻域像素进行了分类。

    fb5dd94e868d27b5c1c265ee04df96dd.png
    图3

    4. 随着水平面越来越高,会设置更多更高的大坝,直到灰度值的最大值,所有区域都在分水岭线上相遇,这些大坝就对整个图像像素的进行了分区。

    整个过程可以查看下面这个动图:

    02a829b00c8ab462d6cda59564d72419.png
    图4

    用上面的算法对图像进行分水岭运算,由于噪声点或其它因素的干扰,可能会得到密密麻麻的小区域,即图像被分得太细(over-segmented,过度分割),这因为图像中有非常多的局部极小值点,每个点都会自成一个小区域。

    其中的解决方法:

    1. 对图像进行高斯平滑操作,抹除很多小的最小值,这些小分区就会合并。
    2. 不从最小值开始增长,可以将相对较高的灰度值像素作为起始点(需要用户手动标记),从标记处开始进行淹没,则很多小区域都会被合并为一个区域,这被称为基于图像标记(mark)的分水岭算法

    下面三个图分别是原图,分水岭过分割的图以及基于标记的分水岭算法得到的图:

    a9a4ea1a3a9961193e658064c62638ca.png
    图5

    其中标记的每个点就相当于分水岭中的注水点,从这些点开始注水使得水平面上升,但是如上图所示,图像中需要分割的区域太多了,手动标记太麻烦,我们可是使用距离转换的方法进行标记,OpenCV中就是使用的这种方法。

    OpenCV中分水岭算法

    在OpenCV中,我们需要给不同区域贴上不同的标签。用大于1的整数表示我们确定为前景或对象的区域,用1表示我们确定为背景或非对象的区域,最后用0表示我们无法确定的区域。然后应用分水岭算法,我们的标记图像将被更新,更新后的标记图像的边界像素值为-1。

    下面对相互接触的硬币应用距离变换和分水岭分割。

    9369e657ce25de12bc739af722ca6af5.png
    图6

    先使用 Otsu's 二值化对图像进行二值化。

    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_INV+cv2.THRESH_OTSU)

    e17a2841bba7b49373d6857de2fe337a.png
    图7

    先使用开运算去除图像中的细小白色噪点,然后通过腐蚀运算移除边界像素,得到的图像中的白色区域肯定是真实前景,即靠近硬币中心的区域(下面左边的图);膨胀运算使得一部分背景成为了物体到的边界,得到的图像中的黑色区域肯定是真实背景,即远离硬币的区域(下面中间的图)。

    剩下的区域(硬币的边界附近)还不能确定是前景还是背景。可通过膨胀图减去腐蚀图得到,下图中的白色部分为不确定区域(下面右边的图)。

    # noise removal
    kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (3,3))
    opening = cv2.morphologyEx(thresh, cv2.MORPH_OPEN, kernel, iterations=2)
    
    sure_bg = cv2.dilate(opening, kernel, iterations=2)  # sure background area
    sure_fg = cv2.erode(opening, kernel, iterations=2)  # sure foreground area
    unknown = cv2.subtract(sure_bg, sure_fg)  # unknown area

    99023cfbc8d6639919d9cf029ce4f6cb.png
    图8

    剩下的区域不确定是硬币还是背景,这些区域通常在前景和背景接触的区域(或者两个不同硬币接触的区域),我们称之为边界。通过分水岭算法应该能找到确定的边界。

    由于硬币之间彼此接触,我们使用另一个确定前景的方法,就是带阈值的距离变换

    下面左边的图为得到的距离转换图像,其中每个像素的值为其到最近的背景像素(灰度值为0)的距离,可以看到硬币的中心像素值最大(中心离背景像素最远)。对其进行二值处理就得到了分离的前景图(下面中间的图),白色区域肯定是硬币区域,而且还相互分离,下面右边的图为之前的膨胀图减去中间这个表示前景的图。

    # Perform the distance transform algorithm
    dist_transform = cv2.distanceTransform(opening, cv2.DIST_L2, 5)
    # Normalize the distance image for range = {0.0, 1.0}
    cv2.normalize(dist_transform, dist_transform, 0, 1.0, cv2.NORM_MINMAX)
    
    # Finding sure foreground area
    ret, sure_fg = cv2.threshold(dist_transform, 0.5*dist_transform.max(), 255, 0)
    
    # Finding unknown region
    sure_fg = np.uint8(sure_fg)
    unknown = cv2.subtract(sure_bg,sure_fg)

    827faf8c91e1becada5d9c0579a9a6be.png
    图9

    现在我们可以确定哪些是硬币区域,哪些是背景区域。然后创建标记(marker,它是一个与原始图像大小相同的矩阵,int32数据类型),表示其中的每个区域。分水岭算法将标记的0的区域视为不确定区域,将标记为1的区域视为背景区域,将标记大于1的正整数表示我们想得到的前景。

    我们可以使用 cv2.connectedComponents() 来实现这个功能,它是用0标记图像的背景,用大于0的整数标记其他对象。所以我们需要对其进行加一,用1来标记图像的背景。

    cv2.connectedComponents() 将传入图像中的白色区域视为组件(前景)。

    # Marker labelling
    ret, markers = cv2.connectedComponents(sure_fg)
    # Add one to all labels so that sure background is not 0, but 1
    markers = markers+1
    # Now, mark the region of unknown with zero
    markers[unknown==255] = 0

    注意:得到的markers矩阵的元素类型为 int32,要使用 imshow() 进行显示,需要将其转换为 uint8 类型( markers=np.uint8(markers) )。

    我们对得到的markers进行显示:

    markers_copy = markers.copy()
    markers_copy[markers==0] = 150  # 灰色表示背景
    markers_copy[markers==1] = 0    # 黑色表示背景
    markers_copy[markers>1] = 255   # 白色表示前景
    
    markers_copy = np.uint8(markers_copy)

    4c0ed83f6ec1bfe3344cb2c306275c39.png
    图10

    标记图像已经完成了,最后应用分水岭算法。然后标记图像将被修改,边界区域将被标记为-1。

    # 使用分水岭算法执行基于标记的图像分割,将图像中的对象与背景分离
    markers = cv2.watershed(img, markers)
    img[markers==-1] = [0,0,255]  # 将边界标记为红色

    经过分水岭算法得到的新的标记图像和分割后的图像如下图所示:

    5d34333d9826d1cb28d22b4eae1d43d4.png
    图11

    任何两个相邻连接的组件不一定被分水岭边界(-1的像素)分开;例如在传递给 watershed 函数的初始标记图像中的物体相互接触。

    总结

    我们通过一个例子介绍了分水岭算法的整个过程,主要分为以下几步:

    1. 对图进行灰度化和二值化得到二值图像
    2. 通过膨胀得到确定的背景区域,通过距离转换得到确定的前景区域,剩余部分为不确定区域
    3. 对确定的前景图像进行连接组件处理,得到标记图像
    4. 根据标记图像对原图像应用分水岭算法,更新标记图像

    参考:

    OpenCV Watershed Algorithm

    IMAGE SEGMENTATION AND MATHEMATICAL MORPHOLOGY

    Classic Watershed

    基于标记的分水岭分割算法/OpenCV中距离变换

    剪刀手/分水岭分割法

    The Watershed Transform

    对扑克牌应用分水岭的例子

    展开全文
  • 本期聊一下用于图像分割分水岭算法,其核心思想就是把我们需要用于分隔的图像想象成一个由山峰-分水岭-山谷三部分组成的实况图,图像的前景,即我们需要分割出来的图像部分;图像的背景,即我们图像的背景部分,...

    之前说好的,以后主要精力都会放到图像处理方面,如果有这一块的大佬还请多多指教……

    本期聊一下用于图像分割的分水岭算法,其核心思想就是把我们需要用于分隔的图像想象成一个由山峰-分水岭-山谷三部分组成的实况图,图像的前景,即我们需要分割出来的图像部分;图像的背景,即我们图像的背景部分,我们将前景部分看作山峰,背景部分看作山谷,二者的交界处,即分水岭,看作未知区域。如果我们往山谷里灌水的话,等水蔓延到分水岭处便会被截断,此时山谷被淹没而山峰则不受影响,即背景部分被淹没而前景部分被保留下来,也可以理解成把前景部分像抠图一样给分割出来。

    代码是用的官方的,见地址:https://docs.opencv.org/3.1.0/d3/db4/tutorial_py_watershed.html

    对每一行代码都做了注释,力求精致,如果大家对其中某个函数不理解可以去搜一下,基本上都能找到的,直接上全部代码吧,就不一块块地分解了:

    # # -*- coding:utf-8 -*-
    import numpy as np
    import cv2
    img = cv2.imread('timg1.jpg')  #读取图片
    gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) #将彩色图片转成灰度图
    ret, thresh = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY_INV + cv2.THRESH_OTSU)  #固定阈值二值化
    
    # 去除噪声
    kernel = np.ones((3,3),np.uint8) #创建3×3大小的数组,数值均为1
    opening = cv2.morphologyEx(thresh,cv2.MORPH_OPEN,kernel, iterations = 2) #开运算,即先腐蚀后膨胀
    # 确定背景区域
    sure_bg = cv2.dilate(opening,kernel,iterations=3) #膨胀运算,取局部最大值,效果是把图片"变胖"
    # 确定前景区域,在本例中为象棋区域
    dist_transform = cv2.distanceTransform(opening,cv2.DIST_L2,5) #用于计算图像中每一个非零点到自己最近零点的距离
    ret, sure_fg = cv2.threshold(dist_transform,0.7*dist_transform.max(),255,0)  #二值化
    # 确定未知区域
    sure_fg = np.uint8(sure_fg)   #转换类型
    unknown = cv2.subtract(sure_bg,sure_fg)  #图像矩阵相减,即背景区域减去前景区域得到未知区域
    
    ret, markers = cv2.connectedComponents(sure_fg)  #创建一个标记图,和原图大小一致
    markers = markers + 1 # 将背景标记为1(connectedComponents函数返回的markers为0,即将背景区域标记为0,我们改变其为1)
    markers[unknown == 255] = 0  # 标记未知区域为0
    
    markers = cv2.watershed(img,markers)  #调用分水岭算法
    img[markers == -1] = [255,0,0]  #将边界区域标记为-1
    cv2.namedWindow('first',cv2.WINDOW_AUTOSIZE)  #cv2.namedWindow('窗口标题',默认参数)
    cv2.imshow('second',unknown)
    cv2.imshow('first',img)
    cv2.waitKey(0)
    cv2.destroyAllWindows()

    代码量不是很大,现对其中几个函数做进一步的说明:

    1.

    ret, dst = cv2.threshold(src, thresh, maxval,type)

    src: 输入图,只能输入单通道图像,通常来说为灰度图

    dst: 输出图

    ret:输出的阈值

    thresh: 阈值

    maxval: 当像素值超过了阈值(或者小于阈值,根据type来决定),所赋予的值

    type:二值化操作的类型,包含以下5种类型: cv2.THRESH_BINARY; cv2.THRESH_BINARY_INV; cv2.THRESH_TRUNC; cv2.THRESH_TOZERO;cv2.THRESH_TOZERO_INV

     

    2.

    dist_transform=cv2.distanceTransform(src, distanceType, maskSize[, dst])
    
    src为输入的二值图像。distanceType为计算距离的方式,可以是如下值
    DIST_USER    = ⑴,  //!< User defined distance
    DIST_L1      = 1,   //!< distance = |x1-x2| + |y1-y2|
    DIST_L2      = 2,   //!< the simple euclidean distance
    DIST_C       = 3,   //!< distance = max(|x1-x2|,|y1-y2|)
    DIST_L12     = 4,   //!< L1-L2 metric: distance = 2(sqrt(1+x*x/2) - 1))
    DIST_FAIR    = 5,   //!< distance = c^2(|x|/c-log(1+|x|/c)), c = 1.3998
    DIST_WELSCH  = 6,   //!< distance = c^2/2(1-exp(-(x/c)^2)), c = 2.9846
    DIST_HUBER   = 7    //!< distance = |x|<c ? x^2/2 : c(|x|-c/2), c=1.345
    
    maskSize是蒙板尺寸,只有0,3,5
    DIST_MASK_3       = 3, //!< mask=3
    DIST_MASK_5       = 5, //!< mask=5
    DIST_MASK_PRECISE = 0  //!< mask=0
    我们用围棋来测试一下:

    大家发现没有,只能分割出黑子。。这是巧合嘛:我们再用一张更复杂的围棋来测试一下

     

    依然是只能分割出黑子。。。最后用官方图片收尾:

     

    展开全文
  • 图像分割分水岭算法

    热门讨论 2011-05-08 20:13:22
    图像分割分水岭算法,能正确分割出所需目标,能成功运行,matlab的完整代码
  • 分水岭算法是现在医学图像分割的经典算法,具有非常高的参考价值
  • 图像分割分水岭算法

    万次阅读 多人点赞 2019-04-17 15:18:59
    使用C++、opencv进行分水岭分割图像 分水岭概念是以对图像进行三维可视化处理为基础的:其中两个是坐标,另一个是灰度级。基于“地形学”的这种解释,我们考虑三类点: a.属于局部性最小值的点,也可能存在一个...

    使用C++、opencv进行分水岭分割图像

    分水岭概念是以对图像进行三维可视化处理为基础的:其中两个是坐标,另一个是灰度级。基于“地形学”的这种解释,我们考虑三类点:

    a.属于局部性最小值的点,也可能存在一个最小值面,该平面内的都是最小值点

    b.当一滴水放在某点的位置上的时候,水一定会下落到一个单一的最小值点

    c.当水处在某个点的位置上时,水会等概率地流向不止一个这样的最小值点

    对一个特定的区域最小值,满足条件(b)的点的集合称为这个最小值的“汇水盆地”或“分水岭”。满足条件(c)的点的集合组成地形表面的峰线,称做“分割线”或“分水线”。 

    分水岭分割方法,是一种基于拓扑理论的数学形态学的分割方法,目前较著名且使用较多的有2种算法:

    (1) 自下而上的模拟泛洪的算法 (2) 自上而下的模拟降水的算法 

    这里介绍泛洪算法的过程。

    算法主要思想:

    我们把图像看作是测地学上的拓扑地貌,图像中每一点像素的灰度值表示该点的海拔高度,模拟泛洪算法的基本思想是:假设在每个区域最小值的位置上打一个洞并且让水以均匀的上升速率从洞中涌出,从低到高淹没整个地形。当处在不同的汇聚盆地中的水将要聚合在一起时,修建的大坝将阻止聚合。水将达到在水线上只能见到各个水坝的顶部这样一个程度。这些大坝的边界对应于分水岭的分割线。所以,它们是由分水岭算法提取出来的(连续的)边界线。 

     原图像:                                                          地形俯视图:

      

    原图像显示了一个简单的灰度级图像,其中“山峰”的高度与输入图像的灰度级值成比例。为了阻止上升的水从这些结构的边缘溢出,我们想像将整幅地形图的周围用比最高山峰还高的大坝包围起来。最高山峰的值是由输入图像灰度级具有的最大值决定的。

     

        

    图一被水淹没的第一个阶段,这里水用浅灰色表示,覆盖了对应于图中深色背景的区域。在图二和三中,我们看到水分别在第一和第二汇水盆地中上升。由于水持续上升,最终水将从一个汇水盆地中溢出到另一个之中。

     

                                            

    左图中显示了溢出的第一个征兆。这里,水确实从左边的盆地溢出到右边的盆地,并且两者之间有一个短“坝”(由单像素构成)阻止这一水位的水聚合在一起。随着水位不断上升,如右图所显示的那样。这幅图中在两个汇水盆地之间显示了一条更长的坝,另一条水坝在右上角。这条水坝阻止了盆地中的水和对应于背景的水的聚合。

    这个过程不断延续直到到达水位的最大值(对应于图像中灰度级的最大值)。水坝最后剩下的部分对应于分水线,这条线就是要得到的分割结果。

    对于这个例子,分水线在图中显示为叠加到原图上的一个像素宽的深色路径。注意一条重要的性质就是分水线组成一条连通的路径,由此给出了区域之间的连续的边界。 

    动图演示了整个分水岭算法的过程:

    算法实现:

     算法应用:

    分水岭算法对噪声等影响非常敏感。所以在真实图像中,由于噪声点或者其它干扰因素的存在,使用分水岭算法常常存在过度分割的现象,这是因为很多很小的局部极值点的存在,比如下面的图像,这样的分割效果是毫无用处的。

                         

    为了解决过度分割的问题,可以使用基于标记(mark)图像的分水岭算法,就是通过先验知识,来指导分水岭算法,以便获得更好的图像分段效果。通常的mark图像,都是在某个区域定义了一些灰度层级,在这个区域的洪水淹没过程中,水平面都是从定义的高度开始的,这样可以避免一些很小的噪声极值区域的分割。下面的动图很好的演示了基于mark的分水岭算法过程:

    上面的过度分割图像,我们通过指定mark区域,可以得到很好的分段效果:

           

    以上参考:冈萨雷斯《数字图象处理(第三版)》和https://www.cnblogs.com/mikewolf2002/p/3304118.html


    相关API:

     void setMousecallback(const string& winname, MouseCallback onMouse, void* userdata=0)   

    winname:窗口的名字
    onMouse:鼠标响应函数,回调函数。指定窗口里每次鼠标时间发生的时候,被调用的函数指针。 这个函数的原型应该为void on_Mouse(int event, int x, int y, int flags, void* param);
    userdate:传给回调函数的参数  

     

    void on_Mouse(int event, int x, int y, int flags, void* param)

    event: CV_EVENT_*变量之一
    x和y:鼠标指针在图像坐标系的坐标(不是窗口坐标系) 
    flags:CV_EVENT_FLAG的组合, param是用户定义的传递到setMouseCallback函数调用的参数。

    附常用的event:CV_EVENT_MOUSEMOVE、CV_EVENT_LBUTTONDOWN 、CV_EVENT_RBUTTONDOWN、   CV_EVENT_LBUTTONUP  、  CV_EVENT_RBUTTONUP   

    和标志位flags有关的:CV_EVENT_FLAG_LBUTTON 
     

    C++: void watershed(InputArray image,InputoutputArray markers)

    第一个参数,InputArray类型的src,输入图像,即源图像,填Mat类的对象即可,且需为8位三通道的彩色图像。

    第二个参数,InputOutput Array类型的markers,函数调用后的运算结果存在这里,输入/输出32位单通道图像的标记结果。即这个参数用于存放函数调后的输出结果,需和源图片有一样的尺寸和类型。


    代码实现:

    #include "stdafx.h"
    #include "opencv2/imgproc/imgproc.hpp"  
    #include "opencv2/highgui/highgui.hpp"  
    #include <iostream>  
    #include <fstream>  
    
    using namespace cv;
    using namespace std;
    
    #define WINDOW_NAME1 "【程序窗口1】"        //为窗口标题定义的宏   
    #define WINDOW_NAME2 "【分水岭算法效果图】"        //为窗口标题定义的宏  
    
    //描述:全局变量的声明  
    Mat g_maskImage, g_srcImage;
    Point prevPt(-1, -1);
    //描述:全局函数的声明  
    static void ShowHelpText();
    static void on_Mouse(int event, int x, int y, int flags, void*);
    
    int main()
    {
    	//【0】改变console字体颜色  
    	system("color 02");
    
    	//【1】载入原图并显示,初始化掩膜和灰度图
    	g_srcImage = imread("D:\\pic-sam\\哀.JPG", 1);
    	namedWindow(WINDOW_NAME1, WINDOW_NORMAL);
    	imshow(WINDOW_NAME1, g_srcImage);
    	Mat srcImage, grayImage;
    	g_srcImage.copyTo(srcImage);
    	cvtColor(g_srcImage, g_maskImage, COLOR_BGR2GRAY);
    	cvtColor(g_maskImage, grayImage, COLOR_GRAY2BGR);
    	g_maskImage = Scalar::all(0);
    	//【2】设置鼠标回调函数
    	setMouseCallback(WINDOW_NAME1, on_Mouse, 0);
    
    	//【3】轮询按键,进行处理
    	while (1)
    	{
    		//获取键值
    		int c = waitKey(0);
    
    		//若按键键值为ESC时,退出
    		if ((char)c == 27)
    			break;
    
    		//按键键值为2时,恢复源图
    		if ((char)c == '2')
    		{
    			g_maskImage = Scalar::all(0);
    			srcImage.copyTo(g_srcImage);
    			imshow("image", g_srcImage);
    		}
    
    		//若检测到按键值为1或者空格,则进行处理
    		if ((char)c == '1' || (char)c == ' ')
    		{
    			//定义一些参数
    			int i, j, compCount = 0;
    			vector<vector<Point> > contours;
    			vector<Vec4i> hierarchy;
    
    			//寻找轮廓
    			findContours(g_maskImage, contours, hierarchy, RETR_CCOMP, CHAIN_APPROX_SIMPLE);
    
    			//轮廓为空时的处理
    			if (contours.empty())
    				continue;
    
    			//拷贝掩膜
    			Mat maskImage(g_maskImage.size(), CV_32S);
    			maskImage = Scalar::all(0);
    
    			//循环绘制出轮廓
    			for (int index = 0; index >= 0; index = hierarchy[index][0], compCount++)
    				drawContours(maskImage, contours, index, Scalar::all(compCount + 1), -1, 8, hierarchy, INT_MAX);
    
    			//compCount为零时的处理
    			if (compCount == 0)
    				continue;
    
    			//生成随机颜色
    			/*vector<Vec3b> colorTab;
    			for (i = 0; i < compCount; i++)
    			{
    				int b = theRNG().uniform(0, 255);
    				int g = theRNG().uniform(0, 255);
    				int r = theRNG().uniform(0, 255);
    
    				colorTab.push_back(Vec3b((uchar)b, (uchar)g, (uchar)r));
    			}*/
    
    			//计算处理时间并输出到窗口中
    			double dTime = (double)getTickCount();
    			watershed(srcImage, maskImage);
    			dTime = (double)getTickCount() - dTime;
    			printf("\t处理时间 = %gms\n", dTime*1000. / getTickFrequency());
    
    			//双层循环,将分水岭图像遍历存入watershedImage中
    			Mat watershedImage(maskImage.size(), CV_8UC3);
    			int index1 = 0;
    			for (i = 0; i < maskImage.rows; i++)
    				for (j = 0; j < maskImage.cols; j++)
    				{
    					if(maskImage.at<int>(i, j)>index1)
    					index1 = maskImage.at<int>(i, j);
    				}
    			for (i = 0; i < maskImage.rows; i++)
    				for (j = 0; j < maskImage.cols; j++)
    				{
    					int index = maskImage.at<int>(i, j);
    					//对watershed函数生成的index的规律不是很清楚,经测试,并不是按照标记顺序给出index的
    					//具体每一块的index是怎么给出的还需要研究源码
    					if (index == -1)
    						watershedImage.at<Vec3b>(i, j) = Vec3b(255, 255, 255);
    					else if (index <= 0 || index > compCount)
    						watershedImage.at<Vec3b>(i, j) = Vec3b(0, 0, 0);
    					else if (index ==index1)
    						watershedImage.at<Vec3b>(i, j) = Vec3b(255, 255, 255);
    					else
    						watershedImage.at<Vec3b>(i, j) = Vec3b(index*10, 0, 0);//这里想给不同的物体标记为不同程度的颜色
    																				//方便后面去除背景,显示目标物体
    				}
    
    			//混合灰度图和分水岭效果图并显示最终的窗口
    			//watershedImage = watershedImage*0.5 + grayImage*0.5;
    			imshow(WINDOW_NAME2, watershedImage);//直接显示分水岭的效果图
    			//这里想直接根据index,将背景显示为黑色,需要分割出来的目标物体直接显示
    			//但对index生成的规律还未搞清楚,结果可能不是很稳定
    			Mat src = imread("D:\\pic-sam\\哀.JPG", 1);
    			for (int i = 0; i < src.rows; i++)
    				for (int j = 0; j < src.cols; j++)
    				{
    					int a = abs(watershedImage.at<Vec3b>(i, j)[0] - 250) / 150;
    					src.at<Vec3b>(i, j)[0] *= a;
    					src.at<Vec3b>(i, j)[1] *= a;
    					src.at<Vec3b>(i, j)[2] *= a;
    				}
    			namedWindow("dst", WINDOW_NORMAL);
    			imshow("dst", src);
    		}
    	}	
    	return 0;
    }
    
    //鼠标消息回调函数  
    static void on_Mouse(int event, int x, int y, int flags, void*)
    {
    	//处理鼠标不在窗口中的情况  
    	if (x < 0 || x >= g_srcImage.cols || y < 0 || y >= g_srcImage.rows)
    		return;
    
    	//处理鼠标左键相关消息  
    	if (event == CV_EVENT_LBUTTONUP || !(flags & CV_EVENT_FLAG_LBUTTON))
    		prevPt = Point(-1, -1);
    	else if (event == CV_EVENT_LBUTTONDOWN)
    		prevPt = Point(x, y);
    
    	//鼠标左键按下并移动,绘制出线条  
    	else if (event == CV_EVENT_MOUSEMOVE && (flags & CV_EVENT_FLAG_LBUTTON))
    	{
    		Point pt(x, y);
    		if (prevPt.x < 0)
    			prevPt = pt;
    		line(g_maskImage, prevPt, pt, Scalar::all(255), 4, 8, 0);
    		line(g_srcImage, prevPt, pt, Scalar::all(255), 4, 8, 0);
    		prevPt = pt;
    		imshow(WINDOW_NAME1, g_srcImage);
    	}
    }
    
    //      描述:输出一些帮助信息    
    static void ShowHelpText()
    {
    	printf("\n\n\t\t\t   当前使用的OpenCV版本为:" CV_VERSION);
    	printf("\n\n  ----------------------------------------------------------------------------\n");
    	//输出一些帮助信息    
    	printf("\n\n\n\t欢迎来到【分水岭算法】示例程序~\n\n");
    	printf("\t请先用鼠标在图片窗口中标记出大致的区域,\n\n\t然后再按键【1】或者【SPACE】启动算法。"
    		"\n\n\t按键操作说明: \n\n"
    		"\t\t键盘按键【1】或者【SPACE】- 运行的分水岭分割算法\n"
    		"\t\t键盘按键【2】- 恢复原始图片\n"
    		"\t\t键盘按键【ESC】- 退出程序\n\n\n");
    }

    源图像:

    进行标记的图像:

    分水岭算法得到的图像:

    分割后图像:

    代码的第108-122行是对opencv分水岭算法生成的结果图进行分析,目前对watershed函数生成的index的规律不是很清楚,经测试,并不是按照标记顺序给出index的,具体每一块的index是怎么给出的还需要研究源码

    代码第130-138行,目的是想直接根据分水岭算法生成的图像中的index,将背景显示为黑色,需要分割出来的目标物体直接显示,但对index生成的规律还未搞清楚,结果可能不是很稳定

    以上部分参考: 毛星云 《OpenCV3编程入门》

    -----------------------------------------------------

    2019年4月19日增加:

    查阅到opencv分水岭算法中,在“循环绘制出轮廓”时用到一个参数compCount,这个参数并不是记录轮廓数目的,它的作用是把每个轮廓设为同一像素值,而maskImage中的像素值就是用1-compcount 的像素值标注的,这样问题又转化为不清楚在查找轮廓时,算法是按照什么样的顺序找出轮廓放入vector中的。

     

    展开全文
  • 参考网址:https://blog.csdn.net/xihuaxi/article/details/72639149 ... 分水岭图像分割算法借助地形学概念进行图像分割,近年来广泛使用。 1. 基本原理和步骤 1)原理 分水岭方法将图像看作3-D的地形...
  • 今天在做灰度图像分割, 用了Normalized,clahe、gamma 、腐蚀/膨胀、开/闭运算、高斯滤波、中值滤波、双边滤波、阈值滤波、顶帽变换、底帽变换、图像增强、...又看了分水岭算法、GrabCut 算法的几篇文章: http...
  • 分水岭算法-图像分割 1.原理 有了上面对图像灰度三维模型的直观感受,会更好理解分水岭算法的思想。 在分水岭算法中,一幅图像中灰度值高的区域被看作山峰,灰度值低的区域被看作山谷。 然后从山谷的最低点灌水,水...
  • 分水岭算法是比较经典的图像分割算法。最近看到一副区域检测和统计的图像,感觉可以通过分水岭算法进行实现,于是顺便对opencv的分水岭算法进行学习。由于示例是python的代码,没有C++的代码,所以打算先用C++实现...
  • OpenCV—图像分割中的分水岭算法原理与应用

    万次阅读 多人点赞 2015-10-18 09:58:56
    目前有很多图像分割方法,其中分水岭算法是一种基于区域的图像分割算法,分水岭算法因实现方便,已经在医疗图像,模式识别等领域得到了广泛的应用。然而基于梯度图像的直接分水岭算法容易导致图像的过分割,本文介绍...
  • python+opencv-13 分水岭算法实现图像分割图像分割分水岭算法确定一幅图像的前景图像 图像分割 图像分割是一种基本的图像处理技术,是指将图像分成不同特性的区域,并对目标进行提取的技术,它是由图像处理到图像...
  • 分水岭算法是一种图像区域分割法,分割的过程中将图片转化为灰度图,然后我会将灰度值看作是海拔,然后向较低点注水,这种基于地形学的解释,我们着重考虑三种点: 极小值点,该点对应一个盆地的最低点,当我们在...
  • Halcon 分水岭算法图像分割
  • 分水岭图像分割算法

    2018-11-11 16:52:19
    采用分水岭分割算法进行图像分割,能比较准确的分割自然条件下的果树等图像。仍有一些不足之处,希望大家指点
  • 为了提取分水岭,人们提出了各种各样的算法,在这些算法中,Vincent和Soille提出了一种基于模拟沉浸的实现方法。在图像处理领域,灰度图像可以被视为地形表面,图像中每个像素的灰度代表这点的高度,其每一个局部极...
  • 图像处理:分水岭算法图像分割分水岭算法 分水岭算法是一种图像区域分割法,分割的过程中将图片转化为灰度图,然后我会将灰度值看作是海拔,然后向较低点注水,这种基于地形学的解释,我们着重考虑三种点: 极...
  • 编译环境:vs2005 图像分割,采用分水岭算法 (编译通过,可以直接用)
  • 目前有很多图像分割方法,其中分水岭算法是一种基于区域的图像分割算法,分水岭算法因实现方便,已经在医疗图像,模式识别等领域得到了广泛的应用。 1.传统分水岭算法基本原理 分水岭比较经典的计算方法是L.Vincent...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 994
精华内容 397
关键字:

图像分割分水岭算法