OpenCV Python 分水岭图像分割
【目标】
- 学习使用分水岭方法进行基于标记的图像分割
- cv2.watershed()
【理论】
任何灰度图像都可以被视为地形表面,其中高强度表示山峰和丘陵,而低强度表示山谷,。你开始用不同颜色的水(标签)填充每个孤立的山谷(局部最小值)。随着水的上升,根据附近山峰(梯度),来自不同山谷的水,显然具有不同的颜色,将开始融合。为了避免这种情况,你需要在水汇合的地方建造障碍物,你继续注水和建造屏障,直到所有山峰都淹没在水下,然后,您创建的障碍会给出分割结果。这就是分水岭别后的“哲学”,您可以访问分水岭上的网页看动画,如下:
但是,由于图像中的噪声或任何其他不规则性,这种方法会产生过度分段的结果,因此,OpenCV实现了一种基于标记的分水岭算法,您可以制定哪些是要合并的所有谷点,哪些不是,这是一种交互式图像分割,我们所做的是为我们所知道的对象赋予不同的标签,用一种颜色标记我们确定是前景或对象的区域。用另一种颜色标记我们确定为背景或非对象的区域。最后,用0标记我们不确定的区域,这是我们的标记,然后应用分水岭算法,然后,我们的标记将使用我们给出的标签进行更新,对象的边界将为-1。
- 分割的流程图
- 找到标记和分割标准(标准或函数常用于分离区域,常常是对比度或梯度,但不是必要的。
- 执行标记控制的分水岭算法。
【代码】
利用距离变换和分水岭分割黏在一起的目标。
import numpy as np
import cv2
from matplotlib import pyplot as plt
# 读入图像
img = cv2.imread('assets/water_coins.jpg')
gray = cv2.imread('assets/water_coins.jpg', 0)
# 阈值化
ret, thresh = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY_INV + cv2.THRESH_OTSU )
# 开运算(先腐蚀后膨胀),去噪声
kernel = np.ones((3, 3), np.uint8)
opening = cv2.morphologyEx(thresh, cv2.MORPH_OPEN, kernel, iterations = 2)
# 背景区域
sure_bg = cv2.dilate(opening, kernel, iterations = 3)
# 通过距离变换,然后阈值化找前景
dist_transform = cv2.distanceTransform(opening, cv2.DIST_L2, 5)
# 这个地方的阈值(0.5 * dist_transform.max())调节很重要,直接关系到后面的分割效果
ret, sure_fg = cv2.threshold(dist_transform, 0.5 * dist_transform.max(), 255, cv2.THRESH_BINARY)
# 计算未知区域
sure_fg = np.uint8(sure_fg)
unknown = cv2.subtract(sure_bg, sure_fg)
# 标记连通区域
ret, markers = cv2.connectedComponents(sure_fg)
# 所有的标记+1,背景改为1,而不是0
markers = markers + 1
# # 标记不确定区域为0
markers[unknown==255] = 0
# 用分水岭方法找到标记,如果标记为 -1,则该位置设置蓝色,即边缘颜色
markers = cv2.watershed(img, markers)
img[markers == -1] = [255, 0, 0]
# 显示分割的图像
image2 = np.uint8(img)
cv2.imshow("img2", image2)
# 显示各阶段的图像
plt.subplot(231), plt.imshow(gray, 'gray'), plt.title(
'Original'), plt.xticks([]), plt.yticks([])
plt.subplot(232), plt.imshow(sure_bg, 'gray'), plt.title(
'sure_bg'), plt.xticks([]), plt.yticks([])
plt.subplot(233), plt.imshow(sure_fg, 'gray'), plt.title(
'sure_fg'), plt.xticks([]), plt.yticks([])
plt.subplot(234), plt.imshow(dist_transform), plt.title(
'dist_transform'), plt.xticks([]), plt.yticks([])
plt.subplot(235), plt.imshow(markers), plt.title(
'markers'), plt.xticks([]), plt.yticks([])
plt.subplot(236), plt.imshow(img), plt.title(
'img'), plt.xticks([]), plt.yticks([])
plt.show()
cv2.waitKey(0)
cv2.destroyAllWindows()
如果不进行
markers = markers + 1
,则结果为:
如果不进行
markers[unknown==255] = 0
,则结果为:
所以种子点 0 值 生成和选择很重要,直接影响到最终结果。
【接口】
- distanceTransform
cv.distanceTransform( src, distanceType, maskSize[, dst[, dstType]] ) -> dst
cv.distanceTransformWithLabels( src, distanceType, maskSize[, dst[, labels[, labelType]]] ) -> dst, labels
计算图像中每个像素到最近0像素的精确或近似距离。如果为零值图像,那么距离当然为0。当
maskSize == DIST_MASK_PRECISE
和distanceType == DIST_L2
, 运行算法 [73],函数已经用 TBB 进行了并行化优化了。其他情况下,使用算法[29]。这就是说寻找最近零像素的路径可以是 水平,垂直,对角 ,Knight’s Move(骑士运动?),整体的距离是通过一系列基础距离算出来的。水平垂直用a
表示,对角用b
表示,骑士移动用c
表示DIST_L1: a = 1, b=2
DIST_L2:
3 x 3: a=0.955, b=1.3693
5 x 5: a=1, b=1.4, c=2.1969
DIST_C: a = 1, b = 1
通常,对于快速的粗略距离估计用 DIST_L2 3x3 mask,如果精确的话用 DIST_L2 5x5 mask。不管怎么样,所有这些精确或近似的距离都是像素数量的线性函数。
- src: 8位单通道二值图像
- dst: 距离计算结果的图像,可以是8位或32位浮点单通道图像,图像尺寸与源图像一致;
- labels: 输出的2D标签(也是 Voronoi 图-泰森多边形图),32位单通道。
- distanceType: 距离类型
- maskSize: 距离变换的Mask
- labelType: 标签类型 see DistanceTransformLabelTypes.
- distanceType 距离类型
- DistanceTransformMasks 距离变换mask
- DistanceTransformLabelTypes 距离变换标签类型
- connectedComponents
cv.connectedComponents( image[, labels[, connectivity[, ltype]]] ) -> retval, labels
cv.connectedComponentsWithAlgorithm( image, connectivity, ltype, ccltype[, labels] ) -> retval, labels
计算二值图像中连通域标签
支持 Bolelli (Spaghetti) [26], Grana (BBDT) [97] and Wu’s (SAUF) [278] 算法;
- image: 8位单通道图像
- labels: 目标图像的标签
- connectivity: 4 邻域或 8 邻域
- ltype: 输出图像标签类型 支持 CV_32S, CV_16U
- ccltype: 连通域算法类型 ConnectedComponentsAlgorithmsTypes
- ConnectedComponentsAlgorithmsTypes
- watershed
cv2.watershed( image, markers ) -> markers
执行基于标记图像的分割,利用分水岭的算法。 [171] .
在将图像传递给函数之前,必须在具有正(>0)索引的图像标记中大致勾勒出所需的区域。因此,每个区域都表示为一个或多个像素值为1、2、3等的连接组件。可以使用findContours和drawContours从二进制掩码中检索此类标记(请参见watershed.cpp演示)。标记是未来图像区域的“种子”。标记中的所有其他像素(其与轮廓区域的关系未知,应由算法定义)应设置为0。在函数输出中,标记中的每个像素都设置为“种子”分量的值,或在区域之间的边界处设置为-1。
- image: 8位3通道图像
- markers: 输入输出的32位单通道标记图像;
【参考】
- OpenCV 官方文档
- CMM page on Watershed Transformation
- Pedro Felzenszwalb and Daniel Huttenlocher. Distance transforms of sampled functions. Technical report, Cornell University, 2004.
- Gunilla Borgefors. Distance transformations in digital images. Computer vision, graphics, and image processing, 34(3):344–371, 1986.
- Federico Bolelli, Stefano Allegretti, Lorenzo Baraldi, and Costantino Grana. Spaghetti Labeling: Directed Acyclic Graphs for Block-Based Connected Components Labeling. IEEE Transactions on Image Processing, 29(1):1999–2012, 2019.
- Costantino Grana, Daniele Borghesani, and Rita Cucchiara. Optimized Block-Based Connected Components Labeling With Decision Trees. IEEE Transactions on Image Processing, 19(6):1596–1609, 2010.
- Kesheng Wu, Ekow Otoo, and Kenji Suzuki. Optimizing two-pass connected-component labeling algorithms. Pattern Analysis and Applications, 12(2):117–135, Jun 2009.
- Fernand Meyer. Color image segmentation. In Image Processing and its Applications, 1992., International Conference on, pages 303–306. IET, 1992.