分水岭算法(Watershed algorithm)与OpenCV实现

前言

         分水岭算法主要用于图像的分割!

         这个算法需要输入一个灰度图,在接下来的洪水漫堤过程中,相邻的积水盆地之间的分水岭便慢慢构建起来。一般情况下,这会引起过分割,尤其是具有噪声的图像。

         图像必须要预处理,以消除噪声;分割结果必须要基于一些根据进行区域合并,以减小过分割造成的影响!

技术
         假设我们要将下面这幅图像进行分割,需要几步?分三步,第一步,冰箱门打开,第二步。。。打住,- -| 进入正题,我们要用OpenCV来实现,不就一步么?调用那个watershed函数不就行了?实际上,可没那么简单!且听我细细道来 ~-~!


分水岭算法(Watershed algorithm)与OpenCV实现_第1张图片


OpenCV实现了一个基于标记图层的分水岭算法,所谓基于标记图层,就是不用手动选择种子点了,直接输入一个包含标记点(种子点)的图像即可。函数原型如下:

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

其中,image代表输入图像(必须是8-bit3通道图像,也就是说是灰度图像),这也就是说,要对一个彩色图像进行分割,就要先将image转换为灰度图,幸运的是这可以通过cvtColor()(原型为C++: void cvtColor( InputArray src,OutputArray dst, int code, int dstCn=0 ),code代表转换类型,具体的所有代码在这里可以找到)做到;markers是输入的标记图层(包含标记点的图层,这些标记点就是进行洪水的入口,一般是区域极小值点)。

利用cvtColor将原图转换为灰度图如下:

分水岭算法(Watershed algorithm)与OpenCV实现_第2张图片

这就是说要利用分水岭算法对一幅图像进行分割,必须提供其对应的标记图层。那么问题来了?挖掘机技术哪家强?。。。额,串词了 - -|  那么markers图层从哪里来呢?不用怕,需要的markers图层可以通过findContours()函数勾勒出来,findContours()函数的原型如下:

C++: void findContours( InputOutputArray image, OutputArrayOfArrays contours, OutputArray hierarchy, int mode, int method, Point offset=Point())
C++: void findContours( InputOutputArray image, OutputArrayOfArrays contours, int mode, int method, Point offset=Point())

其中,image参数输入的图像必须是8-bit单通道的图像。可以使用compare() , inRange(), threshold() , adaptiveThreshold() , Canny() 从彩色或者灰度图转换成二值图。
这里我们使用adaptiveThreshold()方法转化一个灰度图到二值图,以使用findContours()方法。adaptiveThreshold()的原型是:

C++: void adaptiveThreshold(InputArray src, OutputArray dst, double maxValue, int adaptiveMethod, int thresholdType, int blockSize, double C)

src是8-bit 单通道的图像;dst和src是同一类型的图像;maxValue是条件满足时的赋值;adaptiveMethod是确定合适的阈值的算法,有两个选项:ADAPTIVE_THRESH_MEAN_C 或者ADAPTIVE_THRESH_GAUSSIAN_C,分别对应于均值和高斯算法;thresholdType是指阈值分割类型,必须是THRESH_BINARY或者THRESH_BINARY_INV,如果是THRESH_BINARY,那么大于阈值的将会赋值为maxValue(一般为255),否则就赋值为0,THRESH_BINARY_INV是相反的规则!

分水岭算法(Watershed algorithm)与OpenCV实现_第3张图片


利用以上思路转换为二值图后的结果如下:

分水岭算法(Watershed algorithm)与OpenCV实现_第4张图片

findContours结果如下:

分水岭算法(Watershed algorithm)与OpenCV实现_第5张图片

OK,将markers文件传入watershed函数的参数,最终结果如下:

分水岭算法(Watershed algorithm)与OpenCV实现_第6张图片


代码我给附上:

/**
* 定义一个使用分水岭算法的辅助类
*/
class WatershedSegmenter{
private:
    cv::Mat markers;
public:
    void setMarkers(cv::Mat& markerImage)
    {
        markerImage.convertTo(markers, CV_32S);
    }

    cv::Mat process(cv::Mat &image)
    {
        cv::watershed(image, markers);
        markers.convertTo(markers,CV_8U);
        return markers;
    }
};

/**
* 接受一个图像参数,显示出分水岭算法分割的结果
*/
void watershedSegment (Mat img){
	Mat gray(img.rows, img.cols,CV_8UC1);
	cvtColor(img, gray, CV_BGR2GRAY);	//转换为8-bit,3通道的灰度图
	Mat binary = Mat::zeros(gray.rows, gray.cols, CV_8UC1);
	adaptiveThreshold(gray, binary, 255, ADAPTIVE_THRESH_GAUSSIAN_C, THRESH_BINARY_INV, 5, 10);		//将灰度图转换为二值图
	Mat markers = Mat::zeros(gray.rows, gray.cols, CV_8UC1);

	//使用findContour()函数找出图像的轮廓
	vector > contours;
	vector hierarchy;
	findContours(binary, contours, hierarchy, CV_RETR_LIST, CV_CHAIN_APPROX_NONE);

	//将contours结果放入到markers中,便于访问
	int idx = 0;
	for( ; idx >= 0; idx = hierarchy[idx][0]){
		Scalar color(rand()&255, rand()&255, rand()&255);
		drawContours(markers, contours, idx, color, CV_FILLED, 8, hierarchy);
	}

	//调用分水岭算法分割图像
	WatershedSegmenter segmenter;
        segmenter.setMarkers(markers);
        cv::Mat result = segmenter.process(img);

	//显示分割结果
	namedWindow("segmentation_result", 0);
	imshow("segmentation_result", result);
}

总结

Watershed是一个很好的算法,效率也很高。在图像分割中,应用的比较广泛,据说很多商业软件都有实现!

但是其缺点也很明显——过分割现象。怎么办呢?可以有两种思路,从源头或者过后收拾残局:从源头?就是在分割过程中控制分割行为,这个好复杂的说!要修改算法,以兄弟我现在的水平,可是力不从心,哈哈;那么我们可以采取第二种思路,将过分割的地物对象进行合并分类,听起来好像很有道理的样子哦,我们下次讨论一下哈。

你可能感兴趣的:(图像处理,OpenCV,分水岭算法,C++,watershed)