当前位置: 首页 > ds >正文

OpenCV中的分水岭算法 (C/C++)

OpenCV中的分水岭算法 (C/C++) 🏞️

分水岭算法 (Watershed Algorithm) 是一种在图像处理和计算机视觉中广泛应用的图像分割方法。它特别适用于分离图像中相互接触或重叠的对象。其基本思想是将灰度图像看作一个地形景观,其中灰度值代表海拔高度。算法模拟从用户定义的标记点(“种子点”)开始向“盆地”注水的过程,当不同盆地的水汇合时,便形成了“分水岭线”,这些线就是对象的边界。


1. 算法原理简介

想象一下,一个地形表面被水淹没,水从代表不同区域的标记(局部最小值)开始上升。当来自不同标记的水相遇时,就在它们之间建立一道屏障(分水岭)。最终,这些屏障就勾勒出分割区域的轮廓。

直接将分水岭算法应用于梯度图像通常会导致过度分割 (over-segmentation),因为噪声和微小的局部梯度变化会产生许多不必要的集水盆。为了解决这个问题,通常采用基于标记 (marker-based) 的分水岭算法。用户(或通过其他算法)预先定义一些标记,这些标记代表了:

  • 确定是前景的区域 (Sure Foreground)
  • 确定是背景的区域 (Sure Background)
  • 其他区域则标记为未知

算法将仅从这些标记开始淹没,从而大大减少过度分割。


2. OpenCV中的 cv::watershed

OpenCV 提供了 cv::watershed 函数来实现基于标记的分水岭算法。

函数原型:

void cv::watershed(cv::InputArray image,     // 输入图像,必须是8位3通道彩色图像 (CV_8UC3)cv::InputOutputArray markers // 输入/输出标记图像。它应该是一个32位单通道整数图像 (CV_32SC1)。// 在输入时,它包含预定义的标记。// 在输出时,它被修改为分割结果,其中分水岭线被标记为 -1。
);

