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

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;

热门相关:大唐扫把星   亡国公主穿成王府寡妇:二嫁王妃   大文豪   重生童养媳:枭宠不乖娇妻   纨绔仙医