《生物医学图像处理》实验五 实验报告¶
声明:本实验所有代码基于gemini 3 pro辅助完成,本人已充分理解实验代码内容
Project 1: Edge Detection¶
实验内容¶
本实验旨在利用基于梯度算子的图像处理技术,从脑部 MRI 图像(T1 加权与 T2 加权两种模态)中提取病灶的边缘轮廓。实验核心流程包括:
- 预处理:对原始数据进行归一化及 5x5 平滑滤波以抑制噪声。
- 梯度计算:手动实现 Sobel 梯度核,计算图像梯度幅值。
- 边缘二值化:通过设定全局阈值 $T$ 获取初步边缘掩膜。
- 边缘优化:去除大脑最外圈的高亮边缘(头骨/皮层),减少非目标干扰。
- 形态学处理:执行闭运算连接断裂的边缘点。
- 目标提取:通过连通域分析筛选出候选病灶边缘。
实验原理¶
- 5x5 平滑滤波:通过空间域卷积降低图像的高频噪声。由于 Sobel 算子本质上是导数运算,对噪声极其敏感,平滑是保证边缘连续性的前提。
- Sobel 算子:使用水平核 $G_x$ 和垂直核 $G_y$ 分别提取图像在两个方向的亮度变化。 $$G_x = \begin{bmatrix} -1 & 0 & 1 \\ -2 & 0 & 2 \\ -1 & 0 & 1 \end{bmatrix}, \quad G_y = \begin{bmatrix} -1 & -2 & -1 \\ 0 & 0 & 0 \\ 1 & 2 & 1 \end{bmatrix}$$ 最终梯度幅值 $M = \sqrt{G_x^2 + G_y^2}$。
- 闭运算:先膨胀后腐蚀。其作用是“桥接”裂缝,能够将 Sobel 提取出的碎裂边缘线段连接成相对完整的环状。
- 连通域分析:根据像素的邻接关系标记集合。通过计算连通块的面积(像素数)来剔除细小的噪点,保留具有解剖意义的结构(病灶边缘)。
import numpy as np
import cv2
import matplotlib.pyplot as plt
from scipy.signal import convolve2d
import os
def apply_sobel_kernel(image):
"""
实现 Sobel 梯度核计算
"""
Kx = np.array([[-1, 0, 1],
[-2, 0, 2],
[-1, 0, 1]], dtype=np.float32)
Ky = np.array([[-1, -2, -1],
[ 0, 0, 0],
[ 1, 2, 1]], dtype=np.float32)
Gx = convolve2d(image, Kx, mode='same', boundary='symm')
Gy = convolve2d(image, Ky, mode='same', boundary='symm')
magnitude = np.sqrt(Gx**2 + Gy**2)
if magnitude.max() > 0:
magnitude = (magnitude / magnitude.max()) * 255
return magnitude.astype(np.uint8)
def segment_lesion_edge_only(img, threshold_T):
# 1. 5x5 平滑滤波器
img_smooth = cv2.GaussianBlur(img, (5, 5), 0)
# 2. Sobel 滤波器
grad_mag = apply_sobel_kernel(img_smooth)
# 3. 阈值 T
_, edge_mask = cv2.threshold(grad_mag, threshold_T, 255, cv2.THRESH_BINARY)
# 4. 去除大脑外围边缘
# 获取大脑主体区域,用较大的核进行腐蚀,彻底排除头骨和皮层边缘
_, brain_mask = cv2.threshold(img_smooth, 20, 255, cv2.THRESH_BINARY)
kernel_erode = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (25, 25))
brain_core = cv2.erode(brain_mask, kernel_erode, iterations=1)
internal_edges = cv2.bitwise_and(edge_mask, brain_core)
# 5. 闭运算
# 注意:这里缩小了闭运算的核 (5x5 或 7x7),为了连接断点,同时防止边缘线糊成一团厚厚的面
kernel_close = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (5,5))
closed_edges = cv2.morphologyEx(internal_edges, cv2.MORPH_CLOSE, kernel_close)
# 6. 连通域处理 (提取病灶边缘环)
# 对线段进行连通域分析
num_labels, labels, stats, centroids = cv2.connectedComponentsWithStats(closed_edges, connectivity=8)
lesion_edge_mask = np.zeros_like(img)
valid_labels = []
for i in range(1, num_labels): # 跳过背景(0)
area = stats[i, cv2.CC_STAT_AREA] # 这里的 area 实际上是这条“线”包含的像素个数
# 过滤掉细碎的短线(噪点),保留较长的边缘线
if 150 < area < 3000:
valid_labels.append(i)
if valid_labels:
# 在符合长度的边缘线中,取像素最多(最长/最完整)的那条作为病灶轮廓
best_label = max(valid_labels, key=lambda x: stats[x, cv2.CC_STAT_AREA])
lesion_edge_mask[labels == best_label] = 255
return grad_mag, closed_edges, lesion_edge_mask
def main():
if not (os.path.exists('T1.npy') and os.path.exists('T2.npy')):
print("请确保 T1.npy 和 T2.npy 在当前目录下。")
return
img_t1 = np.load('T1.npy')
img_t2 = np.load('T2.npy')
img_t1 = cv2.normalize(img_t1, None, 0, 255, cv2.NORM_MINMAX).astype(np.uint8)
img_t2 = cv2.normalize(img_t2, None, 0, 255, cv2.NORM_MINMAX).astype(np.uint8)
# 阈值可根据实际效果微调
T1_THRESHOLD = 45
T2_THRESHOLD = 55
t1_grad, t1_edges, t1_lesion = segment_lesion_edge_only(img_t1, T1_THRESHOLD)
t2_grad, t2_edges, t2_lesion = segment_lesion_edge_only(img_t2, T2_THRESHOLD)
# ================= 结果可视化 =================
fig, axes = plt.subplots(2, 4, figsize=(16, 8))
plt.subplots_adjust(wspace=0.3, hspace=0.3)
titles = ['Original', 'Sobel Gradient', 'Closed Edges', 'Final Lesion Edge']
images_t1 = [img_t1, t1_grad, t1_edges, t1_lesion]
for i in range(4):
axes[0, i].imshow(images_t1[i], cmap='gray')
axes[0, i].set_title(f'T1: {titles[i]}')
axes[0, i].axis('off')
images_t2 = [img_t2, t2_grad, t2_edges, t2_lesion]
for i in range(4):
axes[1, i].imshow(images_t2[i], cmap='gray')
axes[1, i].set_title(f'T2: {titles[i]}')
axes[1, i].axis('off')
plt.show()
if __name__ == "__main__":
main()
实验结果与分析¶
T1 与 T2 提取效果对比¶
- T2 模态效果显著优于 T1:
- T2 图像中,病灶(肿瘤及周围水肿)呈现明显的高信号(亮色),与周围正常的脑组织(灰色)形成了极强的对比。因此,Sobel 算子在病灶边界能捕捉到非常清晰、连续的梯度峰值。
- T1 图像中,病灶呈现低信号(暗色),且其灰度级与周围的灰质、脑沟回非常接近。这导致 Sobel 算子提取出的病灶边缘非常微弱,且极易混杂在大脑内部结构的纹理边缘中。
- 可视化表现:
- T2 的呈现出一个相对清晰的“环状”,但仍然与准确的边缘有较大差距;而 T1 的最终结果中,病灶边缘破碎严重,且包含了大量无关的解剖碎块。
边缘提取效果不佳的原因分析¶
实验中,可以明显看到病灶的边缘提取效果并不是特别好。T1和T2提取出来的病灶边缘都没有闭合,且均未提取到完整的边缘。可能的原因如下:
- 灰度不均与梯度缺失:病灶边缘并非在 360° 方向上都有剧烈的灰度变化。在某些局部,病灶与正常组织的灰度完全一致,导致 Sobel 算子在该处无法产生梯度,形成“物理断层”。
- 脑部解剖结构的干扰:脑沟(沟壑)和脑回(褶皱)在 Sobel 梯度图中也会产生强烈的边缘响应。对于 T1 图像,这些结构的梯度往往强于病灶边缘,导致连通域筛选时难以通过“面积”这一单一维度将其剔除。
- 全局阈值 $T$ 的局限性:实验采用了全局统一阈值。然而,图像不同区域的对比度不同,固定的 $T$ 难以同时满足“保留微弱病灶边缘”和“抑制背景噪声”的需求。
- 闭运算核的大小矛盾:若闭运算内核太小,无法连接巨大的断层;若内核太大,则会使原本精细的边缘线变成肥大的块状,失去边缘的几何特征。
算法改进思路¶
- 对比度增强:在 Sobel 运算前,对原始图像进行 CLAHE(限制对比度自适应直方图均衡化)。这能人为拉大病灶与背景的灰度差,尤其对提升 T1 模态的效果至关重要。
- 多尺度边缘融合:结合 Canny 算子或 Laplacian 算子。Canny 算子包含非极大值抑制,能产生更细、更准确的边缘,有助于提升结果的精细度。
- 空洞填充与细化:在获得闭合边缘后,可以先执行孔洞填充得到实心掩膜,再通过形态学骨架提取还原回单像素宽度的完美边缘线。
- 局部自适应阈值:替代全局阈值 $T$,根据像素邻域的分布动态计算阈值,以适应 MRI 图像信号不均的特性。
- 引入先验空间位置:由于病灶通常位于大脑内部特定区域,可以在筛选连通域时加入“质心位置”约束,剔除靠近边缘的无关干扰。
Project 2: Thresholding¶
实验内容¶
本实验重点研究图像分割中的阈值处理技术,通过对比手动实现的两种经典算法,探究其在不同 MRI 模态下的表现:
- 迭代全局阈值分割 (Iterative Global Thresholding):实现自动化寻找阈值的迭代过程,并测试不同初始阈值 $T_{init}$(本实验选择60, 125, 190)对最终分割结果及算法收敛性的影响。
- 大津法 (Otsu's Optimum Thresholding):通过最大化类间方差(Between-class Variance)自动寻找最优阈值,并绘制类间方差曲线以验证结果。
- 对比分析:针对 T1 和 T2 脑部 MRI 图像,分析两种算法在提取病灶及脑组织结构上的差异。
实验原理¶
迭代全局阈值法¶
该算法是一种基于图像整体灰度分布的启发式搜索方法,步骤如下:
- 初始设置:选择一个初始阈值 $T$(通常取图像灰度的平均值或中值)。
- 分割与均值计算:根据 $T$ 将图像像素分为 $G_1$(大于 $T$)和 $G_2$(小于等于 $T$)两部分。分别计算这两部分的平均灰度值 $\mu_1$ 和 $\mu_2$。
- 更新阈值:计算新的阈值 $T_{new} = \frac{\mu_1 + \mu_2}{2}$。
- 收敛判定:重复上述过程,直到 $|T - T_{new}|$ 小于预设的极小值 $\Delta$。
大津法 (Otsu's Method)¶
大津法被公认为全局阈值选择的最佳算法之一,其核心是寻找使“类间方差”最大的阈值 $k$。
- 公式:类间方差 $\sigma_B^2(k) = w_1(k)w_2(k)[\mu_1(k) - \mu_2(k)]^2$。
- 其中 $w_1, w_2$ 分别是背景和前景像素占整幅图像的比例。
- $\mu_1, \mu_2$ 是这两部分的平均灰度。
- 最优性:当类间方差最大时,意味着目标和背景之间的差别最明显,错分概率最小。
实验过程(代码编写)¶
import numpy as np
import cv2
import matplotlib.pyplot as plt
import os
def iterative_threshold_manual(image, initial_T, delta=0.5):
"""
实现迭代全局阈值分割
"""
# 排除背景像素(<=10),仅对脑组织进行计算
pixels = image[image > 10].astype(np.float32)
if len(pixels) == 0: return 0
T = float(initial_T)
while True:
# 分为两类
g1 = pixels[pixels > T]
g2 = pixels[pixels <= T]
# 计算均值
mu1 = np.mean(g1) if len(g1) > 0 else 0
mu2 = np.mean(g2) if len(g2) > 0 else 0
T_new = (mu1 + mu2) / 2.0
# 判断收敛
if abs(T - T_new) < delta:
break
T = T_new
return T
def otsu_threshold_manual(image):
"""
手动实现 Otsu 最优阈值算法,并返回方差曲线
"""
# 排除背景像素,只统计脑组织部分
brain_pixels = image[image > 10]
total_pixels = brain_pixels.size
# 计算直方图
hist = np.histogram(brain_pixels, bins=256, range=(0, 256))[0]
variances = np.zeros(256)
max_variance = -1
best_T = 0
# 遍历所有可能的阈值 t
for t in range(1, 255):
# 计算 w0, w1 (类概率权重)
w0 = np.sum(hist[:t]) / total_pixels
w1 = np.sum(hist[t:]) / total_pixels
if w0 == 0 or w1 == 0:
continue
# 计算 mu0, mu1 (类平均灰度)
mu0 = np.sum(np.arange(t) * hist[:t]) / (w0 * total_pixels)
mu1 = np.sum(np.arange(t, 256) * hist[t:]) / (w1 * total_pixels)
# 计算类间方差 sigma_b^2
variance = w0 * w1 * ((mu0 - mu1) ** 2)
variances[t] = variance
if variance > max_variance:
max_variance = variance
best_T = t
return best_T, variances
def main():
# 检查数据
if not (os.path.exists('T1.npy') and os.path.exists('T2.npy')):
print("请确保数据文件 T1.npy 和 T2.npy 在当前目录下。")
return
# 加载并归一化到 0-255
img_t1 = cv2.normalize(np.load('T1.npy'), None, 0, 255, cv2.NORM_MINMAX).astype(np.uint8)
img_t2 = cv2.normalize(np.load('T2.npy'), None, 0, 255, cv2.NORM_MINMAX).astype(np.uint8)
# 设定三个不同的初始 T 值
initial_Ts = [60, 125, 190]
# 绘图设置:2行(T1, T2) x 6列(原图, 3个迭代结果, Otsu结果, 方差曲线)
fig, axes = plt.subplots(2, 6, figsize=(22, 8))
plt.subplots_adjust(wspace=0.3, hspace=0.3)
for row, img, label in zip([0, 1], [img_t1, img_t2], ['T1', 'T2']):
# 1. 原始图像
axes[row, 0].imshow(img, cmap='gray')
axes[row, 0].set_title(f"{label} Original")
# 2. 迭代法:三个不同的初始 T 值
for i, t_init in enumerate(initial_Ts):
t_final = iterative_threshold_manual(img, t_init)
_, bin_img = cv2.threshold(img, t_final, 255, cv2.THRESH_BINARY)
axes[row, i+1].imshow(bin_img, cmap='gray')
axes[row, i+1].set_title(f"Iterative T_init={t_init}\nFinal T={t_final:.1f}")
# 3. Otsu 法结果
otsu_t, variances = otsu_threshold_manual(img)
_, otsu_bin = cv2.threshold(img, otsu_t, 255, cv2.THRESH_BINARY)
axes[row, 4].imshow(otsu_bin, cmap='gray')
axes[row, 4].set_title(f"Otsu's Result\nOptimal T={otsu_t}")
# 4. Otsu 类间方差曲线
axes[row, 5].plot(variances, color='blue', linewidth=1.5)
axes[row, 5].axvline(otsu_t, color='red', linestyle='--', label=f'Best T={otsu_t}')
axes[row, 5].set_title(f"{label} Otsu Variance Curve")
axes[row, 5].set_xlabel("Threshold t")
axes[row, 5].set_ylabel("Variance")
axes[row, 5].legend()
axes[row, 5].grid(True, alpha=0.3)
# 隐藏图像部分的坐标轴
for ax in axes[:, :5].flatten():
ax.axis('off')
plt.suptitle("Project 2: Global and Optimal Thresholding Segmentation", fontsize=16)
plt.show()
if __name__ == "__main__":
main()
实验结果与分析¶
初始阈值 $T_{init}$ 对迭代法的影响¶
根据实验结果图分析:
- 收敛稳定性:在 T1 图像中,$T_{init}$ 分别取 60, 125, 190,最终得到的收敛阈值 $T$ 分别为 116.8, 118.6, 118.6。在 T2 图像中,最终阈值均稳定在 151.9 附近。
- 结论:迭代阈值法的初始阈值的选取虽然不同,但算法最终都能收敛到极为接近的全局平衡点,对最终分割视觉效果的影响微乎其微。
T1 与 T2 模态的分割效果差异¶
- T2 模态效果较好:由于 T2 图像中的病灶呈高亮信号,其灰度级处于直方图的高端。无论是迭代法还是 Otsu 法,选取的阈值(约 152-153)均能精准地将高亮的病灶和脑脊液从背景中提取出来。
- T1 模态效果有限:T1 图像中病灶通常表现为低信号(黑影)。由于全局阈值分割本质上是提取亮部目标,因此在 T1 中,病灶被归类为背景,而算法提取出的是白质等较亮组织。实验结果显示,T1 的分割结果主要呈现了脑部的基本解剖轮廓,无法像 T2 那样直接通过阈值定位病灶。
Otsu 法与迭代法的效果比较¶
- 结果数值:从图中可见,在 T1 中,迭代法最终 $T \approx 118.6$,Otsu $T=118$;在 T2 中,迭代法 $T \approx 151.9$,Otsu $T=153$。
- 视觉一致性:两者得到的二值化图像在视觉上几乎没有区别,说明在脑部 MRI 这种目标与背景差异明显的图像中,两者的性能表现高度趋同。
算法优缺点说明¶
| 算法 | 优点 | 缺点 |
|---|---|---|
| 迭代阈值法 | 1. 原理简单,计算开销极小。 2. 逻辑直观,通过物理意义上的“中点”寻找平衡。 3. 对初始值不敏感,收敛速度快。 |
1. 属于启发式方法,缺乏严格的统计学证明其为最优解。 2. 在多噪声或灰度分布极度不均时,可能陷入局部震荡。 |
| 大津法 (Otsu) | 1. 全自动,无需设置任何初值。 2. 数学严谨,基于类间方差最大化,具有理论上的最优性。 3. 适应性广,对直方图双峰明显的图像效果极佳。 |
1. 计算量相对略大(需遍历 0-255 灰度级)。 2. 当目标与背景的面积比例极其悬殊时,阈值会向面积大的一方偏移,导致分割失败。 |
Project 3: Segmentation¶
实验内容¶
本实验旨在研究和实现多种高级图像分割算法,以实现对脑部 T2 影像中病灶区域的精准提取与体积定量计算。实验内容包括:
- 区域生长 (Region Growing):利用交互式选取的种子点,根据灰度相似性准则进行区域扩张。
- 标准 K-means 聚类:仅基于像素灰度特征将图像划分为 $K$ 个簇。
- 改进 K-means 聚类:构建包含灰度和空间坐标 $(x, y)$ 的多维特征向量,测试不同聚类数 $K$ 与空间权重 $W$ 的组合。
- 分水岭算法 (Watershed):利用形态学梯度作为地形,结合手动标记(Markers)解决过度分割问题。
- 体积定量化:根据 $1\text{mm} \times 1\text{mm}$ 的分辨率计算各方法提取的病灶体积($mm^3$)。
实验原理¶
区域生长 (Region Growing)¶
- 核心逻辑:从指定的种子点 $S$ 开始,检查邻域像素 $P$。若 $|I(P) - I(S)| \leq \text{Tolerance}$,则将 $P$ 合并入区域。
- 公式:区域集合 $R$ 满足 $\forall P \in R, |f(P) - \mu_R| \leq T$。
改进 K-means 聚类¶
- 特征扩展:将单一灰度特征扩展为三维向量 $V = [I, W \cdot X, W \cdot Y]$。
- 距离度量:使用欧氏距离进行分配
- 空间权重 $W$ 的意义:当 $W > 0$ 时,算法不仅考虑像素“有多亮”,还考虑像素“在哪里”,从而增强聚类结果的空间连通性。
分水岭算法 (Watershed)¶
- 地形概念:将图像梯度幅值视为地形高度,局部极小值点作为集水盆地的中心。
- 改进策略:引入手动 Markers(前景种子与背景种子),通过控制注水起始点,强制算法在特定边界处停止,从而克服传统分水岭对噪声敏感导致的“过度分割”。
import numpy as np
import cv2
import matplotlib.pyplot as plt
import os
# 全局变量
seed_point = None
img_t2 = None
def get_kmeans_mask(img, k, w_spatial, seed_x, seed_y):
"""手动实现改进 K-means:灰度 + 空间坐标"""
h, w = img.shape
y_idx, x_idx = np.indices((h, w))
# 构建特征向量:[灰度, 归一化x*权重, 归一化y*权重]
data = np.stack([
img.flatten() / 255.0,
(x_idx.flatten() / w) * w_spatial,
(y_idx.flatten() / h) * w_spatial
], axis=1).astype(np.float32)
criteria = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 10, 1.0)
_, labels, _ = cv2.kmeans(data, k, None, criteria, 10, cv2.KMEANS_PP_CENTERS)
labels = labels.reshape((h, w))
# 根据种子点坐标提取对应的聚类簇
target_label = labels[seed_y, seed_x]
mask = np.zeros_like(img)
mask[labels == target_label] = 255
return mask, labels
def main():
global seed_point, img_t2
if not os.path.exists('T2.npy'):
print("错误:未找到 T2.npy 文件")
return
# 1. 预处理
raw = np.load('T2.npy')
img_t2 = cv2.normalize(raw, None, 0, 255, cv2.NORM_MINMAX).astype(np.uint8)
h, w = img_t2.shape
# 2. 交互选取种子点
print(">>> 请在弹出窗口点击病灶中心...")
cv2.imshow("1. Click Lesion Center", img_t2)
def on_click(event, x, y, flags, param):
global seed_point
if event == cv2.EVENT_LBUTTONDOWN:
seed_point = (x, y)
cv2.destroyWindow("1. Click Lesion Center")
cv2.setMouseCallback("1. Click Lesion Center", on_click)
cv2.waitKey(0)
if seed_point is None:
print("未选取种子点,程序退出。")
return
sx, sy = seed_point
# 3. 区域生长:带滑动条实时调优
print(">>> 正在运行区域生长,请调整滑动条后按【回车键】确认...")
cv2.namedWindow("2. Adjust Region Growing")
cv2.createTrackbar("Tol", "2. Adjust Region Growing", 15, 50, lambda x: None)
mask_rg = np.zeros_like(img_t2)
final_rg_vol = 0
while True:
tol = cv2.getTrackbarPos("Tol", "2. Adjust Region Growing")
ff_mask = np.zeros((h + 2, w + 2), np.uint8)
flags = 4 | (255 << 8) | cv2.FLOODFILL_MASK_ONLY
cv2.floodFill(img_t2, ff_mask, (sx, sy), 0, (tol,), (tol,), flags)
mask_rg = ff_mask[1:-1, 1:-1]
final_rg_vol = np.sum(mask_rg == 255)
preview = cv2.cvtColor(img_t2, cv2.COLOR_GRAY2BGR)
preview[mask_rg == 255] = [0, 0, 255]
cv2.putText(preview, f"Vol: {final_rg_vol}mm2", (10, 30), cv2.FONT_HERSHEY_SIMPLEX, 1, (0,0,255), 2)
cv2.imshow("2. Adjust Region Growing", preview)
key = cv2.waitKey(1) & 0xFF
if key == 13: # 回车确认
cv2.destroyAllWindows()
break
# 4. 改进 K-Means 多参数对比测试
print(">>> 正在进行 K-Means 多参数对比测试...")
k_values = [3, 5, 7]
weights = [0.0, 0.1, 0.2]
kmeans_results = [] # 用于最后输出体积
km_fig, km_axes = plt.subplots(len(k_values), len(weights), figsize=(15, 12))
plt.subplots_adjust(hspace=0.4)
for i, k in enumerate(k_values):
for j, w_s in enumerate(weights):
mask, _ = get_kmeans_mask(img_t2, k, w_s, sx, sy)
vol = np.sum(mask == 255)
# 记录结果
method_name = f"Standard (K={k})" if w_s == 0 else f"Improved (K={k}, W={w_s})"
kmeans_results.append((method_name, vol))
km_axes[i, j].imshow(mask, cmap='gray')
km_axes[i, j].set_title(f"{method_name}\nVol: {vol}mm²", fontsize=10)
km_axes[i, j].axis('off')
# 5. 分水岭 (Watershed)
_, brain_mask = cv2.threshold(img_t2, 20, 255, cv2.THRESH_BINARY)
markers = np.zeros(img_t2.shape, dtype=np.int32)
markers[brain_mask == 0] = 1
cv2.circle(markers, (sx, sy), 5, 2, -1)
gradient = cv2.morphologyEx(img_t2, cv2.MORPH_GRADIENT, np.ones((3,3)))
cv2.watershed(cv2.cvtColor(gradient, cv2.COLOR_GRAY2BGR), markers)
mask_water = np.zeros_like(img_t2)
mask_water[markers == 2] = 255
water_vol = np.sum(mask_water == 255)
# 6. 最终展示图
res_fig, res_axes = plt.subplots(1, 3, figsize=(18, 6))
res_axes[0].imshow(img_t2, cmap='gray')
res_axes[0].scatter(sx, sy, c='red', s=50, label='Seed')
res_axes[0].set_title("Original T2 + Seed")
res_axes[1].imshow(mask_rg, cmap='gray')
res_axes[1].set_title(f"Region Growing Mask\nVol: {final_rg_vol}mm²")
res_axes[2].imshow(mask_water, cmap='gray')
res_axes[2].set_title(f"Watershed Mask\nVol: {water_vol}mm²")
for ax in res_axes: ax.axis('off')
# 7. 控制台输出体积统计结果
print("\n" + "="*40)
print(" 病灶体积统计报告 (mm²)")
print("="*40)
print(f"{'分割方法':<30} | {'体积 (mm²)':<10}")
print("-" * 45)
print(f"{'区域生长 (Region Growing)':<30} | {final_rg_vol:<10}")
print(f"{'分水岭 (Watershed)':<30} | {water_vol:<10}")
print("-" * 45)
for name, v in kmeans_results:
print(f"{name:<30} | {v:<10}")
print("="*40)
print("\n[完成] 图像已生成。请查看对比图表并关闭窗口结束程序。")
plt.show()
if __name__ == "__main__":
main()
>>> 请在弹出窗口点击病灶中心...
>>> 正在运行区域生长,请调整滑动条后按【回车键】确认...
>>> 正在进行 K-Means 多参数对比测试...
========================================
病灶体积统计报告 (mm²)
========================================
分割方法 | 体积 (mm²)
---------------------------------------------
区域生长 (Region Growing) | 2469
分水岭 (Watershed) | 3095
---------------------------------------------
Standard (K=3) | 4594
Improved (K=3, W=0.1) | 5060
Improved (K=3, W=0.2) | 5174
Standard (K=5) | 3452
Improved (K=5, W=0.1) | 4164
Improved (K=5, W=0.2) | 5647
Standard (K=7) | 3250
Improved (K=7, W=0.1) | 4195
Improved (K=7, W=0.2) | 4749
========================================
[完成] 图像已生成。请查看对比图表并关闭窗口结束程序。
实验结果与分析¶
区域生长算法分析¶
- 提取效果:在容忍度设置合理且种子点位于病灶核心时,能快速提取出连通性极好的病灶区域。
- 缺陷分析:由于 T2 影像中脑脊液同样具有高亮特征,若容忍度稍大,病灶区域会顺着亮度接近的脑脊液发生“泄漏”,导致提取体积异常增大(溢出)。
- 体积表现:通常较为稳定,但在病灶内部灰度不均时,容易出现内部空洞。而且无法完整提取病灶。
分水岭算法分析¶
在 T2 影像中,分水岭算法计算出的体积小于 K-means,原因如下:
梯度阻断:分水岭算法是在梯度图上运行的。病灶(肿瘤)内部往往存在灰度不均,如果病灶内部存在一个局部的强梯度(即亮度突变点),分水岭的“水流”会被这些内部的“高山”阻断,导致它只能充满最核心的区域,而无法向真实的病灶边缘扩张。
保守分割特性:分水岭非常依赖 Marker(种子点)的设置。如果种子点仅设在病灶的最核心,而病灶边缘的梯度响应较弱(边缘模糊),算法会倾向于在最明显的内部脊线处停止。这导致它只提取了病灶的核心高亮部分,而丢失了周围灰度略低、但仍属于病灶的边缘水肿区。
欠分割问题:相比于 K-means 的区域统计特性,分水岭更像是一种“边缘检测”的延伸。它对局部极值非常敏感,容易陷入局部解,从而产生体积偏小的结果。
改进 K-means 算法参数测试与分析¶
实验测试了 $K=\{3, 5, 7\}$ 与 $W=\{0.0, 0.1, 0.2\}$ 的组合:
- K 值的影响:
- $K=3$ 时,分类过于粗糙,病灶常与正常脑组织混淆。
- $K=5$ 或 $7$ 时,病灶能作为独立的高亮度簇被分离,效果更佳。
- 空间权重 $W$ 的影响:
- 当 $W$ 增大时,聚类簇的形状会变得越来越规整(趋于圆形),这有助于去除病灶外的飞地噪点。
- 但 $W$ 过大会导致错误提取:因为算法此时极度依赖空间距离。如果种子点附近的正常脑组织(非病灶区)在空间上离病灶核心很近,即便它们的灰度并不属于高亮区,也会被算法强行“拉入”病灶簇中。这会导致病灶边缘过度向外扩张,提取出非病灶区域,从而使体积数值虚高。
- 最佳效果组:$K=5, W=0.1$。这一组参数在保证病灶完整性的同时,较好地平衡了灰度准确性和空间连通性,没有出现严重的条纹现象。
各算法比较与应用场景¶
| 算法 | 优点 | 缺点 | 应用场景 |
|---|---|---|---|
| 区域生长 | 保证结果的强连通性,适合提取单一组织。 | 对种子点极度敏感,易发生泄漏。 | 边界清晰、背景简单的病灶提取。 |
| 标准 K-means | 无需手动设点,计算速度快。 | 结果破碎,受噪声影响大,无法区分不同位置的亮区。 | 图像初步分类、多组织普查。 |
| 改进 K-means | 结果圆润连续,能剔除远处噪点干扰。 | $W$ 过大会引入非病灶区,产生几何畸变。 | 存在信号不均、需平滑边缘的病灶分割。 |
| 分水岭算法 | 边缘定位最精准,能处理粘连。 | 极易过度分割,参数调整极其复杂。 | 对体积精确度要求极高的解剖结构分割。 |
实验心得¶
本实验围绕脑部 T1、T2 模态 MRI 图像,分阶段实现了病灶提取任务:
- 边缘检测 (Project 1):基于 Sobel 算子提取梯度,结合形态学闭运算连接断裂边缘,并通过连通域面积过滤剔除背景噪声与解剖结构干扰。
- 阈值分割 (Project 2):对比了迭代全局阈值法与 Otsu 算法。通过最大化类间方差(Between-class Variance)寻找最优切分点,并分析了背景像素对收敛阈值的影响。
- 高级分割 (Project 3):实现了区域生长、分水岭算法及改进 K-means 聚类。重点研究了空间权重 $W$ 对聚类内聚性的贡献,并定量计算了病灶体积。
实验证明,针对病灶特征不一的 MRI 影像,单一算法难以实现全场景适用。结合空间先验的改进 K-means 在形态保持上具有优势,而分水岭算法在边界定位上更具参考价值。多算法融合对比是实现临床定量分析的必要手段。
在代码实现过程中,主要的技术挑战集中在以下两个方面:
- 坐标索引对齐:在处理 OpenCV 函数(遵循
(x, y)坐标)与 NumPy 矩阵访问(遵循[row, col]即[y, x])时,初期的索引倒置导致种子点定位偏差。通过显式定义变量名解决了这一逻辑冲突。 - 特征归一化:K-means 聚类中,若未对空间坐标和灰度值进行等比例缩放,会导致特征量级失衡。通过将坐标映射至 $[0, 1]$ 区间并动态调整权重 $W$,成功解决了聚类结果中的几何畸变问题。
感谢吴丹老师在课堂上对图像处理理论的深入讲解,使我们建立了系统的知识框架;同时感谢助教学长在实验过程中提供的技术支持与反馈,帮助我们解决了诸多代码实现的细节难题。