心脏左心房切片分割数据集:切面划分、标签验证与深度学习训练实践
2026/9/15 19:42:35 网站建设 项目流程

简介:面向医学图像分割任务,这份数据集聚焦心脏左心房切片,提供轴位面、冠状面、矢状面三个切面划分好的2D图像与对应标签文件,mask中1为心脏、0为背景,可直接用于训练和评估分割模型。数据集共2000个文件,以1998张png格式的图片/掩膜为主体,另含1个python可视化脚本和1个json配置文件,整体约93.88MB。三个切面分别为x轴方向320×320分辨率共1351对图像,y轴方向320×130分辨率共1151对,z轴方向320×130分辨率共828对,结构清晰,便于按需取用。附带的可视化脚本可随机抽取一张图,将原图、GT图和GT叠加在原图上的效果一次展示并保存,适合快速排查标注质量或观察细节。已有361人学习使用,适合医学影像分析、深度学习分割方向的研究者作为实验数据与示例代码。

1. 心脏左心房切片分割数据集,先想清楚“切面”到底切在哪

拿到“医学图像分割数据集:心脏左心房切片分割【包含3个切面划分好的数据集(2类别)、标签文件、可视化代码】”这个标题,第一反应不是下载链接,而是三个问题:切面是谁切的、怎么切的、切完剩下什么。心脏 MRI 通常是三维体数据,直接喂给分割网络不是不行,但显存和标注成本都扛不住,所以常见做法是沿固定解剖方向切成 2D 切片,再逐片做分割。标题里“3个切面”大概率指矢状面、冠状面、横断面这组标准正交方向,也可能是房室短轴的不同层位。无论哪种,数据集的价值核心在“划分好的标签文件”:每个切片对应一张 mask,2 类别意味着背景算一类、左心房算一类。可视化代码负责把原图、mask、叠加图摆在一起,确认“切面”和“标签”对齐。这篇文章把数据集的结构、标签约定、切面划分逻辑、可视化验证和训练入口一次讲清。

2. 数据集结构与标签文件:先摸清文件布局,再谈训练

2.1 用一条 tree 命令确认切面和标签的组织方式

拿到压缩包先解压,然后看目录。常见做法是3_cut或类似目录下按切面分子目录,每个子目录里是图像文件和同名 mask 文件。我一般先跑的检查命令是:

unzip LA_dataset.zip -d LA_dataset cd LA_dataset find . -maxdepth 3 -type f | head -50

输出里能直接看到是“按切面分文件夹”还是“全部平铺、文件名带 cut 标记”。两种都不影响后续训练,但影响数据加载器的写法和可视化脚本的路径拼接。如果看到类似axial/img_001.pngsagittal/img_001.pngcoronal/img_001.png的布局,那就是按切面组织,数据加载器里给三个子目录分别建Dataset实例就行。

find列出的文件名是性别、序列号还是纯数字编号并不重要,重要的是“原图和标签是否同名同后缀”。如果原图是img_001.png、标签是img_001_mask.png,加载器里用replace或拼接来对应;如果同名但放在不同子目录,就按目录区分。标题已明确“标签文件”已划分好,所以焦点不是重新标注,而是先把对应关系验证掉,别拖到训练时报 shape mismatch。

2.2 2 类别语义:0 是背景,255 是左心房,非法值要提前查

医学分割标签常见两种约定:0/1 整数型,或 0/255 掩码型。2 类别数据集几乎都是 0 表示背景、1(或 255)表示目标器官,但很多公开数据集偏好 255,因为视觉上更亮、叠加更直观。这里用 Python 快速统计一下标签分布:

import numpy as np from PIL import Image def check_mask(path): m = np.array(Image.open(path)) uniq, counts = np.unique(m, return_counts=True) print(path, dict(zip(uniq.tolist(), counts.tolist()))) # 非法值检查:不应该是 0/1/255 之外的值 valid = set([0, 1, 255]) bad = set(uniq.tolist()) - valid assert len(bad) == 0, f"非法像素值: {bad}" check_mask("3_cut/axial/img_001_mask.png")

