OpenCV 学习笔记(Watershed)
Watershed,中文一般翻译为分水岭算法。分水岭算法是一种图像区域分割算法,它把位置接近,灰度值也接近的像素点连接起来形成一个封闭的区域。关于分水岭算法的具体原理可以参考下面的网址:
这里只介绍在 OpenCV中的如何使用 Watershed 算法。Opencv 中 watershed函数原型如下:
void watershed( InputArray image, InputOutputArray markers );
image 是 3 通道彩色(CV_8UC3)图像,markers 是单通道(CV_32S) 图像。为什么 markers 要求是CV_32S 呢,我觉得应该是图像的区域数可能会很大,CV_8U 不够用,索性就搞个 CV_32S,这个类型是绝对够用的。
这里重点说说markers 的作用,传统的 watershed 算法是不需要markers 的。但是经常会把图像分割成太多的小区域。markers 的作用就是我们预先把一些区域标注好,这些标注了的区域称之为种子点。watershed 算法会把这些标记的区域慢慢膨胀填充整个图像。
如何预先的标记这些区域呢?这就是个关键的问题了。我们可以交互式的人为的标记。也可以通过一些其他的算法来确定一部分区域。下面的例子就会讲解如何用 connectedComponents 函数来自动的标记区域。
首先我们来看看我们要标注的图像是什么样的:
这幅图像中有很多个小球。我们就是要标记出这些小球来。首先这幅图像中有些噪声,我们先进行滤波操作。这个噪声类似于椒盐噪声,所以用 medianBlur 效果会比较好。之后我们再用 GaussianBlur 进一步平滑图像。之后就可以做二值化了。做完二值化后再把图像腐蚀一下以确保小球之间不会相连。
cv::Mat image = cv::imread("zeiss.jpg");
cv::cvtColor(image(cv::Rect(0, 0, 1024, 700)), image, CV_BGRA2BGR);
cv::Mat imageBlur;
cv::medianBlur(image, imageBlur, 7);
cv::GaussianBlur(imageBlur, imageBlur, cv::Size(5, 5), 0);
cv::Mat grayMat, grayMatBW;
cv::cvtColor(imageBlur, grayMat, CV_BGR2GRAY);
cv::imshow("zeiss", grayMat);
cv::threshold( grayMat, grayMatBW, 0, 255 ,cv::THRESH_OTSU );
cv::Mat foreground;
cv::erode(grayMatBW, foreground, cv::Mat(), cv::Point(-1, -1), 4);
cv::imshow("foreground", foreground);
下面把每个小球都标记成不同的数字。这个可以使用 connectedComponents 函数。标记完小球后还要标记背景区域。方法类似。最后把小球和背景区域叠加到一张图中。
cv::Mat foreground;
cv::erode(grayMatBW, foreground, cv::Mat(), cv::Point(-1, -1), 4);
cv::imshow("foreground", foreground);
cv::connectedComponents(foreground, foreground, 4, CV_32S);
foreground.convertTo(foreground, CV_8U);
cv::Mat background;
cv::dilate(grayMatBW, background, cv::Mat(), cv::Point(-1, -1), 4);
cv::threshold(background, background, 1, 128, cv::THRESH_BINARY_INV);
bg.convertTo(bg, CV_32S);
cv::Mat marker = background + foreground;
marker.convertTo(marker, CV_32S);
至此,我们就准备好marker 了, 下面调用 watershed 函数。
cv::watershed(image, marker);
cv::Mat segment;
marker.convertTo(segment, CV_8U);
std::vector<cv::Vec3b> colormap = buildRandomColormap(255);
cv::Mat outcolor = imageFromColormap(segment, colormap);
cv::imshow("out", outcolor);
segment 就是我们分隔出的各个区域,取值为 128 的是背景区域。其余的各个小球。为了更好的显示,我们还写了两个辅助函数。
std::vector<cv::Vec3b> buildRandomColormap(int N)
{
cv::RNG rng;
std::vector<cv::Vec3b> colormap;
for(int i = 0; i < N; i++)
{
colormap.push_back( cv::Vec3b(rng.uniform(0, 255), rng.uniform(0, 255), rng.uniform(0, 255)) );
}
return colormap;
}
cv::Mat imageFromColormap(const cv::Mat &grayImage, const std::vector<cv::Vec3b> &colormap)
{
cv::Mat out(grayImage.size(), CV_8UC3);
out = cv::Scalar(0, 0, 0);
int N = colormap.size();
//cv::Mat_<cv::Vec3b> img(out);
for(int row = 0; row < grayImage.rows; row ++)
{
cv::Vec3b * pDest = out.ptr<cv::Vec3b>(row);
const uchar *pSrc = grayImage.ptr<const uchar>(row);
for(int col = 0; col < grayImage.cols; col ++)
{
if(pSrc[col] >= N)
{
pDest[col] = cv::Vec3b(pSrc[col], pSrc[col], pSrc[col]);
}
else
{
pDest[col] = colormap[pSrc[col]];
}
}
}
return out;
}
最终的输出效果如下图。可以看到各个区域都填充了不同的颜色。但是小球的边界找的不是特别的理想。这也是 watershed 算法的缺点吧。