首页 > 编程语言 >浅析OpenCV分水岭变换watershed函数的markers参数[C++]

浅析OpenCV分水岭变换watershed函数的markers参数[C++]

时间:2024-04-29 23:45:22浏览次数:35  
标签:dist Mat imshow markers CV OpenCV watershed mark 浅析

0. 前言

本文是笔者在学习C++ OpenCV库时学习心得,在学习分水岭变换函数时,由于缺少相关学习资料,导致笔者理解吃力,故写此文章阐述一下对该函数的理解,希望对其他学习人士提供帮助。
本文主要介绍了watershed函数参数以及参数实际表示
请您按文章次序阅读
您需要提前了解的相关知识有:OpenCV图像类型、findContours函数。
完整代码请见附录

1. API介绍

void watershed( InputArray image, InputOutputArray markers );

  • image参数
    第一个参数image,InputArray类型的输入图像,且需为8位三通道的彩色图像。
    您可以使用如下代码创建image:
    Mat mSrclmage = imread("./img.jpg", ImreadModes::IMREAD_COLOR);
    上述代码读取了当前目录下的img.jpg图像,读取图像格式为8位三通道,在OpenCV中表示为CV_8UC3。
  • markers参数
    它包含了不同区域的轮廓,每个轮廓有一个自己唯一的编号(例如1、2、3....),而轮廓与轮廓之间的分界处的值被置为“0”(有的文章认为是"-1",笔者电脑测试出来是"0"),以做区分。

2. 分水岭变换原理

分水岭算法的启发思路是:把一幅灰度图像看成地理上的地形表面,每个像素的灰度值代表高度,灰度值大的区域看成山丘,灰度值小的区域看成凹地,类似于下图。

假如开始下雨,凹地首先被雨水填上,如果雨水一直下直到下到地平面(假设地平面的灰度值是100,小于100的都是凹地,大于100的都是山丘),此时灰度值小于100的都变成蓝色了,大于100的像素组成的图案就是一幅灰度图的分水岭线,其实也就是用阈值找到图像的轮廓。找到轮廓后,假设雨继续下,此时我们要在轮廓和轮廓之间筑坝防止水互相注入,然后雨继续下,每个轮廓又不断注水,被水淹的地方就变成蓝色,然后每个轮廓区域就又形成自己的轮廓,其实就是找到每个轮廓的轮廓,就实现了图像的分割。
整个流程如下图所示。

读到这里对分水岭就略知一二了吧。
采用3D图像,分水岭顺序如下图所示,从左到右,从上到下次序。

右下角图片中的红线就是最后分水岭的结果,成功实现了将图像"分割"效果。

3. 使用素材
硬币.jpg

4. 代码解析
本文中仅展示核心代码,库文件以及main函数暂时不考虑。

step-1

点击查看代码
Mat src = imread(path1,ImreadModes::IMREAD_COLOR);
imshow("原图", src);//src->CU_8UC3
上述代码读取了图片并展示,读取的图片是CV_8UC3格式,即8位3通道彩色图像,效果如下图所示。

step-2

点击查看代码
	// 均值漂移,边缘保留,平滑色彩细节
	Mat gray, binary, shifted;
	pyrMeanShiftFiltering(src, shifted, 21, 51);
	imshow("均值漂移", shifted);
上述代码对原图像实施均值漂移操作,目的是平滑色彩细节,效果如下图所示。

step-3

点击查看代码
	// 二值化
	cvtColor(shifted, gray, COLOR_BGR2GRAY);
	threshold(gray, binary, 0, 255, THRESH_BINARY | THRESH_OTSU);
	imshow("二值化", binary);//binary->CV_8UC1
上述代码将均值漂移后的图片转为灰度图片并将其二值化,此时binary为单通道灰度图像,结果如下图所示。

step-4

点击查看代码
	// 距离变换
	Mat dist;
	distanceTransform(binary, dist, DIST_L2, 3, CV_32FC1);
	normalize(dist, dist, 0, 1, NORM_MINMAX);
	imshow("距离变换", dist);//dist->CV_32FC1
上述代码对二值图像实施距离变换操作,此时dist为32位单通道浮点类型,结果如下图所示。

step-5

点击查看代码
	// 二值化,获取种子
	threshold(dist, dist, 0.4, 1, THRESH_BINARY);
	imshow("距离变换 二值化", dist);//dist->CV_32FC1
上述代码对距离变换图像进行阈值操作,结果如下图所示。

step-6

