OpenCV图像分割中的分水岭算法原理与应用详解
本文详解OpenCV中分水岭算法的原理与应用。针对传统算法易导致过分割的问题,重点介绍基于标记点的改进算法。通过封装WatershedSegmenter类,演示如何获取前景与背景标记图像,并调用cv::watershed实现复杂背景下的高精度前景目标分割,附带完整C++代码示例。
图像分割旨在依据特定原则,将一幅图像划分为若干个互不重叠的小区域,这是图像处理领域最基础的研究方向之一。当前图像分割技术众多,其中分水岭算法作为一种基于区域的分割方法,因实现便捷,已在医疗影像分析和模式识别等场景中得到广泛应用。
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::watershed时,需要输入一个标记图像,该图像的像素值类型为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
合成标记图像
将前景、背景及未知区域合成为一个标记图像。则标记图像中通过255标记前景物体,通过128标记背景,通过0标记未知区域。
//Get markers image
Mat markers1 = fg1 + bg1; //使用Mat类的重载运算符+来合并图像。
cv::namedWindow("markers Image");
cv::imshow("markers Image",markers1);
waitKey();

标记图像
(3)分水岭算法分割图像
将标记图像和原图输入分水岭算法封装的类WatershedSegmenter,执行分水岭算法,并显示算法运行的结果。
// 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();

原图的前景分割图(黑色背景)

原图的前景分割图(白色背景)
从上图的分割结果可以看出,基于标记图像的分水岭算法较好地实现了复杂背景下前景目标的分割。算法应用的关键步骤为标记图像的获取,目前很多文献提出了各类获取标记图像的方法,具体使用方式还需根据所处理的图像来量身确定。

贴出实验原始图像:)
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持本站。
免责声明:本文内容仅供参考,不代表本站观点。转载或引用请注明出处。









