文章目录
- 理论
- 代码
原文路径:opencv-4.6.0\doc\py_tutorials\py_imgproc\py_watershed
目标
在这一章当中,
- 我们将学习使用分水岭算法使用基于标记的图像分割
- 我们将看到:cv.watershed()
理论
任何灰度图像都可以被视为地形表面,其中高强度表示峰和丘陵,而低强度表示山谷。 您开始用不同颜色的水(标签)填充每个孤立的山谷(局部最小值)。 随着水的上升,根据附近的山峰(梯度),来自不同山谷的水,显然具有不同的颜色,将开始融合。 为了避免这种情况,您可以在水汇合的地方建造障碍物。 你继续填水和建造障碍物,直到所有山峰都被水淹没。 然后,您创建的障碍将为您提供分割结果。 这就是分水岭背后的“哲学”。 您可以访问关于分水岭的CMM网页通过一些动画来理解它。
但由于图像中的噪声或任何其他不规则性,这种方法会给您带来过度分割的结果。 因此,OpenCV 实现了一种基于标记的分水岭算法,您可以指定哪些谷点要合并,哪些不合并。 它是一种交互式图像分割。 我们所做的就是为我们所知道的对象赋予不同的标签。 用一种颜色(或强度)标记我们确定是前景或物体的区域,用另一种颜色标记我们确定是背景或非物体的区域,最后标记我们不确定的区域, 用 0 标记它。这是我们的标记。 然后应用分水岭算法。 然后我们的标记将使用我们给出的标签进行更新,并且对象的边界的值为-1。
代码
下面我们将看到一个有关如何使用距离变换和分水岭来分割相互接触的对象的示例。
考虑下面的硬币图像,硬币相互接触。 即使你阈值它,它也会相互接触。
我们首先找到硬币的大致估计值。 为此,我们可以使用 Otsu 的二值化。
import numpy as np
import cv2 as cv
from matplotlib import pyplot as plt
img = cv.imread('coins.png')
gray = cv.cvtColor(img,cv.COLOR_BGR2GRAY)
ret, thresh = cv.threshold(gray,0,255,cv.THRESH_BINARY_INV+cv.THRESH_OTSU)
结果:
现在我们需要去除图像中的任何小白噪声。 为此,我们可以使用形态学开运算。 为了去除对象中的任何小孔,我们可以使用形态学闭运算。 所以,现在我们可以确定靠近物体中心的区域是前景,远离物体的区域是背景。 我们唯一不确定的区域是硬币的边界区域。
所以我们需要提取我们确定是硬币的区域。 腐蚀去除边界像素。 所以无论剩下什么,我们都可以确定它是硬币。 如果物体不互相接触的话,这就会起作用。 但由于它们彼此接触,另一个不错的选择是找到距离变换并应用适当的阈值。 接下来我们需要找到我们确定不是硬币的区域。 为此,我们扩大了结果。 膨胀将对象边界增加到背景。 这样,我们可以确保结果中背景中的任何区域确实是背景,因为边界区域已被删除。 请参见下图。
剩下的区域是我们不知道的区域,无论是硬币还是背景。 分水岭算法应该能找到它。 这些区域通常位于前景和背景相遇的硬币边界周围(甚至两个不同的硬币相遇)。 我们称之为边界。 可以通过sure_bg区域减去sure_fg区域得到。
# 消除噪音
kernel = np.ones((3,3),np.uint8)
opening = cv.morphologyEx(thresh,cv.MORPH_OPEN,kernel, iterations = 2)
# 确定背景区域
sure_bg = cv.dilate(opening,kernel,iterations=3)
# 寻找确定的前景区域
dist_transform = cv.distanceTransform(opening,cv.DIST_L2,5)
ret, sure_fg = cv.threshold(dist_transform,0.7*dist_transform.max(),255,0)
# 寻找未知区域
sure_fg = np.uint8(sure_fg)
unknown = cv.subtract(sure_bg,sure_fg)
查看结果。 在阈值图像中,我们得到一些硬币区域,我们确信这些区域是硬币,但它们现在已分离。 (在某些情况下,您可能只对前景分割感兴趣,而不是对分离相互接触的对象感兴趣。在这种情况下,您不需要使用距离变换,只需腐蚀就足够了。腐蚀只是提取某些前景区域的另一种方法,那就是 全部。)
现在我们可以确定哪些是硬币区域,哪些是背景等等。 因此,我们创建标记(它是一个与原始图像大小相同的数组,但数据类型为 int32)并标记其中的区域。 我们确定知道的区域(无论是前景还是背景)都用任何正整数标记,但是不同的整数,而我们不确定的区域则保留为零。 为此,我们使用 cv.connectedComponents()。 它用 0 标记图像的背景,然后用从 1 开始的整数标记其他对象。
但我们知道,如果背景标记为0,分水岭会将其视为未知区域。 所以我们想用不同的整数来标记它。 相反,我们将用 0 标记由未知定义的未知区域。
# 标记标签
ret, markers = cv.connectedComponents(sure_fg)
# 为所有标签添加 1,确保背景不是 0,而是 1
markers = markers+1
# 现在,用零标记未知区域
markers[unknown==255] = 0
查看 JET 颜色图中显示的结果。 深蓝色区域显示未知区域。 某些硬币的颜色不同,价值也不同。 与未知区域相比,确定背景的剩余区域以浅蓝色显示。
现在我们的标记已准备就绪。 现在是最后一步,应用分水岭的时候了。 然后标记图像将被修改。 边界区域将标记为-1。
markers = cv.watershed(img,markers)
img[markers == -1] = [255,0,0]
请参阅下面的结果。 对于某些硬币,它们接触的区域被正确分割,而对于某些硬币则不然。