ACDC心脏MRI数据集预处理为VOC格式
在医学图像分析领域,深度学习模型的性能高度依赖于高质量、结构化良好的训练数据。尽管像Segment Anything Model (SAM)和Qwen3-VL这类多模态大模型展现出强大的图文理解能力,但它们对输入数据格式仍有明确要求——尤其是语义分割任务中广泛采用的PASCAL VOC 格式。
然而,真实世界中的医学影像数据往往以复杂的三维形式存储,例如 ACDC 心脏 MRI 数据集使用的 NIfTI(.nii.gz)格式。这种“原生”格式虽然适合临床查看和科研分析,却不便于直接接入主流视觉模型进行端到端训练或微调。
于是问题来了:如何将一个包含时间序列、多切片、三维空间结构的心脏 MRI 数据集,高效、无损地转换为标准的二维 VOC 语义分割数据集?本文将以 ACDC 数据集为例,完整演示从原始.nii.gz文件到可用于 U-Net、DeepLab 或 Qwen3-VL 微调的标准 VOC 数据集的全流程实践。
为什么选择 ACDC 数据集?
ACDC(Automated Cardiac Diagnosis Challenge)是 MICCAI 2017 发起的一项公开挑战赛所发布的数据集,专为心脏自动分割与功能评估设计。它具备以下优势:
- 每例患者均由两名放射科医生独立标注心腔轮廓(LV: 左心室, RV: 右心室, MYO: 心肌)
- 覆盖舒张末期(ED)和收缩末期(ES)两个关键时相
- 提供病种标签(健康、心肌梗死 MI、扩张型心肌病 DCM 等),支持分类+分割联合建模
- 图像质量高,层厚一致,伪影少
更重要的是,其文件命名和组织极为规范,非常适合自动化脚本处理。
典型的目录结构如下:
training/ ├── patient001/ │ ├── Info.cfg # 包含病种、ED/ES帧编号等元信息 │ ├── patient001_4d.nii.gz # 四维动态影像(3D+time) │ ├── patient001_frame01.nii.gz # 第一时期(如ED)的三维图像 │ ├── patient001_frame01_gt.nii.gz # 对应分割标签 │ ├── patient001_frame12.nii.gz # 第二时期(如ES) │ └── patient001_frame12_gt.nii.gz ...每个frameXX.nii.gz实际是一个三维数组(H=216, W=256, N_slices),其中N_slices因患者而异(通常6–14层)。我们的目标就是把这一个个三维体数据“切开”,提取出每一张二维切片,并分别保存为图像和标签文件。
目标格式:PASCAL VOC 是什么?
VOC 格式最初由 PASCAL Visual Object Classes 挑战赛定义,至今仍是语义分割任务的事实标准之一。其核心目录结构如下:
VOCdevkit/ └── VOC2007/ ├── JPEGImages/ # 输入图像,.jpg ├── SegmentationClass/ # 语义标签,.png,灰度图,像素值=类别ID ├── ImageSets/ └── Segmentation/ ├── train.txt # 列出训练图像名(不含扩展名) ├── val.txt ├── trainval.txt关键点在于:
- 图像使用.jpg存储,可压缩但不丢失视觉信息;
- 标签必须用.png保存为索引图像(indexed image),即每个像素值代表类别 ID(0=背景, 1=RV, 2=MYO, 3=LV),不能是伪彩色渲染图;
- 训练/验证划分通过文本文件控制,确保不同集合间无数据泄露。
这套格式被 PyTorch 官方VOCSegmentation类、MMSegmentation、Detectron2 等主流框架原生支持,极大简化了后续建模流程。
处理流程详解
整个预处理过程可分为五个阶段:
1. 解压.nii.gz文件并整理路径
虽然 Python 的nibabel库可以直接读取.nii.gz,但我们仍建议先解压到临时目录,避免反复压缩解压影响效率。
mkdir -p ACDC_nii/images ACDC_nii/labels然后运行以下脚本批量复制并重命名相关文件:
import os from os.path import join import shutil ori_ACDC_train_path = './ACDC_challenge_20170617/training' def niigz2nii(): for patient in os.listdir(ori_ACDC_train_path): patient_path = join(ori_ACDC_train_path, patient) for file in os.listdir(patient_path): if 'frame01.nii.gz' in file and 'gt' not in file: src = join(patient_path, file) dst = join('ACDC_nii/images', f"{patient}_{file}") shutil.copy(src, dst) elif 'frame01_gt.nii.gz' in file: src = join(patient_path, file) dst = join('ACDC_nii/labels', f"{patient}_{file}") shutil.copy(src, dst) niigz2nii()这样我们就得到了统一命名的.nii文件列表,便于后续遍历处理。
2. 使用nibabel加载并切片
接下来是核心步骤:将每个三维体积数据沿 Z 轴切分为多个二维切片。
import nibabel as nib import matplotlib.pyplot as plt import numpy as np import os def convert_nii_to_jpg(): image_id = 0 nii_dir = "ACDC_nii/images" output_dir = "VOCdevkit/VOC2007/JPEGImages" os.makedirs(output_dir, exist_ok=True) for file in sorted(os.listdir(nii_dir)): if not file.endswith('.nii'): continue filepath = join(nii_dir, file) nii_img = nib.load(filepath) data = nii_img.get_fdata() # shape: (216, 256, Z) for z in range(data.shape[2]): image_id += 1 slice_data = data[:, :, z] filename = f"{file.split('_')[0]}_{str(image_id).zfill(6)}.jpg" plt.imsave(join(output_dir, filename), slice_data, cmap='gray') convert_nii_to_jpg()同理处理标签文件:
def convert_nii_to_png(): image_id = 0 nii_dir = "ACDC_nii/labels" temp_dir = "tmp" os.makedirs(temp_dir, exist_ok=True) for file in sorted(os.listdir(nii_dir)): if not file.endswith('.nii'): continue filepath = join(nii_dir, file) nii_img = nib.load(filepath) data = nii_img.get_fdata() for z in range(data.shape[2]): image_id += 1 slice_data = data[:, :, z] filename = f"{file.split('_')[0]}_{str(image_id).zfill(6)}.png" plt.imsave(join(temp_dir, filename), slice_data, cmap='gray') convert_nii_to_png()⚠️ 注意:这里我们先将标签保存到
tmp/目录,因为plt.imsave()会对 float 数组做归一化处理,导致原始类别值(0,1,2,3)被映射为 [0,255] 区间内的连续灰度值,造成严重错误!
3. 修复标签像素值:从“伪彩色”还原为“索引图”
这是最容易出错的关键一步。许多初学者误以为plt.imsave(..., cmap='gray')输出的就是正确标签,其实不然——它输出的是可视化图像,而非可用于训练的索引图。
正确的做法是手动还原原始类别。由于原始标签只有四个离散值(0~3),我们可以根据灰度区间反向映射:
from PIL import Image import numpy as np import os def fix_label_values(): for filename in os.listdir("tmp"): src_path = os.path.join("tmp", filename) dst_path = os.path.join("VOCdevkit/VOC2007/SegmentationClass", filename) img = Image.open(src_path) arr = np.array(img) # 此时值域约为 [0, 255] # 构造映射关系(基于四类均匀分布假设) mapping = {} for val in np.unique(arr): if val < 64: cat = 0 elif val < 128: cat = 1 elif val < 192: cat = 2 else: cat = 3 mapping[val] = cat fixed_arr = np.vectorize(mapping.get)(arr).astype(np.uint8) Image.fromarray(fixed_arr, mode='L').save(dst_path) os.makedirs("VOCdevkit/VOC2007/SegmentationClass", exist_ok=True) fix_label_values()验证是否成功:
label = np.array(Image.open("VOCdevkit/VOC2007/SegmentationClass/patient001_000001.png")) print(np.unique(label)) # 应输出 [0 1 2 3]✅ 成功!这才是真正的语义分割标签。
4. 按患者级别划分训练/验证集
为了避免数据泄露,我们必须采用patient-level split,即同一个患者的全部切片只能出现在 train 或 val 中,不可混杂。
import os import random from tqdm import tqdm random.seed(42) train_percent = 0.9 seg_dir = 'VOCdevkit/VOC2007/SegmentationClass' save_dir = 'VOCdevkit/VOC2007/ImageSets/Segmentation' os.makedirs(save_dir, exist_ok=True) all_files = [f for f in os.listdir(seg_dir) if f.endswith('.png')] patient_ids = list(set(f[:10] for f in all_files)) # 如 patient001 random.shuffle(patient_ids) num_patients = len(patient_ids) num_train = int(num_patients * train_percent) train_pats = set(patient_ids[:num_train]) val_pats = set(patient_ids[num_train:]) train_list = [] val_list = [] for fname in all_files: pid = fname[:10] basename = fname[:-4] # 去掉 .png if pid in train_pats: train_list.append(basename) else: val_list.append(basename) # 写入分割文件 with open(os.path.join(save_dir, 'train.txt'), 'w') as f: f.write('\n'.join(sorted(train_list)) + '\n') with open(os.path.join(save_dir, 'val.txt'), 'w') as f: f.write('\n'.join(sorted(val_list)) + '\n') with open(os.path.join(save_dir, 'trainval.txt'), 'w') as f: f.write('\n'.join(sorted(train_list + val_list)) + '\n') print(f"Total patients: {num_patients}") print(f"Train patients: {len(train_pats)} ({len(train_list)} images)") print(f"Val patients: {len(val_pats)} ({len(val_list)} images)")典型输出:
Total patients: 100 Train patients: 90 (987 images) Val patients: 10 (112 images)这种划分方式既保证了样本多样性,又符合临床实际——模型不会“记住”某个特定患者的解剖特征。
可视化效果展示
处理完成后,你可以直观检查结果:
🖼️输入图像(JPEGImages):
→ 灰度 MRI 切片,清晰显示心脏横断面结构
🎨标签图像(SegmentationClass):
→ 像素级标注,颜色对应类别:
- 黑色(0):背景
- 深灰(1):右心室(RV)
- 中灰(2):心肌(MYO)
- 浅灰(3):左心室(LV)
这些图像可直接用于训练 U-Net、HRNet、SegFormer 等主流分割模型,也可作为 Qwen3-VL 的 fine-tuning 输入,实现“看图说话”式的报告生成任务。
扩展思考:为何不用 3D 分割?
你可能会问:心脏本来就是三维器官,为什么不保留 3D 结构,而要降维成 2D?
这是一个非常好的问题。事实上,3D 分割确实能更好地建模空间连续性,尤其在心功能评估中具有优势。但在实际工程中,我们常选择 2D 方案,原因包括:
- 显存限制:3D 卷积网络(如 3D U-Net)对 GPU 显存消耗极大,普通设备难以支撑;
- 数据量不足:ACDC 仅100例患者,每例约10张切片,总样本约1000张,不足以充分训练复杂3D模型;
- 迁移学习便利性:大多数预训练模型(如 ResNet、Swin Transformer)都是2D架构,2D流程更易集成;
- 推理速度更快:2D 推理可在毫秒级完成,适合实时交互场景(如 Qwen3-VL 的网页端演示);
当然,如果你有充足的计算资源和标注数据,完全可以在此基础上构建 3D pipeline —— 本文提供的2D格式也可作为中间产物使用。
最终成果的应用场景
经过上述处理,你已经拥有了一个标准的 VOC 格式心脏 MRI 分割数据集,可用于多种下游任务:
- ✅ 训练轻量级 U-Net 模型实现快速心脏分割
- ✅ 微调 DeepLabV3+ 或 SegFormer 提升边界精度
- ✅ 接入 Qwen3-VL 实现“图像 → 文字”报告生成(如:“左心室大小正常,收缩功能良好”)
- ✅ 构建零样本视觉问答系统(VQA):“指出图中右心室区域?”
- ✅ 搭配网页推理平台进行实时交互演示
更重要的是,这一整套预处理流程具有很强的通用性,稍作修改即可适配其他医学影像数据集(如 BraTS、LiTS、MMWHS 等)。
结语
数据决定上限,算法逼近下限。在医学 AI 落地过程中,真正耗费精力的往往不是模型设计,而是如何将原始、异构、非标准化的临床数据转化为“机器友好”的训练格式。
本文以 ACDC 数据集为例,展示了从 NIfTI 到 VOC 的完整转换链路,涵盖了文件解析、切片提取、标签修复、集划分等多个关键技术环节。希望这套实践方案能为你在医学图像分割、多模态建模等方向的研究与开发提供有力支持。
📌 小贴士:建议将处理好的数据集打包上传至 Qwen3-VL 平台,构建专属医学视觉问答微调基线,进一步释放大模型潜力。
👏 欢迎添加微信cvxiaoyixiao获取处理好的数据集副本,或交流更多医学AI落地经验!