OpenCV—图像分割中的分水岭算法原理与应用
图像分割是按照一定的原则,将一幅图像分为若干个互不相交的小局域的过程,它是图像处理中最为基础的研究领域之一。目前有很多图像分割方法,其中分水岭算法是一种基于区域的图像分割算法,分水岭算法因实现方便,已经在医疗图像,模式识别等领域得到了广泛的应用。
1.传统分水岭算法基本原理
分水岭比较经典的计算方法是L.Vincent于1991年在PAMI上提出的[1]。传统的分水岭分割方法,是一种基于拓扑理论的数学形态学的分割方法,其基本思想是把图像看作是测地学上的拓扑地貌,图像中每一像素的灰度值表示该点的海拔高度,每一个局部极小值及其影响区域称为集水盆地,而集水盆地的边界则形成分水岭。分水岭的概念和形成可以通过模拟浸入过程来说明。在每一个局部极小值表面,刺穿一个小孔,然后把整个模型慢慢浸人水中,随着浸入的加深,每一个局部极小值的影响域慢慢向外扩展,在两个集水盆汇合处构筑大坝如下图所示,即形成分水岭。
传统分水岭算法示意图
然而基于梯度图像的直接分水岭算法容易导致图像的过分割,产生这一现象的原因主要是由于输入的图像存在过多的极小区域而产生许多小的集水盆地,从而导致分割后的图像不能将图像中有意义的区域表示出来。所以必须对分割结果的相似区域进行合并。
[1]L.Vincent, P Soille. Watersheds in digital space: An efficientalgorithms based on immersion simulation[J]. IEEE Trans. on Pattern Analysisand Machine Intelligence, 1991, 13(6): 583-598.
2.改进的分水岭算法基本原理
因为传统分水岭算法存在过分割的不足,OpenCV提供了一种改进的分水岭算法,使用一系列预定义标记来引导图像分割的定义方式。使用OpenCV的分水岭算法cv::wathershed,需要输入一个标记图像,图像的像素值为32位有符号正数(CV_32S类型),每个非零像素代表一个标签。它的原理是对图像中部分像素做标记,表明它的所属区域是已知的。分水岭算法可以根据这个初始标签确定其他像素所属的区域。传统的基于梯度的分水岭算法和改进后基于标记的分水岭算法示意图如下图所示。
传统基于梯度的分水岭算法和基于标记的分水岭算法原理图
从上图可以看出,传统基于梯度的分水岭算法由于局部最小值过多造成分割后的分水岭较多。而基于标记的分水岭算法,水淹过程从预先定义好的标记图像(像素)开始,较好的克服了过度分割的不足。本质上讲,基于标记点的改进算法是利用先验知识来帮助分割的一种方法。因此,改进算法的关键在于如何获得准确的标记图像,即如何将前景物体与背景准确的标记出来。
3.基于标记点的分水岭算法应用
基于标记点的分水岭算法应用步骤
● 封装分水岭算法类
● 获取标记图像
获取前景像素,并用255标记前景
获取背景像素,并用128标记背景,未知像素,使用0标记
合成标记图像
● 将原图和标记图像输入分水岭算法
● 显示结果
(1)封装分水岭算法类
将分水岭算法cv::watershed(image,markers)封装进类WatershedSegmenter,并保存为头文件以便于操作。(本段封装代码参考《OpenCV计算机视觉编程攻略(第二版)》)
- #if !defined WATERSHS
- #define WATERSHS
- #include <opencv2/core/core.hpp>
- #include <opencv2/imgproc/imgproc.hpp>
- class WatershedSegmenter {
- private:
- cv::Mat markers;
- public:
- void setMarkers(const cv::Mat& markerImage) {
- // Convert to image of ints
- markerImage.convertTo(markers,CV_32S);
- }
- cv::Mat process(const cv::Mat &image) {
- // Apply watershed
- cv::watershed(image,markers);
- return markers;
- }
- // Return result in the form of an image
- cv::Mat getSegmentation() {
- cv::Mat tmp;
- // all segment with label higher than 255
- // will be assigned value 255
- markers.convertTo(tmp,CV_8U);
- return tmp;
- }
- // Return watershed in the form of an image以图像的形式返回分水岭
- cv::Mat getWatersheds() {
- cv::Mat tmp;
- //在变换前,把每个像素p转换为255p+255(在conertTo中实现)
- markers.convertTo(tmp,CV_8U,255,255);
- return tmp;
- }
- };
- #endif
(2)获取标记图像
标记前景
读取原图
- // Read input image
- cv::Mat image1= cv::imread("image.jpg");
- if (!image1.data)
- return 0;
- // Display the color image
- cv::resize(image1, image1, cv::Size(), 0.7, 0.7);
- cv::namedWindow("Original Image1");
- cv::imshow("Original Image1",image1);
原图
以下代码目的是获取前景物体的像素,并用255标记。这里使用阈值分割初步分割前景和背景,接着使用形态学闭运算连接二值图像中前景的各个部分,并平滑边缘。如何更好的获取前景像素,需要根据实际图像的情况灵活处理。
- // Identify image pixels with object
- Mat binary;
- cv::cvtColor(image1,binary,COLOR_BGRA2GRAY);
- cv::threshold(binary,binary,30,255,THRESH_BINARY_INV);//阈值分割原图的灰度图,获得二值图像
- // Display the binary image
- cv::namedWindow("binary Image1");
- cv::imshow("binary Image1",binary);
- waitKey();
- // CLOSE operation
- cv::Mat element5(5,5,CV_8U,cv::Scalar(1));//5*5正方形,8位uchar型,全1结构元素
- cv::Mat fg1;
- cv::morphologyEx(binary, fg1,cv::MORPH_CLOSE,element5,Point(-1,-1),1);// 闭运算填充物体内细小空洞、连接邻近物体
- // Display the foreground image
- cv::namedWindow("Foreground Image");
- cv::imshow("Foreground Image",fg1);
- waitKey();
阈值分割原图像的灰度图
闭运算获取前景
标记背景和未知区域
在上面阈值分割得到的二值图像binary的基础上,通过对白色前景的深度膨胀运算获得一个超过前景实际大小的物体,紧接着用反向阈值将深度膨胀后的图像中的黑色部分转换成128,即完成了对背景像素的标记。实际上,在0~255范围内,任意不为0或255的值均可作为背景的标记。当然如果有其他类型的物体,可以使用另外一个数值作为其标记。也就是说,多个目标可以有多个标记来帮助分水岭算法正确分割图像。
- // Identify image pixels without objects
- cv::Mat bg1;
- cv::dilate(binary,bg1,cv::Mat(),cv::Point(-1,-1),4);//膨胀4次,锚点为结构元素中心点
- cv::threshold(bg1,bg1,1,128,cv::THRESH_BINARY_INV);//>=1的像素设置为128(即背景)
- // Display the background image
- cv::namedWindow("Background Image");
- cv::imshow("Background Image",bg1);
- waitKey();
将背景设置为128,未知区域设置为0
- // Apply watershed segmentation
- WatershedSegmenter segmenter1; //实例化一个分水岭分割方法的对象
- segmenter1.setMarkers(markers1);//设置算法的标记图像,使得水淹过程从这组预先定义好的标记像素开始
- segmenter1.process(image1); //传入待分割原图
- // Display segmentation result
- cv::namedWindow("Segmentation1");
- cv::imshow("Segmentation1",segmenter1.getSegmentation());//将修改后的标记图markers转换为可显示的8位灰度图并返回分割结果(白色为前景,灰色为背景,0为边缘)
- waitKey();
- // Display watersheds
- cv::namedWindow("Watersheds1");
- cv::imshow("Watersheds1",segmenter1.getWatersheds());//以图像的形式返回分水岭(分割线条)
- waitKey();
代码segmenter1.process(image)将修改标记图像markers,每个值为0的像素都会被赋予一个输入标签,而边缘处的像素赋值为-1,得到的标签图像如下图所示。
显示分水岭分割图像
分水岭分割线显示
(4)显示结果图像
本步骤的目的是将前景物体的分割结果在黑/白底色中显示出来。背景颜色由黑转白时使用了Mat矩阵扫描的.ptr方法与指针运算。- // Get the masked image
- Mat maskimage = segmenter1.getSegmentation();
- cv::threshold(maskimage,maskimage,250,1,THRESH_BINARY);
- cv::cvtColor(maskimage,maskimage,COLOR_GRAY2BGR);
- maskimage = image1.mul(maskimage);
- cv::namedWindow("maskimage");
- cv::imshow("maskimage",maskimage);
- waitKey();
- // Turn background (0) to white (255)
- int nl= maskimage.rows; // number of lines
- int nc= maskimage.cols * maskimage.channels(); // total number of elements per line
- for (int j=0; j<nl; j++) {
- uchar* data= maskimage.ptr<uchar>(j);
- for (int i=0; i<nc; i++)
- {
- // process each pixel ---------------------
- if (*data==0) //将背景由黑色改为白色显示
- *data=255;
- data++;//指针操作:如为uchar型指针则移动1个字节,即移动到下1列
- }
- }
- cv::namedWindow("result");
- cv::imshow("result",maskimage);
- waitKey();
原图的前景分割图(黑色背景)
原图的前景分割图(白色背景)
从上图的分割结果可以看出,基于标记图像的分水岭算法较好的实现了复杂背景下前景目标分割。算法应用的关键步骤为标记图像的获取,目前很多文献提出了各类获取标记图像的方法,如何使用还需要根据所处理的图像来量身确定。
贴出实验原始图像:)