OpenCV图像分割中的分水岭算法原理与应用详解

 更新时间:2020年4月25日 17:28  点击:1926

图像分割是按照一定的原则,将一幅图像分为若干个互不相交的小局域的过程,它是图像处理中最为基础的研究领域之一。目前有很多图像分割方法,其中分水岭算法是一种基于区域的图像分割算法,分水岭算法因实现方便,已经在医疗图像,模式识别等领域得到了广泛的应用。

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

合成标记图像

将前景、背景及未知区域合成为一个标记图像。则标记图像中通过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(); 

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


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

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

贴出实验原始图像:)

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持猪先飞。

[!--infotagslink--]

相关文章

  • python opencv 画外接矩形框的完整代码

    这篇文章主要介绍了python-opencv-画外接矩形框的实例代码,代码简单易懂,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下...2021-09-04
  • python opencv通过4坐标剪裁图片

    图片剪裁是常用的方法,那么如何通过4坐标剪裁图片,本文就详细的来介绍一下,感兴趣的小伙伴们可以参考一下...2021-06-04
  • OpenCV如何去除图片中的阴影的实现

    这篇文章主要介绍了OpenCV如何去除图片中的阴影的实现,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧...2021-03-29
  • 解决使用OpenCV中的imread()内存报错问题

    这篇文章主要介绍了解决使用OpenCV中的imread()内存报错问题,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧...2021-03-16
  • python OpenCV学习笔记

    这篇文章主要介绍了python OpenCV的相关资料,帮助大家更好的理解和学习使用python的opencv,感兴趣的朋友可以了解下...2021-03-31
  • 使用OpenCV去除面积较小的连通域

    这篇文章主要介绍了使用OpenCV去除面积较小的连通域,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧...2020-07-05
  • 详解pycharm的python包opencv(cv2)无代码提示问题的解决

    这篇文章主要介绍了详解pycharm的python包opencv(cv2)无代码提示问题的解决,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧...2021-01-29
  • 在C#中使用OpenCV(使用OpenCVSharp)的实现

    这篇文章主要介绍了在C#中使用OpenCV(使用OpenCVSharp)的实现,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧...2020-11-15
  • C++ opencv实现车道线识别

    这篇文章主要为大家详细介绍了C++ opencv实现车道线识别,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下...2021-02-20
  • Opencv LBPH人脸识别算法详解

    这篇文章主要为大家详细介绍了Opencv LBPH人脸识别算法的相关资料,具有一定的参考价值,感兴趣的小伙伴们可以参考一下...2020-04-25
  • 详解opencv中画圆circle函数和椭圆ellipse函数

    这篇文章主要介绍了opencv中画圆circle函数和椭圆ellipse函数,本文通过实例代码给大家介绍的非常详细,具有一定的参考借鉴价值,需要的朋友可以参考下...2020-05-09
  • python中的opencv和PIL(pillow)转化操作

    这篇文章主要介绍了python中的opencv和PIL(pillow)转化操作,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧...2021-03-16
  • python基于opencv检测程序运行效率

    这篇文章主要介绍了python基于opencv检测程序运行效率,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下...2020-05-09
  • 浅谈OpenCV中的新函数connectedComponentsWithStats用法

    这篇文章主要介绍了浅谈OpenCV中的新函数connectedComponentsWithStats用法,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧...2020-07-05
  • Opencv实现绿幕视频背景替换功能

    这篇文章主要为大家详细介绍了Opencv实现绿幕视频背景替换功能,具有一定的参考价值,感兴趣的小伙伴们可以参考一下...2020-04-25
  • OpenCV-Python实现凸包的获取

    凸包是一个计算几何中的概念,在图像处理过程中,我们常常需要寻找图像中包围某个物体的凸包,本文就使用OpenCV实现,感兴趣的可以了解一下...2021-06-08
  • python基于OpenCV模板匹配识别图片中的数字

    这篇文章主要介绍了python基于OpenCV模板匹配识别图片中的数字,帮助大家更好的理解和学习使用python,感兴趣的朋友可以了解下...2021-03-31
  • 使用opencv识别图像红色区域,并输出红色区域中心点坐标

    这篇文章主要介绍了使用opencv识别图像红色区域,并输出红色区域中心点坐标,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧...2020-06-03
  • Python编程OpenCV和Numpy图像处理库实现图片去水印

    这篇文章主要介绍了Python编程中如何实现图片去水印本文采用了OpenCV和Numpy的图像处理的方法来实现,文中附含详细示例代码,有需要的朋友可以借鉴参考下...2021-09-26
  • Opencv python 图片生成视频的方法示例

    这篇文章主要介绍了Opencv python 图片生成视频的方法示例,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧...2020-11-18