参考博客:
(1)迈克老狼2012 https://www.cnblogs.com/mikewolf2002/p/3304118.html
(2)-牧野- http://blog.csdn.net/dcrmg/article/details/52498440
分水岭算法是一种图像区域分割法,在分割的过程中,它会把跟临近像素间的相似性作为重要的参考依据,从而将在空间位置上相近并且灰度值相近(求梯度)的像素点互相连接起来构成一个封闭的轮廓。分水岭算法常用的操作步骤:彩色图像灰度化,然后再求梯度图,最后在梯度图的基础上进行分水岭算法,求得分段图像的边缘线。
下面左边的灰度图,可以描述为右边的地形图,地形的高度是由灰度图的灰度值决定,灰度为0对应地形图的地面,灰度值最大的像素对应地形图的最高点。
我们可以自己编程实现灰度图的地形图显示
对灰度图的地形学解释,我们我们考虑三类点:
1. 局部最小值点,该点对应一个盆地的最低点,当我们在盆地里滴一滴水的时候,由于重力作用,水最终会汇聚到该点。注意:可能存在一个最小值面,该平面内的都是最小值点。
2. 盆地的其它位置点,该位置滴的水滴会汇聚到局部最小点。
3. 盆地的边缘点,是该盆地和其它盆地交接点,在该点滴一滴水,会等概率的流向任何一个盆地。
假设我们在盆地的最小值点,打一个洞,然后往盆地里面注水,并阻止两个盆地的水汇集,我们会在两个盆地的水汇集的时刻,在交接的边缘线上(也即分水岭线),建一个坝,来阻止两个盆地的水汇集成一片水域。这样图像就被分成2个像素集,一个是注水盆地像素集,一个是分水岭线像素集。
在真实图像中,由于噪声点或者其它干扰因素的存在,使用分水岭算法常常存在过度分割的现象,这是因为很多很小的局部极值点的存在,比如下面的图像,这样的分割效果是毫无用处的。
为了解决过度分割的问题,可以使用基于标记(mark)图像的分水岭算法,就是通过先验知识,来指导分水岭算法,以便获得更好的图像分段效果。通常的mark图像,都是在某个区域定义了一些灰度层级,在这个区域的洪水淹没过程中,水平面都是从定义的高度开始的,这样可以避免一些很小的噪声极值区域的分割。
下面的gif图很好的演示了基于mark的分水岭算法过程:
上面的过度分段图像,我们通过指定mark区域,可以得到很好的分段效果:
watershed()
void cv::watershed ( InputArray image,
InputOutputArray markers
)
Python:
markers = cv.watershed( image, markers )
#include
使用分水岭算法进行基于标记的图像分割。
该函数实现了分水岭算法的一个变种,基于非参数标记的分割算法,
在将图像传递给函数之前,必须使用正(>)索引粗略地在图像标记中勾画出所需的区域。因此,每个区域都被表示为一个或多个具有像素值1、2、3等的连接组件。这样的标记可以使用findContours 和 drawContours从二进制掩码中检索(参见watershed.cpp演示)。这些标记是未来图像区域的“种子”。标记中的所有其他像素,其与轮廓区域的关系未知,应由算法定义,应设置为0。在函数输出中,标记中的每个像素被设置为“种子”组件的值,或者在区域之间的边界被设置为-1。
简单概括一下就是说第二个入参markers必须包含了种子点信息。Opencv官方例程中使用鼠标划线标记,其实就是在定义种子,只不过需要手动操作,而使用findContours可以自动标记种子点。而分水岭方法完成之后并不会直接生成分割后的图像,还需要进一步的显示处理,如此看来,只有两个参数的watershed其实并不简单。
Note:任意两个相邻连接的分量不一定被分水岭边界(-1的像素)隔开;例如,它们可以在传递给函数的初始标记图像中相互接触。
Parameters
image | 输入8位3通道图像。 |
markers | 输入/输出标记的32位单通道图像(map)。它应该有相同的大小作为图像。 |
现在我们看看OpenCV中如何使用分水岭算法。
首先我们打开一副图像:
// 打开另一幅图像
cv::Mat image= cv::imread("../tower.jpg");
if (!image.data)
{
cout<<"不能打开图像!"<
接下来,我们要创建mark图像。mark图像格式是有符号整数,其中没有被mark的部分用0表示,其它不同区域的mark标记,我们用非零值表示,通常为1-255,但也可以为其它值,比如大于255的值,不同mark区域甚至可以用同样的值,这个值大小对最后分割可能没有影响(也可能影响),最好不同mark区域还是用不同的值表示,这样能够确保结果正确,之所以用有符号整数,是因为opencv在分水岭算法内部,要用-1,-2等来标记注水区域,最终在mark图像中生成的分水岭线就是用-1表示。
我们通常会创建uchar格式的灰度图,指定mark区域,然后转化为有符号整数的图像格式。
首先对整个背景区域我们创建一个mark域,是下图中白色框框住的部分,其灰度值为255,第二个选择mark域为塔,就是黑色框框住的一块区域,其灰度值为64,最后就是树mark域,蓝色框的部分,其灰度值为128。在分水岭算法时候,会分别对这个3个区域来进行注水操作,如果两个注水盆地被一个mark域覆盖,则它们之间不会有分水岭线产生。
对于mark图像,opencv分水岭算法在初始化时候,会把最外圈的值置为-1,作为整个图像的边界,所以我们第一个mark区域,选择倒数第2外圈,因为设置到最外圈,最后还是会被冲掉。
注意:mark图像是32bit的有符号整数,所以在使用分水岭算法前,我们先对mark图像做一个转化。算法执行完后,再转化为0-255的灰度图。
imageMask.convertTo(imageMask,CV_32S);
// 设置marker和处理图像
cv::watershed(image,imageMask);
cv::Mat mark1;
imageMask.convertTo(mark1,CV_8U);
cv::namedWindow("marker");
cv::imshow("marker",mark1);
此时imageMask图像从无符号整数转化为uchar后,如下图所示,第一个mask区域注水,将会使得整个图像为白色,之后分别在第二个,第三个区域的盆地注水,会产生相应的注水图,注水的区域的值即为mark的值,128和64, 分水岭线则为0,注:在转化前分水岭线的值为-1,转化后成为0。
我们使用一个转化函数把分水岭线转化为黑色,其它的部分都白黑色,转化函数的公式为:
最后显示分水岭线,得到下图:(注:在转化前,分水岭线的值为-1)