点击查看代码
	// 通过寻找轮廓,绘制轮廓,获取标记
	Mat dist_m;
	dist.convertTo(dist_m, CV_8UC1);//findContours函数不支持CV_32FC1,所以要转为CV_8UC1
	vector<vector<Point>> contours;
	findContours(dist_m, contours, RETR_EXTERNAL, CHAIN_APPROX_SIMPLE, Point(0, 0));
	Mat markers = Mat::zeros(src.size(), CV_32SC1);
	for (int t = 0; t < contours.size(); t++)
	{
		drawContours(markers, contours, t, Scalar::all(t + 1), -1);
	}
    //以下代码是为了显示图像,imshow不支持CV_32SC1类型
	Mat mTemp;
	markers.convertTo(mTemp, CV_8UC1);//markers->CV_32SC1
	imshow("轮廓", mTemp);//mTemp->CV_8UC1
上述代码将dist转为findContours所支持的参数类型并查找轮廓,然后将轮廓绘制到markers上并填充,注意,每一个轮廓的灰度值是不一样的,例如第一个轮廓灰度值是1,第二个轮廓灰度值是2,依此类推。结果如下图所示。

你可能会很好奇,上图为啥是纯黑色?其实不然,因为轮廓灰度值很小,人的肉眼无法分辨。笔者使用如下代码将图像增强。

点击查看代码
    .....
    .......
    Mat mTemp;
	markers.convertTo(mTemp, CV_8UC1);//markers->CV_32SC1
	imshow("轮廓", mTemp);//mTemp->CV_8UC1

	for (int a = 0; a < mTemp.rows; ++a)
	{
		for (int b = 0; b < mTemp.cols; ++b)
		{
			mTemp.at<uchar>(a, b) = (mTemp.at<uchar>(a, b)) * 5;
		}
	}
	imshow("增强", mTemp);
增强后的结果如下图所示。

增强后的图像就能清晰的看到轮廓了。

step-7

点击查看代码
	// 形态学操作 - 彩色图像,目的是去掉干扰,让效果更好
	Mat k = getStructuringElement(MORPH_RECT, Size(3, 3), Point(-1, -1));
	morphologyEx(src, src, MORPH_ERODE, k);
	imshow("新原图", src);
上述代码是将原图像实施形态学侵蚀操作,目的是去除干扰细节。结果如下图所示。

step-8

点击查看代码
	// 完成分水岭变换
	watershed(src, markers);
    //如下代码是为了展示分水岭变换后的结果
	Mat mark = Mat::zeros(markers.size(), CV_8UC1);
	markers.convertTo(mark, CV_8UC1);
	imshow("分水岭变换", mark);//mark->CV_8UC1
上述代码将原图像和“种子”图像(markers)实施分水岭变换操作,结果如下图所示。

同样的,上图也是几乎纯黑色图片,人的肉眼无法查看,使用图像增强代码,将图像增强,增强后的结果如下图所示。

如果你足够细心的话,你会发现每一个区域的灰度值不一样,其实这和step-6有很大关系。
为了更细致的探讨watershed(src, markers);语句的输出结果markers(markers也是输入参数也是输出参数),我们不妨将markers结果写入到.txt文件,结果如下图所示。

你可以看到markers就是0、1、2、3.....,笔者将markers的txt文件进行一些缩放,如下图所示。

这个结果十分美妙,简直不可思议,txt文件中存在着大量的0、1、2、3等等,这些“id”代表着不同的分割“区域”,如果你仔细读了分水岭变换的原理部分,其实我在那里已经说明了。接下来笔者将为这些不同的id染色,让其更加显眼,处理后的结果如下图所示。

step-9

点击查看代码
	//生成随机颜色
	vector<Vec3b> colors;
	for (size_t i = 0; i < contours.size(); i++)
	{
		int r = theRNG().uniform(0, 255);
		int g = theRNG().uniform(0, 255);
		int b = theRNG().uniform(0, 255);
		colors.push_back(Vec3b((uchar)b, (uchar)g, (uchar)r));
	}

	Mat mPaint = Mat::zeros(mark.size(), CV_8UC3);
	for (int a = 0; a < mark.rows; ++a)
	{
		for (int b = 0; b < mark.cols; ++b)
		{
			int idx = mark.at<uchar>(a, b);
			if (idx <=0)//edge
			{
				mPaint.at<Vec3b>(a, b) = Vec3b(0, 0, 0);
			}
			else
			{
				mPaint.at<Vec3b>(a, b) = colors[idx - 1];
			}
		}
	}
	imshow("分水岭变换2", mPaint);
上述代码创建了contours.size()个颜色,为每一个“区域”进行“染色”,idx变量就是txt中的id,笔者将边染成黑色,其余区域染成其他颜色,染色后的结果如下图所示。

