查看全课大纲(第 20 / 93 节)
- 1.计算机视觉概述
- 2.计算机视觉应用场景
- 3.困难与挑战
- 4.开源库介绍
- 5.图像显示与存储
- 6.CLAHE
- 7.腐蚀膨胀与开闭运算
- 8.滤波与边缘填充
- 9.各种滤波
- 10.图像金字塔
- 11.傅里叶变换
- 12.短时傅里叶变换与小波变换
- 13.颜色特征与颜色直方图
- 14.几何特征_边缘
- 15.几何特征-角点与斑点
- 16.局部特征-SIFT特征描述子
- 17.局部特征-SURF特征描述子
- 18.局部特征-ORB特征描述子
- 19.其他特征描述子
- 20.传统图像分割技术
- 21.GraphCut
- 22.GrabCut
- 23.人脸检测的传统方法
- 24.行人检测的传统方法HOG+SVM
- 25.行人检测DPM
- 26.人工神经网络的架构
- 27.目标函数与梯度下降
- 28.误差反向传播
- 29.可手算的神经网络
- 30.改进目标函数与克服梯度消散
- 31.神经网络之传统神经网络与深度网络的差异
- 32.各种梯度下降算法
- 33.如何选择合适的梯度下降算法
- 34.避免过适应
- 35.CNN的架构
- 36.CNN的误差反向传播_卷积层
- 37.CNN的误差反向传播_池化层
- 38.分类所用的数据集
- 39.Alexnet详解
- 40.VGG详解
- 41.GoogLeNet_v1
- 42.BatchNormalization
- 43.GoogLeNet_v3v4
- 44.ResNet
- 45.ResNeXt
- 46.CNN设计准则
- 47.图像检索与推土机距离
- 48.基于颜色相似性
- 49.基于形状相似性
- 50.基于纹理相似性
- 51.基于局部特征相似性
- 52.DL的二值哈希编码
- 53.高维空间中检索KDTree
- 54.高维空间中检索LSH
- 55.目标识别任务综述
- 56.RCNN
- 57.SPP-Net
- 58.Fast R-CNN
- 59.Faster R-CNN
- 60.R-CNN系列模型回顾
- 61.R-FCN的设计思路
- 62.R-FCN的实现
- 63.YOLOv1
- 64.YOLOv2_9000
- 65.YOLOv3
- 66.图像分割应用场景与数据集
- 67.上采样的实现方式
- 68.FCN网络架构的由来
- 69.FCN构建细节
- 70.DeepLabv1
- 71.CRF
- 72.DeepLabv2
- 73.DeepLabv3
- 74.特定的医学影像分割任务
- 75.U-Net
- 76.3D U-Net
- 77.V-Net
- 78.DenseNet
- 79.病理切片分析任务及数据集
- 80.病理切片分析
- 81.图说任务及其传统处理方法
- 82.深度语言模型与RNN
- 83.LSTM及其变种
- 84.NIC模型
- 85.SAT模型
- 86.图说的性能指标
- 87.判别模型与生成模型
- 88.KL散度与JS散度
- 89.VAE
- 90.GAN的基本原理
- 91.DCGAN与CGAN
- 92.Wasserstein GAN
- 93.SRGAN
传统图像分割技术
小象实战讲义 · 基于深度学习的计算机视觉
在计算机视觉的诸多任务中,图像分割旨在将图像划分成若干个有意义的区域,是理解图像内容的关键一步。在深度学习技术普及之前,研究者们已经发展出了一系列基于图像底层特征(如灰度、颜色、纹理)的经典分割算法。本节我们将系统学习这些传统方法,理解它们如何利用像素间的“不连续性”和“相似性”来划分区域,为后续学习基于深度学习的语义分割打下坚实的基础。
💡 核心导读
- 定义与目标:理解图像分割的核心是依据特征(灰度、颜色等)将图像划分为互不交叠的区域,使区域内特征相似,区域间特征差异明显。
- 两大基石:掌握图像分割的两大基本特性——基于“不连续性”的边缘检测和基于“相似性”的区域聚合。
- 经典方法:学习三种主要的传统分割方法:基于阈值的分割、基于边缘的分割以及基于区域的分割(包括区域生长法和分水岭算法)。
- 应用与局限:认识这些方法在简单场景(如前景背景分割)中的应用,并理解其在复杂场景(如颜色接近)下的局限性。
图像分割:定义与基本思想
图像分割,从传统意义上说,是指根据图像的灰度、颜色、纹理或形状等特征,将图像划分成若干个互不相交(即交集为空集)的区域。其核心目标是:使这些特征在同一区域内呈现出相似性,而在不同区域间呈现出明显的差异性。
例如,我们希望将下图中的骆驼头(前景)与背景天空和沙地分离开来。理想情况下,分割出的骆驼区域内部颜色、纹理相似,而该区域与背景区域在特征上则有显著不同。
原始图像 -> [期望的分割结果:骆驼轮廓清晰,与背景分离]这种基于底层视觉特征的划分,与后续课程中将学习的基于语义的分割有本质区别。后者依赖于深度学习模型理解图像内容(如“这是骆驼”、“那是天空”),而本节的传统方法仅依赖于像素值的数学统计特性。
所有经典数字图像分割算法都基于灰度值的两个基本特性之一:
- 不连续性:指图像局部特征的突变,例如边缘或角点。基于此的分割方法试图找到这些不连续点,并将其连接成区域边界。
- 相似性:指将具有相近特征(如灰度值、颜色)的像素聚集在一起,形成一个连通区域。
接下来,我们将围绕这两大特性,介绍几种代表性的传统分割方法。
基于阈值的分割
这是最直观、最简单的分割方法。其基本思想是:设定一个或多个灰度阈值,将图像中每个像素的灰度值与阈值比较,根据比较结果将其归入不同的类别。
核心操作:设定一个灰度阈值 。对于图像中的任意像素点 ,其灰度值为 ,则分割结果 为: 这样,图像就被分成了两部分:大于等于 的像素群和小于 的像素群。选择合适的 ,就能实现分割。
效果与应用:这种方法通常会产生类似木刻版画或黑白肖像画的效果,适用于前景和背景灰度对比鲜明的场景。但其效果严重依赖于阈值 的选择。
关键问题:如何确定最佳阈值 ? 有多种方法,例如:
- 双峰法:假设图像灰度直方图呈现双峰(分别对应前景和背景),取两峰之间的谷底作为阈值。
- 迭代法:选择一个初始阈值,迭代计算前景和背景的平均灰度,并以它们的均值作为新阈值,直至收敛。
- 大津法(Otsu 方法):被广泛认为是基于阈值的经典自动阈值选择方法。它由日本学者大津展之提出,其基本思想是最大化类间方差,或等价地最小化类内方差,从而找到使前景和背景分离得最“干净”的阈值。
大津法原理简述: 记 为前景与背景的分割阈值。设前景像素数占图像比例为 ,平均灰度为 ;背景像素数占图像比例为 ,平均灰度为 。则图像的总平均灰度为: 类间方差 定义为: 大津法遍历所有可能的阈值 ,选择使类间方差 最大的那个 作为最佳阈值。从数学角度看,这比简单地找直方图最低点或追求面积相等更为可靠。
下面我们用 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)确定图像中的边缘像素点,然后将这些像素点连接起来,构成所需的区域边界。
核心思想:边缘是图像中两个不同区域的边界线上连续的像素点的集合,反映了灰度、颜色、纹理等特性的突变。找到边缘,就等于找到了区域的轮廓。
典型流程:
- 边缘检测:使用梯度算子(如Sobel)或更高级的边缘检测器(如Canny)计算图像的边缘强度图。
- 边缘连接:将检测到的、属于同一边界的边缘像素连接起来,形成连续的轮廓线。
- 区域闭合:确保轮廓线是闭合的,从而划分出内部区域。
这种方法在物体与背景对比明显、边缘清晰时效果很好。但当图像噪声较大、边缘模糊或断裂时,边缘连接和区域闭合会变得非常困难。
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结果则给出了更细、更清晰的单像素边缘。基于这些边缘点,理论上可以连接出方块的轮廓,从而实现分割。
基于区域的分割
这类方法利用了图像特征的相似性。其基本思路是:将图像中特征(如灰度、颜色)相似的相邻像素聚合起来,形成区域。主要包括种子区域生长法和分水岭算法。
区域生长法
区域生长是根据一种事先定义的准则,将像素或者子区域聚合成更大区域的过程。它需要保证分割后的区域满足:内部连通、区域间互斥,且所有区域的并集能构成全图。
算法流程:
- 选择种子点:根据某种规则(如最亮的点、用户点击)选择一个或多个起始像素(种子)。
- 定义生长准则:通常基于灰度差、颜色距离或纹理相似性。例如,定义一个“最大像素灰度值距离” 。
- 迭代生长:检查种子区域邻域内的像素。如果某个邻域像素与当前区域的平均灰度值之差的绝对值不大于 ,则将该像素加入区域,并更新区域的平均灰度值。
- 终止生长:当没有新的像素满足生长准则时,该区域停止生长。
- 重复:如果还有未归属的种子点,重复上述过程。
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+的像素则被排除在外。
分水岭算法
分水岭算法是一种基于拓扑理论的图像区域分割法。它将图像视为一个地形表面,其中:
- 图像的梯度值高的地方(如边缘)对应“山脉”。
- 梯度值低的地方(均匀区域内部)对应“山谷”或“盆地”。
核心思想:想象从每个“山谷”(局部最小值)开始注水,水位逐渐上升。当来自不同山谷的水即将汇合时,就在汇合处筑起一道“水坝”。这些“水坝”就是分割边界。
算法步骤:
- 计算梯度图:对图像(通常先灰度化)使用Sobel等算子计算梯度幅值。梯度图构成了“地形”的高度。
- 寻找标记:确定肯定属于不同区域的像素点(即“山谷”的底部)。这可以通过寻找梯度图的局部最小值,或由用户交互提供。
- 分水岭变换:从标记开始模拟注水过程,合并像素,直到水坝形成。
- 生成分割:水坝的位置即为分割边界。
分水岭算法对噪声和细微的灰度变化非常敏感,容易导致“过分割”(将一个大区域分成很多小碎片)。通常需要配合预处理(如平滑)或后处理来改善效果。
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行。这说明初始标记的设计直接决定分割结果:标记过粗或缺失时,算法无法凭空恢复正确边界。
📝 动手练一练
- 概念辨析:回顾本节介绍的基于边缘的分割和区域生长法,你认为它们最根本的差异在哪里?各自更适合处理什么类型的图像?
👉 点击查看参考答案
根本差异:
- 基于边缘的分割:关注图像特征的不连续性。它首先检测出突变点(边缘),然后通过连接这些点来形成区域边界。这是一种“由外向内”的思路,先找边界,再定义区域。
- 区域生长法:关注图像特征的相似性。它从内部种子点出发,将特征相似的相邻像素聚合起来,逐渐“生长”成区域。这是一种“由内向外”的思路,先定义区域核心,再扩展边界。
适用场景:
- 基于边缘的分割:更适合处理物体与背景对比鲜明、边缘清晰锐利的图像。例如,工业零件检测、文字分割等。
- 区域生长法:更适合处理区域内部特征均匀、但区域间可能有渐变或噪声的图像。例如,医学图像中某个器官或组织的分割,只要其内部灰度相对一致。
- 算法实践:在区域生长法的代码示例中,我们将生长阈值
threshold设为了5。如果将其改为 2,你认为生长得到的区域会变大、变小还是不变?请简要说明原因,并尝试修改代码验证你的猜想。
👉 点击查看参考答案
猜想:区域会变小。
原因:生长阈值定义了像素能被加入区域的最大灰度差异。阈值从5减小到2,意味着生长准则变得更加严格,只有与当前区域平均灰度值更接近的像素才能被加入。例如本例中灰度值为13、14、15的像素,以及灰度值为8的像素,在阈值为5时可以被加入,阈值降到2后它们与区域当前均值的差超过2,就被挡在区域之外。
验证:修改代码中的 threshold = 5 为 threshold = 2 并重新运行,观察 grown_region 的输出。你会发现,原来的11个像素会缩小为5个像素:第一行的3个像素,加上第二行的前2个像素。
本章小结
本节我们深入探讨了深度学习时代之前的经典图像分割技术。这些方法不依赖于语义理解,而是基于图像像素本身的底层特征(灰度、颜色)进行数学上的划分。
- 核心思想:利用区域内特征的相似性和区域间特征的不连续性。
- 阈值分割:简单直接,通过设定灰度阈值进行二分类,其效果高度依赖于阈值选择算法(如大津法)。
- 边缘分割:通过检测梯度突变点(边缘)来勾勒物体轮廓,适用于边缘清晰的场景。
- 区域分割:
- 区域生长:从种子点出发,像滚雪球一样聚合相似像素,形成连通区域。
- 分水岭:将图像视为地形,通过模拟注水过程,在山脉(边缘)处筑坝,形成分割边界。
这些传统方法是计算机视觉发展史上的重要里程碑,它们解决了许多实际问题,并且其思想(如基于相似性/不连续性)至今仍对现代算法有启发。然而,它们也共同面临一些局限性:对噪声敏感、依赖于人工参数(阈值、种子点)、难以处理纹理复杂或语义信息关键的场景。这正引出了我们对下一代技术——基于深度学习的语义分割——的学习需求。
📋 行动清单
- 理解核心:确保能清晰阐述图像分割基于“相似性”和“不连续性”两大特性的不同实现路径。
- 动手验证:运行本节的所有代码示例,尝试修改参数(如阈值、种子点),观察分割结果的变化,加深对算法行为的理解。
- 思考局限:找一张颜色接近、前景背景不易区分的图片(如讲师举例的骆驼图),思考本节介绍的哪种方法可能完全失效,为什么?
—— 小象教研组
配套资料 · 直接下载
课件、代码、数据集等配套资料全部免费,点击即可直接下载,无需分享或解锁。