OpenCV库中watershed函数(分水岭算法)的详细使用例程

声明:如果有写的不对的地方欢迎指正!

一、分水岭算法

关于分水岭算法的具体原理我就不说了,网上搜一下很多。OpenCV中的watershed函数实现的分水岭算法是基于“标记”的分割算法,用于解决传统的分水岭算法过度分割的问题。试想,一副图片中肯定有N多个“山谷”,它们中的很多是我们不想要的。
对于标记的原则,我总结是:你认为它们属于一个区域,就用标记将它们连接起来,对于另一个区域,再用另一个标记连接。就像这样:
图片中我认为有三个区域,所以做了三个标记。看到这里,你就可以把文章结尾的代码和图片拷到你的工程中试一试效果了。

二、代码分析

要想watershed函数,我们先要做一些准备工作:

1. 做标记

 做标记的原则在上面已经说过了,具体对应代码中on_Mouse函数里面的内容,这是一个鼠标事件回调函数。

else if (event == EVENT_MOUSEMOVE && (flags & EVENT_FLAG_LBUTTON))
{
    Point pt(x, y);
    if (previousPoint.x < 0)
        previousPoint = pt;
    //绘制白色线条
    line(inpaintMask, previousPoint, pt, Scalar::all(255), 5, 8, 0);
    line(srcImage1, previousPoint, pt, Scalar::all(255), 5, 8, 0);
    previousPoint = pt;
    imshow(WINDOW_NAME1, srcImage1);
}

在鼠标左键点按并移动时画线,其中的maskImage是一个CV_8UC1类型的掩模,绘制完的结果就是黑色背景上有几条线(标记),srcImage用于实时显示我们做标记的结果。

2. 寻找轮廓

对我们做过标记的maskImage寻找轮廓,这部分代码写在if ((char)c == '1')中,findContours函数这里不展开说明。
vector> contours;
vector hierarchy;

findContours(maskImage, contours, hierarchy, RETR_CCOMP, CHAIN_APPROX_SIMPLE);

3. 绘制轮廓

这里我们声明了一个CV_32S类型的Mat用于绘制轮廓,然后作为watershed的第二个参数传入。对于drawContours()函数的color参数,我们用的是Scalar::all(index + 1),也就是1,2,3这样的数,后面的代码我们会根据这些数绘制可以显示的图像。
for (int index = 0; index < contours.size(); index++)
    drawContours(maskWaterShed, contours, index, Scalar::all(index + 1), -1, 8, hierarchy, INT_MAX);

4. 分水岭算法分割

下面就是调用OpenCV中的watershed函数进行分割
watershed(srcImage_, maskWaterShed);
注意它的两个参数:srcImage_是没做任何修改的原图,CV_8UC3类型;
maskWaterShed声明为CV_32S类型(32位单通道),全部元素为0,然后作为drawContours的第一个参数传入
(第3步),在上面绘制轮廓,最后作为watershed的参数。另外,参数maskWaterShed是InputOutputArray类型,
作为输入,也作为输出保存函数调用的结果。
经过watershed函数的处理,不同区域间的值被置为-1(边界)没有标记清楚的区域被置为0,其他每个区域
的值保持不变:1,2,...,contours.size()。

5. 绘制结果图像

由于watershed的结果中只有-1,0,1,2这样的数,不能直接显示,所以我们还要做进一步的处理将结果显示出来
Mat resImage = Mat(srcImage.size(), CV_8UC3);  // 声明一个最后要显示的图像
for (int i = 0; i < maskImage.rows; i++)
{
    for (int j = 0; j < maskImage.cols; j++)
    {// 根据经过watershed处理过的maskWaterShed来绘制每个区域的颜色
        int index = maskWaterShed.at(i, j);  // 这里的maskWaterShed是经过watershed处理的
        if (index == -1)  // 区域间的值被置为-1(边界)
            resImage.at(i, j) = Vec3b(255, 255, 255);
        else if (index <= 0 || index > contours.size())  // 没有标记清楚的区域被置为0
            resImage.at(i, j) = Vec3b(0, 0, 0);
        else  // 其他每个区域的值保持不变:1,2,...,contours.size()
            resImage.at(i, j) = colorTab[index - 1];  // 然后把这些区域绘制成不同颜色
    }
}
imshow("resImage", resImage);
显示出来是这样

我们用三个标记图片分成了三个区域,每个区域用不同的颜色表示,区域间用白色的线隔开。
我们也可以用
addWeighted(resImage, 0.3, srcImage_, 0.7, 0, resImage);
imshow("分水岭结果", resImage);
将它和原图做加权相加,结果是这样:
或者将某个区域作为前景显示出来,另外两个区域作为背景显示为黑色,对应代码在if ((char)c == '0')中,这里只贴出结果

多次点按【0】键还可以显示不同前景。

三、代码和原图

#include 
#include 

using namespace std;
using namespace cv;

Mat srcImage, srcImage_, maskImage;
Mat maskWaterShed;  // watershed()函数的参数
Point clickPoint;	// 鼠标点下去的位置

void on_Mouse(int event, int x, int y, int flags, void*);
void helpText();

