查看全课大纲(第 20 / 93 节)
  1. 1.计算机视觉概述
  2. 2.计算机视觉应用场景
  3. 3.困难与挑战
  4. 4.开源库介绍
  5. 5.图像显示与存储
  6. 6.CLAHE
  7. 7.腐蚀膨胀与开闭运算
  8. 8.滤波与边缘填充
  9. 9.各种滤波
  10. 10.图像金字塔
  11. 11.傅里叶变换
  12. 12.短时傅里叶变换与小波变换
  13. 13.颜色特征与颜色直方图
  14. 14.几何特征_边缘
  15. 15.几何特征-角点与斑点
  16. 16.局部特征-SIFT特征描述子
  17. 17.局部特征-SURF特征描述子
  18. 18.局部特征-ORB特征描述子
  19. 19.其他特征描述子
  20. 20.传统图像分割技术
  21. 21.GraphCut
  22. 22.GrabCut
  23. 23.人脸检测的传统方法
  24. 24.行人检测的传统方法HOG+SVM
  25. 25.行人检测DPM
  26. 26.人工神经网络的架构
  27. 27.目标函数与梯度下降
  28. 28.误差反向传播
  29. 29.可手算的神经网络
  30. 30.改进目标函数与克服梯度消散
  31. 31.神经网络之传统神经网络与深度网络的差异
  32. 32.各种梯度下降算法
  33. 33.如何选择合适的梯度下降算法
  34. 34.避免过适应
  35. 35.CNN的架构
  36. 36.CNN的误差反向传播_卷积层
  37. 37.CNN的误差反向传播_池化层
  38. 38.分类所用的数据集
  39. 39.Alexnet详解
  40. 40.VGG详解
  41. 41.GoogLeNet_v1
  42. 42.BatchNormalization
  43. 43.GoogLeNet_v3v4
  44. 44.ResNet
  45. 45.ResNeXt
  46. 46.CNN设计准则
  47. 47.图像检索与推土机距离
  48. 48.基于颜色相似性
  49. 49.基于形状相似性
  50. 50.基于纹理相似性
  51. 51.基于局部特征相似性
  52. 52.DL的二值哈希编码
  53. 53.高维空间中检索KDTree
  54. 54.高维空间中检索LSH
  55. 55.目标识别任务综述
  56. 56.RCNN
  57. 57.SPP-Net
  58. 58.Fast R-CNN
  59. 59.Faster R-CNN
  60. 60.R-CNN系列模型回顾
  61. 61.R-FCN的设计思路
  62. 62.R-FCN的实现
  63. 63.YOLOv1
  64. 64.YOLOv2_9000
  65. 65.YOLOv3
  66. 66.图像分割应用场景与数据集
  67. 67.上采样的实现方式
  68. 68.FCN网络架构的由来
  69. 69.FCN构建细节
  70. 70.DeepLabv1
  71. 71.CRF
  72. 72.DeepLabv2
  73. 73.DeepLabv3
  74. 74.特定的医学影像分割任务
  75. 75.U-Net
  76. 76.3D U-Net
  77. 77.V-Net
  78. 78.DenseNet
  79. 79.病理切片分析任务及数据集
  80. 80.病理切片分析
  81. 81.图说任务及其传统处理方法
  82. 82.深度语言模型与RNN
  83. 83.LSTM及其变种
  84. 84.NIC模型
  85. 85.SAT模型
  86. 86.图说的性能指标
  87. 87.判别模型与生成模型
  88. 88.KL散度与JS散度
  89. 89.VAE
  90. 90.GAN的基本原理
  91. 91.DCGAN与CGAN
  92. 92.Wasserstein GAN
  93. 93.SRGAN

传统图像分割技术

小象实战讲义 · 基于深度学习的计算机视觉