markers 图像详解:

  • 在输入 markers 图像时,不同的区域需要用不同的正整数标记:
    • 0:表示未知区域,这些区域将由算法决定其归属。
    • 1:通常用于标记确定的背景区域。
    • >1 (如 2, 3, …)`:用于标记不同的确定前景对象区域。每个对象一个唯一的正整数。
  • 在函数执行后,markers 图像会被修改:
    • 属于特定标记区域的像素会被赋予该标记的值。
    • 分水岭线(边界)上的像素会被标记为 -1

3. 典型工作流程与C++实现步骤

使用分水岭算法进行图像分割通常遵循以下步骤:

  1. 加载图像:读取源图像。确保它是或可以转换为 CV_8UC3 格式。
  2. 预处理与标记生成:这是最关键的一步。
    • 灰度化与二值化:将图像转换为灰度图,然后通常使用阈值法(如 cv::threshold 配合 cv::THRESH_OTSU)得到二值图像,以初步分离前景和背景。
    • 去除噪声(确定背景):对二值图像进行形态学开运算 (cv::morphologyExcv::MORPH_OPEN) 去除小的噪声点。然后进行形态学膨胀 (cv::dilate),得到的区域可以认为是确定的背景 (Sure Background)。
    • 寻找确定前景
      • 使用距离变换 (cv::distanceTransform) 计算二值图像中每个前景像素到最近背景像素的距离。
      • 对距离变换的结果进行阈值处理,得到一些可以认为是确定的前景 (Sure Foreground) 的区域。这些区域通常是对象的“核心”。
    • 标记未知区域:将确定背景和确定前景之外的区域标记为未知。
    • 创建 markers 图像:根据上述确定的前景、背景区域,初始化一个 CV_32SC1 类型的 markers 图像。可以使用 cv::connectedComponents 对确定前景区域进行连通组件分析,为每个组件分配一个唯一的标签。
  3. 应用分水岭算法:调用 cv::watershed(image, markers);
  4. 可视化结果:遍历修改后的 markers 图像,将分水岭线(值为 -1 的像素)在原图上用醒目的颜色标出。

示例代码:

#include <opencv2/opencv.hpp>
#include <iostream>
#include <vector>int main() {// 1. 加载图像cv::Mat src = cv::imread("your_image_with_touching_objects.png");if (src.empty()) {std::cerr << "Error: Could not load image." << std::endl;return -1;}cv::imshow("Source Image", src);// --- 2. 预处理与标记生成 ---cv::Mat gray, binary;cv::cvtColor(src, gray, cv::COLOR_BGR2GRAY);// 使用Otsu二值化cv::threshold(gray, binary, 0, 255, cv::THRESH_BINARY_INV + cv::THRESH_OTSU);cv::imshow("Binary Image", binary);// 去除噪声 (形态学开运算)cv::Mat kernel = cv::getStructuringElement(cv::MORPH_RECT, cv::Size(3, 3));cv::Mat opening;cv::morphologyEx(binary, opening, cv::MORPH_OPEN, kernel, cv::Point(-1, -1), 2);cv::imshow("Opening (Noise Removal)", opening);// 确定背景区域 (膨胀)cv::Mat sure_bg;cv::dilate(opening, sure_bg, kernel, cv::Point(-1, -1), 3);cv::imshow("Sure Background", sure_bg);// 确定前景区域 (距离变换 + 阈值)cv::Mat dist_transform;cv::distanceTransform(opening, dist_transform, cv::DIST_L2, 5);cv::normalize(dist_transform, dist_transform, 0, 1.0, cv::NORM_MINMAX); // 归一化到0-1cv::imshow("Distance Transform", dist_transform * 255); // 可视化时乘以255cv::Mat sure_fg;double minVal, maxVal;cv::minMaxLoc(dist_transform, &minVal, &maxVal);cv::threshold(dist_transform, sure_fg, maxVal * 0.6, 255, cv::THRESH_BINARY); // 阈值可调sure_fg.convertTo(sure_fg, CV_8U);cv::imshow("Sure Foreground", sure_fg);// 寻找未知区域 (背景 - 前景)cv::Mat unknown;cv::subtract(sure_bg, sure_fg, unknown);cv::imshow("Unknown Region", unknown);// 创建 markers 图像cv::Mat markers;cv::connectedComponents(sure_fg, markers); // 连通组件标记前景// 将背景标记为1 (OpenCV中通常将背景标记为1,前景从2开始)// connectedComponents 会将背景标记为0,所以我们需要调整for (int i = 0; i < markers.rows; i++) {for (int j = 0; j < markers.cols; j++) {markers.at<int>(i, j) = markers.at<int>(i, j) + 1; // 确保前景标记从2开始if (sure_bg.at<uchar>(i, j) == 0) { // 如果是原始二值图中的背景(黑色),则认为是未知区域markers.at<int>(i, j) = 0;     // 标记为未知}if (sure_bg.at<uchar>(i, j) == 255 && sure_fg.at<uchar>(i, j) == 0) { // 膨胀后的背景,且不是前景markers.at<int>(i, j) = 1;    // 标记为确定背景}}}// 对于之前通过 `subtract` 得到的 `unknown` 区域,如果其像素值为255(表示这部分在sure_bg中但不在sure_fg中),// 且我们想确保它被算法处理(而不是固定为背景1),可以将其在markers中标记为0。// 但通常,将 `sure_bg` 减去 `sure_fg` 得到的区域作为 `unknown` 并在 `markers` 中标记为0更直接。// 此处已通过上一步骤的条件判断(`sure_bg.at<uchar>(i, j) == 0`)来处理了部分未知区域。//更精细的 `markers` 创建:// 1. 背景标记为1// 2. 前景用 `connectedComponents` 标记为 2, 3, ...// 3. `sure_bg` 中是背景但 `sure_fg` 中不是前景的区域,标记为1 (已在循环中完成)// 4. `unknown` 区域 (即 `sure_bg` 和 `sure_fg` 之间的模糊地带) 标记为0 (已在循环中完成)// --- 3. 应用分水岭算法 ---cv::watershed(src, markers); // src必须是CV_8UC3// --- 4. 可视化结果 ---// 将分水岭线 (-1) 标记为红色for (int i = 0; i < markers.rows; i++) {for (int j = 0; j < markers.cols; j++) {if (markers.at<int>(i, j) == -1) {src.at<cv::Vec3b>(i, j) = cv::Vec3b(0, 0, 255); // 红色}}}cv::imshow("Segmented (Watershed lines)", src);cv::waitKey(0);return 0;
}

注意: 上述代码中的参数(如形态学操作的迭代次数、距离变换后的阈值等)需要根据具体图像进行仔细调整。


4. 关键考量与技巧 ⚙️

  • 标记的质量至关重要:分水岭算法的成功在很大程度上取决于初始标记的准确性。不好的标记会导致分割不佳。
  • 避免过度分割:基于标记的分水岭是避免过度分割的主要手段。如果直接对梯度图像应用,几乎肯定会得到大量无用的小区域。
  • 输入图像类型cv::watershed 要求输入图像 imageCV_8UC3 (8位3通道彩色图),markers 图像是 CV_32SC1 (32位单通道整数图)。
  • 参数调整:预处理步骤中的参数(如形态学核的大小、阈值、膨胀/腐蚀次数)需要根据待分割对象的特性进行实验和调整。
  • 连通组件分析cv::connectedComponents 是生成前景对象独立标记的有效工具。

5. 总结 ✨

OpenCV 中的分水岭算法是一种强大的图像分割工具,尤其擅长分离接触或重叠的对象。其核心在于通过精心设计的预处理步骤生成准确的前景和背景标记,然后利用这些标记引导分割过程。虽然参数调整可能需要一些经验,但一旦掌握,它就能在许多复杂的分割任务中发挥巨大作用。

http://www.xdnf.cn/news/10207.html

相关文章:

  • 聊聊前端工程化
  • C#上传图片后压缩
  • 【Dify学习笔记】:Dify离线安装插件教程
  • 【原理扫描】不安全的crossdomain.xml文件和CORS(跨站资源共享)原始验证失败验证与彻底方案
  • (24)多租户 SaaS 平台设计
  • C语言进阶--自定义类型详解(结构体、枚举、联合)
  • AWS WAF设置IP白名单
  • 指数函数的泰勒展开可视化:从数学理论到Python实现
  • 历年西北工业大学计算机保研上机真题
  • 【已解决】YFRateLimitError(‘Too Many Requests. Rate limited. Try after a while.‘)
  • Spring Boot 3 整合 MQ 构建聊天消息存储系统
  • 测试用例及黑盒测试方法
  • Java进化之路:从Java 8到Java 21的重要新特性(深度解析)
  • JS手写代码篇---手写节流函数
  • Linux(8)——进程(控制篇——上)
  • mac mini m4命令行管理员密码设置
  • 【Java基础-环境搭建-创建项目】IntelliJ IDEA创建Java项目的详细步骤
  • 专业课复习笔记 11
  • 评论功能开发全解析:从数据库设计到多语言实现-优雅草卓伊凡
  • 在 Linux 上构建 Kubernetes 单节点集群:Minikube 安装与实战指南
  • 第2章-12 输出三角形面积和周长(走弯路解法)
  • 26 C 语言函数深度解析:定义与调用、返回值要点、参数机制(值传递)、原型声明、文档注释
  • C++ 模版复习
  • 【个人思考】超级玛丽亚小游戏设计文档
  • Unity UI系统中RectTransform详解
  • 用美图秀秀批处理工具定制专属图片水印的方法详解
  • 【技术支持】安卓11开机启动设置
  • IDEA修改JVM内存配置以后,无法启动
  • TC/BC/OC P2P/E2E有啥区别?-PTP协议基础概念介绍
  • C语言操作Kafka