至此,完美撒花!

5. 结语

本文对分水岭变换进行细致阐述,并附带若干丰富的图片帮助理解,同时也是笔者为学习分水岭变换画上一个完美的句号。
笔者花费了5个小时去作图、敲代码,希望各位学者尊重知识产权,请勿商用,十分感谢。

6.附录

点击查看代码
	Mat src = imread(path1,ImreadModes::IMREAD_COLOR);
	imshow("原图", src);//src->CU_8UC3

	// 均值漂移,边缘保留,平滑色彩细节
	Mat gray, binary, shifted;
	pyrMeanShiftFiltering(src, shifted, 21, 51);
	imshow("均值漂移", shifted);

	// 二值化
	cvtColor(shifted, gray, COLOR_BGR2GRAY);
	threshold(gray, binary, 0, 255, THRESH_BINARY | THRESH_OTSU);
	imshow("二值化", binary);//binary->CV_8UC1

	// 距离变换
	Mat dist;
	distanceTransform(binary, dist, DIST_L2, 3, CV_32FC1);
	normalize(dist, dist, 0, 1, NORM_MINMAX);
	imshow("距离变换", dist);//dist->CV_32FC1

	// 二值化,获取种子
	threshold(dist, dist, 0.4, 1, THRESH_BINARY);
	imshow("距离变换 二值化", dist);//dist->CV_32FC1

	// 通过寻找轮廓,绘制轮廓,获取标记
	Mat dist_m;
	dist.convertTo(dist_m, CV_8UC1);
	vector<vector<Point>> contours;
	findContours(dist_m, contours, RETR_EXTERNAL, CHAIN_APPROX_SIMPLE, Point(0, 0));
	Mat markers = Mat::zeros(src.size(), CV_32SC1);
	for (int t = 0; t < contours.size(); t++)
	{
		drawContours(markers, contours, t, Scalar::all(t + 1), -1);
	}
    //为了展示用
	Mat mTemp;
	markers.convertTo(mTemp, CV_8UC1);//markers->CV_32SC1
	imshow("轮廓", mTemp);//mTemp->CV_8UC1

	//for (int a = 0; a < mTemp.rows; ++a)
	//{
	//	for (int b = 0; b < mTemp.cols; ++b)
	//	{
	//		mTemp.at<uchar>(a, b) = (mTemp.at<uchar>(a, b)) * 5;
	//	}
	//}
	//imshow("增强", mTemp);
	//return;


	// 形态学操作 - 彩色图像,目的是去掉干扰,让效果更好
	Mat k = getStructuringElement(MORPH_RECT, Size(3, 3), Point(-1, -1));
	morphologyEx(src, src, MORPH_ERODE, k);
	imshow("新原图", src);

	// 完成分水岭变换
	watershed(src, markers);
	Mat mark = Mat::zeros(markers.size(), CV_8UC1);
	markers.convertTo(mark, CV_8UC1);
	imshow("分水岭变换", mark);//mark->CV_8UC1

	//for (int a = 0; a < mark.rows; ++a)
	//{
	//	for (int b = 0; b < mark.cols; ++b)
	//	{
	//		mark.at<uchar>(a, b) = (mark.at<uchar>(a, b)) * 5;
	//	}
	//}
	//imshow("增强", mark);
	//return;

	//fstream file;
	//file.open("./content.txt", ios::out);
	//
	//for (int a = 0; a < mark.rows; ++a)
	//{
	//	for (int b = 0; b < mark.cols; ++b)
	//	{
	//		int idx = mark.at<uchar>(a, b);
	//		char str[1024] = "";
	//		sprintf_s(str,1024,"%2d", idx);
	//		file.write(str,strlen(str));
	//	}
	//	file << '\n';
	//}
	//file.close();
	//return;

	//生成随机颜色
	vector<Vec3b> colors;
	for (size_t i = 0; i < contours.size(); i++)
	{
		int r = theRNG().uniform(0, 255);
		int g = theRNG().uniform(0, 255);
		int b = theRNG().uniform(0, 255);
		colors.push_back(Vec3b((uchar)b, (uchar)g, (uchar)r));
	}

	Mat mPaint = Mat::zeros(mark.size(), CV_8UC3);
	for (int a = 0; a < mark.rows; ++a)
	{
		for (int b = 0; b < mark.cols; ++b)
		{
			int idx = mark.at<uchar>(a, b);
			if (idx <=0)//edge
			{
				mPaint.at<Vec3b>(a, b) = Vec3b(0, 0, 0);
			}
			else
			{
				mPaint.at<Vec3b>(a, b) = colors[idx - 1];
			}
		}
	}
	imshow("分水岭变换2", mPaint);
	cout << "number of objects : " << contours.size() << endl;

