9.9watershed分水岭分割

实验原理

在计算机视觉中,分水岭算法(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);
}

运行结果3

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值