逻辑说明:先转 numpy 数组,再统计唯一像素值。valid只放 0、1、255 三个预期值,多出任何值说明标签文件可能混入灰度图、边缘线或注释标记。assert失败时不要硬着头皮训练,先看是哪一类异常:是整张图只有一个值(空切片),还是包含 128 这类可疑中间值(可能是插值或压缩伪影)。

提示:医学图像分割的标签绝大多数是单通道 PNG,而不是三通道彩色图。如果Image.open之后modeRGB,要转灰度或直接取第一个通道,否则后续计算 IoU 时 shape 会变成 H×W×3,mask 的类别数也跟着翻倍。

2.3 统计每张切片的器官占比,判断标签是否真的“划分好”

标题说“划分好的数据集”,这个“划分好”除了指训练/验证集划分,也可能指每个切面都自成一套。我不急着先训练,而是先算每个切面里左心房面积占全图面积的比例:

import glob for cut in ["axial", "sagittal", "coronal"]: paths = sorted(glob.glob(f"3_cut/{cut}/*_mask.png")) ratios = [] for p in paths: m = np.array(Image.open(p)) ratios.append((m > 0).mean()) print(cut, "切片数:", len(ratios), "占比均值:", round(np.mean(ratios), 4), "占比标准差:", round(np.std(ratios), 4))

如果某个切面的占比标准差特别大,说明该方向切片可能大量落在左心房边缘,有效标注区域很小,这些样本对训练来说容易被忽略或造成类别不平衡。标准差适中(0.05~0.2 之间)说明切片切到了器官核心区。这里得到的统计信息还能顺手用于损失函数设计:占比极低的切片用普通 Dice Loss 很难学到东西,可以给这类样本加权重,或直接做 hard sample mining。

3. 可视化代码:在训练前把原图、mask、切面对齐这件事验证掉

3.1 用 matplotlib 做三切面对比图,一眼看穿切面方向

可视化代码的价值不只是“看个样子”,而是验证三件事:文件名是否一一对应、mask 是否贴在哪一层解剖结构上、切面方向是否真的不同。以下是我常用的检查脚本:

import matplotlib.pyplot as plt import numpy as np from PIL import Image import glob fig, axes = plt.subplots(3, 3, figsize=(12, 12)) for row, cut in enumerate(["axial", "sagittal", "coronal"]): img_path = sorted(glob.glob(f"3_cut/{cut}/img_*.png"))[0] mask_path = img_path.replace("img_", "img_").replace(".png", "_mask.png") img = np.array(Image.open(img_path)) mask = np.array(Image.open(mask_path)) axes[row, 0].imshow(img, cmap="gray") axes[row, 0].set_title(f"{cut} 原图") axes[row, 1].imshow(mask, cmap="gray") axes[row, 1].set_title(f"{cut} mask") axes[row, 2].imshow(img, cmap="gray", alpha=0.7) axes[row, 2].imshow((mask > 0), cmap="Reds", alpha=0.5) axes[row, 2].set_title(f"{cut} 叠加") plt.tight_layout() plt.savefig("verify_3cuts.png", dpi=160)

逻辑说明:外层循环遍历三个切面目录,内层取第一张图做展示。mask 文件名通过replace(".png", "_mask.png")得到,如果数据集命名规则不是这种,要按实际规则调整。叠加图用两层imshow,底层是原图灰阶,上层是 mask 二值化后的红色半透明层。切片尺寸较大的话,dpi=160是平衡清晰度和文件大小的常用参数。

如果三个切面的叠加结果看起来全是一个方向,问题大概率出在数据加载路径或文件名对应规则上。如果某个切面 mask 大量为全黑,需要回去看该切面有没有被空标签污染。

3.2 切片重叠对齐检查:用边缘像素分布判断切面是否漂移

有时候“切面划分好”只是文件被放进了不同目录,但实际内容可能来自不同病人的不同层位,空间位置对不齐。这时只靠肉眼不一定看得出来,可以用器官质心坐标来做定量检查:

