【OpenCV-Python】教程:3-15 分水岭图像分割

时间:2022-12-07 18:57:09

OpenCV Python 分水岭图像分割

【目标】

  • 学习使用分水岭方法进行基于标记的图像分割
  • cv2.watershed()

【理论】

任何灰度图像都可以被视为地形表面,其中高强度表示山峰和丘陵,而低强度表示山谷,。你开始用不同颜色的水(标签)填充每个孤立的山谷(局部最小值)。随着水的上升,根据附近山峰(梯度),来自不同山谷的水,显然具有不同的颜色,将开始融合。为了避免这种情况,你需要在水汇合的地方建造障碍物,你继续注水和建造屏障,直到所有山峰都淹没在水下,然后,您创建的障碍会给出分割结果。这就是分水岭别后的“哲学”,您可以访问分水岭上的网页看动画,如下:

【OpenCV-Python】教程:3-15 分水岭图像分割

但是,由于图像中的噪声或任何其他不规则性,这种方法会产生过度分段的结果,因此,OpenCV实现了一种基于标记的分水岭算法,您可以制定哪些是要合并的所有谷点,哪些不是,这是一种交互式图像分割,我们所做的是为我们所知道的对象赋予不同的标签,用一种颜色标记我们确定是前景或对象的区域。用另一种颜色标记我们确定为背景或非对象的区域。最后,用0标记我们不确定的区域,这是我们的标记,然后应用分水岭算法,然后,我们的标记将使用我们给出的标签进行更新,对象的边界将为-1。

  • 分割的流程图

【OpenCV-Python】教程:3-15 分水岭图像分割

  1. 找到标记和分割标准(标准或函数常用于分离区域,常常是对比度或梯度,但不是必要的。
  2. 执行标记控制的分水岭算法。

【OpenCV-Python】教程:3-15 分水岭图像分割

【OpenCV-Python】教程:3-15 分水岭图像分割

【OpenCV-Python】教程:3-15 分水岭图像分割

【代码】

利用距离变换和分水岭分割黏在一起的目标。

【OpenCV-Python】教程:3-15 分水岭图像分割

【OpenCV-Python】教程:3-15 分水岭图像分割

【OpenCV-Python】教程:3-15 分水岭图像分割

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,则结果为:

【OpenCV-Python】教程:3-15 分水岭图像分割

如果不进行 markers[unknown==255] = 0,则结果为:

【OpenCV-Python】教程:3-15 分水岭图像分割

所以种子点 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_PRECISEdistanceType == 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 距离类型

【OpenCV-Python】教程:3-15 分水岭图像分割

  • DistanceTransformMasks 距离变换mask

【OpenCV-Python】教程:3-15 分水岭图像分割

  • DistanceTransformLabelTypes 距离变换标签类型

【OpenCV-Python】教程:3-15 分水岭图像分割

  • 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

【OpenCV-Python】教程:3-15 分水岭图像分割

  • watershed
cv2.watershed(	image, markers	) ->	markers

执行基于标记图像的分割,利用分水岭的算法。 [171] .
在将图像传递给函数之前,必须在具有正(>0)索引的图像标记中大致勾勒出所需的区域。因此,每个区域都表示为一个或多个像素值为1、2、3等的连接组件。可以使用findContours和drawContours从二进制掩码中检索此类标记(请参见watershed.cpp演示)。标记是未来图像区域的“种子”。标记中的所有其他像素(其与轮廓区域的关系未知,应由算法定义)应设置为0。在函数输出中,标记中的每个像素都设置为“种子”分量的值,或在区域之间的边界处设置为-1。

  • image: 8位3通道图像
  • markers: 输入输出的32位单通道标记图像;

【参考】

  1. OpenCV 官方文档
  2. CMM page on Watershed Transformation
  3. Pedro Felzenszwalb and Daniel Huttenlocher. Distance transforms of sampled functions. Technical report, Cornell University, 2004.
  4. Gunilla Borgefors. Distance transformations in digital images. Computer vision, graphics, and image processing, 34(3):344–371, 1986.
  5. 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.
  6. 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.
  7. Kesheng Wu, Ekow Otoo, and Kenji Suzuki. Optimizing two-pass connected-component labeling algorithms. Pattern Analysis and Applications, 12(2):117–135, Jun 2009.
  8. Fernand Meyer. Color image segmentation. In Image Processing and its Applications, 1992., International Conference on, pages 303–306. IET, 1992.