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

  • 3
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
基于分水岭算法图像分割是一种常用的图像处理技术,可以将图像分割成多个区域,每个区域内的像素具有相似的特征。在 OpenCV 中,可以使用 cv2.watershed() 函数实现基于分水岭算法图像分割。 下面是一个简单的 Python 示例,演示如何使用基于分水岭算法图像分割: ```python import cv2 import numpy as np # 读取图像 img = cv2.imread('image.jpg') # 转换为灰度图像 gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) # 阈值分割 ret, thresh = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY_INV+cv2.THRESH_OTSU) # 形态学操作 kernel = np.ones((3,3),np.uint8) opening = cv2.morphologyEx(thresh,cv2.MORPH_OPEN,kernel,iterations=2) # 距离变换 dist_transform = cv2.distanceTransform(opening,cv2.DIST_L2,5) ret, sure_fg = cv2.threshold(dist_transform,0.1*dist_transform.max(),255,0) # 背景区域 sure_bg = cv2.dilate(opening,kernel,iterations=3) # 不确定区域 sure_fg = np.uint8(sure_fg) unknown = cv2.subtract(sure_bg,sure_fg) # 标记连通区域 ret, markers = cv2.connectedComponents(sure_fg) markers = markers + 1 markers[unknown==255] = 0 # 应用分水岭算法 markers = cv2.watershed(img,markers) img[markers == -1] = [255,0,0] # 显示结果 cv2.imshow('image', img) cv2.waitKey(0) cv2.destroyAllWindows() ``` 在上面的示例中,首先读取一张图像,并将其转换为灰度图像。然后使用阈值分割算法将图像二值化。接下来,进行形态学操作,以去除图像中的噪声。然后使用距离变换算法计算前景区域,并将其阈值化。接着,使用形态学操作计算背景区域。最后,使用 cv2.connectedComponents() 函数计算不确定区域,并使用标记连通区域的方法生成分水岭算法的输入标记图像。最后,应用 cv2.watershed() 函数进行图像分割,并在窗口中显示结果。 需要注意的是,分水岭算法的结果依赖于输入标记图像的质量,因此需要根据具体情况进行调整,比如阈值分割的参数、形态学操作的参数、距离变换的参数等。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

小白汐汐

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值