int main(int argc, char** argv)
{
	/* 操作提示 */
	helpText();

	srcImage = imread("fly.jpg");
	srcImage_ = srcImage.clone();  // 程序中srcImage会被改变,所以这里做备份
	maskImage = Mat(srcImage.size(), CV_8UC1);  // 掩模,在上面做标记,然后传给findContours
	maskImage = Scalar::all(0);

	int areaCount = 1;  // 计数,在按【0】时绘制每个区域

	imshow("在图像中做标记", srcImage);

	setMouseCallback("在图像中做标记", on_Mouse, 0);

	while (true)
	{
		int c = waitKey(0);

		if ((char)c == 27)	// 按【ESC】键退出
			break;

		if ((char)c == '2')  // 按【2】恢复原图
		{
			maskImage = Scalar::all(0);
			srcImage = srcImage_.clone();
			imshow("在图像中做标记", srcImage);
		}

		if ((char)c == '1')  // 按【1】处理图片
		{
			vector> contours;
			vector hierarchy;

			findContours(maskImage, contours, hierarchy, RETR_CCOMP, CHAIN_APPROX_SIMPLE);

			if (contours.size() == 0)  // 如果没有做标记,即没有轮廓,则退出该if语句
				break;
			cout << contours.size() << "个轮廓" << endl;

			maskWaterShed = Mat(maskImage.size(), CV_32S);
			maskWaterShed = Scalar::all(0);

			/* 在maskWaterShed上绘制轮廓 */
			for (int index = 0; index < contours.size(); index++)
				drawContours(maskWaterShed, contours, index, Scalar::all(index + 1), -1, 8, hierarchy, INT_MAX);
			
			/* 如果imshow这个maskWaterShed,我们会发现它是一片黑,原因是在上面我们只给它赋了1,2,3这样的值,通过代码80行的处理我们才能清楚的看出结果 */
			watershed(srcImage_, maskWaterShed);  // 注释一

			vector colorTab;  // 随机生成几种颜色
			for (int i = 0; i < contours.size(); i++)
			{
				int b = theRNG().uniform(0, 255);
				int g = theRNG().uniform(0, 255);
				int r = theRNG().uniform(0, 255);

				colorTab.push_back(Vec3b((uchar)b, (uchar)g, (uchar)r));
			}

			Mat resImage = Mat(srcImage.size(), CV_8UC3);  // 声明一个最后要显示的图像
			for (int i = 0; i < maskImage.rows; i++)
			{
				for (int j = 0; j < maskImage.cols; j++)
				{	// 根据经过watershed处理过的maskWaterShed来绘制每个区域的颜色
					int index = maskWaterShed.at(i, j);  // 这里的maskWaterShed是经过watershed处理的
					if (index == -1)  // 区域间的值被置为-1(边界)
						resImage.at(i, j) = Vec3b(255, 255, 255);
					else if (index <= 0 || index > contours.size())  // 没有标记清楚的区域被置为0
						resImage.at(i, j) = Vec3b(0, 0, 0);
					else  // 其他每个区域的值保持不变:1,2,...,contours.size()
						resImage.at(i, j) = colorTab[index - 1];  // 然后把这些区域绘制成不同颜色
				}
			}
			imshow("resImage", resImage);
			addWeighted(resImage, 0.3, srcImage_, 0.7, 0, resImage);
			imshow("分水岭结果", resImage);
		}

		if ((char)c == '0')  // 多次点按【0】依次显示每个被分割的区域,需要先按【1】处理图像
		{
			Mat resImage = Mat(srcImage.size(), CV_8UC3);  // 声明一个最后要显示的图像
			for (int i = 0; i < maskImage.rows; i++)
			{
				for (int j = 0; j < maskImage.cols; j++)
				{
					int index = maskWaterShed.at(i, j);
					if (index == areaCount)
						resImage.at(i, j) = srcImage_.at(i, j);
					else
						resImage.at(i, j) = Vec3b(0, 0, 0);
				}
			}
			imshow("分水岭结果", resImage);
			areaCount++;
			if (areaCount == 4)
				areaCount = 1;
		}
	}

	return 0;
}

void on_Mouse(int event, int x, int y, int flags, void*)
{
	// 如果鼠标不在窗口中则返回
	if (x < 0 || x >= srcImage.cols || y < 0 || y >= srcImage.rows)
		return;

	// 如果鼠标左键被按下,获取鼠标当前位置;当鼠标左键按下并且移动时,绘制白线;
	if (event == EVENT_LBUTTONDOWN)
	{
		clickPoint = Point(x, y);
	}		
	else if (event == EVENT_MOUSEMOVE && (flags & EVENT_FLAG_LBUTTON))
	{
		Point point(x, y);
		line(maskImage, clickPoint, point, Scalar::all(255), 5, 8, 0);
		line(srcImage, clickPoint, point, Scalar::all(255), 5, 8, 0);
		clickPoint = point;
		imshow("在图像中做标记", srcImage);
	}
}

void helpText()
{
	cout << "先用鼠标在图片窗口中标记出大致的区域" << endl;
	cout << "如果想把图片分割为N个区域,就要做N个标记" << endl;
	cout << "键盘按键【1】	- 运行的分水岭分割算法" << endl;
	cout << "键盘按键【2】	- 恢复原始图片" << endl;
	cout << "键盘按键【0】	- 依次分割每个区域(必须先按【1】)" << endl;
	cout << "键盘按键【ESC】	- 退出程序" << endl << endl;
}

/* 注释一:watershed(srcImage_, maskWaterShed);
 * 注意它的两个参数
 * srcImage_是没做任何修改的原图,CV_8UC3类型
 * maskWaterShed声明为CV_32S类型(32位单通道),且全部元素为0
 *		然后作为drawContours的第一个参数传入,在上面绘制轮廓
 *		最后作为watershed的参数
 * 另外,watershed的第二个参数maskWaterShed是InputOutputArray类型
 *		即作为输入,也作为输出保存函数调用的结果
 */


你可能感兴趣的:(OpenCV,C++,OpenCV)