官方文档 – https://docs.opencv.org/3.4.0/d3/db4/tutorial_py_watershed.html
任何灰度图像都可以看作是一个地形面,高强度表示山峰和山,而低强度则表示山谷。你开始用不同颜色的水(标签)填充每一个单独的山谷(局部最小值)。随着水的上升,取决于附近的山峰(梯度),来自不同山谷的水,明显不同的颜色会开始融合。为了避免这种情况,你在水合并的地方设置障碍。在所有的山峰都被水淹没之前,你要继续填满水和建造栅栏的工作。然后你创建的障碍会给你分割的结果。这就是分水岭背后的“哲学”。你可以访问CMM webpage on watershed,在一些动画的帮助下了解它。
但是,这种方法会导致由于噪声或图像中任何其他不正常的情况而导致的结果过于分散。所以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)
现在我们需要去除图像中的任何小的白噪声。因此我们要使用形态学开运算。为了去除物体上的小洞,我们要使用形态学闭运算。所以,现在我们可以确定,靠近物体中心的区域是前景和远离物体的区域是背景。只有硬币的边界区域是我们不确定的区域。
所以我们需要提取出我们确信它们是硬币的区域。侵蚀消除了边界像素。不管剩下的是什么,我们都可以确定它是硬币。如果物体不相互接触,那就会起作用。但是由于它们相互接触,另一个好的选择是找到距离变换并应用一个合适的阈值。接下来,我们需要找到我们确信它们不是硬币的区域。为此,我们对结果进行了扩张。扩张将对象边界增加为背景。通过这种方法,我们可以确保背景中的任何区域都是真正的背景,因为边界区域被移除。
剩下的区域是我们不知道的,不管它是硬币还是背景。流域算法应该找到它。这些区域通常都是硬币的边界,在那里前景和背景相遇(或者是两种不同的硬币相遇)。我们称之为边界。那可以从确定的背景区域中减去确定的前景区域来获得。
# 降噪
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)
unknow = cv.subtract(sure_bg, sure_fg)
在阈值图像中,我们得到了一些硬币的区域,我们确定了硬币,它们现在被分离了。(在某些情况下,您可能只对前景分割感兴趣,而不是将相互触摸的对象分隔开。在这种情况下,你不需要使用距离变换,只要侵蚀就足够了。侵蚀是另一种提取前景区域的方法。)
现在我们可以确定哪些是硬币的区域,哪些是背景,哪些是背景。因此,我们创建标记(它是一个与原始图像相同大小的数组,但使用int32数据类型)并对其内部的区域进行标记。我们所知道的区域(无论是前景还是背景)都被标记为正整数,但是不同的整数,而我们不确定的区域则为零。为此,我们使用了cv.connectedComponents()
函数。它将图像的背景标记为0,然后其他对象从1开始标记为整数。
但是我们知道,如果背景是0,那么分水岭将会被认为是未知的区域。我们要用不同的整数来标记它。相反,我们将标记未知区域,以0表示未知区域。
# 标记标签
ret, markers = cv.connectedComponents(sure_fg)
# 在所有标签中添加一个,确保背景不是0,而是1
markers = markers+1
# 现在,将未知区域标记为0
markers[unknown == 255] = 0
深蓝色区域显示未知区域。当然,硬币的颜色是不同的。与未知区域相比,确定背景的剩余区域以较浅的蓝色显示。
现在我们的标记已经准备好了。现在是最后一步的时候了,应用分水岭。然后,标记图像将被修改。边界区域将被标记为-1。
markers = cv.watershed(img, markers)
img[markers == -1] = [255,0,0]
对于一些硬币,它们接触的区域被适当地分割,而对于一些硬币则没有。