使用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::Point(-1,-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做的笔记,可能存在许多不足,欢迎大家批评指正!有问题可以随时和我交流。

Guess you like

Origin blog.csdn.net/qq_43010987/article/details/121342015