目标
在本章中,
理论
任何灰度图像都可以看作地形表面,其中高强度表示山峰和山丘,而低强度表示山谷。你开始用不同颜色的水(标签)填充每一个分离的山谷(局部最小值)。随着水位上升,根据附近山峰(梯度)的情况,来自不同山谷的、颜色明显不同的水将开始融合。为了避免这种情况,你在水融合的位置构建屏障。在你继续填充水和构建屏障的工作,直到所有山峰都被淹没。这时,你创建的屏障可以为你提供分割结果。这就是分水岭背后的“哲学”。你可以访问 分水岭的 CMM 网站,借助一些动画来理解它。
但是,这种方法会由于图像中的噪声或其他不规则性而给你分割过度的问题。所以 OpenCV 实现了一种基于标记的分水岭算法,你在其中指定哪些山谷点可以融合,哪些不可以。这是一种交互式的图像分割。我们所做的是为我们已知的对象赋予不同的标签。用一种颜色(或强度)标记出我们确信是前景或对象的区域,用另一种颜色标记出我们确信是背景或非对象的区域,最后,用 0 标记出我们不确定的区域。那就是我们的标记。然后应用分水岭算法。然后我们的标记将使用我们给出的标签进行更新,而对象的边界将具有 -1 的值。
代码
下面我们将看到一个示例,说明如何利用距离变换和分水岭来分割相互接触的对象。
请看下面的硬币图像,这些硬币彼此接触。即使你进行阈值处理,它们仍然会互相接触。
image
我们从粗略估计这些硬币开始。为此,我们可以使用大津二值化方法。
import numpy as np
import cv2 as cv
from matplotlib import pyplot as plt
assert img is not None, "无法读取文件,请使用 os.path.exists() 检查"
ret, thresh =
cv.threshold(gray,0,255,cv.THRESH_BINARY_INV+cv.THRESH_OTSU)
CV_EXPORTS_W Mat imread(const String &filename, int flags=IMREAD_COLOR)
从文件中加载图像。
void cvtColor(InputArray src, OutputArray dst, int code, int dstCn=0)
将图像从一种色彩空间转换为另一种色彩空间。
double threshold(InputArray src, OutputArray dst, double thresh, double maxval, int type)
将固定阈值应用于每个数组元素。
结果
image
现在我们需要移除图像中的任何小块白色噪声。为此,我们可以使用形态学开运算。为了移除对象中的任何小孔洞,我们可以使用形态学闭运算。因此,我们现在可以确定位于对象中心附近的区域是前景,而远离对象的区域是背景。唯一不确定的区域是硬币的边界区域。
所以我们需要提取我们确信是硬币的区域。腐蚀会移除边界像素。因此,其余的任何部分,我们都可以确信是硬币。如果物体互不接触,这是可行的。但由于它们互有接触,另一种好办法是找到距离变换并应用适当的阈值。接下来,我们需要找到我们确信不是硬币的区域。为此,我们对结果进行膨胀。膨胀会增加对象边界至背景。通过这种方式,我们可以确保结果中背景中的任何区域确实是背景,因为边界区域已被移除。请参见下图。
image
剩余区域是我们无法判断是硬币还是背景的区域。分水岭算法应找到它。这些区域通常位于前景和背景相遇的硬币边界周围(或者甚至两个不同的硬币相遇)。我们称之为边界。它可以通过从 sure_bg 区域中减去 sure_fg 区域获得。
kernel = np.ones((3,3),np.uint8)
sure_bg =
cv.dilate(opening,kernel,iterations=3)
ret, sure_fg =
cv.threshold(dist_transform,0.7*dist_transform.max(),255,0)
sure_fg = np.uint8(sure_fg)
void subtract(InputArray src1, InputArray src2, OutputArray dst, InputArray mask=noArray(), int dtype=-1)
按元素计算两个数组或数组和标量的差。
void dilate(InputArray src, OutputArray dst, InputArray kernel, Point anchor=Point(-1,-1), int iterations=1, int borderType=BORDER_CONSTANT, const Scalar &borderValue=morphologyDefaultBorderValue())
使用特定结构元素膨胀图像。
void morphologyEx(InputArray src, OutputArray dst, int op, InputArray kernel, Point anchor=Point(-1,-1), int iterations=1, int borderType=BORDER_CONSTANT, const Scalar &borderValue=morphologyDefaultBorderValue())
执行高级形态学转换。
void distanceTransform(InputArray src, OutputArray dst, OutputArray labels, int distanceType, int maskSize, int labelType=DIST_LABEL_CCOMP)
为原图的每个像素计算到最近的零像素的距离。
查看结果。在阈值分割图像中,我们会获得一些硬币区域,我们确信它们是硬币,并且现在它们被分离开了。(某些情况下,您可能仅对前景分割感兴趣,而对将相互接触的对象分开不感兴趣。在这种情况下,您无需使用距离转换,仅仅腐蚀就已足够。腐蚀只是提取确定前景区域的另一种方法,仅此而已。)
image
现在,我们确切地知道哪些是硬币区域、哪些是背景等等。因此,我们创建标记(它是一个数组,其大小与原始图像相同,但数据类型为 int32),并在其中标记区域。我们确切知道的区域(无论是前景还是背景)都标记为任意正整数(但不是同一整数),而我们不确定的区域则直接留作零。为此,我们使用 cv.connectedComponents()。它用 0 标记图像的背景,然后用从 1 开始的整数标记其他对象。
但我们知道,如果背景标记为 0,分水岭会将其视为未知区域。所以我们希望用不同的整数标记它。相反,我们将用 0 标记未知区域,由 unknown 定义。
markers = markers+1
markers[unknown==255] = 0
int connectedComponents(InputArray image, OutputArray labels, int connectivity, int ltype, int ccltype)
计算布尔图像的连通分量标记图像
在 JET 色图中查看结果。深蓝色区域显示未知区域。确定的硬币用不同的值着色。与未知区域相比,确定的剩余区域(背景)以更浅的蓝色显示。
image
现在我们的标记已经准备好了。是时候进行最后一步了,应用分水岭。然后将修改标记图像。边界区域将用 -1 标记。
img[markers == -1] = [255,0,0]
void watershed(InputArray image, InputOutputArray markers)
使用分水岭算法执行基于标记的图像分割。
请参阅下面的结果。对于一些硬币,它们接触的区域被正确地分割,而对于其他硬币,则没有。
image
其他资源
- CMM 在 分水岭变换 上的页面
练习
- OpenCV 样本在分水岭分割上有交互式样本,watershed.py。运行它,享受它,然后学习它。