在计算机视觉的诸多任务中,图像分割旨在将图像划分成若干个有意义的区域,是理解图像内容的关键一步。在深度学习技术普及之前,研究者们已经发展出了一系列基于图像底层特征(如灰度、颜色、纹理)的经典分割算法。本节我们将系统学习这些传统方法,理解它们如何利用像素间的“不连续性”和“相似性”来划分区域,为后续学习基于深度学习的语义分割打下坚实的基础。

💡 核心导读

  • 定义与目标:理解图像分割的核心是依据特征(灰度、颜色等)将图像划分为互不交叠的区域,使区域内特征相似,区域间特征差异明显。
  • 两大基石:掌握图像分割的两大基本特性——基于“不连续性”的边缘检测和基于“相似性”的区域聚合。
  • 经典方法:学习三种主要的传统分割方法:基于阈值的分割、基于边缘的分割以及基于区域的分割(包括区域生长法和分水岭算法)。
  • 应用与局限:认识这些方法在简单场景(如前景背景分割)中的应用,并理解其在复杂场景(如颜色接近)下的局限性。

图像分割:定义与基本思想

图像分割,从传统意义上说,是指根据图像的灰度、颜色、纹理或形状等特征,将图像划分成若干个互不相交(即交集为空集)的区域。其核心目标是:使这些特征在同一区域内呈现出相似性,而在不同区域间呈现出明显的差异性。

例如,我们希望将下图中的骆驼头(前景)与背景天空和沙地分离开来。理想情况下,分割出的骆驼区域内部颜色、纹理相似,而该区域与背景区域在特征上则有显著不同。

原始图像 -> [期望的分割结果:骆驼轮廓清晰,与背景分离]

这种基于底层视觉特征的划分,与后续课程中将学习的基于语义的分割有本质区别。后者依赖于深度学习模型理解图像内容(如“这是骆驼”、“那是天空”),而本节的传统方法仅依赖于像素值的数学统计特性。

所有经典数字图像分割算法都基于灰度值的两个基本特性之一:

  1. 不连续性:指图像局部特征的突变,例如边缘或角点。基于此的分割方法试图找到这些不连续点,并将其连接成区域边界。
  2. 相似性:指将具有相近特征(如灰度值、颜色)的像素聚集在一起,形成一个连通区域。

接下来,我们将围绕这两大特性,介绍几种代表性的传统分割方法。

基于阈值的分割

这是最直观、最简单的分割方法。其基本思想是:设定一个或多个灰度阈值,将图像中每个像素的灰度值与阈值比较,根据比较结果将其归入不同的类别。