标签:dist,Mat,imshow,markers,CV,OpenCV,watershed,mark,浅析
From: https://www.cnblogs.com/hello-nullptr/p/18162198

相关文章

  • openEuler操作系统安装OpenCV 3.4.10
    安装依赖JDK要求需求java17系统要求centos8centos7需要自行升级glibc到某个版本,哪一个忘记了,风险很大,不要尝试!基础依赖#缺什么就下什么,其中cmake是最重要的yum-yinstallepel-releaseyuminstallgccgcc-c++yuminstallcmakeyuminstallepel-releaseyum......
  • 23.pg_wal浅析01
    1.PG_WAL?WAL是一套保证数据完整性的标准。简要地说,WAL中心概念是数据文件(这里涉及到表和索引)修改必须在这些动作被记录之后,即描述这些修改操作的日志记录被刷到永久存储中。如果我们遵循这个过程,我们不需要在每次事务提交时刷数据页到磁盘,因我我们知道一旦发生崩溃,我们可以......
  • openCV 图像清晰度检测
    图像清晰度评价算法有很多种,在空域中,主要思路是考察图像的领域对比度,即相邻像素间的灰度特征的梯度差;在频域中,主要思路是考察图像的频率分量,对焦清晰的图像高频分量较多,对焦模糊的图像低频分量较多。这里实现3种清晰度评价方法,分别是Tenengrad梯度方法、Laplacian梯度方法和方差......
  • Centos7 下安装OpenCV 3.4.6 (本人参考这个步骤,在openEuler操作系统安装了OpenCV 3.4.1
    Centos7下安装OpenCV3.4.6 转自 https://blog.csdn.net/MaXiaoTiancsdn/article/details/115336499(本人参考这个步骤,在openEuler操作系统安装了OpenCV3.4.10的版本)  1.参考资料本文主要参考自以下文章,按照这篇文章的安装过程十分顺利,网上的贴子很多但是很......
  • Hessian矩阵以及在血管增强中的应用&mdash;OpenCV实现【2024年更新】
    有别于广为人知的Sobel、Canny等一阶算法,基于Hessian矩阵能够得到图像二阶结果,这将帮助我们深入分析图像本质。Hessian矩阵在图像处理中有着广泛的应用:其中在图像分割领域,包括边缘检测、纹理分析等;在图像增强领域,包括边缘增强、边缘消除等。本文从Hessian矩阵定义出发,通过清晰简......
  • Python基于opencv实现的人脸识别--入门项目
    先去opencv官网下载人脸识别的训练集https://opencv.org/releases/解压目录要记录主要使用haarcascade_frontalface_default.xml摄像头录入人脸(可选)可以弄一个文件夹,里面放一堆图片importcv2face_name='xxxx'#该人脸的名字#加载OpenCV人脸检测分类器face_cas......
  • 编译用于Qt的opencv问题解决
    CMakewasunabletofindabuildprogramcorrespondingto"MinGWMakefiles"解释:这个错误表明CMake无法找到用于生成Makefiles的构建程序。在使用CMake生成项目文件时,如果指定了"MinGWMakefiles",CMake需要一个Make工具来构建项目,而这个工具通常是由MinGW提供的。如......
  • cls_oracle_logs.sh脚本遭遇TNS-12508错误浅析
    cls_oracle_logs.sh脚本的输出日志中有TNS-12508错误,具体如下所示........................................................................LSNRCTL> Current Listener is gspLSNRCTL> Connecting to (DESCRIPTION=(ADDRESS=(PROTOCOL=IPC)(KEY=gsp)))TNS-12508: TNS:......
  • 1.0 - Opencv简记
    图像存储原理:RGB、CMY、HSV(H:色调,S:饱和度,颜色浓淡,V:亮度)。 Opencv应用:1)HSV颜色空间,利用H提取颜色。2)仿射变换通过三对点求出变换矩阵M;透视变换通过四对点求出变换矩阵M。3)二值化操作:普通二值化、自适应二值化(判断黑白的阈值由像素点邻域的像素确定)。......
  • 笔记:OpenCV3和Qt5 计算机视觉应用开发(一)
    目标:学习《OpenCV3和Qt5计算机视觉应用开发》,记录总结学习过程。第一章OpenCV和Qt简介开发环境系统版本:Ubuntu16.04.7LTSQt版本:Qt5.9.5OpenCV版本:opencv-3.3.0虚拟机版本:VMware®Workstation16Pro(16.2.2build-19200509)学习总结1,安装Linux开发环境终端运行:sudoapt-get......