OpenCV学习(二十) :分水岭算法:watershed()
参考博客:
OpenCV—分水岭算法
图像处理——分水岭算法
OpenCV学习(7) 分水岭算法(1)
Opencv分水岭算法——watershed自动图像分割用法 -牧野-
分水岭算法是一种图像区域分割法,在分割的过程中,它会把跟临近像素间的相似性作为重要的参考依据,从而将在空间位置上相近并且灰度值相近(求梯度)的像素点互相连接起来构成一个封闭的轮廓。分水岭算法常用的操作步骤:彩色图像灰度化,然后再求梯度图,最后在梯度图的基础上进行分水岭算法,求得分段图像的边缘线。
1、watershed()函数
void watershed(
InputArray image, // 必须是一个8bit 3通道彩色图像矩阵序列
InputOutputArray markers // markers必须包含了种子点信息:算法会根据markers传入的轮廓作为种子(也就是所谓的注水点),
//对图像上其他的像素点根据分水岭算法规则进行判断,并对每个像素点的区域归属进行划定,
//直到处理完图像上所有像素点。而区域与区域之间的分界处的值被置为“-1”,以做区分。==markers==
在将图像传递给函数之前,您必须粗略地勾勒出索引为正(>0)的图像标记中的所需区域。
因此,每个区域都表示为一个或多个具有像素值1,2,3的连接组件,以此类推。
可以使用findcontours()和drawcontours()从二进制掩码中检索这些标记。这些标记是未来图像区域的“种子”。
标记中的所有其他像素,如果它们与轮廓区域的关系未知,应该由算法定义,则应该设置为0。
在函数输出中,标记中的每个像素被设置为“seed”组件的值,或者在区域之间的边界处设置为-1。
);
2、示例一:手动添加 markers
watershed图像自动分割的实现步骤:
1)图像灰度化、滤波、Canny边缘检测
2)查找轮廓,并且把轮廓信息按照不同的编号绘制到watershed的第二个入参merkers上,相当于标记注水点。
3)watershed分水岭运算
4)绘制分割出来的区域,视觉控还可以使用随机颜色填充,或者跟原始图像融合以下,以得到更好的显示效果。
#include <opencv2/opencv.hpp>using namespace cv;
using namespace std;#define WINDOW_NAME1 "【程序窗口1】" //为窗口标题定义的宏
#define WINDOW_NAME2 "【分水岭算法效果图】" //为窗口标题定义的宏Mat g_maskImage, g_srcImage;
Point prevPt(-1, -1);int main()
{//输出一些帮助信息printf( "\n\n\n\t欢迎来到【分水岭算法】示例程序~\n\n");printf( "\t请先用鼠标在图片窗口中标记出大致的区域,\n\n\t然后再按键【1】或者【SPACE】启动算法。""\n\n\t按键操作说明: \n\n""\t\t键盘按键【1】或者【SPACE】- 运行的分水岭分割算法\n""\t\t键盘按键【2】- 恢复原始图片\n""\t\t键盘按键【ESC】- 退出程序\n\n\n");//【1】载入原图并显示,初始化掩膜和灰度图g_srcImage = imread("F:/C++/2. OPENCV 3.1.0/TEST/1.jpg", 1);imshow( WINDOW_NAME1, g_srcImage );Mat srcImage,grayImage;g_srcImage.copyTo(srcImage);cvtColor(g_srcImage, g_maskImage, COLOR_BGR2GRAY); // 彩色图转灰度图cvtColor(g_maskImage, grayImage, COLOR_GRAY2BGR); // 转回三通道图g_maskImage = Scalar::all(0);//【2】设置鼠标回调函数setMouseCallback( WINDOW_NAME1, on_Mouse, 0 );//【3】轮询按键,进行处理while(1){//获取键值int c = waitKey(0);//若按键键值为ESC时,退出if( (char)c == 27 )break;//按键键值为2时,恢复源图if( (char)c == '2' ){g_maskImage = Scalar::all(0);srcImage.copyTo(g_srcImage);imshow( "image", g_srcImage );}//若检测到按键值为1或者空格,则进行处理if( (char)c == '1' || (char)c == ' ' ){//定义一些参数int i, j, compCount = 0;vector<vector<Point> > contours;vector<Vec4i> hierarchy;//寻找轮廓findContours(g_maskImage, contours, hierarchy, CV_RETR_CCOMP, CV_CHAIN_APPROX_SIMPLE);//轮廓为空时的处理if( contours.empty() )continue;//拷贝掩膜Mat maskImage(g_maskImage.size(), CV_32S); //maskImage = Scalar::all(0);//循环绘制出轮廓for( int index = 0; index >= 0; index = hierarchy[index][0], compCount++ )drawContours(maskImage, contours, index, Scalar::all(compCount+1), -1, 8, hierarchy, INT_MAX);//compCount为零时的处理if( compCount == 0 )continue;//生成随机颜色vector<Vec3b> colorTab;for( i = 0; i < compCount; 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));}// 计算处理时间并输出到窗口中double dTime = (double)getTickCount();// 分水岭算法watershed( srcImage, maskImage ); dTime = (double)getTickCount() - dTime;printf( "\t处理时间 = %gms\n", dTime*1000./getTickFrequency() );//双层循环,将分水岭图像遍历存入watershedImage中Mat watershedImage(maskImage.size(), CV_8UC3);for( i = 0; i < maskImage.rows; i++ )for( j = 0; j < maskImage.cols; j++ ){int index = maskImage.at<int>(i,j);if( index == -1 )watershedImage.at<Vec3b>(i,j) = Vec3b(255,255,255);else if( index <= 0 || index > compCount )watershedImage.at<Vec3b>(i,j) = Vec3b(0,0,0);elsewatershedImage.at<Vec3b>(i,j) = colorTab[index - 1];}//混合灰度图和分水岭效果图并显示最终的窗口watershedImage = watershedImage*0.5 + grayImage*0.5;imshow( WINDOW_NAME2, watershedImage );}}waitKey(0);return 0;
}
鼠标回调函数:
static void on_Mouse( int event, int x, int y, int flags, void* )
{// 1)处理鼠标不在窗口中的情况if( x < 0 || x >= g_srcImage.cols || y < 0 || y >= g_srcImage.rows )return;// 2)处理鼠标左键相关消息if( event == CV_EVENT_LBUTTONUP || !(flags & CV_EVENT_FLAG_LBUTTON) )prevPt = Point(-1,-1);else if( event == CV_EVENT_LBUTTONDOWN )prevPt = Point(x,y);// 3)鼠标左键按下并移动,绘制出白色线条else if( event == CV_EVENT_MOUSEMOVE && (flags & CV_EVENT_FLAG_LBUTTON) ){Point pt(x, y); // 当前坐标值if( prevPt.x < 0 )prevPt = pt;line( g_maskImage, prevPt, pt, Scalar::all(200), 5, 8, 0 ); // 掩模图line( g_srcImage, prevPt, pt, Scalar::all(200), 5, 8, 0 ); //prevPt = pt;imshow(WINDOW_NAME1, g_srcImage);}
}
结果:
3、示例二:自动添加 markers
int main()
{//1、载入原图并显示,初始化掩膜和灰度图Mat image = imread("F:/C++/2. OPENCV 3.1.0/TEST/1.jpg", 1);imshow("Source Image",image);// 2、灰度化,滤波,Canny边缘检测Mat imageGray;cvtColor(image,imageGray,CV_RGB2GRAY);//灰度转换GaussianBlur(imageGray,imageGray,Size(5,5),2); //高斯滤波imshow("Gray Image",imageGray);Canny(imageGray,imageGray,80,150);imshow("Canny Image",imageGray);// 3、查找轮廓vector<vector<Point>> contours;vector<Vec4i> hierarchy;findContours(imageGray,contours,hierarchy,RETR_TREE,CHAIN_APPROX_SIMPLE,Point());Mat imageContours=Mat::zeros(image.size(),CV_8UC1); //轮廓Mat marks(image.size(),CV_32S); //Opencv分水岭第二个矩阵参数marks=Scalar::all(0);int index = 0;int compCount = 0;for( ; index >= 0; index = hierarchy[index][0], compCount++ ){// 4、对marks进行标记,对不同区域的轮廓进行编号,相当于设置注水点,有多少轮廓,就有多少注水点drawContours(marks, contours, index, Scalar::all(compCount+1), 1, 8, hierarchy); // 每个轮廓的标记点不一样drawContours(imageContours,contours,index,Scalar(255),1,8,hierarchy);}//我们来看一下传入的矩阵marks里是什么东西Mat marksShow;convertScaleAbs(marks,marksShow); // 转换前的 marker 图imshow("marksBefore",marksShow);imshow("imageContours 轮廓",imageContours);// 5、分水岭算法watershed(image,marks);// 6、我们再来看一下分水岭算法之后的矩阵marks里是什么东西Mat afterWatershed;convertScaleAbs(marks,afterWatershed); // 转换后的 marker 图imshow("After Watershed",afterWatershed);// 7、对每一个区域进行颜色填充Mat PerspectiveImage=Mat::zeros(image.size(),CV_8UC3);for(int i=0;i<marks.rows;i++){for(int j=0;j<marks.cols;j++){int index=marks.at<int>(i,j);if(marks.at<int>(i,j)==-1){PerspectiveImage.at<Vec3b>(i,j)=Vec3b(255,255,255);}else{PerspectiveImage.at<Vec3b>(i,j) =RandomColor(index);}}}imshow("After ColorFill",PerspectiveImage);//分割并填充颜色的结果跟原始图像融合Mat wshed;addWeighted(image,0.4,PerspectiveImage,0.6,0,wshed);imshow("AddWeighted Image",wshed);waitKey(0);return 0;
}
Vec3b RandomColor(int value) //生成随机颜色函数
{value=value%255; //生成0~255的随机数RNG rng;int aa=rng.uniform(0,value);int bb=rng.uniform(0,value);int cc=rng.uniform(0,value);return Vec3b(aa,bb,cc);
}
3、基于距离的分水岭算法
使用分水岭算法进行图像分割:
(一)获取灰度图像,二值化图像,进行形态学操作,消除噪点
(二)在距离变换前加上一步操作:通过对上面形态学去噪点后的图像,进行膨胀操作,可以得到大部分都是背景的区域(原黑色不是我们需要的部分是背景)
(三)使用距离变换distanceTransform获取确定的前景色
相关知识补充(重点)
(四)在获取了背景区域和前景区域(其实前景区域是我们的种子,我们将从这里进行灌水,向四周涨水,但是这个需要在markers中表示)后,这两个区域中有未重合部分(注1)怎么办?首先确定这些区域(寻找种子)
开始获取未知区域unknown(栅栏会创建在这一区域),为下一步获取种子做准备
(五)获取了这些区域,我们可以获取种子,这是通过connectedComponents实现,获取masker标签,确定的前景区域会在其中显示为以1开始的数据,这就是我们的种子,会从这里开始漫水
重点:
(六)根据未知区域unknown在markers中设置栅栏,并将背景区域加入种子区域,一起漫水
(七)根据种子开始漫水,让水漫起来找到最后的漫出点(栅栏边界),越过这个点后各个山谷中水开始合并。注意watershed会将找到的栅栏在markers中设置为-1