OpenCV 分水岭算法图像分割实战:用 Marker-Based Watershed 分离相互接触的目标

发布时间:2026/9/7 2:57:11
OpenCV 分水岭算法图像分割实战:用 Marker-Based Watershed 分离相互接触的目标 OpenCV 分水岭算法图像分割实战用 Marker-Based Watershed 分离相互接触的目标【免费下载链接】opencvOpen Source Computer Vision Library项目地址: https://gitcode.com/GitHub_Trending/opencv31/opencv本文基于 OpenCV 官方 Python 教程 py_watershed.markdown 展开完整讲解基于标记marker-based的分水岭图像分割从 Otsu 二值化、形态学开运算、距离变换构造肯定前景/肯定背景/未知三类区域到cv.watershed()生成最终分割边界并结合 imgproc 模块源码 说明分水岭算法的实现原理。读完后你可以掌握一套可直接复制运行的分离相互接触物体的完整分割管线并理解分水岭算法为什么必须依赖 marker 才能避免过度分割。1. 分水岭的直觉与 OpenCV 的 marker-based 方案灰度图像可以被视为一张地形图高亮度对应山峰与山丘低亮度对应山谷。算法从每个孤立山谷局部极小值开始注入不同颜色即不同标签的水随着水位上升来自不同山谷、颜色不同的水会开始汇合在汇合处修筑堤坝直到所有山峰都被淹没这些堤坝就是分割结果。这就是分水岭Watered背后的哲学。但这种纯地形式的方法会因图像噪声或不规则纹理产生过度分割oversegmentation。因此 OpenCV 实现的是marker-based 分水岭算法无参数的 marker-based 分割变体文献中常引 Meyer 1992 年的描述见 头文件注释由调用者明确指定哪些区域是前景、哪些是背景、哪些未知确定是前景物体的区域赋予一个正的标签值确定是背景的区域赋予另一个不同的正标签值不确定的区域置 0交由算法判定。调用cv.watershed()后marker 数组会被就地更新未知区域被归入某个已标记区域而物体之间的边界像素会被写为-1。这是整个算法的核心约定也是后处理时提取边界的依据。2. cv.watershed() 接口契约与实现原理Python 侧接口为cv.watershed(image, markers)其底层 C 声明位于 opencv2/imgproc.hppCV_EXPORTS_W void watershed( InputArray image, InputOutputArray markers );参数类型要求说明image8 位 3 通道CV_8UC3输入彩色图像markers32 位单通道CV_32SC1尺寸与 image 相同输入输出参数。正整数为种子区域标签0 为未知区域输出中区域边界像素被写为 -1两个关键约束可以直接从 C 实现 得到确认const int IN_QUEUE -2; // Pixel visited const int WSHED -1; // Pixel belongs to watershed ... CV_Assert( src.type() CV_8UC3 dst.type() CV_32SC1 ); CV_Assert( src.size() dst.size() );即image必须是CV_8UC3markers必须是CV_32SC1且尺寸一致否则断言失败——这就是教程中 marker 数组一定要写成np.int32的原因。从源码结构看算法内部维护 256 个按优先级即与相邻 marker 之间最大颜色通道差值的量化值排列的队列WSQueue q[256]。初始阶段把每个 marker 的邻域像素按其与最近 marker 的颜色差入队主循环始终从优先级最高的队列出队将当前像素的四个邻域上下左右已定标签进行比较若邻域出现两个不同的正标签当前像素即被标记为WSHED-1分水岭边界否则继承唯一标签并继续向 0 值区域扩散。换句话说OpenCV 的分水岭并不是沿梯度方向注水而是按颜色差异从小到大逐步扩张各标记区域遇到不同标记区域的交界即落下边界这与低梯度处先合并、高梯度处筑坝的地形学解释在效果上等价。另外两点值得注意的实现细节实现会先把图像四周一圈像素强制写成 -1 边界segmentation.cpp 中mask[j] mask[j mstep*(size.height-1)] WSHED避免算法在图像边缘越界头文件注释中明确提醒相邻的两个连通区域之间不一定存在 -1 边界例如初始 marker 图中两个区域直接相邻时就可能共享边界而无 -1 线因此不能把-1 像素数当作区域数的可靠度量。3. 实战分离相互接触的硬币下面完整继承官方教程的硬币分割示例。素材为多枚相互接触的硬币——即使做二值化硬币之间依然是粘连的这正是分水岭算法的典型应用场景。3.1 Otsu 二值化得到硬币的粗略轮廓import numpy as np import cv2 as cv from matplotlib import pyplot as plt img cv.imread(coins.png) assert img is not None, file could not be read, check with os.path.exists() gray cv.cvtColor(img, cv.COLOR_BGR2GRAY) ret, thresh cv.threshold(gray, 0, 255, cv.THRESH_BINARY_INV cv.THRESH_OTSU)这里使用cv.THRESH_BINARY_INV反色使硬币区域为 255、背景为 0便于后续距离变换和形态学操作阈值传 0 由 Otsu 自动确定。3.2 构造肯定背景与肯定前景教程给出的策略是去噪用 3×3 核做 2 次形态学开运算消除图像中的小白点噪声肯定背景sure background将开运算结果膨胀 3 次。膨胀使物体边界向背景扩张于是膨胀结果的背景区域必然是真正的背景边界不确定带已被剔除肯定前景sure foreground对开运算结果计算距离变换distance transform再取0.7 * max为阈值二值化。距离变换中每个前景像素的值是该像素到最近零像素背景的距离硬币越靠近中心距离值越大——因此按最大值的 0.7 倍截断后剩下的白色区域就是确定属于某枚硬币内部的部分且原本粘连的硬币在此阶段已被分开。# noise removal kernel np.ones((3, 3), np.uint8) opening cv.morphologyEx(thresh, cv.MORPH_OPEN, kernel, iterations2) # sure background area sure_bg cv.dilate(opening, kernel, iterations3) # Finding sure foreground area dist_transform cv.distanceTransform(opening, cv.DIST_L2, 5) ret, sure_fg cv.threshold(dist_transform, 0.7 * dist_transform.max(), 255, 0) # Finding unknown region sure_fg np.uint8(sure_fg) unknown cv.subtract(sure_bg, sure_fg)参数要点cv.distanceTransform(opening, cv.DIST_L2, 5)DIST_L2为欧氏距离第三个参数 5 是距离精度窗口maskSizeOpenCV 支持 3 和 5值越大计算越精确但越慢cv.threshold(dist_transform, 0.7*dist_transform.max(), 255, 0)对浮点距离图做普通二值化0.7 是经验系数控制肯定前景的保守程度——系数越小前景种子越多、越容易把相邻硬币连成一片unknown cv.subtract(sure_bg, sure_fg)肯定背景减去肯定前景得到硬币边界附近的不确定带。教程特别指出若你只需要前景分割而不需要分离粘连物体可以省掉距离变换仅用腐蚀提取肯定前景即可——腐蚀只是提取 sure foreground 的另一种更简单手段。3.3 生成 marker 数组marker 是与原图同尺寸的int32数组由cv.connectedComponents()初始化它把背景标为 0、其余连通域从 1 开始编号。但 0 对分水岭算法意味着未知而背景是已知区域所以要做两步修正# Marker labelling ret, markers cv.connectedComponents(sure_fg) # Add one to all labels so that sure background is not 0, but 1 markers markers 1 # Now, mark the region of unknown with zero markers[unknown 255] 0执行后各标签的含义标签值含义1肯定背景原 connectedComponents 的 0 12, 3, 4, ...各肯定前景硬币区域原 1, 2, 3, ... 1每枚硬币一个独立编号0未知区域unknown即 sure_bg 与 sure_fg 之间的边界带-1cv.watershed()执行后写入的物体边界3.4 执行分水岭并可视化markers cv.watershed(img, markers) img[markers -1] [255, 0, 0]plt.imshow(cv.cvtColor(img, cv.COLOR_BGR2RGB)) plt.show()cv.watershed(img, markers)就地修改markers未知区域被吸收进某个硬币区域硬币之间的交界处被写为 -1。将 -1 像素染成红色BGR 的[255,0,0]即可直观看到分割边界。需要注意官方教程的如实说明并非每一对接触的硬币都能被正确切开——对于某些接触方式边界可能落在两枚硬币内部而不接触交线这是该方法以及 marker 质量的固有局限。4. 仓库中的相关示例代码教程在 Exercises 一节推荐运行仓库自带的交互式分水岭示例此外还有一个完整的距离变换 分水岭样例二者都在当前仓库中可以直接阅读运行交互式示例samples/python/snippets/watershed.py命令行用法为python watershed.py [image filename]缺省使用fruits.jpg。运行时用鼠标在图像上涂画 marker按键 1–7 切换 marker 颜色SPACE触发重新分割r重置a切换自动更新ESC退出。其核心调用展示了 marker 涂画与cv.watershed(self.img, m)的组合watershed.pydef watershed(self): m self.markers.copy() cv.watershed(self.img, m) overlay self.colors[np.maximum(m, 0)] vis cv.addWeighted(self.img, 0.5, overlay, 0.5, 0.0, dtypecv.CV_8UC3) cv.imshow(watershed, vis)注意np.maximum(m, 0)的作用把边界像素 -1 映射为 0 再索引颜色表避免负值越界——这是处理 watershed 输出的一个常见技巧。该示例直观演示了marker 决定分割结果这一交互特性涂在哪就分在哪。完整管线示例samples/python/tutorial_code/ImgTrans/distance_transformation/imageSegmentation.py对cards.png先做拉普拉斯锐化、Otsu 二值化、距离变换取峰值再用cv.findContours()cv.drawContours()把每个前景种子绘制成不同编号的 marker背景用固定圆点标记最后调用cv.watershed()并以随机颜色填充各区域。它展示了与本文connectedComponents 造 marker不同的另一条 marker 构造路径与 头文件注释 中markers 可用 findContours 和 drawContours 从二值掩膜获得的说明相互印证。C 对等示例见 samples/cpp/snippets/watershed.cpp由 imgproc.hpp 的example注释引用。5. 要点与易错点小结marker 的质量决定分割质量。分水岭本身只是按颜色差异把未知区域分给最近的已知区域硬币能否被分开完全取决于sure_fg是否真的彼此分离距离变换 0.7 阈值正是保证这一点的核心步骤。数据类型的硬性约束markers必须是与图像同尺寸的int32数组CV_32SC1image必须是 8 位三通道CV_8UC3源码中的CV_Assert会直接拒绝其他类型segmentation.cpp。0 与 -1 的语义输入中 0 表示未知输出中 -1 表示边界可视化前用np.maximum(markers, 0)或掩膜markers -1分别处理这两类特殊值。不要假设相邻区域必有边界头文件明确说明两个相邻连通区域可能没有 -1 分隔线做区域统计时以 marker 标签值而非 -1 像素计数为准。只需前景分割时可简化若目标是前景提取而非分离粘连物体用腐蚀代替距离变换提取肯定前景即可省去 distance transform 一步。完整的分步结果图二值化、前景/背景分解、距离变换、marker 伪彩、最终结果均位于教程目录 doc/py_tutorials/py_imgproc/py_watershed/images/可配合上述代码逐步对照理解每个中间产物。【免费下载链接】opencvOpen Source Computer Vision Library项目地址: https://gitcode.com/GitHub_Trending/opencv31/opencv创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考