第三章 图像分割基础:从像素到区域的实战之路
图像分割,说白了就是把图像里我们感兴趣的部分「抠」出来。在超声图像里,这步尤其关键——你要从一堆噪声和伪影中,找到那个病灶或者器官边界。我刚开始做超声算法时,觉得分割不就是调个阈值嘛,后来被现实狠狠教育了一顿。
这一章,我们聊聊三种最基础的分割方法:阈值分割、边缘检测、区域生长。它们虽然「老」,但直到今天,很多超声产品的预处理管线里,依然在用这些方法做快速初筛。
3.1 阈值分割:最简单的「一刀切」
阈值分割的原理,简单到令人发指:设定一个值T,像素灰度大于T的算一类,小于T的算另一类。在超声图像中,囊肿区域通常表现为低回声(暗区),而周围组织回声较强(亮区),这时候阈值分割就派上用场了。
3.1.1 全局阈值
全局阈值就是整张图用一个固定的T。我早期做过一个甲状腺结节分割项目,一开始就用全局阈值,结果发现不同探头、不同增益设置下,同一个病人的图像灰度分布差异巨大。固定阈值根本扛不住。
代码实现很简单:
import cv2
import numpy as np
def global_threshold(image, thresh=128):
# 二值化:大于thresh的设为255,小于的设为0
_, binary = cv2.threshold(image, thresh, 255, cv2.THRESH_BINARY)
return binary
嗯,这里要注意:超声图像的灰度范围通常不是0-255,而是经过对数压缩后的16位数据。直接拿8位阈值去切,会丢失大量细节。我建议先做直方图均衡化,再设定阈值。
3.1.2 Otsu算法:自动找最佳阈值
Otsu算法解决了「阈值怎么选」的问题。它的核心思想是:让分割后的两类像素,类内方差最小、类间方差最大。说白了,就是让「黑得更黑,白得更白」。
你想想看,如果直方图有两个明显的峰,Otsu会自动把阈值设在两个峰之间的谷底。这个特性在超声图像里特别有用——比如区分充满液体的囊肿(低回声)和实性组织(高回声)。
def otsu_threshold(image):
# Otsu自动计算最优阈值
_, binary = cv2.threshold(image, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)
return binary
我个人习惯在预处理阶段先用Otsu做一次粗分割,把明显的前景和背景分开,然后再用更精细的方法做后续处理。这样能大幅减少计算量。
3.2 边缘检测:找到那些「突变」的像素
边缘检测,就是找到图像中灰度变化剧烈的位置。在超声图像里,器官边界、血管壁、病灶轮廓,都是边缘。我做过一个肝脏肿瘤分割项目,边缘检测的质量直接决定了后续分割的精度。
3.2.1 Sobel算子:一阶导数的朴素实现
Sobel算子计算的是图像在x方向和y方向的一阶导数。它用两个3x3的卷积核,分别检测水平和垂直边缘。
为什么用Sobel?因为它简单、快。在超声实时处理中,每帧图像只有几十毫秒的处理时间,Sobel的轻量级特性让它成为首选。
def sobel_edge(image):
# 计算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)
# 计算梯度幅值
magnitude = np.sqrt(grad_x**2 + grad_y**2)
return np.uint8(magnitude)
我记得有一次,用Sobel检测胎儿头围的边缘,结果发现噪声响应特别强。后来加了高斯滤波做平滑,效果才好了很多。记住:Sobel对噪声敏感,用之前一定要去噪。
3.2.2 Canny边缘检测:工业级标准
Canny可以说是边缘检测的「天花板」。它做了三件事:先用高斯滤波去噪,再用非极大值抑制细化边缘,最后用双阈值连接边缘。
双阈值是Canny的精髓。高阈值确定强边缘,低阈值保留弱边缘,但只有与强边缘相连的弱边缘才会被保留。这个机制在超声图像里特别有用——因为病灶边界往往有灰度渐变,单阈值很容易把边界切碎。
def canny_edge(image, low_thresh=50, high_thresh=150):
# Canny自动完成去噪、非极大值抑制、双阈值连接
edges = cv2.Canny(image, low_thresh, high_thresh)
return edges
3.2.3 Laplacian算子:二阶导数的妙用
Laplacian算子是二阶导数,它检测的是灰度变化的「变化率」。说白了,Sobel找的是「哪里在变」,Laplacian找的是「哪里变得最快」。
Laplacian对孤立点和细线特别敏感。在超声图像里,我常用它来增强微钙化点——这些小亮点在乳腺超声中可能是恶性病变的标志。
def laplacian_edge(image):
# Laplacian算子,ksize必须是奇数
laplacian = cv2.Laplacian(image, cv2.CV_64F, ksize=3)
return np.uint8(np.abs(laplacian))
3.3 区域生长法:从种子到区域的「蔓延」
区域生长法的思路很直观:选一个种子点,然后不断把周围「相似」的像素加进来,直到没有新像素满足条件。这个「相似」可以是灰度值接近、纹理相似,甚至可以是距离相近。
在超声图像里,区域生长法特别适合分割那些边界模糊但内部均匀的结构——比如囊肿、积液区域。我做过一个肾囊肿分割项目,囊肿边界在超声上几乎看不清,但内部灰度非常均匀,区域生长法反而比深度学习效果好。
def region_growing(image, seed, threshold=10):
# 种子点坐标 (x, y)
h, w = image.shape
segmented = np.zeros((h, w), dtype=np.uint8)
seed_value = image[seed[1], seed[0]]
# 使用队列实现BFS
queue = [seed]
visited = set()
visited.add(seed)
while queue:
x, y = queue.pop(0)
if segmented[y, x] == 0:
segmented[y, x] = 255
# 检查4邻域
for dx, dy in [(1,0), (-1,0), (0,1), (0,-1)]:
nx, ny = x+dx, y+dy
if 0 <= nx < w and 0 <= ny < h and (nx, ny) not in visited:
if abs(int(image[ny, nx]) - int(seed_value)) < threshold:
queue.append((nx, ny))
visited.add((nx, ny))
return segmented
嗯,这里要注意几个坑:
- 种子点选择:种子点必须落在目标区域内。我一般让医生手动点一下,或者用Otsu粗分割后的最大连通域中心作为种子。
- 阈值设定:阈值太小,区域长不开;阈值太大,会溢出到背景。我建议先做直方图分析,取目标区域灰度标准差的一半作为初始阈值。
- 生长策略:4邻域还是8邻域?超声图像里我习惯用8邻域,因为边界往往有斜向连接。
3.4 三种方法的对比与选择
说了这么多,到底什么时候用哪种?我整理了一个表格,方便你快速决策:
| 方法 | 适用场景 | 优点 | 缺点 | 超声典型应用 |
|---|---|---|---|---|
| 全局阈值 | 目标与背景灰度差异明显 | 速度快,实现简单 | 对光照、增益变化敏感 | 囊肿/积液粗分割 |
| Otsu | 直方图双峰分布 | 自动选阈值,无需人工 | 单峰或平坦直方图失效 | 甲状腺结节初筛 |
| Sobel | 实时处理,快速边缘检测 | 计算量小,方向可控 | 噪声敏感,边缘较粗 | 血管壁检测 |
| Canny | 需要精细、连续的边缘 | 边缘定位准,抗噪性好 | 参数调优较麻烦 | 器官边界提取 |
| Laplacian | 检测孤立点、细线、角点 | 各向同性,旋转不变 | 噪声放大严重 | 微钙化点增强 |
| 区域生长 | 内部均匀、边界模糊的目标 | 分割结果连续、完整 | 种子点依赖,速度较慢 | 囊肿、积液分割 |
最后说一句:这些方法虽然基础,但组合起来威力巨大。我现在的很多超声分割管线,都是先用Otsu做粗分割,再用Canny提取边界,最后用区域生长做精细化调整。别小看这些「老方法」,它们往往是深度学习模型落地时的「救火队员」。
下一章,我们会聊聊更高级的分割方法——基于形态学的处理。到时候你会发现,很多今天讲的内容,都是那些高级方法的基石。