OpenCV3学习(7.2)——图像分割之二(分水岭算法watershed)

分水岭算法原理

     分水岭算法是一种图像区域分割法,在分割的过程中,它会把跟临近像素间的相似性作为重要的参考依据,从而将在空间位置上相近并且灰度值相近的像素点互相连接起来构成一个封闭的轮廓,封闭性是分水岭算法的一个重要特征。其他图像分割方法,如阈值,边缘检测等都不会考虑像素在空间关系上的相似性和封闭性这一概念,彼此像素间互相独立,没有统一性。分水岭算法较其他分割方法更具有思想性,更符合人眼对图像的印象。

       分水岭算法主要用于图像分段,通常是把一副彩色图像灰度化,然后再求梯度图,最后在梯度图的基础上进行分水岭算法,求得分段图像的边缘线就是根据把图像比作一副地貌,然后通过最低点和最高点去分类!地形的高度是由灰度图的灰度值决定,灰度为0对应地形图的地面,灰度值最大的像素对应地形图的最高点。

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

1. 局部最小值点,该点对应一个盆地的最低点,当我们在盆地里滴一滴水的时候,由于重力作用,水最终会汇聚到该点。注意:可能存在一个最小值面,该平面内的都是最小值点。

2. 盆地的其它位置点,该位置滴的水滴会汇聚到局部最小点。

3. 盆地的边缘点,是该盆地和其它盆地交接点,在该点滴一滴水,会等概率的流向任何一个盆地。

                           OpenCV3学习(7.2)——图像分割之二(分水岭算法watershed)_第1张图片

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

原始的分水岭步骤:

 第一步:找到图像的局部最低点,

    第二步:从最低点开始注水,水开始上漫(图像的说法就是梯度法),其中那些最低点已经被标记,不会被淹没,那些中间点是被淹没的。

 第三步:找到局部最高点,就是图中3位置对应的两个点。

 第四步:这样基于局部最小值,和找到的局部最大值,就可以分割图像了。

                                             OpenCV3学习(7.2)——图像分割之二(分水岭算法watershed)_第2张图片

 

opencv改进的分水岭算法:

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

                               OpenCV3学习(7.2)——图像分割之二(分水岭算法watershed)_第3张图片

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

                              OpenCV3学习(7.2)——图像分割之二(分水岭算法watershed)_第4张图片

分水岭算法opencv实现

函数原型:

void watershed( InputArray image, InputOutputArray markers );

 第一个参数 image,必须是一个8bit 3通道彩色图像矩阵序列

第二个参数 markers,在执行分水岭函数watershed之前,必须对第二个参数markers进行处理,它应该包含不同区域的轮廓,每个轮廓有一个自己唯一的编号,轮廓的定位可以通过Opencv中findContours方法实现,这个是执行分水岭之前的要求。markers必须包含了种子点信息。markets创建主要有两种方式:手动选取和findContours。Opencv官方例程中使用鼠标划线标记来定义种子,而使用findContours可以自动标记种子点。分水岭方法完成之后并不会直接生成分割后的图像,还需要进一步的显示处理。

      算法会根据markers传入的轮廓作为种子(也就是所谓的注水点),对图像上其他的像素点根据分水岭算法规则进行判断,并对每个像素点的区域归属进行划定,直到处理完图像上所有像素点。而区域与区域之间的分界处的值被置为“-1”,以做区分。

   (初始阶段:把每个标记的所有邻居像素放到有序队列中去,以确定聚水盆的初始边界, 即每个标记(种子,全为正值,1,2,3...)都是一个初始聚水盆,标记的周围一圈的邻居像素就是聚水盆的初始边界,这里用的是一种逆向思维,不是找标记点,而是判断每一个点是否为标记点的邻居,若是,则该点也被扩充为与标记点同类型的标记点, 若是多个标记点的邻居,选择梯度最小的标记点的类型,作为该点的标记点类型。)

手动选取创建mark图像

        要创建mark图像。mark图像格式是有符号整数,其中没有被mark的部分用0表示,其它不同区域的mark标记,我们用非零值表示,通常为1-255,不同mark区域用不同的值表示,这样能够确保结果正确,之所以用有符号整数,是因为opencv在分水岭算法内部,要用-1,-2等来标记注水区域,最终在mark图像中生成的分水岭线就是用-1表示

     我们通常会创建uchar格式的灰度图,指定mark区域,然后转化为有符号整数的图像格式。

     首先对整个背景区域我们创建一个mark域,是下图中白色框框住的部分,其灰度值为255,第二个选择mark域为塔,就是黑色框框住的一块区域,其灰度值为64,最后就是树mark域,蓝色框的部分,其灰度值为128。在分水岭算法时候,会分别对这个3个区域来进行注水操作,如果两个注水盆地被一个mark域覆盖,则它们之间不会有分水岭线产生。

    对于mark图像,opencv分水岭算法在初始化时候,会把最外圈的值置为-1,作为整个图像的边界,所以我们第一个mark区域,选择倒数第2外圈,因为设置到最外圈,最后还是会被冲掉。