def organ_centroid(mask): ys, xs = np.where(mask > 0) if len(xs) == 0: return None return xs.mean(), ys.mean() for cut in ["axial", "sagittal", "coronal"]: paths = sorted(glob.glob(f"3_cut/{cut}/*_mask.png")) centroids = [] for p in paths: m = np.array(Image.open(p)) c = organ_centroid(m) if c is not None: centroids.append(c) arr = np.array(centroids) print(cut, "质心 x 均值:", round(arr[:, 0].mean(), 2), "质心 y 均值:", round(arr[:, 1].mean(), 2), "x 离散度:", round(arr[:, 0].std(), 2), "y 离散度:", round(arr[:, 1].std(), 2))

如果离散度超过切片尺寸的 10%~15%,说明这些切片不是来自同一体数据的连续切层,可能混入了不同病例。这种数据问题不会报错,但会让模型收敛变慢甚至学到错误的解剖对应关系。发现这种情况后,建议先按病例 ID 重新分组,再考虑是否要剔除异常切片。

3.3 标注噪声初筛:单个前景连通域数量与空洞率

左心房在解剖上是近似闭合的腔室结构,正常切片里前景应该是一个连续区域,偶尔出现一两个细小分支。如果某张 mask 出现几十个分散连通域,大概率是标注噪点或切片边缘残留。检查方式:

from scipy import ndimage def check_components(mask): labeled, n = ndimage.label(mask > 0) sizes = ndimage.sum(mask > 0, labeled, range(1, n + 1)) return n, sizes for p in sorted(glob.glob("3_cut/*/*_mask.png"))[:20]: m = np.array(Image.open(p)) n, sizes = check_components(m) if n > 3: print(p, "连通域数:", n, "最大面积占比:", round(sizes.max() / m.sum(), 3))

ndimage.label对二值图做连通域标记,返回连通域数量和每个域的面积。正常左心房切片通常只有 1~2 个连通域,超过 3 个且有多个小面积区域,需要人工复核该切片标签是否可靠。这里注意一点:连通域检查用的是 4-邻接还是 8-邻接,label默认 4 邻接,对细长结构更敏感,如果误报多可改成structure=np.ones((3,3))用 8 邻接。

4. 用 PyTorch 加载左心房切片数据集并跑通训练闭环

4.1 自定义 Dataset:把切面参数、mask 读取方式、数据增强封装到一起

可视化和统计做完后,进入可复现的训练管线。核心是写一个能同时处理三个切面的 Dataset,自动把原图和 mask 成对加载。以下是可直接改用的实现:

import torch from torch.utils.data import Dataset import numpy as np from PIL import Image import glob class HeartSliceDataset(Dataset): def __init__(self, cut_dirs, transform=None, mask_suffix="_mask.png"): self.paths = [] for d in cut_dirs: self.paths += sorted(glob.glob(f"{d}/*.png")) # 仅保留原图文件名(过滤掉 mask 文件) self.paths = [p for p in self.paths if not p.endswith(mask_suffix)] self.mask_suffix = mask_suffix self.transform = transform def __len__(self): return len(self.paths) def __getitem__(self, idx): img_path = self.paths[idx] mask_path = img_path.replace(".png", self.mask_suffix) img = np.array(Image.open(img_path).convert("L")).astype(np.float32) mask = np.array(Image.open(mask_path).convert("L")) mask = (mask > 0).astype(np.float32) # 255 和 1 都归一化成 1 img = img / 255.0 if self.transform is not None: img, mask = self.transform(img, mask) return torch.from_numpy(img).unsqueeze(0), torch.from_numpy(mask).unsqueeze(0)

逻辑说明:glob.glob(f"{d}/*.png")会把原图和 mask 都搜进来,所以用not p.endswith(mask_suffix)过滤掉 mask 文件。原图转成灰度后归一化到[0,1],mask 统一成0/1二值,这样不管原始是 0/255 还是 0/1,训练时都按二分类处理。unsqueeze(0)是为了给网络输入加上通道维度,数值是 1,后续CrossEntropyLossDiceLoss都能直接用。

注意:transform参数接收的是一个可调用对象,接收(img, mask)并返回处理后的(img, mask),不能直接用 torchvision 那种只认单张图的Compose,否则 mask 会跟原图发生不一致的尺寸或翻转。想要同时翻转时,最省事的方式是用np.fliplr手写两个分支,而不是依赖 torchvision。

