⚠️由于自己的拖延症,3.4.3翻到一半,OpenCV发布了4.0.0了正式版,所以接下来是按照4.0.0翻译的。
⚠️除了版本之外,其他还是照旧,Image Segmentation with Watershed Algorithm,附原文。
在本章,
任何灰度图像都可以看做是一张地理图,高强度的地方就好比是山峰,而低强度的地方就好比是深谷。现在你开始把每一个孤立的深谷(本地强度极小值)用不同颜色的水(标签)填充,在水位上涨的同时,依赖于附近的山峰(梯度),很明显,来自不同低谷不同颜色的水会开始融合。为了避免这些水相互融合,你在这些不同颜色的水的交汇处建立起一些栅栏。然后你继续倒入水、建立栅栏。直到所有的山峰都被水淹没。然后,你建立起来的这些栅栏就是你分割的结果。这就是分水岭算法背后的哲理。你可以访问 CMM webpage on watershed 在一些动画的帮助下理解它。
但是,由于图像中的噪声和一些不规则形状,这种方法会给出一个“分割的太过了”的结果。因此 OpenCV 实现了一个基于标记的分水岭算法,指定了哪些低谷点是需要合并的,哪些不需要合并。这是一个交互式的图像分割算法,我们是这么做的,为我们已知的物体给出不同的标签,用一种颜色(或者(在灰度图中是)强度)标记出我们确信是前景或者物体的区域,用另外一种颜色来标记出我们确信为背景或者非物体的区域,而对于我们不确信的区域,我们就标记为零。这就是我们的标记,然后再应用分水岭算法。然后我们的标记将用我们给出的标签进行更新,对象的边界值将为-1。
下面我们将看到一个例子,关于如何使用距离变换与分水岭分割相互接触的对象。
看下面的硬币图片,硬币是相互接触的。即使你对它用各种阈值法,它还是相互接触。
我们先找出硬币的近似值,为了达到目的,我们可以用大津阈值法。
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)
结果:
现在,我们需要去掉图像中所有的小噪声。为了达到这个目标我们可以用形态变化里的开放方法(之前的章节中提到的先腐蚀再膨胀)。如果要移除物体中的小黑洞,我们可以用闭合方法(膨胀后腐蚀)。因此我们确定,靠近物体中心的区域是前景,远离物体的区域是背景。只剩下那些我们不确定的区域,都分布在硬币的边缘。
所以我们需要提取出我们确定是硬币的区域。腐蚀方法去掉边界区域的像素,剩下的部分我们就可以确认是硬币了。如果物体不相互接触的话,这个方案算是可行的。但因为它们之间有相互接触,找到距离变换(译者附:参看看OpenCv的distanceTransform方法。)并应用一个合适的阈值才是一个好的选择。接下来我们需要找到我们确定不是硬币的区域。为此,我们对结果用膨胀算法。膨胀增加了物体到背景的边界。通过这种方式,我们可以确保任何区域在后台的结果是一个真正的背景,因为边界区域被删除了。见下图。
剩下的区域是我们不知道它是硬币还是背景的部分。分水岭算法应该能算出结果。这些区域通常围绕着硬币的边界,也就是前景和背景相遇的地方(甚至是两种不同的硬币相遇的地方)。我们称之为边框。它可以由确定的前景区域减去确定的背景区域得到。
# noise removal
kernel = np.ones((3,3),np.uint8)
opening = cv.morphologyEx(thresh,cv.MORPH_OPEN,kernel, iterations = 2)
# sure background area
sure_bg = cv.dilate(opening,kernel,iterations=3)
# 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)
现在看结果。在右侧应用过阈值的距离变换图像中,我们拿到了一些确信是属于硬币的区域,并且还被分割开了。(某些情况下,你可能只对于前景分块感兴趣,而对分离相互接触的对象不感兴趣。在这种情况下,你不需要使用距离变换,只需要侵蚀就足够了。侵蚀只是提取前景区域的另一种方法,仅此而已。)
现在我们确定了哪一部分是硬币,哪一部分是背景等等。现在我们创建一个计数器,(它是一个大小与原始图像大小相同的数组,但是具有int32数据类型)并且标记它内部的区域。我们已经确定的区域(无论是前景还是背景),都被标记成任意的不同正整数。而我们不确定的区域都被留下当做是零。要这么做的话直接先用函数cv.connectedComponents()。它标记出了背景为0,而其他物体被标记为整数,从1开始。
但我们知道,如果背景标记为0,分水岭会将其视为未知区域。我们要用不同的整数来标记它。相反,我们将用0标记那些定义为unknown的未知区域。
# 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
以JET色图模式看结果。深蓝色区域显示了未知的部分。确信是硬币的区域用不同的颜色标记了出来,剩下的区域确信为背景,用浅蓝色标记出来,与未知区域形成对比。
现在我们的计数器准备好了。最后一步,应用分水岭算法。然后计数器就会被修改。边界区域将标记为-1。
markers = cv.watershed(img,markers)
img[markers == -1] = [255,0,0]
参见下面的结果。对于一些硬币,它们所接触的区域被合适地分割了,而对于另一些硬币,则没有。
上篇:【翻译:OpenCV-Python教程】霍夫圆变换
下篇:【翻译:OpenCV-Python教程】用GrabCut算法进行交互式的前景提取