本文小编为大家详细介绍“OpenCV基于分水岭算法的图像分割怎么实现”,内容详细,步骤清晰,细节处理妥当,希望这篇“OpenCV基于分水岭算法的图像分割怎么实现”文章能帮助大家解决疑惑,下面跟着小编的思路慢慢深入,一起来学习新知识吧。
1. 分水岭算法
分水岭分割可以通过使用
cv::watershed
函数实现,函数的输入是一个
32
位有符号整数标记图像,其中每个非零像素表示一个标签。通过标记图像中已知属于给定区域的一些像素,利用初始标记,分水岭算法可以确定其他像素所属的区域。
(1) 首先,将标记图像读取为灰度图像,然后将其转换为整数类型:
class WatershedSegmentater {
private:
cv::Mat markers;
public:
void setMarkers(const cv::Mat& markerImage) {
// 转换数据类型
markerImage.convertTo(markers, CV_32S);
}
cv::Mat process(const cv::Mat& image) {
// 应用分水岭算法
cv::watershed(image, markers);
return markers;
}
有多种获取标记的方式,例如,使用预处理步骤识别出属于感兴趣对象的某些像素,然后利用分水岭算法根据初始标记分割完整的对象。在本节中,我们将使用二值图像来识别相应原始图像中的动物。因此,从二值图像中,我们需要识别属于前景(动物)的像素和属于背景(主要是雪地)的像素,我们用标签
255
标记前景像素,用标签
128
标记背景像素,其他像素则标记为
0
。
(2) 初始二值图像包含过多属于图像各个部分的白色像素,为了只保留属于重要对象的像素,我们首先需要腐蚀该图像:
// 消除噪音
cv::Mat fg;
cv::erode(binary, fg, cv::Mat(), cv::Point(-1, -1), 4);
结果如下图所示:
(3) 图中仍然存在一些属于背景(雪地)的像素,我们通过对原始二值图像进行膨胀来选择几个属于背景的像素:
// 标记图像像素
cv::Mat bg;
cv::dilate(binary, bg, cv::Mat(), cv::Point(-1, -1), 4);
cv::threshold(bg, bg, 1, 128, cv::THRESH_BINARY_INV);
结果如下图所示,黑色像素对应于背景像素:
(4) 将这些图像组合起来形成标记图像:
cv::Mat markers(binary.size(), CV_8U, cv::Scalar(0));
markers = fg+bg;
我们使用重载的
+
运算符来组合图像,得到用作分水岭算法的输入:
(5) 在这个输入图像中,白色区域属于前景对象,灰色区域是背景的一部分,黑色区域则属于未知标签,得到分割结果如下:
// 创建分水岭分割对象
WatershedSegmentater segmenter;
segmenter.setMarkers(markers);
segmenter.process(image);
更新标记图像,以便为黑色区域中的像素重新分配标签,而属于边界的像素的值为
-1
。结果标签图像如下:
图像中对象边缘的可视化结果如下图所示:
2. 分水岭算法直观理解
我们使用拓扑图进行类比,为了创建分水岭分割,我们从级别
0
开始注水,随着水位逐渐增加,就形成了集水盆地。这些盆地的大小也会逐渐增加,两个不同盆地的水最终会汇合,发生这种情况时,会创建一个分水岭,以将两个盆地分开。一旦水位达到最高水位,这些水域和分水岭就形成了分水岭分割。
在注水过程中最初会产生许多小盆地,当这些盆地进行合并时,会创建许多分水岭线,从而导致图像被过度分割。为了克服这个问题,已经提出了多种改进算法,在
OpenCV
调用
cv::watershed
函数时,注水过程从一组预定义的标记像素开始,根据分配给初始标记的值对盆地进行标记,当具有相同标签的两个盆地合并时,不会创建分水岭,从而防止过度分割,更新输入标记图像以获得最终的分水岭分割。用户可以输入带有任意数量的标签和未知标签的标记图像,标记图像的像素类型为为
32
位有符号整数,以便能够定义超过
255
个标签。
cv::watershed
函数还允许返回与分水岭关联的像素(使用特殊值
-1
进行标记)。
为了便于显示结果,我们引入两种特殊的方法。第一个方法
getSegmentation()
通过阈值返回标签图像,分水岭值为
0
:
// 返回结果
cv::Mat getSegmentation() {
cv::Mat tmp;
markers.convertTo(tmp, CV_8U);
return tmp;
}
第二种方法
getWatersheds()
返回的图像中,分水岭线使用值
0
进行标记,图像的其余部分像素值为
255
,可以使用
cv::convertTo
方法实现:
// 返回分水岭
cv::Mat getWatersheds() {
cv::Mat tmp;
markers.convertTo(tmp,CV_8U,255,255);
return tmp;
}
在转换之前应用线性变换,可以将像素值
-1
转换为
0
( − 1 × 255 + 255 = 0 -1 imes 255+255=0 −1×255+255=0)。由于将有符号整数转换为无符号字符时需应用饱和操作,大于
255
的像素值将转换为
255
。
我们也可以通过许多不同的方式获得标记图像。例如,可以令用户以交互方式在图像中标记属于对象和背景的像素区域;或者,如果我们需要识别位于图像中心的物体,可以输入一个中心区域标有特定标签的图像,且图像背景标记带有另一个标签,可以按以下方式创建标记图像:
// 标记背景像素
cv::Mat imageMask(image.size(), CV_8U, cv::Scalar(0));
cv::rectangle(imageMask,
cv::Point(5, 5),
cv::Point(image.cols-5, image.rows-5),
cv::Scalar(255),
3);
// 标记前景像素
cv::rectangle(imageMask,
cv::Point(image.cols/2-10, image.rows/2-10),
cv::Point(image.cols/2+10, image.rows/2+10),
cv::Scalar(1),
10);
3. 完整代码
头文件 (
watershedSegmentation.h
) 完整代码如下:
#if !defined WATERSHS
#define WATERSHS
#include <opencv2/core/core.hpp>
#include <opencv2/imgproc/imgproc.hpp>
class WatershedSegmentater {
private:
cv::Mat markers;
public:
void setMarkers(const cv::Mat& markerImage) {
// 转换数据类型
markerImage.convertTo(markers, CV_32S);
}
cv::Mat process(const cv::Mat& image) {
// 应用分水岭算法
cv::watershed(image, markers);
return markers;
}
// 返回结果
cv::Mat getSegmentation() {
cv::Mat tmp;
markers.convertTo(tmp, CV_8U);
return tmp;
}
// 返回分水岭
cv::Mat getWatersheds() {
cv::Mat tmp;
markers.convertTo(tmp,CV_8U,255,255);
return tmp;
}
};
#endif
主文件 (
segment.cpp
) 完整代码如下所示:
#include <iostream>
#include <opencv2/core/core.hpp>
#include <opencv2/highgui/highgui.hpp>
#include <opencv2/imgproc/imgproc.hpp>
#include "watershedSegmentation.h"
int main() {
// 读取输入图像
cv::Mat image = cv::imread("1.png");
if (!image.data) return 0;
cv::namedWindow("Original Image");
cv::imshow("Original Image",image);
// 读取二值图像
cv::Mat binary;
binary = cv::imread("binary.png", 0);
cv::namedWindow("Binary Image");
cv::imshow("Binary Image", binary);
// 消除噪音
cv::Mat fg;
cv::erode(binary, fg, cv::Mat(), cv::Point(-1, -1), 4);
cv::namedWindow("Foreground Image");
cv::imshow("Foreground Image", fg);
// 标记图像像素
cv::Mat bg;
cv::dilate(binary, bg, cv::Mat(), cv::Point(-1, -1), 4);
cv::threshold(bg, bg, 1, 128, cv::THRESH_BINARY_INV);
cv::namedWindow("Background Image");
cv::imshow("Background Image", bg);
cv::Mat markers(binary.size(), CV_8U, cv::Scalar(0));
markers = fg+bg;
cv::namedWindow("Markers");
cv::imshow("Markers", markers);
// 创建分水岭分割对象
WatershedSegmentater segmenter;
segmenter.setMarkers(markers);
segmenter.process(image);
cv::namedWindow("Segmentation");
cv::imshow("Segmentation", segmenter.getSegmentation());
cv::namedWindow("Watersheds");
cv::imshow("Watersheds", segmenter.getWatersheds());
// 打开另一张图像
image = cv::imread("3.png");
// 标记背景像素
cv::Mat imageMask(image.size(), CV_8U, cv::Scalar(0));
cv::rectangle(imageMask,
cv::Point(5, 5),
cv::Point(image.cols-5, image.rows-5),
cv::Scalar(255),
3);
// 标记前景像素
cv::rectangle(imageMask,
cv::Point(image.cols/2-10, image.rows/2-10),
cv::Point(image.cols/2+10, image.rows/2+10),
cv::Scalar(1),
10);
segmenter.setMarkers(imageMask);
segmenter.process(image);
cv::rectangle(image,
cv::Point(5, 5),
cv::Point(image.cols-5, image.rows-5),
cv::Scalar(255, 255, 255),
3);
cv::rectangle(image,
cv::Point(image.cols/2-10, image.rows/2-10),
cv::Point(image.cols/2+10, image.rows/2+10),
cv::Scalar(1, 1, 1),
10);
cv::namedWindow("Image with marker");
cv::imshow("Image with marker", image);
cv::namedWindow("Watershed");
cv::imshow("Watershed", segmenter.getWatersheds());
cv::waitKey();
return 0;
}