4、图像分割:基于阈值的分割、边缘检测(Canny、Sobel)、区域生长与分水岭算法

图像分割,说白了就是把图像里我们感兴趣的目标给“抠”出来。在超声图像里,这活儿尤其难——噪声多、边界模糊、组织纹理复杂。我做了这么多年超声处理,最深的体会就是:没有万能的分割算法,只有最合适的组合拳

今天咱们就聊聊四种经典方法:阈值分割、边缘检测、区域生长和分水岭。每种方法我都踩过坑,也总结了一些实战技巧。

4.1 基于阈值的分割:简单但有效

阈值分割是最直观的方法。你想想看,超声图像里囊肿区域通常暗一些,钙化点亮一些,设定一个灰度阈值就能分开。

核心公式很简单:

g(x,y) = 1  if f(x,y) > T
         0  if f(x,y) ≤ T

但问题来了——T 怎么选?

我早期做乳腺超声囊肿分割时,直接拍脑袋设了个固定阈值 128。结果呢?不同机器、不同探头出来的图像亮度差异巨大,同一个阈值根本不管用。后来我改用 Otsu 算法(大津法),它自动计算最佳阈值,效果稳定多了。

Otsu 算法核心思想:遍历所有可能的阈值,找到使类间方差最大的那个值。类间方差越大,说明前景和背景分离得越开。

代码实现也不复杂:

import cv2
import numpy as np

# 读取超声图像(灰度图)
img = cv2.imread('ultrasound.png', 0)

# Otsu 自动阈值
ret, thresh = cv2.threshold(img, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)

print(f"最佳阈值: {ret}")
cv2.imshow('Otsu 分割结果', thresh)

我的经验:如果超声图像光照不均匀(比如近场亮、远场暗),全局阈值会失效。这时候可以试试 自适应阈值(cv2.adaptiveThreshold),它根据局部邻域计算阈值,能处理光照变化。

4.2 边缘检测:Canny 与 Sobel

边缘检测的目标是找到图像中灰度变化剧烈的位置。在超声图像里,器官边界、血管壁、肿瘤轮廓都是边缘。

4.2.1 Sobel 算子:一阶导数的经典

Sobel 算子计算图像在 x 和 y 方向上的梯度。说白了,就是看像素值变化有多快。

水平方向 Sobel 核:

Gx = [[-1, 0, 1],
      [-2, 0, 2],
      [-1, 0, 1]]

垂直方向 Sobel 核:

Gy = [[-1, -2, -1],
      [ 0,  0,  0],
      [ 1,  2,  1]]

然后梯度幅值:G = sqrt(Gx² + Gy²)

代码示例:

import cv2
import numpy as np

img = cv2.imread('ultrasound.png', 0)

# Sobel 边缘检测
sobel_x = cv2.Sobel(img, cv2.CV_64F, 1, 0, ksize=3)
sobel_y = cv2.Sobel(img, cv2.CV_64F, 0, 1, ksize=3)
sobel = cv2.magnitude(sobel_x, sobel_y)

# 归一化到 0-255
sobel = np.uint8(np.clip(sobel, 0, 255))

cv2.imshow('Sobel 边缘', sobel)

注意:Sobel 对噪声敏感。超声图像本身就有大量斑点噪声,直接 Sobel 会得到一堆假边缘。我建议先做 高斯滤波中值滤波 去噪,再做边缘检测。

4.2.2 Canny 边缘检测:工业级标准

Canny 算法是边缘检测的“扛把子”。它比 Sobel 强在哪?低错误率、精确定位、单像素响应

Canny 的步骤:

  1. 高斯滤波:去噪
  2. 计算梯度幅值和方向:用 Sobel 算子
  3. 非极大值抑制:只保留梯度方向上的局部最大值,让边缘变细
  4. 双阈值检测:高阈值确定强边缘,低阈值连接弱边缘

代码实现:

import cv2

img = cv2.imread('ultrasound.png', 0)

# Canny 边缘检测
# 参数:低阈值 50,高阈值 150
edges = cv2.Canny(img, 50, 150)

cv2.imshow('Canny 边缘', edges)

避坑指南:双阈值怎么设?我曾经在肝脏超声图像上调试,发现高阈值设得太高会丢失弱边缘(比如小血管),设得太低又会有太多噪声。我的经验是:高阈值设为低阈值的 2-3 倍,然后根据结果微调。比如先试 (30, 90),再试 (50, 150)。

4.3 区域生长:从种子点开始

区域生长的思路很朴素:选一个种子点,然后不断把周围相似的像素“拉”进来

