心脏左心房三切面分割数据集:从数据组织到验证的完整实践 简介这是一套面向医学图像分割任务的心脏左心房切片数据集源于同一份3D心脏左心房数据已从轴位面、冠状面、矢状面三个切面分别提取2D图像适合用于训练和评估语义分割模型尤其适合做多切面医学影像分割实验。三个切面均划分好images与masks目录x轴为320×320分辨率含1351对图像y轴与z轴均为320×130分辨率分别含1151对、828对图像mask中1表示心脏、0表示背景可直接配合常规分割框架使用。除标注数据外包内另含一个可视化脚本.py和对应配置说明.json随机抽取一张图片即可同时输出原图、GT和GT叠加蒙版效果图方便快速检查数据质量与分割效果。整个压缩包共2000个文件以PNG图像为主另有Python脚本与JSON文件整体约93.88MB体积适中下载后即可开展实验。目前已有365人浏览学习对有医学图像处理基础、需要现成分割数据集的开发者来说有较好参考价值。1. 心脏左心房切片分割这套三切面数据集把时间花在了真正该花的地方做医学图像分割的人拿到心脏左心房切片数据集时第一反应通常是打开一张图、看一眼掩膜然后直接套网络开训。真正上手才会发现决定Dice上限的往往不是模型结构而是数据目录里那三个切面有没有被认真分开。心脏左心房在轴状位、冠状位、矢状位上的形态差异极大三个切面混在一起训练loss曲线照样能降但验证集指标会忽高忽低最后你根本分不清是网络问题还是数据组织问题。标题里这套方案把最费时间的三件事提前做完了——三切面划分、标签文件归档、可视化脚本对想集中精力验证分割思路的人来说省掉的不只是预处理时间还有大量排错时间。它适合刚接触医学图像分割、想跑通第一条完整pipeline的入门者也适合手头有心脏MRI序列、想按标准三切面组织自有数据的老手参考。2. 三切面与2类别标签先搞懂切面划分和标签文件再动手绝大多数心脏MRI分割任务里数据不是一张孤立图片而是从三维体数据沿不同方向重切出来的二维切片。所谓三个切面指的是轴状位、冠状位、矢状位这三个互相垂直的解剖方向。表面上看它们只是同一体数据的不同视角但在分割任务里这三个方向上的形态差异足以让同一个模型表现出完全不同的性能这也是这套数据集把切面预先划分好的核心价值。2.1 左心房在三种切面上的形态差异为什么不建议三个切面混着训先把三个切面的解剖特点摆出来对比这是判断数据组织方式是否合理的基础。切面左心房形态主要分割难点轴状位近圆形边界相对规整左心耳开口、肺静脉入口容易与心房主体粘连冠状位扁椭圆前后方向投影肺静脉根部与心房重叠明显部分容积效应严重矢状位斜梭形形态最不稳定左心室紧邻交界处难以界定轴状位是大多数心脏分割公开数据集的主力方向因为心房在这个方向上轮廓清晰、对比度好模型最容易学。冠状位的问题在于肺静脉和心房在投影上叠在一起边界成了一条渐变带分割模型很容易把肺静脉根部也划进左心房。矢状位则因为左心房在身体里本来就是斜着摆放的单张切片之间形态跳跃大模型容易把左心室入口误判成心房区域。所以如果你把三个切面全部扔进同一个DataLoader里做随机shuffle网络会同时面对三种分布差异很大的输入。训练loss不会崩但验证Dice会忽高忽低调参时你会误以为是学习率或网络深度的问题。反过来把三个切面拆开组织、分别验证能直接看到模型在哪个方向上弱再决定是做单切面模型还是做切面融合。这也是3个切面划分好的数据集这个设计最值钱的地方它替你省掉了从NIfTI体数据重采样、再做标注对齐这一步最容易出错的工作。2.2 2类别到底指什么标签文件先做值域检查再做可视化2类别这三个字在医学分割数据里最常见的含义是背景加左心房也就是像素值0和1前景是左心房。但很多打包方为了可视化时图好看会把前景存成255甚至把边缘模糊区域存成128。所以拿到标签文件的第一件事不是直接可视化而是统计掩膜值域分布。我一般会先跑下面这段检查代码import numpy as np from PIL import Image def inspect_mask(mask_path, image_path): mask np.asarray(Image.open(mask_path).convert(L)) assert mask.ndim 2, f掩膜必须是单通道实际是 {mask.shape} vals, counts np.unique(mask, return_countsTrue) print(mask value distribution:, dict(zip(vals.tolist(), counts.tolist()))) image np.asarray(Image.open(image_path)) assert image.shape[:2] mask.shape, f原图 {image.shape} 与掩膜 {mask.shape} 尺寸不一致 return vals这里把掩膜强制转成单通道灰度是为了避开某些PNG带透明度通道的情况。np.unique的输出能直接告诉你标签是0/1还是0/255如果出现128这类中间值说明数据里有半透明标注或边缘渐变训练前要先做二值化归一到0或1。直接拿255当二值标签训练Dice可能卡在0.5附近一直上不去因为模型的sigmoid输出范围是0到1和255差了两个数量级。标签文件的分发格式也值得提一句。常见做法有两种一是PNG原图加同名PNG掩膜读取简单可视化也方便二是NIfTI单文件配合独立标签目录复用时要自己写切片和解码逻辑。标题里把标签文件单独列出大概率对应的是前者那种直接可用的掩膜形态。如果你刚从yolov8训练自己的数据集那套目标检测流程转过来最容易踩的思维定式是把类别编号、锚框参数带进分割任务语义分割里没有这些概念标签本身就是和原图同尺寸的二维矩阵一个像素一个类别理解这一点之后后续的模型结构选择也会顺很多。3. 可视化代码怎么用用叠加对比和三切面全览自查数据数据值域检查做完下一步是用可视化代码自查整批数据的质量。可视化代码之所以值得被单独列为交付物不是因为它写起来多难而是好的可视化能同时暴露三个问题掩膜和原图是否对齐、标签是否真的区分了目标与背景、三个切面的标注风格是否一致。我一般把可视化分成两个层次来做。3.1 最小叠加脚本原图与掩膜像素级对齐第一层是单张切片上的叠加对比。把原图和掩膜按像素叠加在一起是最快能发现标签错位和轮廓偏移的方法。下面这个脚本足够应对大多数PNG格式的左心房切片数据集import matplotlib.pyplot as plt import numpy as np from PIL import Image image np.asarray(Image.open(axial_001.png).convert(RGB)) mask np.asarray(Image.open(axial_001_mask.png).convert(L)) mask_bool mask 0 # 兼容 0/255 两种标签编码 overlay image.copy() overlay[mask_bool] (overlay[mask_bool] * 0.3 np.array([255, 0, 0]) * 0.7).astype(np.uint8) fig, axes plt.subplots(1, 3, figsize(12, 4)) axes[0].imshow(image) axes[1].imshow(mask, cmapgray) axes[2].imshow(overlay) for a in axes: a.axis(off) plt.savefig(compare_axial_001.png, dpi150, bbox_inchestight)这段代码的逻辑很直接掩膜转成bool数组背景为False、前景为True然后把前景区域染成红色并叠加到原图上。0.3和0.7是原图与红色的混合权重目的是让红色半透明压在解剖结构上既能看到心房边界又不完全遮住底下的组织纹理。保存用PNG而不用JPG是为了避免压缩伪影干扰后续排查。参数说明里最值得关注的是mask 0这个阈值。它同时兼容0/1和0/255两种编码方式但如果前面检查时发现标签里有128这类中间值这里就需要先做一次二值化再进可视化否则半透明边缘会被当成目标显示出来。另一个容易忽略的点是叠加前必须先断言两张图shape一致我见过不少案例是原图被中心裁剪过、掩膜没做同样操作结果叠加后左心房轮廓整体缩了一圈用这段代码一眼就能看出来。提示如果叠加图里红色区域边缘出现一圈半透明的细边优先怀疑掩膜被做过线性插值resize而不是模型问题。3.2 三切面全览与固定调色板一次看清整个数据集的标注质量单张对比适合细看但要快速评估整个数据集的质量需要把三个切面横着排开做一次全览。这里有一个非常隐蔽的坑matplotlib默认colormap会把掩膜数值映射成不同颜色同一张图在不同脚本里渲染出来色系不一致排查时容易误判。解决办法是固定调色板用红色表示前景、黑色表示背景。import matplotlib.pyplot as plt from matplotlib.colors import ListedColormap import numpy as np from PIL import Image cmap ListedColormap([black, red]) fig, axes plt.subplots(3, 3, figsize(12, 12)) for row, axis_name in enumerate([axial, coronal, sagittal]): for col in range(3): img_path f{axis_name}_sample_{col:02d}.png mask_path f{axis_name}_sample_{col:02d}_mask.png image np.asarray(Image.open(img_path).convert(RGB)) mask np.asarray(Image.open(mask_path).convert(L)) axes[row, col].imshow(image) axes[row, col].imshow(mask, cmapcmap, alpha0.4, interpolationnone) axes[row, col].axis(off) plt.tight_layout() plt.savefig(dataset_overview.png, dpi150)这块全览图的核心价值是切面横向对比。轴状位那一行应该看到比较完整的椭圆形红色区域冠状位那一行红色区域的边界会明显更散矢状位那一行红色区域的形态跨度会很大。如果某一行里出现了大量红色散点说明这个切面的标签可能有噪声而不是模型的问题。alpha0.4是半透明覆盖的透明度配合固定调色板使用可以让红色下面透出原始解剖结构判断标签是否贴齐边界。interpolationnone是另一个关键参数imshow默认会对图像做插值平滑掩膜这种离散标签被平滑后会显得边缘很虚排查时会误以为标注粗糙关掉插值才能看到真实像素边界。4. 医学图像分割数据集的5个避坑点标签错位、插值翻车与切面信息丢失前面章节解决了数据本身的检查和可视化接下来的坑集中在从数据集到模型训练这段链路上。以下五条都是我在实际项目中踩过的每一条都按照现象、原因、解决的顺序写你可以直接对照排查。4.1 标签错位轮廓整体偏移一圈现象是叠加可视化里左心房轮廓整体缩小或者朝某一个方向偏移看起来像一个缩了一圈的剪影。造成这个问题的原因通常有两个原图和掩膜走了不同的预处理链路一个做了中心裁剪另一个没做或者原始数据是NIfTI格式读取时的方向矩阵不一致导致切片被翻转。解决方法是先跑一遍第3章的叠加脚本确认是否有系统性的错位。如果所有切片都朝同一个方向偏移可以先计算掩膜连通域质心和原图高亮区域质心用np.roll做整体平移校正如果错位只出现在个别切片则属于文件配对问题需要按文件名重新整理配对关系。4.2 掩膜插值翻车边界出现半透明渐变现象是掩膜边缘变成一圈灰色渐变模型训练到后期loss降不下去。原因是数据加载里用了cv2.resize且忘了指定最近邻插值默认的双线性插值会把离散的0/1标签插成0.3、0.8这样的浮点值模型输出为了拟合这些过渡值边界就越学越糊。解决办法是掩膜resize强制走Image.NEAREST或者cv2.INTER_NEAREST并且在Dataset里加一行断言assert (mask 0 | mask 1).all()从入口杜绝插值污染。4.3 归一化写死换数据源后模型直接失效现象是同一个训练脚本换成另一个来源的MRI数据后loss不降甚至输入图像整体偏黑。原因很常见从某个示例代码里抄来的img / 255.0只对8位PNG有效而很多心脏MRI序列是int16或float32存储强度范围根本不是0到255。解决方法是每次加载切片时按图像自身做百分位截断先算np.percentile(img, 99.5)作为上限然后np.clip(img, 0, p)再除以p把强度映射到0到1。这样换任何采集协议的数据都不用改代码。4.4 shuffle丢掉切面信息验证集指标忽高忽低现象是训练曲线平滑但验证Dice在不同epoch之间大幅震荡同一个模型跑两次结果差异很大。原因是DataLoader里没把切面信息告诉采样器一个batch里轴状位和矢状位混在一起网络在同一组参数下被迫拟合形态差异极大的输入。这个问题在三切面数据集里尤其容易被浪费数据明明划分好了shuffle一开全白费。解决办法是在Dataset的返回值里加一个axis字段训练循环打印每个batch的axis分布更彻底一点按切面构建三个独立Dataset每个batch只从一个切面里采样验证集也按切面分层划分。4.5 忽略像素间距真实解剖比例失真现象是原图都是512×512但某些切面里左心房看起来特别大另一些又特别小Dice整体上不去。原因是resize成固定尺寸时没有考虑spacing信息不同病人的体素间距不同同样一像素在物理空间里代表的实际大小不一样。解决办法是先读DICOM或NIfTI头部的spacing字段记录切面层内像素间距的分布。如果没有spacing信息退而求其次统计每张图的左心房连通域面积如果面积离散程度过大考虑先裁剪ROI再resize而不是整图缩放到固定尺寸。这些排查手段都不复杂但能救回一整个看起来有问题的数据集。5. 最小训练链把三切面数据集跑成可复现的左心房分割流程数据检查和坑都排完了接下来进入正式训练。常见做法是直接用U-Net类模型配合BCE加Dice的混合损失库选择上用MONAI或segmentation_models_pytorch都能省很多事。这里给出一套最小可复现的流程重点不是网络结构而是如何把三切面信息正确地带进训练链路。5.1 Dataset里必须带切面字段用PIL实现最小加载器先看DataLoader的最小实现。这里故意用PIL而不是cv2是因为PIL的resize接口在掩膜上更容易保持最近邻插值不容易翻车import os from glob import glob import numpy as np import torch from torch.utils.data import Dataset from PIL import Image class LeftAtriumSliceDataset(Dataset): def __init__(self, image_dir, mask_dir, size512, axisaxial): self.image_paths sorted(glob(os.path.join(image_dir, *.png))) self.mask_paths sorted(glob(os.path.join(mask_dir, *.png))) assert len(self.image_paths) len(self.mask_paths), 原图和掩膜数量不一致 self.size size self.axis axis def __len__(self): return len(self.image_paths) def __getitem__(self, idx): image Image.open(self.image_paths[idx]).convert(RGB) mask Image.open(self.mask_paths[idx]).convert(L) image image.resize((self.size, self.size), Image.BILINEAR) mask mask.resize((self.size, self.size), Image.NEAREST) img np.asarray(image).astype(np.float32) / 255.0 msk (np.asarray(mask) 0).astype(np.float32) img torch.from_numpy(img).permute(2, 0, 1) msk torch.from_numpy(msk).unsqueeze(0) return {image: img, mask: msk, axis: self.axis, path: self.image_paths[idx]}这里最核心的是返回字典里带上了axis和path两个字段。axis用于训练循环里做切面分层显示path用于排查具体是哪一张图导致loss异常这两个字段在出问题时能省下大量时间。Image.NEAREST是掩膜resize的必选插值方式原理是离散标签不能做平滑插值。permute(2, 0, 1)把PIL读出的HWC张量转成PyTorch默认的CHW格式。掩膜unsqueeze(0)是给通道维留位置因为二值分割网络输出的是一个通道的sigmoid概率图而不是两个类别的softmax。注意如果原始数据不是8位PNG而是int16上面代码里的/ 255.0要换成百分位截断具体做法见第4.3条。5.2 三个初始超参size、lr、batch的第一版配置左心房分割任务不需要像目标检测那样调很多超参第一版把下面三个参数定好就够跑出一个有参考意义的结果。参数初始值调整方向IMAGE_SIZE512显存不够先降到256不要直接降到128左心房在整图里占比小分辨率过低会丢边界BATCH_SIZE8医学图像样本量通常不大4到16都合理梯度不稳时先调batch而不是先调lrLR1e-4AdamW配合余弦退火验证Dice出现平台期再减半LOSSBCE Dice二分类输出Dice权重取0.5左右即可左心房分割任务选择1e-4作为初始学习率是因为目标在整图中的像素占比小梯度信号偏稀疏过大的学习率会让边界预测在早期就震荡。损失函数用BCE加Dice而不是纯BCE原因是纯BCE对前景占比小的数据偏向预测背景Dice项能把前景区域的匹配程度显式拉进优化目标。训练到100个epoch左右观察验证Dice曲线是否出现平台期。如果20个epoch没有提升直接早停不需要硬跑满预设epoch数。5.3 验证集划分按切面分层而不是纯随机三切面数据集最忌讳的验证集划分方式是纯随机。纯随机可能把同一序列的前后切片分到训练集和验证集造成数据泄漏Dice虚高模型实际泛化能力远低于指标。三种常用方案按推荐度排序方案A按切面分层每个切面取80%训练、20%验证保证轴状位、冠状位、矢状位都出现在验证集中这是最贴合标题数据集结构的做法。方案B按病例分组如果数据来自多个患者同一个患者的所有切片只进训练集或只进验证集这是医学分割更严谨的推荐能避免模型记住病人特征而不是解剖结构。方案C先做方案A再做方案B的交叉验证数据量允许时两者都做用分层交叉验证的均值和方差来评估模型稳定性。结合这套数据的特点第一版先按方案A做即可。后面需要发论文或做严格评估时升级到方案B特别是当标签文件里包含患者ID字段时务必按患者分组。6. 验证不只看Dice用错误热力图和三切面分层指标收尾模型训完只看一个整体Dice就下结论在左心房分割任务里不太够用。这类分割的最终输出通常要回到三维体数据做容积分析二维切片上的小错误会在三维重建时被放大。错误热力图是看空间分布最直观的工具它把模型预测和真实标签逐像素对比用不同颜色标出误检和漏检的位置def error_map(pred_mask, gt_mask): fp (pred_mask 1) (gt_mask 0) fn (pred_mask 0) (gt_mask 1) canvas np.zeros((*pred_mask.shape, 3), dtypenp.uint8) canvas[gt_mask 1] [255, 255, 255] canvas[fp] [255, 0, 0] canvas[fn] [0, 0, 255] return canvas白色是真实左心房区域红色是模型多预测出来的误检蓝色是模型漏掉的真实区域。如果红蓝色块贴着小房边界呈细带状分布说明模型在边界定义模糊的区域摇摆这往往是标注本身的边缘不精确导致的不要急着调网络如果红蓝色块散布在心腔内部那才是明显的预处理错误或欠拟合信号。左心房分割里红色误检最常出现在肺静脉开口处蓝色漏检最常出现在心耳尖端。错误热力图看的是空间分布数值指标里我习惯在Dice之外再加一个HD95也就是95分位豪斯多夫距离。Dice对目标占比小的数据容易虚高HD95能单独反映边界偏差from scipy.ndimage import distance_transform_edt def hd95(pred, gt): pred_dt distance_transform_edt(1 - pred) gt_dt distance_transform_edt(1 - gt) distances_pred pred_dt[gt 1] distances_gt gt_dt[pred 1] return max(np.percentile(distances_pred, 95), np.percentile(distances_gt, 95))这段代码的思路是分别从预测侧和真实侧计算边界距离取双向的95分位既惩罚模型漏掉的那部分真实边界也惩罚模型多画出来的区域。HD95的单位是像素对512×512的输入来说数值在5到10像素以内通常算不错。我自己的习惯是每次训练结束都按三个切面分别打印Dice和HD95矩阵三行三列输出到CSV和模型结构版本一起存档。没有这个习惯的时候我翻过车两个模型整体Dice几乎一样实际一个轴状位强、一个矢状位强部署到三维体数据后表现完全不同。按切面分层看指标能直接暴露模型偏向哪个方向的形态先验这个信息比整体指标真实得多。希望这套从数据检查、可视化、避坑到切面分层验证的路径能帮到你。本文还有配套的精品资源点击获取