前面博文中提到的图像阈值化,图像边缘检测,图像轮廓检测实际上都是一种图像分割法,图像分割在图像处理识别是非常重要的,这点没有做过图像识别或图像处理的人都很容易想到,就不多解释了。所以图像分割法就根据不同的应用需要出现了很多方法。
本篇博文介绍利用分水岭进行图像分割的方法。它是一种区域分割法,区域分割法利用图像的空间性质,以像素点之间的相似性为依据,根据不同的分割准则进行图像分割。这样能弥补阈值、边缘检测、轮廓检测中忽略像素点空间关系的缺点。
分水岭算法应用于图像分割领域,不仅能够保留了一些传统分割方法的普遍优点(PS:从下面给出的代码中可以看出,在应用分山岭算法前,要用阈值化,边缘检测等对图像作处理得到分水岭标记图,所以,分水岭算法保留了一些传统分割方法的普遍优点),还可以有效克服传统图像分割算法中存在的缺点和弊端,该算法应用于图像分割已经越来越得到研究领域的重视,同时有更加广阔的应用。
分水岭分割是基于自然的启发算法来模拟水流通过地形起伏的现象从而研究总结出来的一种分割方法,其基本原理是将图像特征看作地理上的地貌特征,利用像素的灰度值分布特征,对每个符合特征的区域进行划分,形成边界以构成分水岭。
下面是分水岭算法的物理模型:
在上面的水岭算法示意图中局部极小值、积水盆地,分水岭线以及水坝的概念可以描述为:
(1)区域极小值:导数为0的点,局部范围内的最小值点;
(2)集水盆(汇水盆地):当“水”落到汇水盆地时,“水”会自然而然地流到汇水盆地中的区域极小值点处。每一个汇水盆地中有且仅有一个区域极小值点;
(3)分水岭:当“水”处于分水岭的位置时,会等概率地流向多个与它相邻的汇水盆地中;
(4)水坝:人为修建的分水岭,防止相邻汇水盆地之间的“水”互相交汇影响。
实现分水岭算法有多种方法,最通用最广泛的实现方法的是Vincent和Soille在1991年提出的模拟浸没算法以及Meyer在1994年提出的基于距离函数的算法,OpenCV提供了函数watershed()来实现分水岭算法,它采用的是Meyer在1994年提出的基于距离函数的算法,具体的论文大家可以搜索“Meyer, F. Color Image Segmentation, ICIP92, 1992”
watershed()函数的原型如下:
void watershed(InputArray image, InputOutputArray markers)
可见,非常简单,就两个参数,但实际上不是那么简单的,因为markers可不是给一个Mat就行的,官方文档对这个参数的说明如下:
Before passing the image to the function, you have to roughly outline the desired regions in the image markers with positive (>0) indices. So, every region is represented as one or more connected components with the pixel values 1, 2, 3, and so on. Such
markers can be retrieved from a binary mask using findContours() and drawContours() (see the watershed.cpp demo). The markers are “seeds” of the future image regions. All the other pixels in markers , whose relation to the outlined regions is not known and
should be defined by the algorithm, should be set to 0’s. In the function output, each pixel in markers is set to a value of the “seed” components or to -1 at boundaries between the regions.
翻译如下:
markers中存储了图像的大致轮廓,并且以值1,2,3...分别表示各个components.markers通常由函数findContours() 和 drawContours()结合使用来获得。markers相当于watershed()运行时的种子参数。markers中,不属于轮廓(outlined regions)的点的值应置为0.函数运行后,图像中的像素如果是在由某个轮廓种子生成的区域中,那么其值就置为这个种子的编号,如果像素不在轮廓种子生成的区域中,则置为-1。
PS:上面这段话我想尽可能为大家描述清楚,但实在是文字水平有限,感觉还是说得不清楚,大家见谅。不过不要紧,如果看不懂,下面给出的代码看了之后你就全懂了。
代码如下(代码中用到的图像的下载链接:外链网址已屏蔽):
#include
#include "opencv2/imgproc.hpp"
#include "opencv2/imgcodecs.hpp"
#include "opencv2/highgui.hpp"
#include
#include
using namespace cv;
using namespace std;
int main()
{
cv::Mat srcImage =cv::imread("sea.jpg");
if (!srcImage.data)
return -1;
Mat src_gray;
int thresh = 100; //canny边缘检测的阈值
int max_thresh = 255;
RNG rng(12345);
// 转成灰度并平滑
cvtColor(srcImage, src_gray, CV_BGR2GRAY);
blur(src_gray, src_gray, Size(3, 3));
// 创建窗体
imshow("srcImage", srcImage);
//用Canny算子检测边缘
Mat canny_output;
Canny(src_gray, canny_output, thresh, thresh * 2, 3);
// 寻找轮廓
vector > contours;
vector hierarchy;
findContours(canny_output, contours, hierarchy, RETR_CCOMP, CHAIN_APPROX_SIMPLE);
if( contours.empty() )
{
cout<
return 0;
}
//绘制轮廓到markers,以供watershed函数调用
int i, j, compCount = 0;
Mat markers(canny_output.size(), CV_32S);
markers = Scalar::all(0);
int idx = 0;
for( ; idx >= 0; idx = hierarchy[idx][0], compCount++ )
drawContours(markers, contours, idx, Scalar::all(compCount+1), -1, 8, hierarchy, INT_MAX);
vector 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));
}
watershed(srcImage, markers );//分水岭算法实现函数
Mat wshed(markers.size(), CV_8UC3);
Mat imgGray;
cvtColor(src_gray, imgGray, COLOR_GRAY2BGR);
// paint the watershed image
for( i = 0; i < markers.rows; i++ )
for( j = 0; j < markers.cols; j++ )
{
int index = markers.at(i,j);
if( index == -1 )
wshed.at(i,j) = Vec3b(255,255,255);
else if( index <= 0 || index > compCount )
wshed.at(i,j) = Vec3b(0,0,0);
else
wshed.at(i,j) = colorTab[index - 1];
}
//wshed = wshed*0.5 + imgGray*0.5;
imshow("imgGray",imgGray);
imshow( "watershed transform", wshed );
cv::waitKey(0);
return(0);
}
运行结果如下图所示:
几点说明:
①Canny边缘检测的阈值由thresh变量定,大家可以根据不同的图像去调整,如果想动态调整,那么可以用createTrackbar创建滑动条的方法在窗口中调整阈值,详情见我写的博文外链网址已屏蔽
②从运行结果来看,出现了分水岭水割算法常见的问题,即过度分割的问题。解决过度分割一般采用的方法是合并的方法。具体来说一般是先计算分割部分的像素归属,利用直方直方图信息统计相关特征,对每个分割部分统计直方图对比的相似性,然后根据相似性判断分割的两个部分是否需要合并成一个区域。我根据这个思路写了一个合并函数,然而效果不好,应该说是失败的,所以就不把源码帖出来让大家见笑了,工程名是“watershedSegment_08”,以后如果确实需要这个功能,可以再详细研究。失败的运行结果如下图所示:
③我在某博客上看到过一种获取轮廓之前对图像的操作的思路:首先,对源图像进行灰度化,并使用OTSU进行二值化操作;然后,对二值化图像进行形态学开操作,再利用distanceTransform完成图像距离变换操作;最后,归一化距离变换的图像,再用这个图像求轮廓。我用这个思路写了源码,效果也不理想,所以也不贴代码了,工程编号是“watermeshed_07”,运行结果如下图所示:
-------------------------------------------
欢迎大家加入图像识别技术交流群:271891601,另外,特别欢迎成都从事图像识别工作的朋友交流,我的QQ号2487872782