浅析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
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
step-4
点击查看代码
// 距离变换
Mat dist;
distanceTransform(binary, dist, DIST_L2, 3, CV_32FC1);
normalize(dist, dist, 0, 1, NORM_MINMAX);
imshow("距离变换", dist);//dist->CV_32FC1
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
你可能会很好奇,上图为啥是纯黑色?其实不然,因为轮廓灰度值很小,人的肉眼无法分辨。笔者使用如下代码将图像增强。
点击查看代码
.....
.......
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
同样的,上图也是几乎纯黑色图片,人的肉眼无法查看,使用图像增强代码,将图像增强,增强后的结果如下图所示。
如果你足够细心的话,你会发现每一个区域的灰度值不一样,其实这和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);
至此,完美撒花!
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;