实验原理
在计算机视觉中,分水岭算法(Watershed Algorithm)是一种基于形态学的分割方法,常用于图像分割。OpenCV 提供了 cv::watershed 函数来实现这一算法。分水岭算法的主要思想是将图像视为地形表面,其中像素强度值代表地形高度。算法试图找到图像中的“盆地”(区域)之间的“山脊线”,这些山脊线即为分割边界。
在计算机视觉和图像处理中,分水岭算法(Watershed Algorithm)是一种用于图像分割的重要技术。OpenCV提供了分水岭算法的实现,可以帮助开发者进行图像分割任务。分水岭算法的目标是将图像分割成多个区域或标记,每个区域对应图像中的一个对象。
分水岭算法的基本概念
分水岭算法的灵感来源于地理学中的分水岭概念:地形中的水流汇聚到最低点形成河流,而这些河流之间的高地称为分水岭。在图像分割中,图像中的“山峰”和“山谷”分别对应于图像的明亮区域和暗淡区域,而“分水岭”则对应于不同区域间的边界。
函数原型
在OpenCV中,分水岭算法主要是通过cv::watershed函数来实现的。该函数需要两个主要输入:一个是待分割的图像,另一个是一个标记(Markers)矩阵。
int watershed(InputArray _image, InputOutputArray _markers);
参数说明
_image: 输入图像,通常是灰度图像或彩色图像。
_markers: 输入/输出标记图像。
这是一个 32SC1 类型的单通道图像,其中每个像素值代表一个标记。
初始标记应该是已经确定的前景和背景区域。
输入输出标记矩阵,用于指示图像中的感兴趣区域(Foregroud)和背景区域(Background),以及可能的未知区域。
分水岭分割步骤
1.预处理:
对输入图像进行一些预处理,如灰度化、二值化、形态学操作等,如去噪、边缘检测等。
根据需要,可以转换为灰度图像。
2.标记初始化:
在预处理后的图像中生成标记,标记可以分为已知标记(确定的前景和背景区域)和未知标记(待分割的区域)
确定已知的前景和背景区域,并为它们分配唯一的标记值。
典型的做法是使用连通组件标记法(Connected Component Labeling)来找到这些区域。
对于未知区域,可以设置为零或其他特殊值表示未标记。
3.应用分水岭算法:
使用 cv::watershed 函数,传入图像和标记图像。
函数执行后,标记图像会被修改,其中包含了分割结果。
4.后处理:
可以根据需要对分割结果进行后处理,比如如去除小区域、填充孔洞,去除小的连通组件、平滑边界等。
示例代码1
下面是一个简单的示例代码,展示如何在OpenCV中使用分水岭算法进行图像分割:
#include <opencv2/opencv.hpp>
#include <iostream>
int main()
{
// 读取图像
cv::Mat src = cv::imread("path/to/image.jpg", cv::IMREAD_COLOR);
if (src.empty())
{
std::cout << "Error opening image" << std::endl;
return -1;
}
// 转换为灰度图像
cv::Mat gray;
cv::cvtColor(src, gray, cv::COLOR_BGR2GRAY);
// 二值化
cv::Mat binary;
cv::threshold(gray, binary, 0, 255, cv::THRESH_BINARY_INV + cv::THRESH_OTSU);
// 形态学开运算去除噪声
cv::Mat kernel = cv::getStructuringElement(cv::MORPH_ELLIPSE, cv::Size(3, 3));
cv::morphologyEx(binary, binary, cv::MORPH_OPEN, kernel, cv::Point(-1, -1), 2);
// 确定前景区域
cv::Mat sure_fg = binary.clone();
// 寻找图像轮廓
std::vector<std::vector<cv::Point>> contours;
cv::findContours(binary, contours, cv::RETR_EXTERNAL, cv::CHAIN_APPROX_SIMPLE);
// 创建标记矩阵
cv::Mat markers = cv::Mat::zeros(binary.size(), CV_32S);
// 设置标记
int marker = 1;
for (size_t i = 0; i < contours.size(); ++i)
{
cv::drawContours(markers, contours, static_cast<int>(i), cv::Scalar(marker), -1);
marker += 1;
}
// 执行分水岭算法
cv::watershed(src, markers);
// 将标记矩阵转换为可显示的格式
cv::Mat dst;
cv::convertScaleAbs(markers, dst);
// 显示结果
cv::namedWindow("Original Image", cv::WINDOW_NORMAL);
cv::imshow("Original Image", src);
cv::namedWindow("Watershed Result", cv::WINDOW_NORMAL);
cv::imshow("Watershed Result", dst);
cv::waitKey(0);
return 0;
}
代码解释
1. 读取图像:加载原始图像。
2. 转换为灰度图像:将彩色图像转换为灰度图像。
3. 二值化:使用Otsu方法进行自适应阈值分割,得到二值图像。
4. 形态学开运算:去除二值图像中的噪声。
5. 确定前景区域:复制二值图像作为确定的前景区域。
6. 寻找图像轮廓:使用cv::findContours函数找到图像中的轮廓。
7. 创建标记矩阵:初始化一个标记矩阵,用于存储标记信息。
8. 设置标记:为每个轮廓区域设置不同的标记值。
9. 执行分水岭算法:调用cv::watershed函数进行分水岭分割。
10. 显示结果:将标记矩阵转换为可视化的格式,并显示分割结果。
注意事项
•预处理:分水岭算法对图像质量非常敏感,因此预处理步骤非常重要,尤其是二值化和去噪。
•标记设置:标记矩阵的设置决定了分割的效果,需要合理选择标记区域。
•结果解释:分水岭算法的结果需要进一步解释,可能需要进行后处理以去除小区域或填补孔洞。
通过上述步骤和示例代码,您可以了解如何在OpenCV中使用分水岭算法进行图像分割。
根据具体的应用场景,您可能需要调整预处理和标记设置的策略。
运行结果1
示例代码2
下面是一个简单的示例,展示了如何使用 OpenCV 的 cv::watershed 函数来进行图像分割:
#include "pch.h"
#include <opencv2/opencv.hpp>
#include <iostream>
int main()
{
cv::Mat img = cv::imread("033.jpeg");
if (img.empty())
{
std::cout << "Error: Image not found." << std::endl;
return -1;
}
// 转换为灰度图像
cv::Mat gray;
cvtColor(img, gray, cv::COLOR_BGR2GRAY);
// 进行阈值处理以获取二值图像
cv::Mat binary;
threshold(gray, binary, 0, 255, cv::THRESH_BINARY_INV + cv::THRESH_OTSU);
// 进行形态学开运算去除噪声
cv::Mat kernel = getStructuringElement(cv::MORPH_ELLIPSE, cv::Size(3, 3));
morphologyEx(binary, binary, cv::MORPH_OPEN, kernel, cv::Point(-1, -1));
// 寻找轮廓
std::vector<std::vector<cv::Point>> contours;
std::vector<cv::Vec4i> hierarchy;
findContours(binary, contours, hierarchy, cv::RETR_CCOMP, cv::CHAIN_APPROX_SIMPLE);
// 创建标记图像
cv::Mat markers = cv::Mat::zeros(img.size(), CV_32S);
// 初始化标记
for (size_t i = 0; i < contours.size(); i++)
{
drawContours(markers, contours, static_cast<int>(i), cv::Scalar(i + 1), -1);
}
// 应用分水岭算法
cv::watershed(img, markers);
// 将标记图像转换为可视化格式
cv::Mat vis;
//markers.convertTo(vis, CV_8UC3, cv::Scalar(255, 255, 255) / 255);
markers.convertTo(vis, CV_8UC3, 255);
// 显示结果
cv::namedWindow("Original Image", cv::WINDOW_NORMAL);
cv::imshow("Original Image", img);
cv::namedWindow("Markers After Watershed", cv::WINDOW_NORMAL);
cv::imshow("Markers After Watershed", vis);
cv::waitKey(0);
return 0;
}
在这个例子中,我们首先读取一个图像并将其转换为灰度图像。
接着,使用 Otsu 方法进行阈值处理,并通过形态学开运算去除噪声。
然后,我们找到图像中的轮廓,并将这些轮廓作为初始标记。最后,应用分水岭算法并显示分割结果。
总结
cv::watershed 是一个强大的工具,可用于图像分割。
它特别适用于那些需要精确分割对象边界的应用场景。
为了获得最佳效果,通常需要对输入图像进行预处理,并仔细选择初始标记。
运行结果2
示例代码3
#include "pch.h"
//#pragma comment(lib, "opencv_world450d.lib")
#include "opencv2/imgproc.hpp"
#include "opencv2/highgui.hpp"
#include <opencv2/imgproc/types_c.h>
#include <iostream>
using namespace std;
using namespace cv;
#include "opencv2/imgproc/imgproc.hpp"
#include "opencv2/highgui/highgui.hpp"
#include <iostream>
using namespace cv;
using namespace std;
Vec3b RandomColor(int value); //生成随机颜色函数
int main(int argc, char* argv[])
{
Mat image = imread("03.jpeg"); //载入RGB彩色图像
if (image.empty())
{
cout << "图像读入为空" << endl;
}
namedWindow("Source Image", WINDOW_NORMAL);
imshow("Source Image", image);
//灰度化,滤波,Canny边缘检测
Mat imageGray;
cvtColor(image, imageGray, CV_RGB2GRAY);//灰度转换
GaussianBlur(imageGray, imageGray, Size(5, 5), 2); //高斯滤波
namedWindow("Gray Image", WINDOW_NORMAL);
imshow("Gray Image", imageGray);
Canny(imageGray, imageGray, 80, 150);
namedWindow("Canny Image", WINDOW_NORMAL);
imshow("Canny Image", imageGray);
//查找轮廓
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++)
{
//对marks进行标记,对不同区域的轮廓进行编号,相当于设置注水点,有多少轮廓,就有多少注水点
drawContours(marks, contours, index, Scalar::all(compCount + 1), 1, 8, hierarchy);
drawContours(imageContours, contours, index, Scalar(255), 1, 8, hierarchy);
}
//我们来看一下传入的矩阵marks里是什么东西
Mat marksShows;
convertScaleAbs(marks, marksShows);
namedWindow("marksShow", WINDOW_NORMAL);
imshow("marksShow", marksShows);
namedWindow("轮廓", WINDOW_NORMAL);
imshow("轮廓", imageContours);
watershed(image, marks);
//我们再来看一下分水岭算法之后的矩阵marks里是什么东西
Mat afterWatershed;
convertScaleAbs(marks, afterWatershed);
namedWindow("After Watershed", WINDOW_NORMAL);
imshow("After Watershed", afterWatershed);
//对每一个区域进行颜色填充
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);
}
}
}
namedWindow("After ColorFill", WINDOW_NORMAL);
imshow("After ColorFill", PerspectiveImage);
//分割并填充颜色的结果跟原始图像融合
Mat wshed;
addWeighted(image, 0.4, PerspectiveImage, 0.6, 0, wshed);
namedWindow("AddWeighted Image", WINDOW_NORMAL);
imshow("AddWeighted Image", wshed);
waitKey();
}
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);
}