分水岭算法.docx
《分水岭算法.docx》由会员分享,可在线阅读,更多相关《分水岭算法.docx(21页珍藏版)》请在冰豆网上搜索。
分水岭算法
图像处理——分水岭算法
分水岭算法是一种图像区域分割法,在分割的过程中,它会把跟临近像素间的相似性作为重要的参考依据,从而将在空间位置上相近并且灰度值相近(求梯度)的像素点互相连接起来构成一个封闭的轮廓。
分水岭算法常用的操作步骤:
彩色图像灰度化,然后再求梯度图,最后在梯度图的基础上进行分水岭算法,求得分段图像的边缘线。
下面左边的灰度图,可以描述为右边的地形图,地形的高度是由灰度图的灰度值决定,灰度为0对应地形图的地面,灰度值最大的像素对应地形图的最高点。
我们可以自己编程实现灰度图的地形图显示,工程FirstOpenCV6就实现了简单的这个功能,比如上边的灰度图,显示为:
对灰度图的地形学解释,我们我们考虑三类点:
1.局部最小值点,该点对应一个盆地的最低点,当我们在盆地里滴一滴水的时候,由于重力作用,水最终会汇聚到该点。
注意:
可能存在一个最小值面,该平面内的都是最小值点。
2.盆地的其它位置点,该位置滴的水滴会汇聚到局部最小点。
3.盆地的边缘点,是该盆地和其它盆地交接点,在该点滴一滴水,会等概率的流向任何一个盆地。
假设我们在盆地的最小值点,打一个洞,然后往盆地里面注水,并阻止两个盆地的水汇集,我们会在两个盆地的水汇集的时刻,在交接的边缘线上(也即分水岭线),建一个坝,来阻止两个盆地的水汇集成一片水域。
这样图像就被分成2个像素集,一个是注水盆地像素集,一个是分水岭线像素集。
下面的gif图很好的演示了分水岭算法的效果:
在真实图像中,由于噪声点或者其它干扰因素的存在,使用分水岭算法常常存在过度分割的现象,这是因为很多很小的局部极值点的存在,比如下面的图像,这样的分割效果是毫无用处的。
为了解决过度分割的问题,可以使用基于标记(mark)图像的分水岭算法,就是通过先验知识,来指导分水岭算法,以便获得更好的图像分段效果。
通常的mark图像,都是在某个区域定义了一些灰度层级,在这个区域的洪水淹没过程中,水平面都是从定义的高度开始的,这样可以避免一些很小的噪声极值区域的分割。
下面的gif图很好的演示了基于mark的分水岭算法过程:
上面的过度分段图像,我们通过指定mark区域,可以得到很好的分段效果:
Opencv中watershed函数原型:
[cpp] viewplain copy
1.void watershed( InputArray image, InputOutputArray markers );
第一个参数image,必须是一个8bit3通道彩色图像矩阵序列,第一个参数没什么要说的。
关键是第二个参数markers,Opencv官方文档的说明如下:
Beforepassingtheimagetothefunction,youhavetoroughlyoutlinethedesiredregionsintheimagemarkerswithpositive(>0)indices.So,everyregionisrepresentedasoneormoreconnectedcomponentswiththepixelvalues1,2,3,andsoon.SuchmarkerscanberetrievedfromabinarymaskusingfindContours()anddrawContours().Themarkersare“seeds”ofthefutureimageregions.Alltheotherpixelsinmarkers,whoserelationtotheoutlinedregionsisnotknownandshouldbedefinedbythealgorithm,shouldbesetto0’s.Inthefunctionoutput,eachpixelinmarkersissettoavalueofthe“seed”componentsorto-1atboundariesbetweentheregions.
就不一句一句翻译了,大意说的是在执行分水岭函数watershed之前,必须对第二个参数markers进行处理,它应该包含不同区域的轮廓,每个轮廓有一个自己唯一的编号,轮廓的定位可以通过Opencv中findContours方法实现,这个是执行分水岭之前的要求。
接下来执行分水岭会发生什么呢?
算法会根据markers传入的轮廓作为种子(也就是所谓的注水点),对图像上其他的像素点根据分水岭算法规则进行判断,并对每个像素点的区域归属进行划定,直到处理完图像上所有像素点。
而区域与区域之间的分界处的值被置为“-1”,以做区分。
简单概括一下就是说第二个入参markers必须包含了种子点信息。
Opencv官方例程中使用鼠标划线标记,其实就是在定义种子,只不过需要手动操作,而使用findContours可以自动标记种子点。
而分水岭方法完成之后并不会直接生成分割后的图像,还需要进一步的显示处理,如此看来,只有两个参数的watershed其实并不简单。
下边通过图示来看一下watershed函数的第二个参数markers在算法执行前后发生了什么变化。
对于一个原图:
经过灰度化、滤波、Canny边缘检测、findContours轮廓查找、轮廓绘制等步骤后终于得到了符合Opencv要求的merkers,我们把merkers转换成8bit单通道灰度图看看它里边到底是什么内容:
这个是分水岭运算前的merkers:
这个是findContours检测到的轮廓:
看效果,基本上跟图像的轮廓是一样的,也是简单的勾勒出了物体的外形。
但如果仔细观察就能发现,图像上不同线条的灰度值是不同的,底部略暗,越往上灰度越高。
由于这幅图像边缘比较少,对比不是很明显,再来看一幅轮廓数量较多的图效果:
这个是分水岭运算前的merkers:
这个是findContours检测到的轮廓:
从这两幅图对比可以很明显看到,从图像底部往上,线条的灰度值是越来越高的,并且merkers图像底部部分线条的灰度值由于太低,已经观察不到了。
相互连接在一起的线条灰度值是一样的,这些线条和不同的灰度值又能说明什么呢?
答案是:
每一个线条代表了一个种子,线条的不同灰度值其实代表了对不同注水种子的编号,有多少不同灰度值的线条,就有多少个种子,图像最后分割后就有多少个区域。
再来看一下执行完分水岭方法之后merkers里边的内容发生了什么变化:
可以看到,执行完watershed之后,merkers里边被分割出来的区域已经非常明显了,空间上临近并且灰度值上相近的区域被划分为一个区域,灰度值是一样,不同区域间被划分开,这其实就是分水岭对图像的分割效果了。
总的概括一下watershed图像自动分割的实现步骤:
1.图像灰度化、滤波、Canny边缘检测
2.查找轮廓,并且把轮廓信息按照不同的编号绘制到watershed的第二个入参merkers上,相当于标记注水点。
3.watershed分水岭运算
4.绘制分割出来的区域,视觉控还可以使用随机颜色填充,或者跟原始图像融合以下,以得到更好的显示效果。
以下是Opencv分水岭算法watershed实现的完整过程:
[cpp] viewplain copy
1.#include "opencv2/imgproc/imgproc.hpp"
2.#include "opencv2/highgui/highgui.hpp"
3.
4.#include
5.
6.using namespace cv;
7.using namespace std;
8.
9.Vec3b RandomColor(int value); //生成随机颜色函数
10.
11.int main( int argc, char* argv[] )
12.{
13. Mat image=imread(argv[1]); //载入RGB彩色图像
14. imshow("Source Image",image);
15.
16. //灰度化,滤波,Canny边缘检测
17. Mat imageGray;
18. cvtColor(image,imageGray,CV_RGB2GRAY);//灰度转换
19. GaussianBlur(imageGray,imageGray,Size(5,5),2); //高斯滤波
20. imshow("Gray Image",imageGray);
21. Canny(imageGray,imageGray,80,150);
22. imshow("Canny Image",imageGray);
23.
24. //查找轮廓
25. vector> contours;
26. vector hierarchy;
27. findContours(imageGray,contours,hierarchy,RETR_TREE,CHAIN_APPROX_SIMPLE,Point());
28. Mat imageContours=Mat:
:
zeros(image.size(),CV_8UC1); //轮廓
29. Mat marks(image.size(),CV_32S); //Opencv分水岭第二个矩阵参数
30. marks=Scalar:
:
all(0);
31. int index = 0;
32. int compCount = 0;
33. for( ; index >= 0; index = hierarchy[index][0], compCount++ )
34. {
35. //对marks进行标记,对不同区域的轮廓进行编号,相当于设置注水点,有多少轮廓,就有多少注水点
36. drawContours(marks, contours, index, Scalar:
:
all(compCount+1), 1, 8, hierarchy);
37. drawContours(imageContours,contours,index,Scalar(255),1,8,hierarchy);
38. }
39.
40. //我们来看一下传入的矩阵marks里是什么东西
41. Mat marksShows;
42. convertScaleAbs(marks,marksShows);
43. imshow("marksShow",marksShows);
44. imshow("轮廓",imageContours);
45. watershed(image,marks);
46.
47. //我们再来看一下分水岭算法之后的矩阵marks里是什么东西
48. Mat afterWatershed;
49. convertScaleAbs(marks,afterWatershed);
50. imshow("After Watershed",afterWatershed);
51.
52. //对每一个区域进行颜色填充
53. Mat PerspectiveImage=Mat:
:
zeros(image.size(),CV_8UC3);
54. for(int i=0;i55. {
56. for(int j=0;j57. {
58. int index=marks.at(i,j);
59. if(marks.at(i,j)==-1)
60. {
61. PerspectiveImage.at(i,j)=Vec3b(255,255,255);
62. }
63. else
64. {
65. PerspectiveImage.at(i,j) =RandomColor(index);
66. }
67. }
68. }
69. imshow("After ColorFill",PerspectiveImage);
70.
71. //分割并填充颜色的结果跟原始图像融合
72. Mat wshed;
73. addWeighted(image,0.4,PerspectiveImage,0.6,0,wshed);
74. imshow("AddWeighted Image",wshed);
75.
76. waitKey();
77.}
78.
79.Vec3b RandomColor(int value) 20.8px; font-family:
sans-serif;">//生成随机颜色函数
80.{
81. value=value%255; //生成0~255的随机数
82. RNG rng;
83. int aa=rng.uniform(0,value);
84. int bb=rng.uniform(0,value);
85. int cc=rng.uniform(0,value);
86. return Vec3b(aa,bb,cc);
87.}
第一幅图像分割效果:
按比例跟原始图像融合:
第二幅图像原始图:
分割效果:
按比例跟原始图像融合: