7.4. 基于分水岭算法的图像分割

7.4.1. 目标

在本章中,
-我们将学习使用分水岭算法使用基于标记的图像分割-我们将看到: cv2.分水岭()

7.4.2. 理论

任何灰度图像都可以看作是一个地形表面,高强度表示山峰和丘陵,低强度表示山谷。你开始用不同颜色的水(标签)填满每个孤立的山谷(局部最小值)。随着水位的上升,取决于附近的山峰(坡度),来自不同山谷的水,显然不同颜色的水会开始融合。为了避免这种情况,你要在水汇合的地方设置屏障。你继续充水和建造障碍物,直到所有的山峰都在水下。然后,您创建的屏障将为您提供分割结果。这就是分水岭背后的“哲学”。你可以参观 CMM webpage on watershed 通过一些动画来理解它。

但这种方法会由于图像中的噪声或任何其他不规则性而导致过分割结果。所以OpenCV实现了一个基于标记的分水岭算法,您可以指定哪些是要合并的所有谷点,哪些不是。它是一种交互式的图像分割。我们所做的就是给我们所知道的东西贴上不同的标签。用一种颜色(或强度)标记我们确定是前景或对象的区域,用另一种颜色标记我们确定是背景或非对象的区域,最后用0标记我们不确定的区域。那是我们的标记。然后应用分水岭算法。然后我们的标记将用我们给出的标签更新,对象的边界值将为-1。

7.4.3. 代码

下面我们将看到一个关于如何使用距离变换和分水岭分割相互接触的对象的示例。

考虑下面的硬币图片,硬币互相接触。即使你启动它,它也会相互接触。

我们从找到硬币的大致估计数开始。为此,我们可以使用Otsu的二值化。

>>> %matplotlib inline
>>>
>>> import numpy as np
>>> import cv2
>>> from matplotlib import pyplot as plt
>>>
>>> img = cv2.imread('/cvdata/water_coins.jpg')
>>> gray = cv2.cvtColor(img,cv2.COLOR_BGR2GRAY)
>>> ret, thresh = cv2.threshold(gray,0,255,cv2.THRESH_BINARY_INV+cv2.THRESH_OTSU)
>>> plt.imshow(thresh)
<matplotlib.image.AxesImage at 0x7fe4b9f20dd8>
../_images/sec04-watershed_2_1.png

结果:

现在我们需要去除图像中的任何小的白噪声。为此,我们可以使用形态开口。要去除物体上的任何小孔,我们可以使用形态闭合。所以,现在我们可以确定靠近物体中心的区域是前景,而远离物体的区域是背景。唯一我们不确定的区域是硬币的边界区域。

所以我们需要提取出我们确定它们是硬币的区域。侵蚀移除边界像素。所以不管剩下什么,我们都可以确定是硬币。如果物体不互相接触,那就行了。但由于它们彼此接触,另一个好的选择是找到距离变换并应用适当的阈值。下一步我们需要找到我们确信它们不是硬币的地方。为此,我们扩大了结果。膨胀增加对象边界到背景。这样,我们就可以确定结果中背景中的任何区域都是真实的背景,因为边界区域被移除了。见下图。

剩下的区域是那些我们不知道的区域,无论是硬币还是背景。分水岭算法应该找到它。这些区域通常围绕着硬币的边界,前景和背景相交(甚至两个不同的硬币相交)。我们称之为边界。它可以从sure-bg区域减去sure-fg区域得到。

>>> # noise removal
>>> kernel = np.ones((3,3),np.uint8)
>>> opening = cv2.morphologyEx(thresh,cv2.MORPH_OPEN,kernel, iterations = 2)
>>>
>>> # sure background area
>>> sure_bg = cv2.dilate(opening,kernel,iterations=3)
>>> plt.imshow(sure_bg)
<matplotlib.image.AxesImage at 0x7fe4b9e3c7b8>
../_images/sec04-watershed_5_1.png
>>> # Finding sure foreground area
>>> dist_transform = cv2.distanceTransform(opening,cv2.DIST_L2,5)
>>> ret, sure_fg = cv2.threshold(dist_transform,0.7*dist_transform.max(),255,0)
>>>
>>> # Finding unknown region
>>> sure_fg = np.uint8(sure_fg)
>>> unknown = cv2.subtract(sure_bg,sure_fg)
>>> plt.imshow(sure_fg)
<matplotlib.image.AxesImage at 0x7fe4b9e13f98>
../_images/sec04-watershed_7_1.png

看看结果。在阈值图像中,我们得到一些区域的硬币,我们确定的硬币,他们现在分离。(在某些情况下,您可能只对前景分割感兴趣,而不是分离相互接触的对象。在这种情况下,你不需要使用距离变换,只要侵蚀就足够了。侵蚀只是另一种提取前景区域的方法,仅此而已。)

现在我们可以确定哪些是硬币的区域,哪些是背景和全部。因此,我们创建了marker(它是一个与原始图像大小相同的数组,但具有int32数据类型)并标记其中的区域。我们确定的区域(无论是前景还是背景)被标记为任何正整数,但是不同的整数,我们不确定的区域被保留为零。我们用这个 连接组件() . 它用0标记图像的背景,然后用从1开始的整数标记其他对象。

但我们知道,如果背景标记为0,流域会将其视为未知区域。所以我们想用不同的整数来标记它。相反,我们将标记未知区域,定义为 unknown ,使用0。

>>> # Marker labelling
>>> ret, markers = cv2.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
>>> plt.imshow(markers)
<matplotlib.image.AxesImage at 0x7fe4b9d736a0>
../_images/sec04-watershed_10_1.png

请参见JET colormap中显示的结果。深蓝色区域显示未知区域。当然硬币有不同的颜色。与未知区域相比,确定背景显示为浅蓝色的剩余区域。

现在我们的标记准备好了。现在是最后一步,应用分水岭。然后标记图像将被修改。边界区域将标记为-1。

>>> markers = cv2.watershed(img,markers)
>>> img[markers == -1] = [255,0,0]
>>> plt.imshow(markers)
<matplotlib.image.AxesImage at 0x7fe4b9d44a58>
../_images/sec04-watershed_13_1.png

见下面的结果。对一些硬币来说,它们接触的区域被正确分割,而对一些硬币来说,它们不是。

7.4.4. 额外资源

  1. CMM页面打开 Watershed Tranformation

7.4.5. 练习

  1. OpenCV样本有一个关于分水岭分割的交互式样本, \(watershed.py\) . 运行它,享受它,然后学习它。