
简介面向医学图像分割学习与实验场景这套心脏左心房切片数据集从3D原始数据中沿轴位面、冠状面、矢状面分别切出2D图像并按x、y、z三个切面整理为images与masks目录标签图中0为背景、1为心脏可直接用于U-Net等语义分割模型的训练、验证与标注质量检查。其中x轴切面有1351张图、y轴1151张、z轴828张均为对应的原图与mask成对存放。压缩包共2000个文件以1998张PNG切片图为主另含1个Python可视化脚本和1个JSON数据文件脚本可随机抽取一张原始图片将原图、GT标签及GT在原图上的蒙板效果同屏展示并保存到当前目录方便快速核对不同切面的标注结果。资源整体约93.88MB目录结构清晰覆盖轴位、冠状位、矢状位三种视图适合医学影像初学者练习分割流程也适合研究者作为小规模实验数据。当前已有365人学习下载。1. 心脏左心房切片分割数据集三个切面两类别拿到的第一件事不是训练而是拆目录医学图像分割数据集现在不少但能直接拿来训练、不用自己逐张标注的并不多见。这个“心脏左心房切片分割”数据集价值在于它已经把三个切面的切片划分好了每个切片对应一张2类别标签图外加可视化代码适合用来做U-Net这类分割模型的基线实验也适合入门医学影像处理。我见过太多人拿到数据集直接开训结果输出全黑或指标虚高回头一查问题出在标签文件没读懂。第一件事永远是搞清楚切面怎么分、mask怎么存、类别顺序是什么。2. 拆开数据集看门道3个切面、2类别、标签文件与可视化代码各是什么一个医学图像分割数据集能不能用先看目录和文件命名。拿到手先解压用tree看一下整体结构。三个切面通常是指心脏在三个解剖平面上的切片常见的是横断面axial、矢状面sagittal、冠状面coronal在心脏MRI或CT中这三个平面能分别展现左心房不同的空间形态。如果原始数据来自超声也可能是短轴、四腔、两腔三种切面。目录结构直接决定你后面训练流程怎么写所以这一章花半小时摸清楚能省下后面几天的返工时间。2.1 目录结构三个切面怎么放直接决定后面训练流程怎么写常见做法是三个切面分别放在axial、sagittal、coronal子目录下或者用文件名前缀区分。比如heart_la_dataset/ ├── images/ │ ├── axial_001.png │ ├── sagittal_001.png │ └── coronal_001.png ├── masks/ │ ├── axial_001.png │ ├── sagittal_001.png │ └── coronal_001.png └── visualize.py先跑一段命令确认切面数量和样本数是否一致避免后面加载时发现某一切面缺样本。for d in axial sagittal coronal; do echo $d images: $(ls images/${d}_*.png 2/dev/null | wc -l) echo $d masks: $(ls masks/${d}_*.png 2/dev/null | wc -l) done这段脚本按文件名前缀统计每个切面的图像和掩码数量。2/dev/null是为了在没有匹配文件时不打印错误wc -l统计行数。三个切面都应为同一数量级如果某个切面多出或少几十张先排查命名规则再决定后面训练时要不要按图片数量做加权。还有一个更稳妥的办法就是写一个Python脚本直接用文件名严格匹配而不是靠bash通配符。import re from pathlib import Path image_dir Path(images) mask_dir Path(masks) for cf in [axial, sagittal, coronal]: imgs sorted([p for p in image_dir.glob(*.png) if re.match(f^{cf}_\\d\\.png$, p.name)]) masks sorted([p for p in mask_dir.glob(*.png) if re.match(f^{cf}_\\d\\.png$, p.name)]) print(cf, len(imgs), len(masks))这里用re.match把文件名严格限定成“切面前缀_数字.png”防止sagittal被axial的前缀包含匹配到。pathlib.Path.glob只匹配一个层级适合扁平目录。这一步能提前发现命名混用问题尤其是当文件名里有axial_sagittal_001.png这种叠加前缀时正则规则能直接暴露异常。这里要特别提醒一个和COCO2017那种目标检测数据集不一样的地方医学分割数据集很少用复杂JSON目录大多数是“图像同名mask”的扁平结构。也别指望标签文件一定叫label或gt打开一个mask看像素值才是硬道理。COCO的JSON里有categories、annotations这些层级你可以借鉴它的“类别映射”思想但没必要照搬结构。2.2 标签文件先把灰度值统计做出来再谈训练标题说2类别通常指像素值为0的背景和像素值为255或1的左心房。也有数据集把标签存成1和2甚至PNG里是0和1。这一步一定要先统计一下mask里的灰度值有哪些别想当然认成0/255。import numpy as np from PIL import Image import glob mask_files sorted(glob.glob(masks/*.png)) values {} for f in mask_files[:200]: arr np.array(Image.open(f)) vals np.unique(arr) for v in vals: values.setdefault(v, 0) values[v] 1 print(像素值出现次数统计(前200张):, values)这段代码读前200张mask统计每个像素值在这批文件里出现的次数。np.unique返回mask中的全部灰度级如果出现除了0和255之外的数比如128或1说明标签不是简单二值需要把1或128归一化到前景。常见做法是arr[arr0] 1把所有非背景像素统一成前景因为2类别任务只需要区分“左心房”和“非左心房”。这里有个容易翻车的细节如果mask是8位PNG背景0、左心房255读取后不做归一化直接喂给模型Loss会在数值上剧烈抖动。所以预处理阶段必须把255改成1或者在损失函数内部做归一化。我习惯在数据加载里直接除以255。还要注意有些mask虽然视觉上只有两个颜色但PNG编码里带了调色板模式P模式直接用np.array转换出来是索引值而不是灰度值这时候需要先Image.open(f).convert(L)再转数组否则你统计到的可能是调色板索引。还有一个很重要但容易被忽略的点三个切面的标签格式未必完全一致。比如axial的mask是0/255而sagittal的mask可能存成0/1。所以统计像素值时不要只抽查一个切面三个切面都要跑一遍。如果发现某个切面的统计结果与其他两个明显不同多半是导出标注时用了不同的保存参数这时需要在数据加载器里按切面做条件归一化而不是全局统一规则。2.3 可视化代码不只是展示还是数据质量检查工具数据集自带的可视化代码本质就是把原图和mask叠加在一起方便你一眼看出标注质量。如果没有也没关系自己写一个也很简单import matplotlib.pyplot as plt import numpy as np from PIL import Image def visualize(image_path, mask_path, save_pathNone): img np.array(Image.open(image_path).convert(L)) mask np.array(Image.open(mask_path).convert(L)).astype(np.uint8) mask mask // 255 if mask.max() 1 else mask fig, axes plt.subplots(1, 3, figsize(12, 4)) axes[0].imshow(img, cmapgray) axes[0].set_title(Original) axes[1].imshow(mask, cmapgray) axes[1].set_title(Mask) axes[2].imshow(img, cmapgray) axes[2].imshow(mask, cmapjet, alpha0.4) axes[2].set_title(Overlay) plt.tight_layout() if save_path: plt.savefig(save_path, dpi150) plt.show()这里mask // 255是把我前面说的0/255掩码转成0/1如果掩码本来就是0/1这个除法会被if条件跳过。alpha0.4让标注半透明覆盖在图上既能看清边界又不会遮住原图。三张子图分别显示原图、Mask和叠加图方便快速检查边界框是否贴合左心房轮廓。可视化本身不产生指标但它是判断标注是否对齐、切面是否正确的第一道关卡。批量生成对比图也非常有用可以一次跑完整个验证集import glob img_files sorted(glob.glob(images/*.png)) for img_file in img_files: mask_file img_file.replace(images, masks) save_path img_file.replace(images, overlay).replace(.png, _overlay.png) visualize(img_file, mask_file, save_pathsave_path)这段代码把所有原图对应的mask都生成一张叠加图存到overlay目录。replace(images, masks)是基于目录结构的简单映射前提是文件名一致。批量生成后你可以快速翻图找标注错位、漏标、多标的问题。很多数据集标注质量没有想象中高尤其是左心房与肺静脉交界处不同标注者会给出差别很大的边界这种位置往往就是训练时loss降不下去的根源。可视化代码不是为了发给别人看效果而是给你自己做数据清洗用的。3. 动手跑通最小流程加载左心房切片并构建训练管线3.1 数据加载与预处理Resize的插值选择、归一化与增强参数三个切面的图像尺寸可能不一致常见做法是把它们统一缩放到固定大小比如256×256。分割任务中Resize会改变解剖结构的绝对尺度但对网络训练影响有限真正要小心的是插值方式。对图像用双线性插值对mask用最近邻插值否则会引入边缘混叠。import torch from torch.utils.data import Dataset, DataLoader from PIL import Image import numpy as np class LeftAtriumDataset(Dataset): def __init__(self, image_paths, mask_paths, size(256, 256), augmentFalse): self.image_paths image_paths self.mask_paths mask_paths self.size size self.augment augment def __len__(self): return len(self.image_paths) def __getitem__(self, idx): img Image.open(self.image_paths[idx]).convert(L).resize(self.size, Image.BILINEAR) mask Image.open(self.mask_paths[idx]).convert(L).resize(self.size, Image.NEAREST) img np.array(img).astype(np.float32) / 255.0 mask np.array(mask).astype(np.float32) mask (mask 127.5).astype(np.float32) if self.augment: if np.random.rand() 0.5: img np.fliplr(img).copy() mask np.fliplr(mask).copy() if np.random.rand() 0.5: img np.flipud(img).copy() mask np.flipud(mask).copy() img_t torch.from_numpy(img).unsqueeze(0) mask_t torch.from_numpy(mask).unsqueeze(0) return img_t, mask_tconvert(L)把图像转成单通道灰度医学影像一般是灰度图没有必要用三通道。mask读取后先resize成和图像一致的尺寸再用127.5二值化这一步把之前提到的0/255标签统一成0/1。数据增强只做了水平和垂直翻转这是分割任务里对解剖结构最安全的增强旋转和缩放对心脏切片不是不能用但容易让左心房的形状失真前期不建议加。如果你觉得三个切面需要不同增强策略可以在augment参数里传入一个字符串来区分。我的经验是axial和coronal对水平翻转比较敏感sagittal对垂直翻转比较敏感但前期统一翻转就够了。归一化的顺序必须在增强之前因为翻转操作作用于图像数组时是在浮点数上做的如果先转成Tensor再翻转会多一次copy开销而且容易出错。3.2 用U-Net训练一个2分类分割基线损失函数与训练循环U-Net是医学图像分割的经典基线结构上就是编码器-解码器加跳跃连接。这里不重复写完整U-Net代码直接说明训练循环中最关键的三件事损失函数怎么配、验证怎么做、模型怎么保存。device torch.device(cuda if torch.cuda.is_available() else cpu) model UNet(in_channels1, out_channels1).to(device) optimizer torch.optim.Adam(model.parameters(), lr1e-4) scheduler torch.optim.lr_scheduler.ReduceLROnPlateau(optimizer, modemin, patience5, factor0.5) criterion lambda pred, target: 0.5 * torch.nn.functional.binary_cross_entropy_with_logits(pred, target) dice_loss(pred, target) for epoch in range(50): model.train() train_loss 0.0 for img, mask in train_loader: img, mask img.to(device), mask.to(device) pred model(img) loss criterion(pred, mask) optimizer.zero_grad() loss.backward() optimizer.step() train_loss loss.item() * img.size(0) model.eval() val_loss 0.0 with torch.no_grad(): for img, mask in val_loader: img, mask img.to(device), mask.to(device) pred model(img) val_loss criterion(pred, mask).item() * img.size(0) avg_train train_loss / len(train_loader.dataset) avg_val val_loss / len(val_loader.dataset) scheduler.step(avg_val) if (epoch 1) % 5 0: print(fepoch {epoch1:02d}, train_loss {avg_train:.4f}, val_loss {avg_val:.4f})ReduceLROnPlateau是一个很实用的调度器当验证loss连续5轮不下降时学习率自动减半。factor0.5表示每次衰减一半这样不用手动去猜什么时候降学习率。criterion里两个loss各占0.5既能平滑训练也能保证模型不会因为背景占比大而只输出全零。验证循环里一定要用torch.no_grad()否则验证过程会保存计算图显存直接爆掉。打印loss的间隔设成5轮既能看到趋势又不会在终端里刷得太密。这里没写模型结构因为不同框架的U-Net实现大同小异你完全可以直接用现成的分割库关键是训练循环这套流程不要变。还有一点保存模型时不要只看验证loss要同时算验证Dice因为loss有时会因为前景被预测得更保守而变小但Dice可能反而不涨。3.3 训练参数与验证策略batch size、学习率、调度器与模型保存参数推荐初始值理由输入尺寸256×256与常见预训练权重兼容显存占用适中batch size8单卡16G左心房只占整图小部分batch太大容易类别不均衡加剧学习率1e-4AdamScheduler往下降损失函数0.5BCE 0.5DiceLoss单独BCE对背景过拟合单独Dice收敛慢评估指标Dice / HD95面积重叠度边界距离训练轮数50-1002D切片分割50轮足够看趋势这些参数不是固定答案只是基线。跑完50轮如果Dice还低于0.7先检查数据再去怀疑网络。数据层面最常见的就是切面方向不一致比如同一批axial图里混入了几张sagittal导致模型学习不到稳定的左心房形状。模型保存的策略我习惯按验证Dice择优保存而不是只看最后一轮。best_dice 0.0 for epoch in range(epochs): for batch in train_loader: ... val_dice compute_dice_on_val(model, val_loader) if val_dice best_dice: best_dice val_dice torch.save(model.state_dict(), fbest_model_dice_{val_dice:.4f}.pth)compute_dice_on_val需要自己实现原理就是遍历验证集把所有样本的预测mask与真值mask累计求和后计算。这个保存策略的好处是即使训练后期过拟合你手里也有一个调整到最佳状态的权重。注意文件名里带上Dice值方便之后对比不同切面、不同增强策略的模型。4. 可视化验证切片分割结果从mask叠加到Dice系数计算4.1 推理与后处理阈值、连通域与形态学操作的参数选择训练完模型后预测输出是概率图不能直接当mask用。常见做法是先取Sigmoid输出再以0.5为阈值二值化然后用连通域只保留最大连通区域。左心房在单张切片上通常是独立的闭合区域去掉小散点能压掉很多假阳性。import torch import numpy as np from scipy import ndimage def postprocess(pred, min_area50): prob torch.sigmoid(pred).cpu().numpy()[0, 0] binary (prob 0.5).astype(np.uint8) labeled, num ndimage.label(binary) if num 0: return np.zeros_like(binary) sizes ndimage.sum(binary, labeled, range(1, num 1)) keep np.zeros_like(binary) for lab_id, size in enumerate(sizes, start1): if size min_area: keep[labeled lab_id] 1 return keepndimage.label会给每个连通区域编号ndimage.sum统计每个编号的面积。min_area50表示小于50像素的孤立点直接丢弃对256×256图像来说这个阈值能去掉大部分噪声。形态学开运算也可以做但我一般只在边界特别毛糙时才用因为左心房壁薄腐蚀膨胀容易把细结构吃掉。如果三个切面的分辨率差异较大min_area要按切面分别调整比如axial切片上左心房面积大可以把阈值提高到100sagittal切片上心房轮廓小阈值降到30。这个参数没有绝对标准观察几张预测图再定。批量推理时把后处理和Dice计算串起来可以一次性得到整个测试集的预测结果。def predict_test_set(model, test_loader, device): model.eval() all_preds, all_masks [], [] with torch.no_grad(): for img, mask in test_loader: img img.to(device) pred model(img) for i in range(pred.size(0)): pm postprocess(pred[i]) all_preds.append(pm) all_masks.append(mask[i].squeeze(0).numpy()) return all_preds, all_maskspostprocess(pred[i])内部会把张量转成numpy所以这里不需要再显式做GPU到CPU的转换。测试集如果来自三个切面最好在返回时也带上切面标签后面按切面统计指标会省很多事。4.2 计算Dice与边界指标先按切面拆开再算平均Dice系数是分割任务最常用的指标公式是2*|A∩B|/(|A||B|)。实现很简单def dice_score(pred_mask, true_mask): smooth 1e-6 intersection (pred_mask * true_mask).sum() return (2.0 * intersection smooth) / (pred_mask.sum() true_mask.sum() smooth)smooth是为了避免分子分母都为0时除零报错。Dice对类别不均衡不敏感很适合前景占比较小的左心房分割。如果还想看边界质量可以加一个Hausdorff距离但计算量大前期用Dice足够。注意在跨切面评估时要么三个切面分别报告Dice要么加权平均不要混在一起算一个数因为axial切面看到的左心房往往比sagittal大混合会掩盖某一切面的退化。按切面统计的代码可以这样写import collections def report_by_plane(pred_dict, mask_dict, plane_map): stat collections.defaultdict(list) for img_id in pred_dict.keys(): p plane_map[img_id] stat[p].append(dice_score(pred_dict[img_id], mask_dict[img_id])) for p, scores in stat.items(): print(p, dice_mean, np.mean(scores), dice_std, np.std(scores))假设plane_map是一个把文件名映射到“axial/sagittal/coronal”的字典。分开统计的意义在于如果你发现sagittal的Dice明显低于其他两个就不要在最终报告里只写一个总Dice。医生看左心房更关注特定切面上的形态混合指标容易骗过自己也骗过评审。按切面给出均值和标准差还能看出模型稳定性。4.3 错误分割的三种常见模式欠分割、边缘假阳性、切面不一致可视化预测结果时我见过三类高频问题。第一是欠分割预测mask比标注小一圈常见原因是左心房边界模糊模型学到的边界偏保守。第二是边缘假阳性mask在心肌附近多出一圈这往往来自训练标签本身的边界不确定性。第三是切面间不一致三个切面单独看Dice都有0.8但放在一起做3D重建时不连续说明模型过度拟合了某一切面的纹理。判断问题属于哪一类别只盯着一两个指标。把预测mask和标注mask叠加在原图上原图画成灰度mask画成半透明红色。如果多余区域贴着左心房壁走是边界问题如果散布在远离心房的位置是噪声或切面错位。还有一个实用技巧把预测概率图直接显示成灰度图如果概率值在边界处从0.1到0.9跨度很大说明模型对边界位置很确定如果空间过渡很平滑说明模型在犹豫。这种犹豫往往对应标注不一致的区域后期可以针对性修正标签。5. 避坑指南医学图像分割数据集的5个典型问题5.1 切面划分不一致导致训练混乱现象训练集Dice很高验证集突然掉到0.4以下。打开错误样本发现模型输出的轮廓完全偏离左心房位置。原因三个切面的命名或目录划分在预处理时被搞混比如文件名前缀识别用了模糊匹配把sagittal_001.png同时匹配到了axial_001.png。另一个常见原因是原始DICOM序列里切面顺序被翻转导出的PNG文件名顺序与实际物理位置不一致。解决固定唯一的命名规则比如用正则^([a-z])_(\d).png$严格切分不要用字符串包含。加载前先随机抽10张图人工确认切面方向和左右关系。加入训练管线时按切面分组先做单切面模型验证再训混合模型。我见过一个团队因为某个切面文件少了20张直接用重复采样的方式补齐结果模型在重复样本上过拟合验证集Dice波动很大。正确的做法是先用set对比三个切面的文件列表看是否有交叉。5.2 标签文件与图像尺寸不匹配现象训练或可视化时报错size mismatch或者打印出的mask只有图像的一半大小。原因数据集中某些切片来自不同采集协议有的被裁剪过有的原始尺寸就是512×512而mask是256×256。标题里说“切面划分好”但没说不代表所有文件像素尺寸都一致。解决写一个数据校验脚本逐个读取图像和mask的size属性并对比。不一致时按图像尺寸重采样mask用最近邻插值。不要用Image.resize((512,512))直接改mask因为原来的mask可能是对应一张被裁剪图像先对齐原点再缩放。还有一个隐藏坑有些PNG的dpi信息不一致读取到数组后尺寸其实一样但显示时缩放不同让人误以为尺寸不匹配。判断依据只看np.array(img).shape不要看显示尺寸。5.3 类别不平衡导致预测全黑现象模型Loss在下降但验证集预测结果全部是背景Dice为0。原因左心房在整张切片中占比可能只有5%~10%2类别分割中背景像素占绝对多数。如果损失函数只有BCE模型学会输出全背景是损失最小的策略。解决损失函数换成Dice为主的组合或在训练时对前景像素加权。我在第3章给出的0.5*BCE dice_loss就是为了缓解这个问题。还有一个更直接的检查点统计每个batch里前景像素占比如果长期低于1%就要考虑用前景裁剪或多切面拼接来平衡。如果训练中loss在下降但评估时全黑先看预测概率图的分布往往数值全在0附近。这时可以把二值化阈值从0.5降到0.2试试如果出现轮廓说明模型学到了一点特征但置信度低如果还是全黑就是训练策略问题得改loss而不是改阈值。5.4 同一患者的前后切片同时进训练集和验证集现象训练Dice 0.92验证Dice也0.91但放到一个外部病例上只有0.6。这种虚高往往让人误判模型已经到了可用状态。原因心脏MRI或CT的一个患者会产生几十张连续切片前后切片高度相似。如果随机划分数据同一个患者的三维体积会同时出现在训练集和验证集里模型相当于见过正确答案的邻居。解决按患者或按三维体积划分而不是按单张切片划分。这个数据集的三个切面正好能用来做这种分组验证先在axial上训练在sagittal和coronal上测试如果性能明显下降说明过拟合了切面特征而不是学到了左心房结构。这也是我建议把三个切面分别管理而不是混在一起的原因。如果数据集文件名里没有患者ID至少按文件名序号做隔断划分比如前70%序号做训练后30%做验证但这只能避免连续切片泄漏跨患者泛化还是需要额外数据来验证。5.5 可视化代码的color map导致误判现象用cmapjet叠加mask后图像看起来边缘锐利、对比度高让人误以为分割效果很好。原因jet会把0到1的灰度映射成从蓝到红的连续色带由于人眼对红色敏感很小的概率差异也会被放大成明显色块。这不影响数值指标但影响人工质检判断。解决可视化时用二值mask固定透明度不要用概率图的连续色带如果一定要显示概率用cmapgray或cmaphot并且单独放一个子图不与原图叠加。我自己的习惯是叠加图只用白色或红色通道alpha固定0.4这样能看出的是边界贴合度而不是颜色对比度。另外保存对比图时不要用jpg格式mask的边缘会出现压缩伪影PNG虽然体积大但作为质检输出值得这个成本。6. 往深走一步用这个数据集做三维重建与多中心泛化验证6.1 将2D切片结果堆叠成3D左心房mesh三个切面都预测完后可以把轴向切片按文件名顺序堆叠成三维体数据再用skimage.measure.marching_cubes提取左心房表面mesh。这一步能直观暴露2D指标看不出的问题切片间错位、层厚不均、边缘锯齿。做法很简单把预测结果按患者ID分组沿z轴叠成(D, H, W)的数组然后用等值面提取。from skimage import measure volume np.stack(axial_preds, axis0).astype(np.uint8) verts, faces, _, _ measure.marching_cubes(volume, level0.5, spacing(1, 1, 1)) print(fverts {verts.shape}, faces {faces.shape})marching_cubes的level0.5是提取二值mask表面的关键参数spacing需要填入真实层厚和像素间距否则重建出来的左心房会像一个压扁的球。注意如果三个切面不是来自同一套三维体积就不要强行做重建重建前先对齐患者ID和扫描坐标系。这个步骤的价值在于对最终医疗应用做“可解释性检查”2D指标再漂亮重建出一个表面粗糙、有洞的心房也无法交给临床使用。6.2 用留一患者法评估数据集质量与其把三个切面都塞进训练集我更建议先做一次“留一患者”实验选其中一位患者的三个切面作为测试集其余患者训练。这个实验的价值在于衡量数据集标注的可复用性。如果换一个患者后Dice掉了0.3说明模型学到的是该患者特有的像素分布而不是左心房的通用解剖特征。遇到这种情况不要盲目加数据增强先回看标注是否包含了左心房与肺静脉交界、心房耳等容易混淆的区域。左心房的解剖边界本身就存在争议不同影像科医生画出来的mask差异可能比模型误差还大。如果你不确定标注标准可以把数据集里同一结构的多张mask叠加起来看标注离散度离散度大的地方就是模型天花板所在。6.3 我的习惯先跑通、再调参、后质疑指标做这类数据集我现在的流程是先花两小时把目录、标签、可视化跑一遍确认没有基本问题再跑一个最简单的U-Net基线不管效果多差先记录下来然后才是调损失函数和数据增强。最后一定得回到原始图像上随机挑预测对的和错的各20张自己用眼睛判断。指标是黑匣子但叠加图骗不了人。调参多少有点玄学但只有先把数据管线理干净后续优化才有意义。希望这些踩坑经验能让你拿到这个左心房数据集时少走点弯路希望帮到你。本文还有配套的精品资源点击获取