分水岭算法可以将图像中的边缘转化成“山脉”,将均匀区域转化为“山谷”。首先,计算灰度图像的梯度,然后开始从用户指定点(或算法得到的点) 开始持续“灌注”盆地直到这些区域连在一起。基于这样产生的标记就可以吧区域合并在一起,合并后的区域又通过聚集的方式进行分割,好像图像被“填充”起来一样。通过这种方式,与指示点相连的盆地就为指示点“所拥有”。最终我们把图像分割成相应的标记区域。
例1:
#include"cv.h"
#include"highgui.h"
using namespace cv;
Mat markerMask, img;
Point prePt(-1,-1);
void onMouse(int event ,int x,int y,int flags,void* param){
if (event == CV_EVENT_LBUTTONUP || !(flags & CV_EVENT_FLAG_LBUTTON)) prePt = Point(-1,-1);
if (event == CV_EVENT_LBUTTONDOWN) prePt = Point(x,y);
if (event == CV_EVENT_MOUSEMOVE && (flags & CV_EVENT_FLAG_LBUTTON)){
Point pt(x,y);
if (prePt.x < 0) prePt = pt;
line(markerMask,prePt,pt,Scalar(255,255,255),5);
line(img, prePt, pt, Scalar(255, 255, 255), 5);
prePt = pt;
imshow("原图", img);
}
}
int main(){
Mat img0 = imread("D:\\tongliya.jpg",1);
img0.copyTo(img);
namedWindow("原图",1);
imshow("原图",img);
Mat imgGray;//最后显示watershed效果图用
cvtColor(img0,markerMask,CV_BGR2GRAY);
cvtColor(markerMask,imgGray,CV_GRAY2BGR);
markerMask = Scalar::all(0);
setMouseCallback("原图",onMouse,NULL);
while (1){
int c = waitKey(10);
if ((char)c == 'q' || (char)c == 27) return 0;
if ((char)c == 'r'){
img0.copyTo(img);
markerMask = Scalar::all(0);
imshow("原图",img);
}
if (char(c) == 'p'){
vector<vector<Point>> contours;
vector<Vec4i> hierarchy;
int count=0;
findContours(markerMask,contours,hierarchy,CV_RETR_CCOMP,CV_CHAIN_APPROX_SIMPLE);<span style="color:#33CC00;">//输入图像image必须为一个2值单通道图像</span>
if (contours.empty()) continue;
Mat markers(markerMask.size(),CV_32S);
markers = Scalar::all(0);
for (int idx = 0; idx >= 0; idx = hierarchy[idx][0], count++)<span style="color:#33CC00;">//hierarchy[idx][0]表示后一个轮廓</span>
drawContours(markers,contours,idx,Scalar::all(count+1),-1,8,hierarchy,INT_MAX);
if (count == 0) continue;
watershed(img0,markers);
vector<Vec3b> colorTab;
<span style="color:#33CC00;">//随机设置颜色</span>
for (int i = 0; i < count; 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 wshed(markers.size(),CV_8UC3);
for (int i = 0; i < markers.rows;i++)
for (int j = 0; j < markers.cols; j++){
int tmp = markers.at<int>(i,j);
if (tmp == -1) wshed.at<Vec3b>(i, j) = Vec3b(255,255,255);//边界值为-1
else if (tmp <= 0 || tmp>count) wshed.at<Vec3b>(i, j) = Vec3b(0,0,0);
else
wshed.at<Vec3b>(i, j) = colorTab[tmp-1];
}
imshow("分水岭变换1", wshed);
wshed = 0.5*wshed + 0.5*imgGray;
imshow("分水岭变换",wshed);
}
}
return 0;
}
个人理解:
1.watershed()函数接受两个参数,void watershed(InputArray image, InputOutputArraymarkers)
输入图像image为 8位3通道,markers为带有标记图像(or map),结果保存在markers中。
2.markers根据标记把像素 设置为1,2,3.。。。。
3.findContours contours参数为检测的轮廓数组,count记录轮廓组数,markers的结果也为1,2,....count,所以颜色也随机设为count种。
4.根据markers值,不同区域绘制不同颜色,边界值为-1,绘制为白色。
5.结果截图
例2:
#include "cv.h"
#include "highgui.h"
using namespace cv;
int main(){
Mat img = imread("D:\\tongliya.jpg");
imshow("original image",img);
Mat imgGray;
cvtColor(img,imgGray,CV_BGR2GRAY);
imshow("gray image", imgGray);
Mat binary;
//adaptiveThreshold(imgGray,binary,255,ADAPTIVE_THRESH_MEAN_C,CV_THRESH_BINARY,5,0);
threshold(imgGray,binary,100,255,CV_THRESH_BINARY_INV);
imshow("binary image", binary);
Mat fg;
erode(binary,fg,Mat(),Point(-1,-1),1);//255标注前景,腐蚀使高亮区域隔离且缩小,可以获取仅仅属于重要物体的像素
imshow("foreground image",fg);
Mat bg;
dilate(binary,bg,Mat(),Point(-1,-1),1);//背景用128标注,膨胀使高亮区域得到扩展和联通
threshold(bg,bg,1,128,THRESH_BINARY_INV);
imshow("background image", bg);
Mat markers(binary.size(),CV_8U,Scalar::all(0));
markers = fg + bg;//分水岭算法输入参数
imshow("markers image", markers);
markers.convertTo(markers,CV_32S);
watershed(img,markers);//更新标记图像
Mat tmp = markers.clone();
markers.convertTo(tmp,CV_8U);//返回标记图像,分水岭位于值0
imshow("Segmentation",tmp);
markers.convertTo(tmp,CV_8U,255,255);
imshow("Watersheds边界", tmp);
waitKey();
return 0;
}