OpenCV2 使用分水岭算法对图像分割的个人理解 cv::watershed()

本文是基于《opecv2 计算机视觉编程手册》中的案例对分水岭算法进行解读。

先介绍一下分水岭分割方法。它是一种基于拓扑理论的数学形态学的分割方法,其基本思想是把图像看作是测地学上的拓扑地貌,图像中每一点像素的灰度值表示该点的海拔高度,每一个局部极小值及其影响区域称为集水盆,而集水盆的边界则形成分水岭。分水岭的概念和形成可以通过模拟浸入过程来说明。在每一个局部极小值表面,刺穿一个小孔,然后把整个模型慢慢浸入水中,随着浸入的加深,每一个局部极小值的影响域慢慢向外扩展,在两个集水盆汇合处构筑大坝,即形成分水岭。

一般的分水岭算法会对微弱边缘,图像中的噪声,物体表面细微的灰度变化造成过度的分割。opencv中的分水岭算法对此进行了改进,它使用预定义的一组标记来引导对图像的分割。理解这一节,关键是理解这个标记。书上这一节的内容对这个标记讲得很模糊,网上也搜了很多cv::watershed(),大部分内容是opencv1.0案例使用鼠标响应事件的,有个别是opencv2.0的案例,但都只是复制代码,并未做一些解读。这里我谈谈自己的一些看法,希望对大家有所帮助,如有理解不正确的地方还请高手多多指教。
watershedSegmenter.h

#if !defined WATERSHS
#define WATERSHS

#include 
#include 

class WatershedSegmenter {

  private:
      //用来表示标记(图)
      cv::Mat markers;

  public:
       //设置标记图
      void setMarkers(const cv::Mat& markerImage) {

        //watershed()的输入参数必须为一个32位有符号的标记,所以要先进行转换 
        markerImage.convertTo(markers,CV_32S);
      }
      //执行watershed()
      cv::Mat process(const cv::Mat &image) {

        // Apply watershed
        cv::watershed(image,markers);

        return markers;
      }

      // 以图像形式返回结果
      cv::Mat getSegmentation() {

        cv::Mat tmp;
    // 从32S到8U(0-255)会进行饱和运算,所以像素高于255的一律复制为255
        markers.convertTo(tmp,CV_8U);//

        return tmp;
      }

      // 以图像形式返回分水岭(我理解的是分割线)
      cv::Mat getWatersheds() {

        cv::Mat tmp;
        //在设置标记图像,即执行setMarkers()后,边缘的像素会被赋值为-1,其他的用正整数表示
        //下面的这个转换可以让边缘像素变为-1*255+255=0,即黑色,其余的溢出,赋值为255,即白色。
        markers.convertTo(tmp,CV_8U,255,255);
        return tmp;
      }
};


#endif

segment.cpp

#include <iostream>
#include <opencv2/core/core.hpp>
#include <opencv2/imgproc/imgproc.hpp>
#include <opencv2/highgui/highgui.hpp>
#include "watershedSegmentation.h"
int main()
{
    // Read input image 原图
    cv::Mat image= cv::imread("../group.jpg");
    if (!image.data)
        return 0; 

    // Display the image
    cv::namedWindow("Original Image");
    cv::imshow("Original Image",image);

group.jpg
OpenCV2 使用分水岭算法对图像分割的个人理解 cv::watershed()_第1张图片

// 二值图 这里进行了像素反转,因为一般我们用255白色表示前景(物体),用0黑色表示背景
    cv::Mat binary;
    binary= cv::imread("../binary.bmp",0);

    // Display the binary image
    cv::namedWindow("Binary Image");
    cv::imshow("Binary Image",binary);

binary.bmp
OpenCV2 使用分水岭算法对图像分割的个人理解 cv::watershed()_第2张图片

// 由二值图像获得前景。腐蚀。移除噪点与微小物体
    cv::Mat fg;
    cv::erode(binary,fg,cv::Mat(),cv::Point(-1,-1),6);

    // Display the foreground image
    cv::namedWindow("Foreground Image");
    cv::imshow("Foreground Image",fg);

fg前景
OpenCV2 使用分水岭算法对图像分割的个人理解 cv::watershed()_第3张图片

    //膨胀二值图来获取背景(只有草地,没有树林)
    cv::Mat bg;
    cv::dilate(binary,bg,cv::Mat(),cv::Point(-1,-1),6);
    cv::threshold(bg,bg,1,128,cv::THRESH_BINARY_INV);
    //最后一个参数的表示 ifsrc>1,dst=0,else dst=128。这样就使背景全为灰色(128)
    // Display the background image
    cv::namedWindow("Background Image");
    cv::imshow("Background Image",bg);

bg背景
OpenCV2 使用分水岭算法对图像分割的个人理解 cv::watershed()_第4张图片

    // Show markers image
    cv::Mat markers(binary.size(),CV_8U,cv::Scalar(0));
    markers= fg+bg;//使用重载操作符+
    cv::namedWindow("Markers");
    cv::imshow("Markers",markers);

markers标记图像 (最初的markers) 我觉得难点就是对这个标记图像的理解。下面谈谈我的个人看法。这个图像中,随便找一头牛。前景(物体:牛)是白色255,中间是黑色0,再然后才是背景灰128。可以把这3数看成海拔图,两个山岭相连,小的山岭128m,低处山谷0m,高的山岭255m。然后分水岭算法开始扩大白色255区域,往(黑色)注水,一直注到灰色的齐平,注水到黑色与白色部分全是白色。
OpenCV2 使用分水岭算法对图像分割的个人理解 cv::watershed()_第5张图片
设置markers,执行,显示结果


    // Create watershed segmentation object
    WatershedSegmenter segmenter;

    // Set markers and process
    segmenter.setMarkers(markers);
    segmenter.process(image);

    // Display segmentation result
    cv::namedWindow("Segmentation");
    cv::imshow("Segmentation",segmenter.getSegmentation());

注水后就成下图,上面的只是我的理解,可能不太正确,但我觉得这样便于理解怎么从上图白色那么一点变成下图。
结果图
OpenCV2 使用分水岭算法对图像分割的个人理解 cv::watershed()_第6张图片

// Display watersheds
    cv::namedWindow("Watersheds");
    cv::imshow("Watersheds",segmenter.getWatersheds());

分水岭(分割线)
OpenCV2 使用分水岭算法对图像分割的个人理解 cv::watershed()_第7张图片

(完)
还请大家批评指教~

你可能感兴趣的:(opencv2,分水岭算法,图像分割)