使用openCV分水岭算法实现图像分割
Posted 小白汐汐
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了使用openCV分水岭算法实现图像分割相关的知识,希望对你有一定的参考价值。
一、简介
分水岭算法的思想是把图像看作是一个拓扑地貌,同类区域就相当于陡峭边缘内相对平摊的盆地。当从高度为0开始逐步用“水”淹没图像时,会形成好多个聚水的盆地,随着盆地的面积逐渐增大,两个盆地的水最终会汇合到一起,这时就需要创建一个分水岭把这两个盆地分割开。当水位达到最大高度时,创建的盆地和分水岭就组成了分水岭分割图。
二、实现过程
1、数据准备
本实验需要一张原始图像,一张原始图像对应的二值图像,注意:这两张照片的尺寸必须一致,不然会报错。如果没有原始图像的二值图像,可以用以下代码转换。(假设原始图像是RGB图像,转换过程是:RGB—>灰度图—>二值图)
cv::Mat image3;
cv::cvtColor(image, image3, CV_BGR2GRAY);
cv::imwrite("dog_gray.jpg", image3);//RGB转换灰度图像
void Thresholded(cv::Mat image)
{
cv::Mat thresholded; // 定义输出的二值图像
cv::threshold(image, thresholded, 70, // 阈值
255, // 对超过阈值的像素赋值
cv::THRESH_BINARY); // 阈值化类型
cv::bitwise_not(thresholded, thresholded);//对图像做反向处理,白色作为前景物体,黑色作为背景
cv::imshow("thresholded", thresholded);
cv::imwrite("image_2.jpg", thresholded);//输出二值化后的图像,对其进行后续处理
}
原图
原图对应的二值图
2、形态学运算:腐蚀和膨胀
腐蚀是把当前像素替换成所定义像素集合中的最小像素值;膨胀是腐蚀的反运算,把当前像素值替换成所定义像素集合中的最大像素值。由于输入的二值图像只包含黑色(值为0)和白色(值为255)像素,因此每个像素都会被替换成白色和黑色像素。
要形象地理解这两种运算的作用,可考虑背景(黑色)和前景(白色)的物体。腐蚀时,如果结构元素放到某个像素位置时碰到了背景(即交集中有一个像素是黑色的),那么这个像素就变为背景;膨胀时,如果结构元素放到某个背景像素位置时碰到了前景物体,那么这个像素就被标为白色。
(1)腐蚀图像
//消除噪声和细小物体
cv::Mat fg; //前景图
cv::erode(image2, fg, cv::Mat(), cv::Point(-1, -1), 4);//腐蚀图像4次
cv::imshow("Foreground Image", fg);
(2)膨胀图像
对图像做膨胀运算,来选择一些背景像素,得到的黑色像素对应背景像素,在膨胀后要立即通过阈值化运算把他们赋值为128。
// 标识不含物体的图像像素
cv::Mat bg;
cv::dilate(image2, bg, cv::Mat(), cv::Point(-1, -1), 4);//膨胀图像4次
cv::threshold(bg, bg, 1, 128, cv::THRESH_BINARY_INV);
cv::imshow("Background Image", bg);
(3)合并图像
合并这两幅图像,得到标记图像
// 创建标记图像
cv::Mat markers(image2.size(), CV_8U, cv::Scalar(0));
markers = fg + bg;//合并图像,得到标记图像
cv::imshow("markers", markers);
在这个合并的图像中,白色区域属于前景物体,灰色区域属于背景,黑色区域属于未知标签。
3、分水岭算法
分水岭算法就是将合并的图像中,前景和背景区分开,并对黑色区域的像素做出标记(属于前景还是背景)。我创建了一个关于分水岭函数的类WatershedSegmenter。
class WatershedSegmenter
{
private:
cv::Mat markers;
public:
void setMarkers(const cv::Mat& markerImage)
{
//转换成整数型图像
markerImage.convertTo(markers, CV_32S);
}
cv::Mat process(const cv::Mat &image)
{
//应用分水岭函数
//输入对象是一个标记图像,图像的像素值为32位有符号整数,每个非零像素代表一个标签
cv::watershed(image, markers);
return markers;
}
}
//创建分水岭分割类的对象
WatershedSegmenter segmenter;
//设置标记图像,然后执行分割过程
segmenter.setMarkers(markers);
segmenter.process(image);
4、结果展示
在结果输出时,会修改标记图像,每个值为0的像素会被赋予一个输入标签,而边缘处的像素赋值为-1。
返回标签组成的图像(包含值为0的分水岭)。
// 以图像的形式返回结果
cv::Mat getSegmentation() {
cv::Mat tmp;
// 所有标签值大于 255 的区段都赋值为 255
markers.convertTo(tmp, CV_8U);
return tmp;
}
标签图像
返回一幅图像,图像中分水岭线条赋值为0,其他部分赋值为255。
// 以图像的形式返回分水岭
cv::Mat getWatersheds() {
cv::Mat tmp;
// 在变换前,把每个像素 p 转换为 255p+255
markers.convertTo(tmp, CV_8U, 255, 255);
return tmp;
}
边缘图像
三、原理补充
在调用cv::watershed函数时,执行了这样的过程,在水淹过程的开始阶段会创建很多细小的独立盆地。当所有盆地汇合时,就会创建很多分水岭线条,导致图像被过度分割。要解决这个问题,就要对这个算法进行修改,使水淹过程从一组预先定义好的标记像素开始。每个用标记创建的盆地,都按照初始标记的值加上标签。如果两个标签相同的盆地汇合,就不创建分水岭,以避免过度分割。
四、其他实验结果
五、完整代码
#include <iostream>
#include<opencv2/core.hpp> //图像数据结构的核心
#include<opencv2/highgui.hpp> //所有图形接口函数
#include<opencv2/imgproc.hpp>
#include<opencv2/imgproc/imgproc.hpp>
#include <opencv2/imgproc/types_c.h>
#include <opencv2/opencv.hpp>
using namespace std;
class WatershedSegmenter
{
private:
cv::Mat markers;
public:
void setMarkers(const cv::Mat& markerImage)
{
//转换成整数型图像
markerImage.convertTo(markers, CV_32S);
}
cv::Mat process(const cv::Mat &image)
{
//应用分水岭函数
//输入对象是一个标记图像,图像的像素值为32位有符号整数,每个非零像素代表一个标签
cv::watershed(image, markers);
return markers;
}
// 以图像的形式返回结果
cv::Mat getSegmentation() {
cv::Mat tmp;
// 所有标签值大于 255 的区段都赋值为 255
markers.convertTo(tmp, CV_8U);
return tmp;
}
// 以图像的形式返回分水岭
cv::Mat getWatersheds() {
cv::Mat tmp;
// 在变换前,把每个像素 p 转换为 255p+255
markers.convertTo(tmp, CV_8U, 255, 255);
return tmp;
}
};
void Thresholded(cv::Mat image)
{
cv::Mat thresholded; // 定义输出的二值图像
cv::threshold(image, thresholded, 70, // 阈值
255, // 对超过阈值的像素赋值
cv::THRESH_BINARY); // 阈值化类型
cv::bitwise_not(thresholded, thresholded);//对图像做反向处理,白色作为前景物体,黑色作为背景
cv::imshow("thresholded", thresholded);
cv::imwrite("dog_2.jpg", thresholded);//输出二值化后的图像,对其进行后续处理
}
int main()
{
/*******分水岭算法实现图像分割*******/
cv::Mat image = cv::imread("group.jpg");
if (!image.data)
return 0;
/*cv::Mat image3;
cv::cvtColor(image, image3, CV_BGR2GRAY);
cv::imwrite("dog_gray.jpg", image3);*///RGB转换灰度图
//Thresholded(image);//对图像做二值化处理
cv::Mat image2 = cv::imread("binary.bmp",0); //读二值图像
//消除噪声和细小物体
cv::Mat fg; //前景图
cv::erode(image2, fg, cv::Mat(), cv::Point(-1, -1), 4);//腐蚀图像4次
cv::imshow("Foreground Image", fg);
// 标识不含物体的图像像素
cv::Mat bg;
cv::dilate(image2, bg, cv::Mat(), cv::Point(-1, -1), 4);//膨胀图像4次
cv::threshold(bg, bg, 1, 128, cv::THRESH_BINARY_INV);
cv::imshow("Background Image", bg);
// 创建标记图像
cv::Mat markers(image2.size(), CV_8U, cv::Scalar(0));
markers = fg + bg;//合并图像,得到标记图像
cv::imshow("markers", markers);
//创建分水岭分割类的对象
WatershedSegmenter segmenter;
//设置标记图像,然后执行分割过程
segmenter.setMarkers(markers);
segmenter.process(image);
cv::imshow("Segmentation", segmenter.getSegmentation());
cv::imshow("Watersheds", segmenter.getWatersheds());
cv::waitKey(0);
return 0;
}
本篇文章是我学习opencv做的笔记,可能存在许多不足,欢迎大家批评指正!有问题可以随时和我交流。
以上是关于使用openCV分水岭算法实现图像分割的主要内容,如果未能解决你的问题,请参考以下文章
OpenCV中的图像处理 —— 霍夫线 / 圈变换 + 图像分割(分水岭算法) + 交互式前景提取(GrabCut算法)