// 标示背景图像 
cv::Mat imageMask(image.size(),CV_8U,cv::Scalar(0));
cv::rectangle(imageMask,cv::Point(1,1),cv::Point(image.cols-2,image.rows-2),cv::Scalar(255),1); 
// 表示塔 
cv::rectangle(imageMask,cv::Point(image.cols/2-10,image.rows/2-10), 
    cv::Point(image.cols/2+10,image.rows/2+10),cv::Scalar(64),10); 
//树 
cv::rectangle(imageMask,cv::Point(64,284), 
    cv::Point(68,300),cv::Scalar(128),5); 

初始的mark图像数据如下,黄色的部分为我们的第一个mark区域,值为255,第二个区域为褐红色的区域,值为128,第三个绿色的区域,值为64。 

 OpenCV3学习(7.2)——图像分割之二(分水岭算法watershed)_第5张图片 OpenCV3学习(7.2)——图像分割之二(分水岭算法watershed)_第6张图片

注意:mark图像是32bit的有符号整数,所以在使用分水岭算法前,我们先对mark图像做一个转化。算法执行完后,再转化为0-255的灰度图

 imageMask.convertTo(imageMask,CV_32S); 
  // 设置marker和处理图像  
  cv::watershed(image,imageMask);
  cv::Mat mark1; 
  imageMask.convertTo(mark1,CV_8U); 

     此时imageMask图像从无符号整数转化为uchar后,如下图所示,第一个mask区域注水,将会使得整个图像为白色,之后分别在第二个,第三个区域的盆地注水,会产生相应的注水图,注水的区域的值即为mark的值,128和64, 分水岭线则为0,注:在转化前分水岭线的值为-1,转化后成为0。(注水区域的颜色就是包围该区域的轮廓的颜色)

                                   OpenCV3学习(7.2)——图像分割之二(分水岭算法watershed)_第7张图片

opencv中分水岭算法的具体实现方式可参考:https://yq.aliyun.com/articles/325556

