目标
本节将要学习:
使用分水岭算法基于掩模的图像分割
函数:
cv2.watershed()
原理
任何一副灰度图像都可以被看成拓扑平面,灰度值高的区域可以被看成是山峰, 灰度值低的区域可以被看成是山谷。 向每一个山谷中灌不同颜色的水。随着水的位的升高, 不同山谷的水就会相遇汇合,为了防止不同山谷的水汇合, 需要在水汇合的地方构建起堤坝。不停的灌水, 不停的构建堤坝知道所有的山峰都被水淹没。 构建好的堤坝就是对图像的分割。这就是分水岭算法的背后哲理。 可以通过访问网站CMM webpage on watershed来加深自己的理解。
但是这种方法通常都会得到过度分割的结果, 这是由噪声或者图像中其他不规律的因素造成的。 为了减少这种影响,OpenCV 采用了基于掩模的分水岭算法, 在这种算法中要设置那些山谷点会汇合, 那些不会。这是一种交互式的图像分割。需要做的就是给已知的对象打上不同的标签。 如果某个区域肯定是前景或对象,就使用某个颜色(或灰度值)标签标记它。 如果某个区域肯定不是对象而是背景就使用另外一个颜色标签标记。 而剩下的不能确定是前景还是背景的区域就用 0 标记。 这就是标签。然后实施分水岭算法。每一次灌水, 标签就会被更新,当两个不同颜色的标签相遇时就构建堤坝, 直到将所有山峰淹没,最后得到的边界对象(堤坝)的值为 -1。
代码
下面的例子中,将就和距离变换和分水岭算法对紧挨在一起的对象进行分割。
如下图所示,这些硬币紧挨在一起。就算使用阈值操作,它们任然是紧挨着的。
从找到硬币的近似估计开始,可以使用 Otsu's 二值化。
import numpy as np
import cv2
from matplotlib import pyplot as plt
img = cv2.imread('coins.jpg')
gray = cv2.cvtColor(img,cv2.COLOR_BGR2GRAY)
ret, thresh = cv2.threshold(gray,0,255,cv2.THRESH_BINARY_INV+cv2.THRESH_OTSU)
结果如下:
现在要去除图像中的所有的白噪声。这就需要使用形态学中的开运算。
为了去除对象上小的空洞需要使用形态学闭运算。所以现在知道靠近对象中心的区域肯定是前景, 而远离对象中心的区域肯定是背景。而不能确定的区域就是硬币之间的边界。
所以要提取肯定是硬币的区域。腐蚀操作可以去除边缘像素。剩下就可以肯定是硬币了。 当硬币之间没有接触时,这种操作是有效的。但是由于硬币之间是相互接触的,就有了另外一个更好的选择: 距离变换再加上合适的阈值。接下来要找到肯定不是硬币的区域。这是就需要进行膨胀操作了。 膨胀可以将对象的边界延伸到背景中去。这样由于边界区域被去处理,就可以知道那些区域肯定是前景,那些肯定是背景。如下图所示。
剩下的区域就是不知道该如何区分的了。这就是分水岭算法要做的。 这些区域通常是前景与背景的交界处(或者两个前景的交界),称之为边界。 从肯定是不是背景的区域中减去肯定是前景的区域就得到了边界区域。
# 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)
# Finding sure foreground area
# 距离变换的基本含义是计算一个图像中非零像素点到最近的零像素点的距离,也就是到零像素点的最短距离
# 个最常见的距离变换算法就是通过连续的腐蚀操作来实现,腐蚀操作的停止条件是所有前景像素都被完全
# 腐蚀。这样根据腐蚀的先后顺序,我们就得到各个前景像素点到前景中心??像素点的
# 距离。根据各个像素点的距离值,设置为不同的灰度值。这样就完成了二值图像的距离变换
#cv2.distanceTransform(src, distanceType, maskSize)
# 第二个参数 0,1,2 分别表示 CV_DIST_L1, CV_DIST_L2 , CV_DIST_C
dist_transform = cv2.distanceTransform(opening,1,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)
from matplotlib import pyplot as plt
#plt.imshow(unknown),plt.show()
如结果所示,在阈值化之后的图像中, 得到了肯定是硬币的区域,而且硬币之间也被分割开了。 (有些情况下可能只需要对前景进行分割, 而不需要将紧挨在一起的对象分开, 此时就没有必要使用距离变换了,腐蚀就足够了。 当然腐蚀也可以用来提取肯定是前景的区域。)
现在知道了那些是背景那些是硬币了。
那就可以创建标签(一个与原图像大小相同,数据类型为 in32 的数组),
并标记其中的区域了。对已经确定分类的区域(无论是前景还是背景)使用不同的正整数标记,
对不确定的区域使用 0 标记。可以使用函数 cv2.connectedComponents()
来做这件事。
它会把将背景标记为 0,其他的对象使用从 1 开始的正整数标记。
但是,如知道如果背景标记为 0,
那分水岭算法就会把它当成未知区域了。
所以想使用不同的整数标记它们。
而对不确定的区域(函数 cv2.connectedComponents
输出的结果中使用 unknown
定义未知区域)标记为 0。
# Marker labelling
ret, markers1 = cv2.connectedComponents(sure_fg)
# Add one to all labels so that sure background is not 0, but 1
markers = markers1+1
# Now, mark the region of unknown with zero
markers[unknown==255] = 0
结果使用 JET 颜色地图表示。深蓝色区域为未知区域。 肯定是硬币的区域使用不同的颜色标记。 其余区域就是用浅蓝色标记的背景了。 现在标签准备好了。到最后一步:实施分水岭算法了。 标签图像将会被修改,边界区域的标记将变为 -1.
markers3 = cv2.watershed(img,markers)
img[markers3 == -1] = [255,0,0]
from matplotlib import pyplot as plt
#plt.imshow(img),plt.show()
结果如下。有些硬币的边界被分割的很好,也有一些硬币之间的边界分割的不好。
Now our marker is ready. It is time for final step, apply watershed. Then marker image will be modified. The boundary region will be marked with -1.
markers = cv2.watershed(img,markers)
img[markers == -1] = [255,0,0]
See the result below. For some coins, the region where they touch are segmented properly and for some, they are not.
练习
- OpenCV 自带的示例中有一个交互式分水岭分割程序:
watershed.py
。