医学影像研究者的nnUNet实战手册从原始NII到标准格式的完整转换指南第一次打开nnUNet的官方文档时我被那些严格的文件夹结构和命名规则弄得晕头转向。作为一名刚接触医学图像分割的研究生我完全理解那种面对复杂数据格式要求时的无力感。但经过三个实际项目的磨练后我发现只要掌握了几个关键步骤将个人NII数据集适配到nnUNet框架其实并不像看起来那么困难。1. 理解nnUNet的数据格式哲学nnUNet之所以对数据格式有严格要求背后有其深刻的工程考量。这套标准化体系使得框架能够自动处理各种医学影像数据而无需为每个新数据集编写特定的预处理代码。理解这一点后那些看似繁琐的规则就变得合理起来。核心目录结构应该如下所示DATASET/ ├── nnUNet_raw/ │ ├── nnUNet_raw_data/ │ │ └── TaskXXX_任务名/ │ │ ├── imagesTr/ # 训练图像 │ │ ├── labelsTr/ # 训练标签 │ │ ├── imagesTs/ # 测试图像(可选) │ │ └── labelsTs/ # 测试标签(可选) ├── nnUNet_preprocessed/ # 自动生成 └── nnUNet_trained_models/ # 自动生成对于多模态数据(如PET/CT)nnUNet通过文件名后缀区分不同模态病例001_image_0000.nii.gz # PET图像 病例001_image_0001.nii.gz # CT图像 病例001_label.nii.gz # 对应的分割标签2. 实战构建标准数据集目录让我们通过一个真实案例来演示如何将杂乱无章的原始数据转换为nnUNet标准格式。假设我们有一个包含50例PET/CT扫描的前列腺癌数据集。2.1 创建基础目录结构首先我们需要搭建符合nnUNet要求的文件夹框架。以下Python脚本可以一键完成这个任务import os # 定义基础路径 base_path /path/to/your/DATASET # 创建核心目录 os.makedirs(os.path.join(base_path, nnUNet_raw/nnUNet_raw_data), exist_okTrue) os.makedirs(os.path.join(base_path, nnUNet_preprocessed), exist_okTrue) os.makedirs(os.path.join(base_path, nnUNet_trained_models), exist_okTrue) # 创建具体任务目录(Task101_ProstateCancer为例) task_path os.path.join(base_path, nnUNet_raw/nnUNet_raw_data/Task101_ProstateCancer) os.makedirs(os.path.join(task_path, imagesTr), exist_okTrue) os.makedirs(os.path.join(task_path, labelsTr), exist_okTrue) os.makedirs(os.path.join(task_path, imagesTs), exist_okTrue) # 测试集可选2.2 数据重命名与组织原始数据通常命名混乱我们需要将其转换为nnUNet要求的格式。关键点在于图像文件{病例ID}_image_{模态编号}.nii.gz标签文件{病例ID}_label.nii.gz以下脚本展示了如何批量重命名文件import glob import shutil # 原始数据路径 raw_data_path /path/to/raw/PET-CT_data # 获取所有病例ID case_ids [f.split(_)[0] for f in os.listdir(raw_data_path) if f.endswith(.nii.gz)] case_ids list(set(case_ids)) # 去重 # 复制并重命名文件 for i, case_id in enumerate(case_ids): # 训练集(前40例)和测试集(后10例)划分 split Tr if i 40 else Ts # 处理PET图像(模态0000) pet_src f{case_id}_pet.nii.gz pet_dst f{case_id}_image_0000.nii.gz shutil.copy(os.path.join(raw_data_path, pet_src), os.path.join(task_path, fimages{split}, pet_dst)) # 处理CT图像(模态0001) ct_src f{case_id}_ct.nii.gz ct_dst f{case_id}_image_0001.nii.gz shutil.copy(os.path.join(raw_data_path, ct_src), os.path.join(task_path, fimages{split}, ct_dst)) # 处理标签 label_src f{case_id}_seg.nii.gz label_dst f{case_id}_label.nii.gz shutil.copy(os.path.join(raw_data_path, label_src), os.path.join(task_path, flabels{split}, label_dst))注意nnUNet要求所有NII文件使用.gz压缩格式。如果原始数据未压缩可以使用nibabel库进行转换。3. 生成关键的dataset.json文件dataset.json是nnUNet理解数据集的关键配置文件它描述了数据集的元信息。以下是一个完整的生成脚本import json from collections import OrderedDict # 准备数据集描述 dataset_info OrderedDict() dataset_info[name] ProstateCancer_PETCT dataset_info[description] Prostate cancer segmentation from PET/CT images dataset_info[tensorImageSize] 3D dataset_info[modality] {0: PET, 1: CT} dataset_info[labels] { 0: background, 1: prostate, 2: tumor } # 自动获取训练和测试文件列表 train_images sorted(glob.glob(os.path.join(task_path, imagesTr/*_0000.nii.gz))) dataset_info[numTraining] len(train_images) # 构建训练数据对 dataset_info[training] [{ image: f./imagesTr/{os.path.basename(f).replace(_0000, )}, label: f./labelsTr/{os.path.basename(f).replace(_0000, _label)} } for f in train_images] # 如果有测试集添加测试信息 if os.path.exists(os.path.join(task_path, imagesTs)): test_images sorted(glob.glob(os.path.join(task_path, imagesTs/*_0000.nii.gz))) dataset_info[numTest] len(test_images) dataset_info[test] [f./imagesTs/{os.path.basename(f)} for f in test_images] # 保存json文件 with open(os.path.join(task_path, dataset.json), w) as f: json.dump(dataset_info, f, indent4)关键字段说明字段描述示例值modality图像模态及其编号{0:PET,1:CT}labels标签类别及对应数值{0:background,1:tumor}numTraining训练样本数量40training训练图像-标签对列表[{image:./imagesTr/case1_image.nii.gz,label:./labelsTr/case1_label.nii.gz}]4. 处理多模态数据的特殊考量当处理多模态数据时有几个容易踩坑的地方需要特别注意模态顺序一致性所有病例的模态顺序必须相同。例如如果第一个病例是PET(_0000)和CT(_0001)那么其他病例也必须保持这个顺序。模态缺失处理如果某些病例缺少某个模态nnUNet提供了几种处理方式用空白图像填充缺失模态修改网络架构以适应可变模态输入最简单的方法是直接排除不完整的病例模态特定预处理不同模态可能需要不同的预处理策略。例如# PET图像通常需要标准化摄取值(SUV)归一化 pet_image nib.load(pet_path).get_fdata() pet_image (pet_image - pet_image.min()) / (pet_image.max() - pet_image.min()) # CT图像可能需要限制HU值范围(-1000到1000) ct_image nib.load(ct_path).get_fdata() ct_image np.clip(ct_image, -1000, 1000)5. 验证数据集正确性在投入训练前强烈建议验证数据集的正确性。nnUNet提供了一个实用工具nnUNet_verify_dataset -t 101这个命令会检查以下内容文件夹结构是否符合要求图像和标签的文件名是否匹配图像和标签的空间属性是否一致(如体素大小、方向矩阵)标签值是否与dataset.json中的定义一致常见错误及解决方案错误类型可能原因解决方法图像-标签不匹配文件名不一致或病例缺失检查文件名模式是否一致空间属性不一致图像和标签的affine矩阵不同使用nibabel统一重采样无效标签值标签包含json中未定义的数值检查标注过程或更新json文件6. 高级技巧自定义数据拆分虽然nnUNet默认使用5折交叉验证但有时我们需要自定义数据拆分(如按机构划分)。这可以通过创建splits_final.pkl文件实现import pickle import numpy as np # 假设我们已经有了自定义的训练-验证划分 custom_split { fold0: {train: [1, 2, 3, ..., 30], val: [31, 32, ..., 40]}, fold1: {...}, ... } # 转换为nnUNet需要的格式 splits [] for fold in custom_split.values(): splits.append({ train: [fcase{id}_image for id in fold[train]], val: [fcase{id}_image for id in fold[val]] }) # 保存到预处理目录 preprocess_dir /path/to/DATASET/nnUNet_preprocessed/Task101_ProstateCancer with open(os.path.join(preprocess_dir, splits_final.pkl), wb) as f: pickle.dump(splits, f)7. 实际项目中的经验分享在最近的一个肝脏肿瘤分割项目中我们遇到了几个典型问题内存不足错误处理大尺寸CT图像(如512×512×800)时nnUNet可能因内存不足而崩溃。解决方案在nnUNet_plan_and_preprocess命令中添加--disable_tta禁用测试时增强修改nnUNetPlans.json中的patch_size为更小的值模态对齐问题PET和CT图像有时空间未对齐。我们使用SimpleITK实现了自动配准import SimpleITK as sitk # 将PET图像配准到CT空间 pet sitk.ReadImage(pet_path) ct sitk.ReadImage(ct_path) registration_method sitk.ImageRegistrationMethod() registration_method.SetMetricAsMeanSquares() registration_method.SetOptimizerAsRegularStepGradientDescent( learningRate0.1, minStep1e-4, numberOfIterations100) registration_method.SetInitialTransform(sitk.TranslationTransform(pet.GetDimension())) final_transform registration_method.Execute(sitk.Cast(ct, sitk.sitkFloat32), sitk.Cast(pet, sitk.sitkFloat32)) # 应用变换并保存 resampled_pet sitk.Resample(pet, ct, final_transform, sitk.sitkLinear, 0.0) sitk.WriteImage(resampled_pet, aligned_pet_path)类别不平衡问题肿瘤体素占比可能不到1%。我们在dataset.json中添加了类别权重label_weights: { 0: 0.1, 1: 0.9 }并在训练时使用-dsw参数启用加权损失函数nnUNet_train 3d_fullres nnUNetTrainerV2 101 0 -dsw 1