findContours(opencv官方教程中使用鼠标选中区域,也是采用find Contours找到标记轮廓,对于标记的原则:你认为它们属于一个区域,就用标记将它们连接起来,对于另一个区域,再用另一个标记连接。就像这样

                            OpenCV3学习(7.2)——图像分割之二(分水岭算法watershed)_第8张图片

图片做了三个标记。之后就是对标记掩膜(不是上面的做过标记的图像)进行findContours,进行后续操作。

maskImage = Mat(srcImage.size(), CV_8UC1); // 掩模,在上面做标记,然后传给findContours,srcImage.表示原图

line(maskImage, clickPoint, point, Scalar::all(255), 5, 8, 0);//做标记,白线

对maskImage进行findContours可参考:https://blog.csdn.net/sugarannie/article/details/53080168


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

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

第3步,watershed分水岭算法的具体实现方式:

    原始图像和Mark图像,它们的大小都是32*32,分水岭算法的结果是得到两个连通域的轮廓图。

原始图像:(原始图像必须是3通道图像)      Mark图像:                                           结果图像:

OpenCV3学习(7.2)——图像分割之二(分水岭算法watershed)_第9张图片             OpenCV3学习(7.2)——图像分割之二(分水岭算法watershed)_第10张图片       OpenCV3学习(7.2)——图像分割之二(分水岭算法watershed)_第11张图片

      初始的mark图像数据如下,黄色的部分为我们的第一个mark区域,值为255,第二个区域为褐红色的区域,值为128,第三个绿色的区域,值为64。

      OpenCV3学习(7.2)——图像分割之二(分水岭算法watershed)_第12张图片

 opencv分水岭算法描述如下:

初始化mark矩阵,生成最初的注水区域。

1.设置mark图像的边框值为-1

2. 标记每个mark区域的边界为-2

3. 对于mark图像一个像素值,如果它本身值为0,但上下左右四邻域有一个像素值不为0,则把该点按照RGB高度值放入相应的队列。

      举例说明:如下图像素点,它的mark值为0,但左和上像素值不为0,此时,我们求原始图像中对应像素的高度值,高度值的计算方式如下面公式,其中R表示Red通道值,G表示Green通道值,B表示Blue通道值,下标L表示左,R表示右,T表示上,B表示下:

min(max(abs(R-RL), abs(G-GL), abs(B-BL)),max(abs(R-RT), abs(G-GT), abs(B-BT)),

       max(abs(R-RR), abs(G-GR), abs(B-BR)), max(abs(R-RB), abs(G-GB), abs(B-BB))    )

OpenCV3学习(7.2)——图像分割之二(分水岭算法watershed)_第13张图片OpenCV3学习(7.2)——图像分割之二(分水岭算法watershed)_第14张图片

    上图中指定的像素,它的高度值为0,所以我们把(3,3)点放入高度为0的队列中(总共有256个队列,对应0-255的高度),初始化阶段完成后,我们得到下面的mark图。

    OpenCV3学习(7.2)——图像分割之二(分水岭算法watershed)_第15张图片

之后就进入了递归注水过程,递归过程描述如下(没看懂???):

for(; ; )

{

    扫描0-255高度值队列,

    如果找到一个像素标记,则弹出该标记,并退出扫描。

    如果该像素的四邻域中存在两个不同的非0值,表示该点为两个注水盆地的边缘,即分水岭线,在mark图像中标记该点为-1。

    扫描该点的四邻域,是否存在为0的mark域,存在的话把该邻域点按照rgb高度值,放入相应的队列。

}

    初始的区域标记好了,分岭也找到了,那么可以开始“水漫梁山”了。这就是淹没过程。淹没过程由也是由一个内嵌循环的循环来实现的:外循环是做水位上升(这 里循环次数一定要256以内),waterlevel的上升,可以从Thre开始了,让水位慢慢上升,让它原本 的湖慢慢扩张,尽量利用其应有的空间,而又不至于淹没到其它的邻居湖泊。内循环是扫描每个初始区域(当前Num,从而有Num个循环)的分水岭的点,按照给定的水位进行扩张。

     扩张过程是这样的:扫描到的分水岭的当前点,对其进行四连通邻域进行逐一检查,如果四连通域中有点没有标 记的(那这一定是高度较高的点,较低的前面一定扫描过),那么先对该点以本区域号做标记Num(注意是当前的Num);再判断它在当前水位下是否可生长 (灰阶是否小于等于waterlevel),如果可生长那么加入到qu[Num][waterlevel]种子队列中,将会再次进入内循环,否则如果 在当前水位下不可生长,则加入到这个邻域点的分水岭集合中qu[Num][Ori_image[邻域点]]队列中。如此往复循环,直到对应区域完成, 一个水位,扫描所有的区域的分水岭,这样各自同时在一个水位下扩张,保证了不出现跳跃的情况出现(就是一个水位一个区域全局扩张)。 
 

经过上述的递归过程,最后我们得到的mark图像如下所示,其中绿色格子的-1即为所有的分水岭边界:

               OpenCV3学习(7.2)——图像分割之二(分水岭算法watershed)_第16张图片

from:https://yq.aliyun.com/articles/325556


     经过灰度化、滤波、边缘检测、findContours轮廓查找、轮廓绘制等步骤后终于得到了符合Opencv要求的markers,我们把markers转换成8bit单通道灰度图看看它里边到底是什么内容:

findContours检测并绘制到的轮廓和分水岭运算前的markers:

OpenCV3学习(7.2)——图像分割之二(分水岭算法watershed)_第17张图片

仔细观察就能发现,marksshow图像上不同线条的灰度值是不同的,底部略暗,越往上灰度越高

   看一下代码即知(为每个轮廓设置不同的灰度值主要是为了区分出轮廓,每一个线条代表了一个种子,线条的不同灰度值其实代表了对不同注水种子的编号,有多少不同灰度值的线条,就有多少个种子,图像最后分割后就有多少个区域。):

drawContours(marks, contours, index, Scalar::all(compCount +1), 1, 8, hierarchy);
drawContours(imageContours, contours, index, Scalar(255), 1, 8, hierarchy);
	// 我们来看一下传入的矩阵marks里是什么东西
	convertScaleAbs(marks, marksShows);
	imshow("marksShow", marksShows);

 运行分水岭算法后markers变为:

                                OpenCV3学习(7.2)——图像分割之二(分水岭算法watershed)_第18张图片

       执行完watershed之后,merkers里边被分割出来的区域已经非常明显了,空间上临近并且灰度值上相近的区域被划分为一个区域,灰度值是一样,不同区域间被划分开,这其实就是分水岭对图像的分割效果了。

            OpenCV3学习(7.2)——图像分割之二(分水岭算法watershed)_第19张图片

源码:

#include
#include
#include
#include 
#include 
using namespace cv;
using namespace std;

Vec3b RandomColor(int value);
int main() {
	Mat imggray;
	Mat img = imread("122.png", 1);
	cvtColor(img, imggray, CV_RGB2GRAY);
	GaussianBlur(imggray, imggray, Size(3, 3), 2);
	Canny(imggray, imggray, 80, 140);
	vector> contours;
	vector hierarchy;
	findContours(imggray,contours,hierarchy,RETR_TREE, CHAIN_APPROX_SIMPLE, Point());
	Mat imageContours = Mat::zeros(img.size(), CV_8UC1);  //轮廓	
	Mat marks(img.size(), CV_32S, Scalar::all(0));   //Opencv分水岭第二个矩阵参数
	int index = 0;
	int compCount = 0;
	for (; index >= 0; index = hierarchy[index][0], compCount++)
	{
		//对marks进行标记,对不同区域的轮廓进行编号,相当于设置注水点,有多少轮廓,就有多少注水点
		drawContours(marks, contours, index, Scalar::all(compCount +1), 1, 8, hierarchy);
		drawContours(imageContours, contours, index, Scalar(255), 1, 8, hierarchy);
	}
	// 我们来看一下传入的矩阵marks里是什么东西
	Mat marksShows;
	convertScaleAbs(marks, marksShows);
	imshow("marksShow", marksShows);
	imshow("轮廓", imageContours);
	watershed(img, marks);
	Mat afterWatershed;
	convertScaleAbs(marks, afterWatershed);//将图片转化成为8位图形进行显示
	imshow("After Watershed", afterWatershed);

	//对每一个区域进行颜色填充
	Mat PerspectiveImage = Mat::zeros(img.size(), CV_8UC3);
	for (int i = 0; i(i, j);
			if (marks.at(i, j) == -1)
			{
				PerspectiveImage.at(i, j) = Vec3b(255, 255, 255);
			}
			else
			{
				PerspectiveImage.at(i, j) = RandomColor(index);
			}
		}
	}
	imshow("After ColorFill", PerspectiveImage);

	//分割并填充颜色的结果跟原始图像融合
	Mat wshed;
	addWeighted(img, 0.4, PerspectiveImage, 0.6, 0, wshed);
	imshow("AddWeighted Image", wshed);
	waitKey(0);
}
Vec3b RandomColor(int value)   //生成随机颜色函数
{
	value = value % 255;  //生成0~255的随机数
	RNG rng;
	int aa = rng.uniform(0, value);
	int bb = rng.uniform(0, value);
	int cc = rng.uniform(0, value);
	return Vec3b(aa, bb, cc);
}

