Skip to article frontmatterSkip to article content
Site not loading correctly?

This may be due to an incorrect BASE_URL configuration. See the MyST Documentation for reference.

分水岭算法

原理

分水岭的概念将图像视为一张地形图: 图像中的高强度区域是山脉,低强度区域是山谷。 分水岭分割方法的主要目标 是找到地形图的“山脊线”, 如果一滴水落到这些山脊线上, 它将有同等的机会落在线的任何一侧。

一张灰度图像(左)及其作为地形图的三维表示(右)。
山脊线是呈“b”字形的山顶。

Figure 1:一张灰度图像(左)及其作为地形图的三维表示(右)。 山脊线是呈“b”字形的山顶。

分水岭分割的基本思想是对地形图进行“洪水”填充, 让水以均匀的速度从山谷中上涨。 当两个不同山谷中上涨的水即将汇合时, 会修建一道“大坝”来阻止它们合并。 当水覆盖整个图像表面时, 这些大坝就对应于分割的边界。

在第150行切面上的洪水填充过程图示(参见)。
数字对应于水位,
大坝用红色显示。

Figure 2:在第150行切面上的洪水填充过程图示(参见Figure 3)。 数字对应于水位, 大坝用红色显示。

带有分水岭边界(大坝)的灰度图像,大坝用红色显示。
中考虑的剖面用绿色绘制。

Figure 3:带有分水岭边界(大坝)的灰度图像,大坝用红色显示。 Figure 2中考虑的剖面用绿色绘制。

应用于图像

通常,物体的轮廓并不对应于山脊线, 因此分水岭算法必须应用于图像的梯度(参见Figure 4)。

一张图像(左)、其梯度(中)以及分水岭算法给出的结果。
轮廓是使用Sobel滤波器计算的。

Figure 4:一张图像(左)、其梯度(中)以及分水岭算法给出的结果。 轮廓是使用Sobel滤波器计算的。

局限性

分水岭方法的一个局限性在于当图像中存在许多局部最小值时: 分水岭会导致过度分割,因为每个局部最小值都会产生一个分割区域。 这种现象在Figure 5中得到了说明, 其中的图像与Figure 4中的相同, 但被JPEG压缩中的加性噪声降质。 因此,图像梯度现在呈现出一些小的局部最小值。

的降质版本(左)、
其梯度(中)以及分水岭算法给出的结果(右)。
显然,这个结果并不令人满意。

Figure 5:Figure 4的降质版本(左)、 其梯度(中)以及分水岭算法给出的结果(右)。 显然,这个结果并不令人满意。

为了减少分割区域的数量,可以:

  • 使用标记(markers)手动选择感兴趣的汇水盆地(Figure 6),

  • 在应用算法前对梯度进行平滑(使用低通滤波器)(Figure 7),

  • 合并局部最小值。

与中相同的图像(左)、
其梯度和选择的标记(中),以及分水岭算法给出的结果(右)。

Figure 6:Figure 5中相同的图像(左)、 其梯度和选择的标记(中),以及分水岭算法给出的结果(右)。

与中相同的图像(左)、
图像平滑后版本的梯度(中),以及分水岭算法给出的结果(右)。

Figure 7:Figure 5中相同的图像(左)、 图像平滑后版本的梯度(中),以及分水岭算法给出的结果(右)。

代码示例:用于细胞计数的标记控制分水岭算法

在这里,我们将通过一个经典示例,演示如何使用标记控制的分水岭算法来分割和计数相互接触的物体,例如显微镜图像中的生物细胞。

该过程包括以下几个步骤:

  1. 图像二值化:首先,我们使用Otsu阈值法来粗略地将细胞与背景分离。

  2. 确定确定的前景:由于细胞相互接触,简单的阈值法是不够的。我们对二值图像使用距离变换。距离变换为每个前景像素标记其到最近背景像素的距离。细胞的中心将具有最高的距离值。通过对距离图进行阈值处理,我们可以分离出这些核心区域,作为我们的“确定前景”标记。

  3. 确定确定的背景:我们可以肯定远离细胞的区域是背景。通过对细胞的二值掩模进行膨胀操作,我们可以稍微扩大它们的区域,并将这个较大区域之外的区域确定为“确定背景”。

  4. 确定未知区域:未知区域就是既不是确定前景也不是确定背景的区域。这些是我们希望分水岭算法在其中工作的边界区域。

  5. 创建标记并应用分水岭算法:我们创建一个最终的markers图像,其中包含我们确定的前景(唯一标记)和确定的背景(标记为一个区域)。然后,我们在原始图像的梯度上运行分水岭算法,使用这些标记作为洪水填充的起始点。

import cv2
import numpy as np
import matplotlib.pyplot as plt
from scipy import ndimage as ndi
from skimage.segmentation import watershed
from skimage.feature import peak_local_max

# 加载细胞示例图像
image = cv2.imread('_images/segmentation/example-cells.png')
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)

# 步骤 1: 使用Otsu阈值法对图像进行二值化
_, thresh = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY_INV + cv2.THRESH_OTSU)

# 步骤 2: 使用距离变换找到确定的前景
# 去除噪声
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)
_, sure_fg = cv2.threshold(dist_transform, 0.7*dist_transform.max(), 255, 0)

# 步骤 3: 找到未知区域
sure_fg = np.uint8(sure_fg)
unknown = cv2.subtract(sure_bg, sure_fg)

# 步骤 4: 创建标记
# 标记确定的前景区域
_, markers = cv2.connectedComponents(sure_fg)

# 将所有标签加一,这样确定的背景就不是0,而是1
markers = markers + 1

# 将未知区域标记为0
markers[unknown==255] = 0

# 步骤 5: 应用分水岭算法
markers = cv2.watershed(image, markers)
image[markers == -1] = [255, 0, 0]  # 将分水岭边界标记为红色

# 显示结果
plt.figure(figsize=(12, 8))
titles = ['原始图像', '阈值化', '距离变换', '确定的前景', '确定的背景', '分水岭结果']
images = [cv2.cvtColor(image, cv2.COLOR_BGR2RGB), thresh, dist_transform, sure_fg, sure_bg, cv2.cvtColor(image, cv2.COLOR_BGR2RGB)]

# 校正被原地修改的原始图像显示
original_image = cv2.imread('_images/segmentation/example-cells.png')
images[0] = cv2.cvtColor(original_image, cv2.COLOR_BGR2RGB)
# 校正最终结果显示
final_result = cv2.cvtColor(original_image, cv2.COLOR_BGR2RGB)
final_result[markers == -1] = [255,0,0] # 将边界应用到干净的图像上
images[-1] = final_result


for i in range(6):
    plt.subplot(2, 3, i + 1)
    # 对单通道图像使用'gray'色图
    if images[i].ndim == 2:
        plt.imshow(images[i], cmap='gray')
    else:
        plt.imshow(images[i])
    plt.title(titles[i])
    plt.axis('off')

plt.tight_layout()
plt.show()