本章将学习以下内容:
- 学习基于标记的分水岭算法(marker-based watershed algorithm)在图像分割中的应用
- 学习OpenCV核心函数
cv2.watershed()
理论基础
任何灰度图像都可视为地形表面,其中高灰度值对应山峰(峰值),低灰度值对应山谷(局部极小值)。 传统分水岭模拟过程包括:初始淹没,即用不同颜色标记水(标签)填充各独立山谷; 水位上升即随着水位上涨,不同颜色的水体开始在山峰(梯度)处交汇; 屏障构建即在汇合处建立屏障防止颜色混合; 过程终止即当所有山峰被淹没时,形成的屏障即为分割边界。
由于图像噪声和其他不规则性的影响,传统方法会产生过分割的结果。 因此OpenCV实现了一种基于标记的分水岭算法,让用户可以指定哪些谷底点需要合并,哪些不需要。 这是一种交互式的图像分割方法。
具体操作步骤如下:
对我们已知的目标区域赋予不同的标签, 确定是前景(目标)的区域用一种颜色(或灰度值)标记。 确定是背景(非目标)的区域用另一种颜色标记,不确定的区域标记为0,这些标记就是我们的初始标记, 然后应用分水岭算法。
算法执行后,设定的标记会被更新,对象的边界会被赋予-1的值。
代码示例
下面我们将展示如何结合距离变换和分水岭算法来分割相互接触的物体。
以这张硬币图像为例,可以看到硬币之间是相互接触的。即使进行阈值处理,它们仍然会保持接触状态。
%matplotlib inline
import numpy as np
import cv2
from matplotlib import pyplot as plt
img = cv2.imread('/data/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 0x7f437a9d7050>
代码实现步骤:
首先需要去除图像中的细小白色噪点。 使用形态学开运算去除小噪点,使用形态学闭运算填补物体中的小孔洞。 通过以上处理,现在可以确定,靠近物体中心的区域是前景(硬币),远离物体的区域是背景。 唯一不确定的是硬币边缘的边界区域。
提取确定是硬币的区域,腐蚀操作可以去除边界像素,剩下的区域可以确定是硬币, 这个方法适用于物体不接触的情况。 对于接触的物体,更好的方法是计算距离变换并应用合适的阈值。 确定非硬币区域,可对结果进行膨胀操作, 膨胀会将物体边界扩展到背景区域。 这样可以确保结果中背景区域的真实性(因为边界区域已被移除)
具体效果请参考下图:
不确定区域处理,剩下的区域就是无法确定是硬币还是背景的部分,
这些区域将由分水岭算法来进行判断。这些不确定区域通常位于硬币与背景的交界处、不同硬币之间的接触边界。
我们可以通过数学方法获取这些边界区域:边界区域 = 确定背景区域(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 0x7f437a850080>
# 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 0x7f437a81f110>
从阈值处理后的图像可以看到:我们已经成功分离出部分确定是硬币的区域, 这些硬币区域现在都是相互分离的状态。 (注:在某些应用场景中,如果只需要前景分割而不需要分离相互接触的物体,可以简化处理流程: 不需要使用距离变换,仅使用腐蚀操作就足以提取确定的前景区域,腐蚀本质上只是另一种提取确定前景区域的方法而已)。
现在我们已经明确区分了硬币区域、背景区域等,接下来创建一个与原始图像尺寸相同但数据类型为int32的标记数组,并在其中标注各个区域。
对于确定的前景或背景区域,我们使用不同的正整数进行标记,而不确定的区域则保持为0。
这里使用cv2.connectedComponents()
函数进行标注,该函数默认将图像背景标记为0,其他对象则从1开始依次标记。
不过需要注意的是,如果背景被标记为0,分水岭算法会将其视为未知区域,
因此我们需要将真正的未知区域(由不确定边界定义的区域)标记为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 0x7f437a740e60>
请参阅JET颜色图中显示的结果。深蓝色区域表示未知区域。 当然,硬币的颜色不同。与未知区域相比,确定背景的剩余区域显示为浅蓝色。
至此标记数组准备就绪,进入最终步骤应用分水岭算法,此时标记图像将被修改,其边界区域会被标记为-1。
markers = cv2.watershed(img,markers)
img[markers == -1] = [255,0,0]
plt.imshow(markers)
<matplotlib.image.AxesImage at 0x7f437a7d8bf0>
请参阅下面的结果。对于某些硬币,它们接触的区域是被正确地分割了,其他没有。
扩展阅读
- CMM页面流域改造
练习
- OpenCV样本有一个关于流域分割的交互式样本 $watershed.py$。运行、享受并学习它。