OpenCV 源码中分水岭算法 watershed 函数源码注解:https://blog.csdn.net/u011375993/article/details/46793655

from:https://www.cnblogs.com/wjy-lulu/p/7056466.html

from:https://blog.csdn.net/dcrmg/article/details/52498440

分水岭算法与漫水填充的区别

int floodFill( InputOutputArray image,Point seedPoint, Scalar newVal, CV_OUT Rect* rect=0,
                          Scalar loDiff=Scalar(), Scalar upDiff=Scalar(),int flags=4 );

   漫水填充只能设置一个初始点,种子点仅是标记用。填充准则是:当前选定像素与其连通区中相邻像素中的一个像素的灰度差值,在(loDiff,upDiff ) 范围内。

    分水岭算法可以设置多个不同的种子点,种子点可以鼠标手动选择,也可以通过找轮廓,用轮廓来定义种子。实际用的时候我们一般是先找到图像中的前景目标,将前景目标作为种子点(前景标注,可以运用形态学处理中的开操作和闭操作来得到前景标注。也可通过二值图像的距离变换)

     分水岭变换通过将灰度图的亮度看做是一个山体的表面用于找到“漫水盆地”以及“分水岭屋脊”。如果能对前景和背景位置进行标注,利用分水岭变换进行分割能够有更好的效果。Marker-controlled分水岭分割的步骤如下所示:
1 计算一个分割函数。分割函数是一个图像,这个图像的较暗的区域是你希望分割的目标。可以利用梯度图像来作为分割函数,梯度图像在目标的边界处具有较高的数值而在目标内部具有较小的数值

2 计算前景标注。前景标注是每一个目标中的一个连接的区域。
3 计算背景标注。背景是一个不属于任何目标的部分。
4 修改分割函数以至于分割函数仅仅在前景和背景标注位置具有较小值。
5 重新计算修改后的分割函数的分水岭变换。

你可能感兴趣的:(opencv,opencv',watershed)