4.2 U-Net 训练脚本骨架:Dice Loss 与 Adam 的配合逻辑

左心房分割这种单器官二分类任务,U-Net 是最常见、最可靠的起点,不需要更重的 Transformer 结构。模型本身不用从零写,直接用 smp(segmentation-models-pytorch)或手写标准 U-Net 都行。核心在损失函数的选择:

import torch.nn as nn class DiceLoss(nn.Module): def __init__(self, smooth=1.0): super().__init__() self.smooth = smooth def forward(self, pred, target): pred = torch.sigmoid(pred) pred = pred.view(pred.size(0), -1) target = target.view(target.size(0), -1) intersection = (pred * target).sum(dim=1) union = pred.sum(dim=1) + target.sum(dim=1) dice = (2.0 * intersection + self.smooth) / (union + self.smooth) return 1.0 - dice.mean()

参数说明:smooth=1.0避免分子分母同时为 0 时出现除零错误,同时让梯度在目标区域非常小时依然稳定。对左心房切面里占比可能只有 1%~2% 的极端切片,这个 smooth 值比smooth=1e-6更稳。训练时用BCEWithLogitsLossDiceLoss加权组合是常见做法,比例一般取 0.5/0.5 或 0.3/0.7,类别极度不平衡时 Dice 权重更高。

优化器用 Adam 就够了,学习率1e-3起步,配合ReduceLROnPlateau在验证 Dice 停滞时衰减 0.5。左心房切片数据量通常不算大,batch size 取 8~16 都行,显存不够时不要硬上大 batch,反而应该把切片尺寸缩到 256×256 再训练。

4.3 切面维度在输入层怎么处理:单通道还是三通道

三个切面虽然是不同方向的 2D 切片,但各自都是灰度单通道数据。常见误区是把三个切面叠成三通道输入,当作 RGB 类数据来训练,这在你只有 2D 切片时是损失函数层面的错误。正确做法有两种:

第一种是按切面分别训练三个模型,每个模型只吃一种切面。优点是解剖方向一致,模型不用学“旋转不变性”,收敛快;缺点是推理时要根据输入切面选模型。

第二种是把三个切面当作多视角样本,用共享权重的编码器分别提取特征后融合,属于多视角分割的范畴,实现复杂度明显更高。对于标题描述的数据集,我推荐第一种——三个 U-Net 或一个模型接受切面 ID 作为条件输入。无论哪种,训练脚本里都要把cut目录信息保留在数据加载结果里,否则评估时无法分切面算指标,无法定位哪个方向的预测最差。

训练 30~50 个 epoch 后,验证集 Dice 一般能到 0.85 以上,低于这个水平先查 mask 对齐和标签噪声,而不是换模型。

5. 训练与评估时的切面坑位:三个必踩和对应解法

5.1 切面顺序与临床坐标系:不要盲目做随机旋转增强

自然图像里随机旋转 90 度是常规增强,但医学切片有明确解剖方向。轴向切片的“上”对应人体上方,“下”对应下方,左右也遵循医学图像坐标约定。随机旋转 90 度会让模型学到旋转不变性,但代价是丢失了解剖方向信息。

提示:常见做法是只做左右翻转和轻微平移,翻转轴向切片时还要注意左右互换是否符合标注约定。若要旋转,旋转角度控制在[-15, 15]度之间,且 mask 需要用相同插值方式同步变换。最近邻插值用于 mask,双线性插值用于原图,混用会引入边缘锯齿误差。

5.2 训练集里混入不同切面,导致模型分不清左右方位

如果训练时把三个切面全塞进同一个 DataLoader 而不做切面标记,模型会看到同一解剖结构在不同方向上的投影。U-Net 的卷积核本质是方向敏感的,这种情况下模型只能学到“模糊的平均形状”,三个切面的预测精度都上不去。解决办法是在__getitem__里额外返回一个切面 ID,训练时用这个 ID 决定用哪一个独立的解码头,或者在数据采样时保证每个 batch 只包含同一个切面的样本。

