Image Segmentation with Distance Transform and Watershed Algorithm
在本教程中,您将学习如何:
使用 OpenCV 函数 cv::filter2D 执行一些拉普拉斯滤波以进行图像锐化
使用 OpenCV 函数 cv::distanceTransform 以获得二值图像的派生表示,其中每个像素的值被其到最近的背景像素的距离替换
使用 OpenCV 函数 cv::watershed 将图像中的对象与背景隔离开来
使用距离变换和分水岭算法进行图像分割
本教程代码如下所示。 您也可以从这里下载opencv/imageSegmentation.cpp at 4.x · opencv/opencv · GitHub
/**
* @brief Sample code showing how to segment overlapping objects using Laplacian filtering, in addition to Watershed and Distance Transformation
*示例代码显示如何使用拉普拉斯过滤以及分水岭和距离变换来分割重叠对象
* @author OpenCV Team
*/
#include
#include
#include
#include
using namespace std;
using namespace cv;
int main(int argc, char *argv[])
{
//! [load_image]
// 加载图像
CommandLineParser parser( argc, argv, "{@input | cards.png | input image}" );
Mat src = imread( samples::findFile( parser.get( "@input" ) ) );
if( src.empty() )
{
cout << "Could not open or find the image!\n" << endl;
cout << "Usage: " << argv[0] << " " << endl;
return -1;
}
//显示源图像
imshow("Source Image", src);
//! [load_image]
//! [black_bg]
// Change the background from white to black, since that will help later to extract
// better results during the use of Distance Transform将背景从白色更改为黑色,因为这将有助于以后在使用距离变换期间提取更好的结果
for ( int i = 0; i < src.rows; i++ ) {
for ( int j = 0; j < src.cols; j++ ) {
if ( src.at(i, j) == Vec3b(255,255,255) )
{//将背景从白色更改为黑色
src.at(i, j)[0] = 0;
src.at(i, j)[1] = 0;
src.at(i, j)[2] = 0;
}
}
}
// 显示输出图像
imshow("Black Background Image", src);
//! [black_bg]
//! [sharp]
// 创建一个我们将用来锐化图像的内核
Mat kernel = (Mat_(3,3) <<
1, 1, 1,
1, -8, 1,
1, 1, 1); // 二阶导数的近似值,一个相当强的核
// do the laplacian filtering as it is
// well, we need to convert everything in something more deeper then CV_8U
// because the kernel has some negative values,
// and we can expect in general to have a Laplacian image with negative values
// BUT a 8bits unsigned int (the one we are working with) can contain values from 0 to 255
// so the possible negative number will be truncated
// 按原样进行拉普拉斯滤波
// 好吧,我们需要把所有东西都转换成比 CV_8U 更深的东西 CV_32F
// 因为内核有一些负值,
// 我们通常可以期望有一个带有负值的拉普拉斯图像
// 但是一个 8 位无符号整数(我们正在使用的整数)可以包含从 0 到 255 的值
// 所以可能的负数将被截断
Mat imgLaplacian;//拉普拉斯变换图
filter2D(src, imgLaplacian, CV_32F, kernel);
Mat sharp;//锐化图
src.convertTo(sharp, CV_32F);
Mat imgResult = sharp - imgLaplacian;//锐化图-拉普拉斯变换图 ????????????为什么要这么做?
//转换回 8 位灰度图
imgResult.convertTo(imgResult, CV_8UC3);
imgLaplacian.convertTo(imgLaplacian, CV_8UC3);
// imshow( "Laplace Filtered Image", imgLaplacian );//显示拉普拉斯变换图
imshow( "New Sharped Image", imgResult );//显示锐化图
//! [sharp]
//! [bin]
// 从源图像创建二进制图像
Mat bw;
cvtColor(imgResult, bw, COLOR_BGR2GRAY);
threshold(bw, bw, 40, 255, THRESH_BINARY | THRESH_OTSU);
imshow("Binary Image", bw);
//! [bin]
//! [dist]
// 执行距离变换算法Perform the distance transform algorithm
Mat dist;
distanceTransform(bw, dist, DIST_L2, 3);
// 对 range = {0.0, 1.0} 的距离图像进行归一化 Normalize the distance image for range = {0.0, 1.0}
// 所以我们可以对其进行可视化和阈值化 so we can visualize and threshold it
normalize(dist, dist, 0, 1.0, NORM_MINMAX);
imshow("Distance Transform Image", dist);//显示距离变换的图像
//! [dist]
//! [peaks]
// 获取峰值的阈值 Threshold to obtain the peaks
// 这将是前景对象的标记 This will be the markers for the foreground objects
threshold(dist, dist, 0.4, 1.0, THRESH_BINARY);//二值化
// Dilate a bit the dist image
Mat kernel1 = Mat::ones(3, 3, CV_8U);
dilate(dist, dist, kernel1);//膨胀二值化后的图像
imshow("Peaks", dist);
//! [peaks]
//! [seeds]
// Create the CV_8U version of the distance image
// It is needed for findContours() 创建距离图像的 CV_8U 版本 findContours() 需要它
Mat dist_8u;
dist.convertTo(dist_8u, CV_8U);
// 查找所有标记 Find total markers
vector > contours;
findContours(dist_8u, contours, RETR_EXTERNAL, CHAIN_APPROX_SIMPLE);//找到所有轮廓
// Create the marker image for the watershed algorithm
//为分水岭算法创建 标记图像
Mat markers = Mat::zeros(dist.size(), CV_32S);//32位单通道 黑色背景
//绘制前景标记 Draw the foreground markers
for (size_t i = 0; i < contours.size(); i++)
{//cv2.drawContours(image, contours, contourIdx, color, thickness=None, lineType=None, hierarchy=None, maxLevel=None, offset=None)
drawContours(markers, contours, static_cast(i), Scalar(static_cast(i)+1), -1);
}
// 绘制背景标记 Draw the background marker
circle(markers, Point(5,5), 3, Scalar(255), -1);//标记背景 左上角白色圆点
imshow("Markers", markers*10000);//报错,需注释掉 imshow函数在显示图像时,会将各种类型的数据都映射到[0, 255]。
//! [seeds]
//! [watershed]
// 执行分水岭算法 Perform the watershed algorithm
watershed(imgResult, markers);// watershed(srcImage_, maskWaterShed);
Mat mark;
markers.convertTo(mark, CV_8U);
bitwise_not(mark, mark);//将二值图片的效果反转既黑色变白色,白色变黑色。
//imshow("Markers_v2", mark); // 如果您想查看标记如何,请取消注释
// image looks like at that point
// 生成随机颜色
vector colors;
for (size_t i = 0; i < contours.size(); i++)//每个轮廓一个颜色
{
int b = theRNG().uniform(0, 256);
int g = theRNG().uniform(0, 256);
int r = theRNG().uniform(0, 256);
colors.push_back(Vec3b((uchar)b, (uchar)g, (uchar)r));
}
// 创建最终结果图像
Mat dst = Mat::zeros(markers.size(), CV_8UC3);
// 用随机颜色填充标记的对象
for (int i = 0; i < markers.rows; i++)
{
for (int j = 0; j < markers.cols; j++)
{
int index = markers.at(i,j);//标记的索引 1开始
if (index > 0 && index <= static_cast(contours.size()))
{
dst.at(i,j) = colors[index-1];//三通道像素值
}
}
}
// 可视化最终图像
imshow("Final Result", dst);
//! [watershed]
waitKey();
return 0;
}
// 加载图像
CommandLineParser parser( argc, argv, "{@input | cards.png | input image}" );
Mat src = imread( samples::findFile( parser.get( "@input" ) ) );
if( src.empty() )
{
cout << "Could not open or find the image!\n" << endl;
cout << "Usage: " << argv[0] << " " << endl;
return -1;
}
// 显示源图像
imshow("Source Image", src);
2. 然后,如果我们有一个白色背景的图像,最好将其转换为黑色。 当我们应用距离变换时,这将帮助我们更轻松地区分前景对象:
// Change the background from white to black, since that will help later to extract
// better results during the use of Distance Transform
Mat mask;
inRange(src, Scalar(255, 255, 255), Scalar(255, 255, 255), mask);
src.setTo(Scalar(0, 0, 0), mask);
// Show output image
imshow("Black Background Image", src);
3. 之后我们将锐化我们的图像以锐化前景对象的边缘。 我们将应用具有非常强滤波器(二阶导数的近似)的拉普拉斯滤波器:
//创建一个我们将用来锐化图像的内核
Mat kernel = (Mat_(3,3) <<
1, 1, 1,
1, -8, 1, //二阶导数的近似值,一个相当强的核
1, 1, 1); // an approximation of second derivative, a quite strong kernel
// well, we need to convert everything in something more deeper then CV_8U
// 好吧,我们需要把所有东西都转换成比 CV_8U 更深的东西
// because the kernel has some negative values, 因为内核有一些负值,
// and we can expect in general to have a Laplacian image with negative values
// 我们通常可以期望有一个带有负值的拉普拉斯图像
// BUT a 8bits unsigned int (the one we are working with) can contain values from 0 to 255但是一个 8 位无符号整数(我们正在使用的整数)可以包含从 0 到 255 的值
// so the possible negative number will be truncated所以可能的负数将被截断
Mat imgLaplacian;
filter2D(src, imgLaplacian, CV_32F, kernel);// 按原样进行拉普拉斯过滤
Mat sharp;
src.convertTo(sharp, CV_32F);// 转换成比 CV_8U 更深的东西, 因为内核有一些负值
Mat imgResult = sharp - imgLaplacian;
// convert back to 8bits gray scale转换回 8 位灰度
imgResult.convertTo(imgResult, CV_8UC3);
imgLaplacian.convertTo(imgLaplacian, CV_8UC3);
// imshow( "Laplace Filtered Image", imgLaplacian );
imshow( "New Sharped Image", imgResult );
4. 现在我们将新锐化的源图像分别转换为灰度和二进制图像:
// 从源图像创建二进制图像
Mat bw;
cvtColor(imgResult, bw, COLOR_BGR2GRAY);
threshold(bw, bw, 40, 255, THRESH_BINARY | THRESH_OTSU);
imshow("Binary Image", bw);
5. 我们现在准备在二值图像上应用距离变换。 此外,我们对输出图像进行归一化,以便能够可视化和阈值分割结果:
// Perform the distance transform algorithm
Mat dist;
distanceTransform(bw, dist, DIST_L2, 3);
// 归一化距离图像的范围 = {0.0, 1.0}
// 所以我们可以对其进行可视化和阈值化
normalize(dist, dist, 0, 1.0, NORM_MINMAX);
imshow("Distance Transform Image", dist);
6. 我们对 dist 图像进行阈值化,然后执行一些形态学操作(即膨胀),以便从上图中提取峰值:extract the peaks
// 获取峰值的阈值Threshold to obtain the peaks
// 这将是前景对象的标记
threshold(dist, dist, 0.4, 1.0, THRESH_BINARY);
// 将 dist 图像放大一点Dilate a bit the dist image
Mat kernel1 = Mat::ones(3, 3, CV_8U);
dilate(dist, dist, kernel1);
imshow("Peaks", dist);
7. 然后我们在 cv::findContours 函数的帮助下,从每个 blob 中为分水岭算法创建一个种子/标记:
// Create the CV_8U version of the distance image创建距离图像的 CV_8U 版本
// It is needed for findContours()
Mat dist_8u;
dist.convertTo(dist_8u, CV_8U);
// 查找总的标记Find total markers
vector > contours;
findContours(dist_8u, contours, RETR_EXTERNAL, CHAIN_APPROX_SIMPLE);
// 为分水岭算法创建标记图像
Mat markers = Mat::zeros(dist.size(), CV_32S);
// 绘制前景标记
for (size_t i = 0; i < contours.size(); i++)
{
drawContours(markers, contours, static_cast(i), Scalar(static_cast(i)+1), -1);
}
//绘制背景标记
circle(markers, Point(5,5), 3, Scalar(255), -1);
Mat markers8u;
markers.convertTo(markers8u, CV_8U, 10);
imshow("Markers", markers8u);
8. 最后,我们可以应用分水岭算法,并将结果可视化:
// Perform the watershed algorithm
watershed(imgResult, markers);
Mat mark;
markers.convertTo(mark, CV_8U);
bitwise_not(mark, mark);
// imshow("Markers_v2", mark); // uncomment this if you want to see how the mark
// image looks like at that point如果您想查看标记图像当时的样子,请取消注释
// Generate random colors
vector colors;
for (size_t i = 0; i < contours.size(); i++)
{
int b = theRNG().uniform(0, 256);
int g = theRNG().uniform(0, 256);
int r = theRNG().uniform(0, 256);
colors.push_back(Vec3b((uchar)b, (uchar)g, (uchar)r));
}
// Create the result image
Mat dst = Mat::zeros(markers.size(), CV_8UC3);
// Fill labeled objects with random colors
for (int i = 0; i < markers.rows; i++)
{
for (int j = 0; j < markers.cols; j++)
{
int index = markers.at(i,j);
if (index > 0 && index <= static_cast(contours.size()))
{
dst.at(i,j) = colors[index-1];
}
}
}
// Visualize the final image
imshow("Final Result", dst);
参考:
OpenCV库中watershed函数(分水岭算法)的详细使用例程_SugarAnnie的博客-CSDN博客_watershed函数
OpenCV(26)图像分割 -- 距离变换与分水岭算法(硬币检测、扑克牌检测、车道检测)__睿智_的博客-CSDN博客