适合的场景:目标区域内部灰度均匀,边界清晰。比如超声图像里的囊肿、积液区域。

算法步骤:

  1. 手动或自动选择一个种子点
  2. 检查种子点周围的像素,如果灰度差小于阈值,就加入区域
  3. 以新加入的像素为种子,继续生长
  4. 直到没有新像素加入为止

代码示例(简化版):

import cv2
import numpy as np

def region_growing(img, seed, threshold=10):
    h, w = img.shape
    segmented = np.zeros((h, w), dtype=np.uint8)
    seed_value = img[seed[0], seed[1]]
    
    # 使用队列进行 BFS
    queue = [seed]
    segmented[seed[0], seed[1]] = 255
    
    while queue:
        x, y = queue.pop(0)
        # 检查 4 邻域
        for dx, dy in [(-1,0), (1,0), (0,-1), (0,1)]:
            nx, ny = x + dx, y + dy
            if 0 <= nx < h and 0 <= ny < w:
                if segmented[nx, ny] == 0:
                    diff = abs(int(img[nx, ny]) - int(seed_value))
                    if diff < threshold:
                        segmented[nx, ny] = 255
                        queue.append((nx, ny))
    return segmented

# 使用
img = cv2.imread('cyst.png', 0)
seed = (100, 150)  # 手动选种子点
result = region_growing(img, seed, threshold=15)
cv2.imshow('区域生长结果', result)

我的教训:区域生长对种子点位置和阈值非常敏感。有一次我选种子点选在了囊肿边缘,结果生长到一半就“漏”出去了。后来我改成 多种子点 策略——在目标区域内部均匀撒几个种子,取并集,鲁棒性好了很多。

4.4 分水岭算法:处理粘连目标

分水岭算法特别适合处理 多个目标粘连在一起 的情况。比如超声图像里多个相邻的囊肿,或者重叠的血管截面。

它的原理很形象:把图像看成地形图,灰度值高的地方是山峰,低的地方是山谷。然后从山谷开始“注水”,水慢慢上涨,不同水域相遇的地方就是分水岭(即分割边界)

但直接应用分水岭容易 过分割(把同一个目标切成好几块)。我常用的方法是 基于标记的分水岭:先确定哪些区域肯定是前景,哪些肯定是背景,然后只在这些标记之间做分水岭。

代码示例:

import cv2
import numpy as np

# 读取图像并二值化
img = cv2.imread('cells.png')
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
ret, thresh = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY_INV + cv2.THRESH_OTSU)

# 距离变换,找到前景标记
dist = cv2.distanceTransform(thresh, cv2.DIST_L2, 5)
ret, markers = cv2.threshold(dist, 0.7 * dist.max(), 255, 0)
markers = np.uint8(markers)

# 找到未知区域(边界)
kernel = np.ones((3,3), np.uint8)
unknown = cv2.subtract(thresh, markers)

# 标记连通区域
ret, markers = cv2.connectedComponents(markers)
markers = markers + 1  # 背景标记为 1
markers[unknown == 255] = 0  # 未知区域标记为 0

# 应用分水岭
markers = cv2.watershed(img, markers)
img[markers == -1] = [0, 0, 255]  # 边界画成红色

cv2.imshow('分水岭结果', img)

注意:分水岭算法对噪声和微弱边缘非常敏感。超声图像里如果斑点噪声严重,建议先做 形态学开闭运算 平滑一下,再跑分水岭。否则你会得到一张“碎成渣”的分割结果。

4.5 实战建议:如何选择分割方法?

我根据项目经验,整理了一个选择指南:

场景 推荐方法 理由
目标与背景灰度差异大 Otsu 阈值分割 简单快速,自动选阈值
需要提取边界轮廓 Canny 边缘检测 边缘连续、定位准
目标内部均匀、边界清晰 区域生长 适合囊肿、积液等
多个粘连目标 分水岭算法 能分开重叠区域
噪声大、边界模糊 先滤波 + 再 Canny 或区域生长 预处理是关键

我的习惯:在实际项目中,我很少只用一种方法。通常是 阈值分割做粗分割 → 边缘检测或形态学做精修 → 分水岭处理粘连。比如做甲状腺结节分割,先用 Otsu 把结节区域大致抠出来,再用 Canny 精确定位边界,最后用分水岭把多个结节分开。这套流程我用了好几年,效果一直很稳。

好了,图像分割的四种经典方法就聊到这儿。记住:没有最好的算法,只有最合适的流程。多试、多调、多总结,你也能成为分割高手。