5.3 验证指标的陷阱:整体 Dice 掩盖了单切面退化

只打印一个全局 Dice 很容易掩盖问题。某个切面预测崩坏、另一个切面正常,平均 Dice 可能只掉 0.03,肉眼看不出来。正确做法是分开统计:

def per_cut_dice(all_preds, all_masks, all_cuts): from collections import defaultdict dice_dict = defaultdict(list) for pred, mask, cut in zip(all_preds, all_masks, all_cuts): pred = (pred > 0.5).astype(np.float32) inter = (pred * mask).sum() union = pred.sum() + mask.sum() + 1e-6 dice_dict[cut].append(2 * inter / union) for cut in dice_dict: print(cut, "Dice:", round(np.mean(dice_dict[cut]), 4))

逻辑说明:把预测结果按切面 ID 分组,分别计算 Dice。输出三个数字后,如果axial是 0.9、sagittal只有 0.7,说明该切面对应的空间特征没有被模型学到,优先检查该切面是否存在标签错位或样本量过少。

6. 用三维重建视角反向验证切片分割结果

6.1 把二维预测堆回三维体素,检查层间连续性

切片分割是手段,三维心脏结构才是最终的临床关注点。拿到所有切片的预测 mask 后,可以按切面方向把 2D mask 堆叠成 3D 体数据,然后查看 z 轴方向的连续切层是否存在“闪烁”式不连续,这是 2D 分割模型常见的层间抖动问题。堆叠方式取决于原始切片顺序,如果轴向切片是从上到下排列,堆叠时直接按文件名排序即可:

def stack_masks_to_volume(cut_dir, sorted_paths, slice_shape): volume = np.zeros((len(sorted_paths), slice_shape[0], slice_shape[1]), dtype=np.float32) for i, p in enumerate(sorted_paths): m = np.array(Image.open(p).convert("L")) volume[i] = (m > 0).astype(np.float32) return volume

堆叠后沿 z 轴取中间若干层做轮廓对比,用matplotlibcontour把相邻三层的 mask 边界画在同一张图上,边界偏离超过 5 个像素的位置就是需要检查层间配准的地方。

6.2 使用条件随机场或形态学后处理减少空洞和毛刺

2D 分割预测里常出现小空洞和边缘毛刺,这类噪声用形态学闭运算即可处理。闭运算先膨胀后腐蚀,可以填充小空洞并保持轮廓整体形状,对左心房这种近闭合结构非常合适:

from scipy import ndimage def postprocess_mask(pred, close_kernel=5): pred_bin = (pred > 0.5).astype(np.uint8) closed = ndimage.binary_closing(pred_bin, structure=np.ones((close_kernel, close_kernel))) return closed.astype(np.float32)

参数说明:close_kernel=5表示使用 5×5 结构元素,适用于切片尺寸 256 或 512 的输入;如果切片更大或左心房边界在图片中占比较高,可以适当调到 7。structure用全 1 矩阵表示 8 邻域操作,比十字形结构更稳健。闭运算不会显著改变器官面积,但能消除预测结果中细小的断裂,对后续三维体积计算更友好。

6.3 体积计算的校准:像素间距与切层间距缺一不可

如果把分割结果用于评估左心房容积,二维 Dice 提升并不直接等价于体积准确率提升。计算体积时除了分割正确性,还需要像素间距和层间距两个标量。假设轴向切片的像素间距是0.625mm,切层间距是2mm,体积计算就是简单的累加:

pixel_spacing = 0.625 # mm slice_spacing = 2.0 # mm voxel_volume = pixel_spacing * pixel_spacing * slice_spacing # mm^3 total_volume_ml = volume.sum() * voxel_volume / 1000.0 print("左心房体积(ml):", round(total_volume_ml, 2))

逻辑说明:volume是三维二值数组,volume.sum()是前景体素数。乘上单个体素的体积并除以 1000,得到以毫升为单位的体积。/1000.0是因为1 ml = 1000 mm^3。如果不做这一层换算,模型 Dice 再高也无法对齐临床报告的数值范围。二维分割工作做到这一步,才算真正闭环:从切片进,到三维指标出。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询