ACDC心脏MRI数据集详解:从NIfTI到U-Net多类别分割

📅 2026/8/26 23:46:26
ACDC心脏MRI数据集详解:从NIfTI到U-Net多类别分割
简介语义分割是计算机视觉领域的核心任务之一在医学影像分析中它通过对像素级标签的预测实现器官与病灶的精确勾勒。心脏MRI分割作为典型应用场景对心室功能评估和临床疾病诊断具有重要价值。ACDCAutomated Cardiac Diagnosis Challenge数据集是心脏短轴MRI分割的经典基准包含约1800张已对齐的图像与标签覆盖右心室、心肌和左心室三个结构。围绕该数据集从NIfTI医学影像格式的读取、4D数据到2D切片的提取、患者级数据划分到基于U-Net的模型训练与Dice Loss优化系统梳理了医学图像分割的完整工程流程为初学者和工程实践者提供一条清晰可复现的路径。医学图像分割数据集基于ACDC心脏心室图像语义分割数据集约1800张数据和标签已处理多类别图像分割做医学图像分割的同学应该都有体会找一个“能直接跑起来”的数据集往往比训练模型本身还费劲。公开数据集要么标注格式混乱要么需要自己去处理NIfTI和DICOM光数据清洗就能耗掉大半个月。如果你正在做或者准备做多类别语义分割尤其是心脏MRI相关的项目那ACDCAutomated Cardiac Diagnosis Challenge数据集绝对值得认真研究一下。这套数据来自2017年的MICCAI挑战赛核心内容是100个患者的心脏短轴MRI影像及对应的像素级心室标注包含了右心室RV、心肌MYO、左心室LV三个结构的逐像素标签。经过处理后能拿到大约1800张左右已经对齐的图像和标签对是典型的“小而精”多类别分割数据集。这篇文章我会从数据来历、文件结构、加载方式、预处理细节、训练要点到踩坑记录完整梳理一遍适合刚接触医学影像分割的初学者也适合想快速了解这套数据能不能用于自己实验的老手。1. 为什么心脏MR分割要先从ACDC入手数据量与任务难度的平衡点1.1 挑战赛背景5类病例与两个关键时相ACDC的全称是Automated Cardiac Diagnosis Challenge在2017年MICCAI会议上发布。任务非常清晰给定心脏短轴MRI图像算法需要先对心室结构做像素级分割然后基于分割结果自动诊断患者属于哪一类心脏疾病。数据集包含100个患者每个患者都带有患者级别的疾病标签分为5类NOR正常心脏MINF心肌梗死DCM扩张型心肌病HCM肥厚型心肌病RV右心室异常包括右心扩大或右心功能不全每个患者的MRI序列覆盖一个完整心动周期也就是说同一层切片在不同时间点会被拍摄多次整个序列是4D数据3D空间时间。但挑战赛的设计很聪明只在两个关键时相给出了像素级标注一个是收缩末期ESEnd Systole一个是舒张末期EDEnd Diastole。这两个时相在心脏功能评估里是最有代表性的因为ED时心室充盈至最大、ES时心室收缩至最小射血分数、心室容积等关键临床参数全靠这两个时相来计算。从数据规模上看100个患者、每个患者2个时相、每个时相大约有8到12层切片总的标注图大概就是1900张左右。你手里的“约1800张数据和标签”基本就是提取完2D切片、做完清洗之后的规模。1.2 为什么这个数据量“刚刚好”医学影像分割的数据集有个普遍的尴尬真正带像素级标注的公开数据非常稀少。相比之下ACDC的1900张左右2D标注图处在同类型数据集的甜点位置。拿它和几个常见的医学分割基准比一比数据集模态标注图像规模类别数维数ACDC心脏MRI约1900张2D图3个结构2D切片CHAOS腹部MRI/CT约1000张2D图4个器官2D切片Synapse腹部CT约3700张2D图8个器官2D切片BraTS脑MRI约2000个3D体积多模态3个肿瘤结构3D从这个表能看出来ACDC不是最大的但它的优势在于任务聚焦只分割心室三个结构边界相对清晰、结构相对固定而且官方直接给出了训练和验证的划分不用自己折腾数据协议。更重要的是这套数据的质量非常规整。所有MRI都是短轴位切层方向和厚度基本一致灰度范围也相对统一。这意味着你拿到手不需要做特别复杂的空间配准或者方向纠正直接把数据塞给网络就能训练。对于刚入门医学图像分割的人来说ACDC是第一套值得完整跑通的基准数据。2. ACDC原始数据长什么样NIfTI文件结构与标签值解读2.1 4D NIfTI文件的读取方式从官网下载ACDC原始数据后你会看到每个患者一个文件夹里面有几类文件一个4D的nii.gz影像文件、两个3D的nii.gz标签文件和一个info.cfg配置文件。影像文件之所以是4D是因为它把整个心动周期的所有时间帧堆叠在一起。你可以把它理解成一卷电影胶片每一帧是一张3D的短轴MRI体数据而不同帧记录的是同一层切片在不同心脏搏动时刻的状态。用nibabel读取非常简单import nibabel as nib import numpy as np # 读取4D影像 img nib.load(patient001_4d.nii.gz) data img.get_fdata() print(data.shape) # 输出例子: (192, 192, 10, 30)这个shape的含义依次是第一维、第二维单张2D切片的宽和高这里是192x192第三维切片数量空间上的层数从心尖到心底第四维时间帧数一个心动周期被离散成了30个拍摄点读取标签文件的方式类似但标签是3D的gt_es nib.load(patient001_4d_gt.nii.gz).get_fdata() print(gt_es.shape) # 输出例子: (192, 192, 10)这是收缩末期ES对应的分割标签只有空间维度没有时间维度。2.2 标签值、类别和文件命名分割标签里的像素值不是0和1那么简单而是多类别编码。在ACDC中约定如下0背景1右心室腔RV2心肌MYO左心室心肌3左心室腔LV所以这是一套标准的三类别分割任务类别之间存在空间上的嵌套关系左心室腔被心肌包围右心室腔和左心室在解剖上相邻。这种结构对分割模型的边界预测能力要求比较高尤其是心肌和左心室腔之间的分界。除了像素级标签每个患者的文件夹里通常还有一个info.cfg或patient_info.txt文件里面记录了患者编号、疾病类别、ED和ES对应的具体帧号。这个帧号非常关键因为它告诉你标签文件对应的是4D影像的哪两帧。如果不看这个文件光靠猜很容易把ES和ED对应的数据搞错。另外需要注意原始数据里ED和ES的标签有时是分开的两个文件有时合并成一个文件比如用4D标签第一帧是ED、第二帧是ES。不同版本的ACDC发布格式略有差异我建议拿到数据之后先打印一下shape和标签值分布确认结构再动手。提示读NIfTI文件时get_fdata()返回的是float64而标签是离散整数。为了防止后续计算Dice时类别值被自动当成连续值建议在加载标签后立刻用np.round()并转成np.int16存起来这是后续所有处理的前提。3. 从4D NIfTI到2D切片数据加载、目标帧提取与目录组织3.1 为什么切成2D切片训练医疗影像分割里3D分割和2D分割是两条路线。3D网络如3D U-Net能直接利用空间上下文但显存消耗非常大而且ACDC每个患者只有10层左右层间距离相对较远约5-8mm层内分辨率却很高这种各向异性的特性导致直接用3D卷积并不一定是最优解。2D分割的思路是把每一层切片当作独立图像训练网络结构成熟、公开预训练权重多、实验迭代快对初学者尤其友好。ACDC绝大多数公开基线也是用2D U-Net做的所以把4D数据切成2D切片完全够用。3.2 提取目标时相和切片的完整代码核心逻辑分三步先从4D影像中取出ES和ED两个时相对应的3D体数据然后遍历每一层切片保存为图像和标签对。import nibabel as nib import numpy as np import os from tqdm import tqdm # 假设已从info.cfg中解析出ED和ES对应的帧号 ed_frame 0 # 以实际info.cfg为准 es_frame 18 # 以实际info.cfg为准 img_data nib.load(patient001_4d.nii.gz).get_fdata() gt_data nib.load(patient001_4d_gt.nii.gz).get_fdata() # 提取ED和ES的3D体数据 ed_volume img_data[:, :, :, ed_frame] es_volume img_data[:, :, :, es_frame] # 如果是合并标签文件一般第0帧是ED第1帧是ES # 如果是分开的gt_ED和gt_ES文件直接加载即可 gt_ed gt_data[:, :, :, 0] if gt_data.ndim 4 else gt_data gt_es gt_ed # 手动替换为对应文件 # 遍历每层切片保存 for slice_idx in range(ed_volume.shape[2]): image_2d ed_volume[:, :, slice_idx] label_2d gt_ed[:, :, slice_idx] # 过滤掉没有目标的切片全是背景 if label_2d.max() 0: continue np.save(os.path.join(images, fpatient001_ED_slice{slice_idx}.npy), image_2d) np.save(os.path.join(labels, fpatient001_ED_slice{slice_idx}.npy), label_2d)这段代码里有几个细节值得注意过滤全背景切片是必要的因为心脏MRI的上下两端切片往往不包含心室腔保留它们只会给类别平衡带来无关噪声。保存成npy格式比保存成png更合适因为MRI的灰度范围通常不在0-255之间npy可以直接保留原始float数据。后续加载时用np.load就能读回来省去和图像格式打架的麻烦。3.3 按患者划分训练集、验证集和测试集这是医学图像分割项目里最容易翻车、但很多人到后期才意识到的点绝对不要按切片随机划分数据集。同一个患者的ES和ED切片在空间位置和纹理上高度相似如果你把同一个患者的切片同时放进训练集和测试集模型的验证指标会虚高得离谱泛化能力却被高估了。换一批新患者数据上去性能立刻断崖式下降。正确的做法是先把患者按编号分成三组比如60个患者训练、20个验证、20个测试然后再把每个患者对应的切片放进对应的集合中。ACDC官方实际上也给了固定的训练/测试划分策略很多论文都遵循这套划分。举个例子import random patient_ids [fpatient{i:03d} for i in range(1, 101)] random.seed(42) random.shuffle(patient_ids) train_patients patient_ids[:60] val_patients patient_ids[60:80] test_patients patient_ids[80:100]之后在保存切片时根据患者编号把文件路径放进不同的文件夹if patient_id in train_patients: save_dir train elif patient_id in val_patients: save_dir val else: save_dir test3.4 灰度归一化和尺寸统一MRI和自然图像不一样它的灰度值没有绝对物理意义不像CT的HU值那样标准同一个患者在不同扫描参数下灰度分布可能差很多。直接拿原始灰度训练网络很容易学过拟合到某个扫描设备的灰度特性上。我实际测试下来最好用的是z-score归一化对每一张切片单独操作def z_score_normalize(image): mean image.mean() std image.std() if std 0: std 1 return (image - mean) / std还有一种常见做法是percentile裁剪把图像灰度裁到1%到99%之间再缩放到0-1这个在CT上更好用MRI上z-score更稳。尺寸方面原始192x192的切片可以保持不动也可以统一缩放到224x224或256x256。如果要缩放切记图像用双线性插值标签用最近邻插值否则标签边缘会被插出各种小数级别的新类别后面计算Dice时直接乱套。提示z-score归一化如果逐图做在数据增强时也要保持同一个统计量否则等价于在训练过程中不断改变输入分布。更稳妥的做法是先统计整个训练集的均值和标准差把所有图像统一用这个全局统计量归一化。4. 多类别语义分割训练类别不平衡、损失函数与评估指标4.1 背景占比太大怎么办ACDC三类别分割里背景像素占了绝大部分。如果直接拿交叉熵损失硬训网络很快会发现“全预测成背景”也能拿到一个很不错的loss因为背景类别确实占了大头。尤其是心肌这一类别在整张图里占比可能只有10%左右边缘又细长非常容易被模型直接忽略。解决类别不平衡的常用思路有两个加权重和换损失函数。加权重就是给不同类别的交叉熵设置不同权重和背景相比心肌和左心室给更高的权重。但权重系数需要反复调不太省心。更好的方案是Dice Loss和交叉熵的组合。Dice Loss天然对类别不敏感它直接优化的是区域重叠程度不管这个类别在图像里占5%还是50%都会得到同量级的梯度贡献。import torch import torch.nn as nn import torch.nn.functional as F class DiceLoss(nn.Module): def __init__(self, smooth1.0): super().__init__() self.smooth smooth def forward(self, logits, targets): num_classes logits.shape[1] probs F.softmax(logits, dim1) targets_onehot F.one_hot(targets, num_classes).permute(0, 3, 1, 2).float() dice 0 for cls in range(1, num_classes): # 忽略背景类 intersection (probs[:, cls] * targets_onehot[:, cls]).sum() union probs[:, cls].sum() targets_onehot[:, cls].sum() dice (2 * intersection self.smooth) / (union self.smooth) return 1 - dice / (num_classes - 1)实际训练时我会把交叉熵和Dice Loss加在一起当总损失ce_loss nn.CrossEntropyLoss() dice_loss DiceLoss() total_loss ce_loss(logits, targets) dice_loss(logits, targets)这个组合基本是医学图像分割的默认配方。CE负责稳定前期的梯度方向Dice负责把边界细节拉回来两者互补很少出问题。4.2 评估指标别只盯着整体准确率分类任务里大家习惯看Accuracy但分割任务里整体准确率几乎没有参考价值。假设背景占80%模型把背景全猜对了、前景全猜错准确率也有80%以上看起来挺高实际等于废的。医学分割的常用指标有三个指标计算方式关注点Dice系数2×预测与标签交集 / (预测面积标签面积)区域重叠程度IoU交集 / 并集区域重叠程度比Dice更保守HD9595% Hausdorff距离预测边界到标签边界的最大距离的95分位边界误差对分割边缘极其敏感多类别分割里通常先逐类计算Dice再对三个结构取一个平均值来反映整体效果。ACDC官方排名用的也是类似加权Dice的思路这样每个类别都被公平对待。4.3 模型选型从U-Net基线和segmentation-models-pytorch开始ACDC最经典的基线自然还是U-Net。如果你用的是PyTorch我建议直接上segmentation-models-pytorch这个库几行代码就能加载各种主干网络的U-Net版本pip install segmentation-models-pytorchimport segmentation_models_pytorch as smp model smp.Unet( encoder_nameresnet34, encoder_weightsimagenet, in_channels1, classes3, )这里有两个需要留意的参数in_channels设置为1因为MRI是单通道灰度classes设置为3因为背景不算在网络输出里网络只预测RV、MYO、LV三个前景结构。刚开始跑的时候无论是用ResNet34作为encoder还是直接用更轻量的U-Net原始结构效果都在伯仲之间。关键是要先把训练流程跑通再谈提升。如果你注意到这些热搜词里频繁出现“segmentation-models-python”和相关的大模型语义分割那是因为这个库确实是当前PyTorch社区里做语义分割最趁手的工具之一。5. 用ACDC跑完一个完整实验的防翻车清单边界条件与经验补充5.1 数据加载阶段的常见坑我在这套数据上踩过的坑可以列一长串其中最典型的几个NIfTI方向问题。医学影像有严格的坐标系统同一个扫描在affine矩阵中可能记录着不同的方向。ACDC官方数据虽然方向比较规整但如果你拿别的数据集或者自己写NIfTI读取一定要先print(img.affine)查看方向必要时用nibabel.as_closest_canonical()把数据转到标准RAS方向。在ACDC里方向问题不常见但一旦出现切片看起来都是翻转的训练出来的模型就会被你带偏。标签四舍五入问题。NIfTI标签保存时可能带有微小浮点噪声比如原本应该是1的像素读出来是0.99999。如果直接当整数用写损失函数时会多出来一个“幽灵类别”。我习惯在读入后立刻np.round并转整数这个动作值得养成肌肉记忆。时间帧对错。我在第一次处理ACDC时犯过最蠢的错误ED和ES的标签各只有一个3D文件但因为4D影像的时间帧数和标签文件数对不上我就想当然地把第0帧当ED、把最后一帧当ES。后来看info.cfg才发现ES在第十几帧。好在我当时是先做了可视化检查发现了ES切片上心室明显收缩这才及时纠正。所以我不厌其烦地强调先看配置文件再可视化最后再批量处理。5.2 训练流程的稳定性问题同样一份数据有时候只是换了GPU、换了PyTorch版本指标就浮动1%-2%这是深度学习里特别让人头疼的事情。想要保证实验结果可复现有几个固定动作必须做import random import numpy as np import torch def set_seed(seed): random.seed(seed) np.random.seed(seed) torch.manual_seed(seed) torch.cuda.manual_seed_all(seed) torch.backends.cudnn.deterministic True torch.backends.cudnn.benchmark Falsecudnn.benchmarkFalse是很多人容易忽略的它会牺牲一点训练速度来保证卷积算法的确定性。另外DataLoader里的num_workers和shuffle会影响数据顺序如果要做严格的消融对比最好固定这些参数并输入相同的随机种子。5.3 从性能指标到临床落地之间的差距用ACDC训练得到的Dice系数可能在0.85-0.92之间按结构不同有差异这个分数在学术基准上已经不错了。但如果你真想把这个分割结果用于临床辅助诊断光看Dice远远不够。举个例子Dice系数对大面积结构的重叠程度很敏感但心室壁的厚度只有几毫米Dice即使很高边界上2-3毫米的偏差在临床上就已经不可接受了。这个时候HD95反而比Dice更能反映实际使用价值。ACDC的诊断环节还需要从分割结果中计算心功能指标如EDV、ESV、EF等这就要保证分割出来的心室容积和真实值足够接近这比单纯追求像素级重叠更复杂。所以如果你未来要做临床落地建议在训练完之后额外做一件事从分割标签中提取轮廓和MRI原始影像做一次可视化对比。不是叠在一张图上自欺欺人而是把误差分布画出来观察。我见过不少模型Dice得分挺高但边界在基底区域心脏上部的流出道系统性偏移这类问题只有可视化才能暴露。6. 实操总结这1800张图到手后我建议按这几个步骤推进现在回头整理一下如果你已经拿到了“约1800张数据和标签已处理”的ACDC分割数据集下一步的建议路径是这样的先做静态检查。不急着写训练代码先用上面提到的代码加载几张图把图像和标签叠加可视化确认标签值和类别数目符合预期只有0、1、2、3确认没有方向翻转和错位。建立目录结构。按train/val/test三个集合把切片存成npy文件并且记录下患者ID与切片的对应关系方便以后按需做交叉验证。定义一个简单的训练脚本。用2D U-Net加resnet34编码器配上交叉熵加Dice Loss优化器选AdamW初始学习率1e-4训练大概50个epoch。每个epoch结束后在验证集上按类别计算Dice同时追踪全平均Dice。做一轮消融实验。把增强关掉跑一遍、把增强打开再跑一遍观察过拟合程度和泛化能力。医学图像数据量小数据增强尤其是随机旋转和弹性形变对最终指标的影响往往比换模型大得多。记录一切。把数据划分方式、归一化参数、增强参数、训练超参、硬件环境全部写进实验日志。这轮记录会在你后续写论文、写报告或者和别人对比结果时省下无数时间。这套数据我反复用过好几轮每次都能折腾出新问题但这也正是它适合学习的地方规模不大、任务明确、评价指标清晰你能把完整的流程走通才有可能处理更复杂的医学数据。如果要继续深入可以在ACDC基础上尝试的方向包括3D分割网络直接把ES/ED两个时相作为输入、基于Transformer的分割模型如Swin U-Net、基于大模型微调语义分割SAM类模型在医学图像上的边界精度问题、以及从分割结果进一步计算心脏功能参数的多任务框架。无论往哪个方向走这套数据都足够陪你把路走完。本文还有配套的精品资源点击获取