核心操作:设定一个灰度阈值 TT。对于图像中的任意像素点 (x,y)(x, y),其灰度值为 I(x,y)I(x, y),则分割结果 S(x,y)S(x, y) 为: S(x,y)={1if I(x,y)≥T0if I(x,y)<TS(x, y) = \begin{cases} 1 & \text{if } I(x, y) \ge T \\ 0 & \text{if } I(x, y) < T \end{cases} 这样,图像就被分成了两部分:大于等于 TT 的像素群和小于 TT 的像素群。选择合适的 TT,就能实现分割。

效果与应用:这种方法通常会产生类似木刻版画或黑白肖像画的效果,适用于前景和背景灰度对比鲜明的场景。但其效果严重依赖于阈值 TT 的选择。

关键问题:如何确定最佳阈值 TT? 有多种方法,例如:

  • 双峰法:假设图像灰度直方图呈现双峰(分别对应前景和背景),取两峰之间的谷底作为阈值。
  • 迭代法:选择一个初始阈值,迭代计算前景和背景的平均灰度,并以它们的均值作为新阈值,直至收敛。
  • 大津法(Otsu 方法):被广泛认为是基于阈值的经典自动阈值选择方法。它由日本学者大津展之提出,其基本思想是最大化类间方差,或等价地最小化类内方差,从而找到使前景和背景分离得最“干净”的阈值。

大津法原理简述: 记 tt 为前景与背景的分割阈值。设前景像素数占图像比例为 w0w_0,平均灰度为 u0u_0;背景像素数占图像比例为 w1w_1,平均灰度为 u1u_1。则图像的总平均灰度为: u=w0⋅u0+w1⋅u1u = w_0 \cdot u_0 + w_1 \cdot u_1 类间方差 gg 定义为: g=w0⋅(u0−u)2+w1⋅(u1−u)2=w0⋅w1⋅(u0−u1)2g = w_0 \cdot (u_0 - u)^2 + w_1 \cdot (u_1 - u)^2 = w_0 \cdot w_1 \cdot (u_0 - u_1)^2 大津法遍历所有可能的阈值 tt,选择使类间方差 gg 最大的那个 tt 作为最佳阈值。从数学角度看,这比简单地找直方图最低点或追求面积相等更为可靠。

下面我们用 numpy 和 cv2 实现大津法,并观察其效果。我们将手动构造一个简单的双峰图像。

import numpy as np
import cv2

# 构造一个简单的8x8图像,模拟双峰分布:低灰度背景和高灰度前景
# 背景灰度 ~50,前景灰度 ~200
np.random.seed(42)  # 固定随机种子,确保结果可复现
background = np.random.normal(loc=50, scale=5, size=(8, 8)).astype(np.uint8)
foreground = np.random.normal(loc=200, scale=5, size=(3, 3)).astype(np.uint8)

# 将前景块嵌入背景中
image = background.copy()
image[2:5, 2:5] = foreground

print("构造的图像数据:")
print(image)
print("\n图像灰度直方图(简化统计):")
hist = cv2.calcHist([image], [0], None, [256], [0, 256])
# 打印非零的直方图值
for i in range(256):
    if hist[i] > 0:
        print(f"灰度值 {i}: {int(hist[i])} 个像素")

# 使用OpenCV的大津法自动计算阈值
otsu_threshold, _ = cv2.threshold(image, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)
print(f"\n大津法计算得到的最佳阈值: {otsu_threshold}")

# 应用该阈值进行分割
_, binary_image = cv2.threshold(image, otsu_threshold, 255, cv2.THRESH_BINARY)
print("\n应用阈值分割后的二值图像:")
print(binary_image)
构造的图像数据:
[[ 52  49  53  57  48  48  57  53]
 [ 47  52  47  47  51  40  41  47]
 [ 44  51 204 206 199  48  50  42]
 [ 47  50 205 201 196  48  46  59]
 [ 49  44 201 207 199  40  43  50]
 [ 53  50  49  48  42  46  47  55]
 [ 51  41  51  48  46  53  55  54]
 [ 45  48  51  54  47  49  44  44]]

图像灰度直方图(简化统计):
灰度值 40: 2 个像素
灰度值 41: 2 个像素
灰度值 42: 2 个像素
灰度值 43: 1 个像素
灰度值 44: 4 个像素
灰度值 45: 1 个像素
灰度值 46: 3 个像素
灰度值 47: 7 个像素
灰度值 48: 7 个像素
灰度值 49: 4 个像素
灰度值 50: 4 个像素
灰度值 51: 5 个像素
灰度值 52: 2 个像素
灰度值 53: 4 个像素
灰度值 54: 2 个像素
灰度值 55: 2 个像素
灰度值 57: 2 个像素
灰度值 59: 1 个像素
灰度值 196: 1 个像素
灰度值 199: 2 个像素
灰度值 201: 2 个像素
灰度值 204: 1 个像素
灰度值 205: 1 个像素
灰度值 206: 1 个像素
灰度值 207: 1 个像素

大津法计算得到的最佳阈值: 59.0

应用阈值分割后的二值图像:
[[  0   0   0   0   0   0   0   0]
 [  0   0   0   0   0   0   0   0]
 [  0   0 255 255 255   0   0   0]
 [  0   0 255 255 255   0   0   0]
 [  0   0 255 255 255   0   0   0]
 [  0   0   0   0   0   0   0   0]
 [  0   0   0   0   0   0   0   0]
 [  0   0   0   0   0   0   0   0]]

可以看到,大津法成功地在低灰度群(~50)和高灰度群(~200)之间找到了阈值 59。因为 59 与 196 之间是一段没有像素的空白区间,把阈值取在 59 到 195 之间的任何值都会得到完全相同的分割结果,OpenCV 返回了这段区间下端的 59.0,并准确地将嵌入的3x3高亮前景块分割了出来。

基于边缘的分割

这类方法利用了图像特征的不连续性。其基本思路是:先通过边缘检测算法(如Sobel、Canny)确定图像中的边缘像素点,然后将这些像素点连接起来,构成所需的区域边界。

核心思想:边缘是图像中两个不同区域的边界线上连续的像素点的集合,反映了灰度、颜色、纹理等特性的突变。找到边缘,就等于找到了区域的轮廓。

典型流程:

  1. 边缘检测:使用梯度算子(如Sobel)或更高级的边缘检测器(如Canny)计算图像的边缘强度图。
  2. 边缘连接:将检测到的、属于同一边界的边缘像素连接起来,形成连续的轮廓线。
  3. 区域闭合:确保轮廓线是闭合的,从而划分出内部区域。

这种方法在物体与背景对比明显、边缘清晰时效果很好。但当图像噪声较大、边缘模糊或断裂时,边缘连接和区域闭合会变得非常困难。

import numpy as np
import cv2

# 构造一个简单的包含边缘的图像:一个白色方块在黑色背景上
image = np.zeros((10, 10), dtype=np.uint8)
image[3:7, 3:7] = 255  # 4x4的白色方块

print("原始图像:")
print(image)

# 1. 使用Sobel算子计算x和y方向的梯度
grad_x = cv2.Sobel(image, cv2.CV_64F, 1, 0, ksize=3)
grad_y = cv2.Sobel(image, cv2.CV_64F, 0, 1, ksize=3)

# 计算梯度幅值(近似,截断到 uint8 可表示的范围,避免溢出回绕)
grad_magnitude = np.abs(grad_x) + np.abs(grad_y)
grad_magnitude = np.clip(grad_magnitude, 0, 255).astype(np.uint8)

print("\nSobel梯度幅值图像(显示了边缘位置):")
print(grad_magnitude)

# 2. 使用Canny边缘检测器(更高级,包含非极大值抑制和双阈值)
# 由于图像很简单,我们使用较低的阈值
edges = cv2.Canny(image, 50, 150)
print("\nCanny边缘检测结果:")
print(edges)
原始图像:
[[  0   0   0   0   0   0   0   0   0   0]
 [  0   0   0   0   0   0   0   0   0   0]
 [  0   0   0   0   0   0   0   0   0   0]
 [  0   0   0 255 255 255 255   0   0   0]
 [  0   0   0 255 255 255 255   0   0   0]
 [  0   0   0 255 255 255 255   0   0   0]
 [  0   0   0 255 255 255 255   0   0   0]
 [  0   0   0   0   0   0   0   0   0   0]
 [  0   0   0   0   0   0   0   0   0   0]
 [  0   0   0   0   0   0   0   0   0   0]]

Sobel梯度幅值图像(显示了边缘位置):
[[  0   0   0   0   0   0   0   0   0   0]
 [  0   0   0   0   0   0   0   0   0   0]
 [  0   0 255 255 255 255 255 255   0   0]
 [  0   0 255 255 255 255 255 255   0   0]
 [  0   0 255 255   0   0 255 255   0   0]
 [  0   0 255 255   0   0 255 255   0   0]
 [  0   0 255 255 255 255 255 255   0   0]
 [  0   0 255 255 255 255 255 255   0   0]
 [  0   0   0   0   0   0   0   0   0   0]
 [  0   0   0   0   0   0   0   0   0   0]]

Canny边缘检测结果:
[[  0   0   0   0   0   0   0   0   0   0]
 [  0   0   0   0   0   0   0   0   0   0]
 [  0   0   0   0 255 255   0   0   0   0]
 [  0   0   0 255   0   0 255   0   0   0]
 [  0   0 255   0   0   0 255   0   0   0]
 [  0   0 255   0   0   0 255   0   0   0]
 [  0   0   0 255 255 255 255   0   0   0]
 [  0   0   0   0   0   0   0   0   0   0]
 [  0   0   0   0   0   0   0   0   0   0]
 [  0   0   0   0   0   0   0   0   0   0]]

从输出可以看出,Sobel和Canny都成功检测到了白色方块的边界。Sobel结果中边界较粗(因为梯度计算),而Canny结果则给出了更细、更清晰的单像素边缘。基于这些边缘点,理论上可以连接出方块的轮廓,从而实现分割。

基于区域的分割

这类方法利用了图像特征的相似性。其基本思路是:将图像中特征(如灰度、颜色)相似的相邻像素聚合起来,形成区域。主要包括种子区域生长法和分水岭算法。

区域生长法

区域生长是根据一种事先定义的准则,将像素或者子区域聚合成更大区域的过程。它需要保证分割后的区域满足:内部连通、区域间互斥,且所有区域的并集能构成全图。

算法流程:

  1. 选择种子点:根据某种规则(如最亮的点、用户点击)选择一个或多个起始像素(种子)。
  2. 定义生长准则:通常基于灰度差、颜色距离或纹理相似性。例如,定义一个“最大像素灰度值距离” DmaxD_{max}。
  3. 迭代生长:检查种子区域邻域内的像素。如果某个邻域像素与当前区域的平均灰度值之差的绝对值不大于 DmaxD_{max},则将该像素加入区域,并更新区域的平均灰度值。
  4. 终止生长:当没有新的像素满足生长准则时,该区域停止生长。
  5. 重复:如果还有未归属的种子点,重复上述过程。
import numpy as np

def region_growing(image, seed, threshold):
    """
    简单的区域生长算法演示
    image: 输入灰度图像 (numpy array)
    seed: 种子点坐标 (row, col)
    threshold: 生长阈值(最大灰度差)
    """
    height, width = image.shape
    visited = np.zeros_like(image, dtype=bool)
    region = np.zeros_like(image, dtype=bool)
    
    # 初始化:种子点入栈,标记为已访问和属于区域
    stack = [seed]
    visited[seed] = True
    region[seed] = True
    region_mean = float(image[seed])
    region_pixel_count = 1
    
    # 4邻域方向
    directions = [(-1, 0), (1, 0), (0, -1), (0, 1)]
    
    while stack:
        current_row, current_col = stack.pop()
        
        for d_row, d_col in directions:
            n_row, n_col = current_row + d_row, current_col + d_col
            
            # 检查边界和是否访问过
            if (0 <= n_row < height and 0 <= n_col < width and 
                not visited[n_row, n_col]):
                
                visited[n_row, n_col] = True
                pixel_value = image[n_row, n_col]
                
                # 生长准则:新像素与当前区域均值的差 <= 阈值
                if abs(pixel_value - region_mean) <= threshold:
                    region[n_row, n_col] = True
                    stack.append((n_row, n_col))
                    # 更新区域均值(增量计算,避免重复求和)
                    region_mean = (region_mean * region_pixel_count + pixel_value) / (region_pixel_count + 1)
                    region_pixel_count += 1
    return region

# 构造一个简单的图像,模拟一个高亮区域
image = np.array([
    [10, 11,  9, 20, 21],
    [12, 10, 13, 22, 19],
    [ 8, 14, 15, 25, 24],
    [18, 17, 16, 30, 28],
    [50, 52, 48, 55, 53]
], dtype=np.uint8)

print("原始图像:")
print(image)

# 选择种子点 (0,0),阈值为5
seed_point = (0, 0)
threshold = 5
grown_region = region_growing(image, seed_point, threshold)

print(f"\n以点{seed_point}(灰度值{image[seed_point]})为种子,阈值{threshold},生长得到的区域(True表示属于区域):")
print(grown_region)

# 可视化:将区域内的像素值设为255,其他为0
result = np.where(grown_region, 255, 0).astype(np.uint8)
print("\n区域生长结果可视化(区域为白色):")
print(result)
原始图像:
[[10 11  9 20 21]
 [12 10 13 22 19]
 [ 8 14 15 25 24]
 [18 17 16 30 28]
 [50 52 48 55 53]]

以点(0, 0)(灰度值10)为种子,阈值5,生长得到的区域(True表示属于区域):
[[ True  True  True False False]
 [ True  True  True False False]
 [ True  True  True False False]
 [False  True  True False False]
 [False False False False False]]

区域生长结果可视化(区域为白色):
[[255 255 255   0   0]
 [255 255 255   0   0]
 [255 255 255   0   0]
 [  0 255 255   0   0]
 [  0   0   0   0   0]]

可以看到,算法从种子点(0,0)开始,将灰度值与当前区域均值之差不超过5的相邻像素聚合;由于区域均值随生长从10漂移到约12.7,灰度16、17被纳入,而灰度18被排除。最终得到第0–2行前3列的9个像素,以及第3行第1–2列的2个像素,共11个像素;右下角灰度值较高的像素(50+)和中间灰度值突变到20+的像素则被排除在外。

分水岭算法

分水岭算法是一种基于拓扑理论的图像区域分割法。它将图像视为一个地形表面,其中:

  • 图像的梯度值高的地方(如边缘)对应“山脉”。
  • 梯度值低的地方(均匀区域内部)对应“山谷”或“盆地”。

核心思想:想象从每个“山谷”(局部最小值)开始注水,水位逐渐上升。当来自不同山谷的水即将汇合时,就在汇合处筑起一道“水坝”。这些“水坝”就是分割边界。

算法步骤:

  1. 计算梯度图:对图像(通常先灰度化)使用Sobel等算子计算梯度幅值。梯度图构成了“地形”的高度。
  2. 寻找标记:确定肯定属于不同区域的像素点(即“山谷”的底部)。这可以通过寻找梯度图的局部最小值,或由用户交互提供。
  3. 分水岭变换:从标记开始模拟注水过程,合并像素,直到水坝形成。
  4. 生成分割:水坝的位置即为分割边界。

分水岭算法对噪声和细微的灰度变化非常敏感,容易导致“过分割”(将一个大区域分成很多小碎片)。通常需要配合预处理(如平滑)或后处理来改善效果。

import numpy as np
import cv2

# 构造一个简单的图像,包含两个明显的“盆地”
image = np.array([
    [0, 0, 0, 0, 0, 0, 0],
    [0, 1, 1, 0, 2, 2, 0],
    [0, 1, 1, 0, 2, 2, 0],
    [0, 0, 0, 3, 0, 0, 0],
    [0, 4, 4, 0, 5, 5, 0],
    [0, 4, 4, 0, 5, 5, 0],
    [0, 0, 0, 0, 0, 0, 0]
], dtype=np.uint8) * 30  # 放大灰度差异

print("原始图像(模拟四个 2×2 亮块与一个孤立亮点):")
print(image)

# 1. 计算梯度图(这里我们用Sobel近似,对于这个简单图像,梯度在边缘处高)
grad_x = cv2.Sobel(image, cv2.CV_64F, 1, 0, ksize=3)
grad_y = cv2.Sobel(image, cv2.CV_64F, 0, 1, ksize=3)
gradient = np.sqrt(grad_x**2 + grad_y**2)
gradient = np.uint8(np.clip(gradient, 0, 255))

print("\n梯度幅值图(高值对应边缘/山脉):")
print(gradient)

# 为了演示分水岭,我们需要创建“标记”。
# 在真实场景中,标记可能来自用户交互或前景/背景提取。
# 这里我们手动指定:图像最外圈作为背景种子,两个物体的中心作为目标种子。
markers = np.zeros_like(image, dtype=np.int32)
# 标记图像最外圈为背景种子(1)
markers[0, :] = 1
markers[-1, :] = 1
markers[:, 0] = 1
markers[:, -1] = 1
# 标记左上方物体为2
markers[1:3, 1:3] = 2
# 标记右上方物体为3
markers[1:3, 4:6] = 3

print("\n初始标记图(1:背景种子, 2:物体A, 3:物体B):")
print(markers)

# 2. 应用分水岭算法
# OpenCV的分水岭要求输入是3通道8位图像,且会修改标记图
image_color = cv2.cvtColor(image, cv2.COLOR_GRAY2BGR)
cv2.watershed(image_color, markers)

print("\n分水岭算法后的标记图(边界被标记为-1):")
print(markers)

# 提取边界(标记为-1的位置)
boundaries = (markers == -1).astype(np.uint8) * 255
print("\n分割边界(-1的位置):")
print(boundaries)
原始图像(模拟四个 2×2 亮块与一个孤立亮点):
[[  0   0   0   0   0   0   0]
 [  0  30  30   0  60  60   0]
 [  0  30  30   0  60  60   0]
 [  0   0   0  90   0   0   0]
 [  0 120 120   0 150 150   0]
 [  0 120 120   0 150 150   0]
 [  0   0   0   0   0   0   0]]

梯度幅值图(高值对应边缘/山脉):
[[  0  60  60  60 120 120   0]
 [ 60 127 127 127 254 254 120]
 [ 60 127   0 127 127 254 120]
 [180 255 255 189 255 255 180]
 [240 255 255 127 255 255 255]
 [240 255 255 255 255 255 255]
 [  0 240 240  60 255 255   0]]

初始标记图(1:背景种子, 2:物体A, 3:物体B):
[[1 1 1 1 1 1 1]
 [1 2 2 0 3 3 1]
 [1 2 2 0 3 3 1]
 [1 0 0 0 0 0 1]
 [1 0 0 0 0 0 1]
 [1 0 0 0 0 0 1]
 [1 1 1 1 1 1 1]]

分水岭算法后的标记图(边界被标记为-1):
[[-1 -1 -1 -1 -1 -1 -1]
 [-1  2  2 -1  3  3 -1]
 [-1  2  2 -1  3  3 -1]
 [-1  2  2 -1  3  3 -1]
 [-1  2  2  2 -1  3 -1]
 [-1  2  2  2 -1  3 -1]
 [-1 -1 -1 -1 -1 -1 -1]]

分割边界(-1的位置):
[[255 255 255 255 255 255 255]
 [255   0   0 255   0   0 255]
 [255   0   0 255   0   0 255]
 [255   0   0 255   0   0 255]
 [255   0   0   0 255   0 255]
 [255   0   0   0 255   0 255]
 [255 255 255 255 255 255 255]]

在这个简化的例子中,我们只把图像最外圈标记为背景种子、把两个物体的中心标记为目标种子。需要说明的是,OpenCV 的 watershed 直接在输入图像的灰度面上按标记注水;本节的梯度图仅用于直观展示“山脉/山谷”概念,并未作为 watershed 的输入。算法在标记引导下,在物体 A、B 之间(第3列)以及物体与背景之间筑起边界(标记为 -1);未标记的像素会按灰度地形被分配给相邻盆地,例如物体 A 向下扩展到了第4–5行。这说明初始标记的设计直接决定分割结果:标记过粗或缺失时,算法无法凭空恢复正确边界。

📝 动手练一练

  1. 概念辨析:回顾本节介绍的基于边缘的分割和区域生长法,你认为它们最根本的差异在哪里?各自更适合处理什么类型的图像?
👉 点击查看参考答案

根本差异:

  • 基于边缘的分割:关注图像特征的不连续性。它首先检测出突变点(边缘),然后通过连接这些点来形成区域边界。这是一种“由外向内”的思路,先找边界,再定义区域。
  • 区域生长法:关注图像特征的相似性。它从内部种子点出发,将特征相似的相邻像素聚合起来,逐渐“生长”成区域。这是一种“由内向外”的思路,先定义区域核心,再扩展边界。

适用场景:

  • 基于边缘的分割:更适合处理物体与背景对比鲜明、边缘清晰锐利的图像。例如,工业零件检测、文字分割等。
  • 区域生长法:更适合处理区域内部特征均匀、但区域间可能有渐变或噪声的图像。例如,医学图像中某个器官或组织的分割,只要其内部灰度相对一致。
  1. 算法实践:在区域生长法的代码示例中,我们将生长阈值 threshold 设为了5。如果将其改为 2,你认为生长得到的区域会变大、变小还是不变?请简要说明原因,并尝试修改代码验证你的猜想。
👉 点击查看参考答案

猜想:区域会变小。

原因:生长阈值定义了像素能被加入区域的最大灰度差异。阈值从5减小到2,意味着生长准则变得更加严格,只有与当前区域平均灰度值更接近的像素才能被加入。例如本例中灰度值为13、14、15的像素,以及灰度值为8的像素,在阈值为5时可以被加入,阈值降到2后它们与区域当前均值的差超过2,就被挡在区域之外。

验证:修改代码中的 threshold = 5 为 threshold = 2 并重新运行,观察 grown_region 的输出。你会发现,原来的11个像素会缩小为5个像素:第一行的3个像素,加上第二行的前2个像素。

本章小结

本节我们深入探讨了深度学习时代之前的经典图像分割技术。这些方法不依赖于语义理解,而是基于图像像素本身的底层特征(灰度、颜色)进行数学上的划分。

  • 核心思想:利用区域内特征的相似性和区域间特征的不连续性。
  • 阈值分割:简单直接,通过设定灰度阈值进行二分类,其效果高度依赖于阈值选择算法(如大津法)。
  • 边缘分割:通过检测梯度突变点(边缘)来勾勒物体轮廓,适用于边缘清晰的场景。
  • 区域分割:
    • 区域生长:从种子点出发,像滚雪球一样聚合相似像素,形成连通区域。
    • 分水岭:将图像视为地形,通过模拟注水过程,在山脉(边缘)处筑坝,形成分割边界。

这些传统方法是计算机视觉发展史上的重要里程碑,它们解决了许多实际问题,并且其思想(如基于相似性/不连续性)至今仍对现代算法有启发。然而,它们也共同面临一些局限性:对噪声敏感、依赖于人工参数(阈值、种子点)、难以处理纹理复杂或语义信息关键的场景。这正引出了我们对下一代技术——基于深度学习的语义分割——的学习需求。

📋 行动清单

  • 理解核心:确保能清晰阐述图像分割基于“相似性”和“不连续性”两大特性的不同实现路径。
  • 动手验证:运行本节的所有代码示例,尝试修改参数(如阈值、种子点),观察分割结果的变化,加深对算法行为的理解。
  • 思考局限:找一张颜色接近、前景背景不易区分的图片(如讲师举例的骆驼图),思考本节介绍的哪种方法可能完全失效,为什么?

—— 小象教研组

配套资料 · 直接下载

课件、代码、数据集等配套资料全部免费,点击即可直接下载,无需分享或解锁。

  • 第4章《未有深度学习之前》配套课件(PDF)
    PDF · 2.5MB · 原课教辅
    下载
  • 第4章《未有深度学习之前》配套代码包
    ZIP · 2.2MB · 原课教辅
    下载