本文是基于《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);
// 二值图 这里进行了像素反转,因为一般我们用255白色表示前景(物体),用0黑色表示背景
cv::Mat binary;
binary= cv::imread("../binary.bmp",0);
// Display the binary image
cv::namedWindow("Binary Image");
cv::imshow("Binary Image",binary);
// 由二值图像获得前景。腐蚀。移除噪点与微小物体
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);
//膨胀二值图来获取背景(只有草地,没有树林)
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);
// 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区域,往(黑色)注水,一直注到灰色的齐平,注水到黑色与白色部分全是白色。
设置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());
注水后就成下图,上面的只是我的理解,可能不太正确,但我觉得这样便于理解怎么从上图白色那么一点变成下图。
结果图
// Display watersheds
cv::namedWindow("Watersheds");
cv::imshow("Watersheds",segmenter.getWatersheds());
(完)
还请大家批评指教~