
简介面向医学图像分割任务这份数据集聚焦心脏左心房切片提供轴位面、冠状面、矢状面三个切面划分好的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个切面”大概率指矢状面、冠状面、横断面这组标准正交方向也可能是房室短轴的不同层位。无论哪种数据集的价值核心在“划分好的标签文件”每个切片对应一张 mask2 类别意味着背景算一类、左心房算一类。可视化代码负责把原图、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.png、sagittal/img_001.png、coronal/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_countsTrue) 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之后mode是RGB要转灰度或直接取第一个通道否则后续计算 IoU 时 shape 会变成 H×W×3mask 的类别数也跟着翻倍。2.3 统计每张切片的器官占比判断标签是否真的“划分好”标题说“划分好的数据集”这个“划分好”除了指训练/验证集划分也可能指每个切面都自成一套。我不急着先训练而是先算每个切面里左心房面积占全图面积的比例import glob for cut in [axial, sagittal, coronal]: paths sorted(glob.glob(f3_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.050.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(f3_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, cmapgray) axes[row, 0].set_title(f{cut} 原图) axes[row, 1].imshow(mask, cmapgray) axes[row, 1].set_title(f{cut} mask) axes[row, 2].imshow(img, cmapgray, alpha0.7) axes[row, 2].imshow((mask 0), cmapReds, alpha0.5) axes[row, 2].set_title(f{cut} 叠加) plt.tight_layout() plt.savefig(verify_3cuts.png, dpi160)逻辑说明外层循环遍历三个切面目录内层取第一张图做展示。mask 文件名通过replace(.png, _mask.png)得到如果数据集命名规则不是这种要按实际规则调整。叠加图用两层imshow底层是原图灰阶上层是 mask 二值化后的红色半透明层。切片尺寸较大的话dpi160是平衡清晰度和文件大小的常用参数。如果三个切面的叠加结果看起来全是一个方向问题大概率出在数据加载路径或文件名对应规则上。如果某个切面 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(f3_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对二值图做连通域标记返回连通域数量和每个域的面积。正常左心房切片通常只有 12 个连通域超过 3 个且有多个小面积区域需要人工复核该切片标签是否可靠。这里注意一点连通域检查用的是 4-邻接还是 8-邻接label默认 4 邻接对细长结构更敏感如果误报多可改成structurenp.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, transformNone, 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后续CrossEntropyLoss或DiceLoss都能直接用。注意transform参数接收的是一个可调用对象接收(img, mask)并返回处理后的(img, mask)不能直接用 torchvision 那种只认单张图的Compose否则 mask 会跟原图发生不一致的尺寸或翻转。想要同时翻转时最省事的方式是用np.fliplr手写两个分支而不是依赖 torchvision。4.2 U-Net 训练脚本骨架Dice Loss 与 Adam 的配合逻辑左心房分割这种单器官二分类任务U-Net 是最常见、最可靠的起点不需要更重的 Transformer 结构。模型本身不用从零写直接用 smpsegmentation-models-pytorch或手写标准 U-Net 都行。核心在损失函数的选择import torch.nn as nn class DiceLoss(nn.Module): def __init__(self, smooth1.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(dim1) union pred.sum(dim1) target.sum(dim1) dice (2.0 * intersection self.smooth) / (union self.smooth) return 1.0 - dice.mean()参数说明smooth1.0避免分子分母同时为 0 时出现除零错误同时让梯度在目标区域非常小时依然稳定。对左心房切面里占比可能只有 1%2% 的极端切片这个 smooth 值比smooth1e-6更稳。训练时用BCEWithLogitsLoss加DiceLoss加权组合是常见做法比例一般取 0.5/0.5 或 0.3/0.7类别极度不平衡时 Dice 权重更高。优化器用 Adam 就够了学习率1e-3起步配合ReduceLROnPlateau在验证 Dice 停滞时衰减 0.5。左心房切片数据量通常不算大batch size 取 816 都行显存不够时不要硬上大 batch反而应该把切片尺寸缩到 256×256 再训练。4.3 切面维度在输入层怎么处理单通道还是三通道三个切面虽然是不同方向的 2D 切片但各自都是灰度单通道数据。常见误区是把三个切面叠成三通道输入当作 RGB 类数据来训练这在你只有 2D 切片时是损失函数层面的错误。正确做法有两种第一种是按切面分别训练三个模型每个模型只吃一种切面。优点是解剖方向一致模型不用学“旋转不变性”收敛快缺点是推理时要根据输入切面选模型。第二种是把三个切面当作多视角样本用共享权重的编码器分别提取特征后融合属于多视角分割的范畴实现复杂度明显更高。对于标题描述的数据集我推荐第一种——三个 U-Net 或一个模型接受切面 ID 作为条件输入。无论哪种训练脚本里都要把cut目录信息保留在数据加载结果里否则评估时无法分切面算指标无法定位哪个方向的预测最差。训练 3050 个 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]), dtypenp.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 轴取中间若干层做轮廓对比用matplotlib的contour把相邻三层的 mask 边界画在同一张图上边界偏离超过 5 个像素的位置就是需要检查层间配准的地方。6.2 使用条件随机场或形态学后处理减少空洞和毛刺2D 分割预测里常出现小空洞和边缘毛刺这类噪声用形态学闭运算即可处理。闭运算先膨胀后腐蚀可以填充小空洞并保持轮廓整体形状对左心房这种近闭合结构非常合适from scipy import ndimage def postprocess_mask(pred, close_kernel5): pred_bin (pred 0.5).astype(np.uint8) closed ndimage.binary_closing(pred_bin, structurenp.ones((close_kernel, close_kernel))) return closed.astype(np.float32)参数说明close_kernel5表示使用 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 再高也无法对齐临床报告的数值范围。二维分割工作做到这一步才算真正闭环从切片进到三维指标出。本文还有配套的精品资源点击获取