
简介这份数据集采用 nnU-Net 标准格式组织面向医学影像分割方向的深度学习者与研究者解决脊柱 CT 影像中椎骨、椎间盘等结构难以自动区分的问题。包内共 81 个文件其中 80 个为 NIfTI 格式的 .nii.gz 影像与标签文件另有 1 个 dataset.json 元数据文件整体压缩包约 950.51MB。imagesTr 目录存放训练用影像labelsTr 目录存放对应分割标签便于直接用于 nnU-Net 模型训练与验证dataset.json 则提供类别、预处理、像素范围等关键配置信息。已有 140 人浏览该资源适合需要快速获得标准脊柱分割训练数据的科研与算法开发人员可显著减少数据整理与格式转换成本。1. nnU-Net 数据格式没对齐spine 分割训练大概率白跑拿到一批脊柱 CT 数据准备做椎骨或椎间盘分割第一个拦住你的往往不是模型结构而是 nnU-Net 的数据格式。nnU-Net 之所以能做到「上传数据直接训练」是因为它对输入约定得非常死imagesTr 和 labelsTr 的目录划分、每个文件的 _0000 后缀、dataset.json 里 modality 与 labels 的定义任何一环对不上报错会出现在 plan、预处理甚至训练到一半的时候。我见过太多人把时间耗在「这个错误到底什么意思」上而不是花十分钟把 spine 数据格式一次整理到位。这篇就按格式约定、影像整理、跑通训练、数据集验证这条线把整个流程讲清楚读者按步骤走即可复现。2. nnU-Net 数据格式硬性约定目录、命名与 dataset.json 字段规则2.1 目录结构与命名为什么 _0000 后缀不能省nnU-Net 在 plan 阶段做的事本质上是从数据里自动推导预处理方案而推导的前提是它能在固定位置找到固定格式的文件。raw 目录下每个数据集一个文件夹v1 用Task120_SpineSeg这种命名v2 用Dataset120_SpineSeg这种命名数字只要全局不重复就行。文件夹内部必须有imagesTr和labelsTr两个子目录测试集用的是imagesTs没有测试集时这个目录可以不存在。文件名规则是所有新手最容易忽视的地方。训练集影像文件名要以_0000结尾比如case_0000.nii.gz。多模态数据时同一个 case 的第二个通道是case_0001.nii.gz第三个是case_0002.nii.gz以此类推。而标签文件名不能带这个数字后缀直接叫case.nii.gz。这个约定决定了预处理时 nnU-Net 如何把多模态合并成一个(C, D, H, W)的张量也决定了它怎么把标签和影像配对。提示所有 case 的名字只要_0000前面的部分一致即可前后缀不能混用。比如一个叫spine01_0000.nii.gz另一个叫S01_0000.nii.gznnU-Net 会把它们当成两个不同的 case但标签文件名必须严格对应。在整理大量文件时我习惯先用一段命令把命名规则核对一遍而不是等报错cd nnUNet_raw/Dataset120_SpineSeg # 检查 imagesTr 里的通道编号是否只有 0000且没有遗漏 find imagesTr -name *.nii.gz | sed s/.*_\([0-9]*\)\.nii\.gz/\1/ | sort | uniq -c # 检查标签文件和影像文件是否一一对应 comm -3 (ls labelsTr | sed s/\.nii\.gz// | sort) \ (ls imagesTr | sed s/_0000// | sort)第一段命令把每个文件的通道编号提取出来理论上只会出现0000。第二段命令对比标签和影像的文件名comm -3列出两边不一致的行如果有输出说明有 case 只存在于其中一个目录。这两步 30 秒能跑完能拦住大半的「训练刚启动就崩」。2.2 dataset.json 的字段逐个讲从 channel_names 到 labelsdataset.json是整个数据集的「说明文件」nnU-Net 的所有下游逻辑都会读它。v1 里的键叫modalityv2 里改成了channel_names很多从旧项目迁移过来的人在这里踩坑。一份脊柱分割数据集的dataset.json常见是这样{ channel_names: { 0: CT }, labels: { background: 0, vertebra_T1: 1, vertebra_T2: 2, vertebra_L1: 3, vertebra_L2: 4 }, numTraining: 120, file_ending: .nii.gz, overwrite_image_reader_writer: SimpleITKIO }这里的字段逐个说明字段取值示例作用channel_names0: CT声明每个通道的影像类型v1 里叫modalitylabelsbackground: 0标注类别名与像素值的映射0 固定为背景numTraining120训练集 case 数量必须和 imagesTr 里实际数量一致file_ending.nii.gz文件后缀nnU-Net 按这个找到对应文件overwrite_image_reader_writerSimpleITKIO强制指定读图后端处理异常头文件时用labels里的背景项值为 0这是 nnU-Net 判断前景背景的分界线类别名本身只影响可视化不参与计算。但如果某个标签值声明了但训练集里没有任何一个 case 包含它plan 阶段会出现空类别的 warning后续的 Dice 计算也会拿它当分母导致该类别永远算 0 分。因此 label 数量不是越多越好只有实际标了的类别才写进去。overwrite_image_reader_writer是很多人不知道的救命字段。NIfTI 文件的 header 里如果 orientation 信息写得不规范默认的NibabelIOWithReorient可能会把某个 case 转错方向。强制改成SimpleITKIO后读取行为统一报错信息也会变直白。代价是读取速度略慢但相比方向错误导致模型学废这点开销完全值得。2.3 plan 阶段如何消费这些格式背景与前景的统计逻辑nnUNetv2_plan_and_preprocess跑起来后会先做 foreground 强度统计。具体做法是读取每个 case 的标签把像素值为 0 的地方视为背景大于 0 的地方视为前景再在前景区间上计算 CT 值的 0.5 和 99.5 分位数作为后续归一化的裁剪依据。这一套逻辑完全依赖labels里的定义——如果一个 case 的背景和前景写反了或者标签里有 255 这种既不是 0 也不是声明类别的值统计出来的强度范围会把整个预处理带偏。同一阶段还会计算每个类别的前景体素占比和表面体积。脊柱数据里常见的情况是T1 到 T12 都有标注但某个 case 只标了下胸椎上胸椎全部留空。此时这个 case 的上胸椎区域会被当成背景负样本网络在训练时会在这些区域学出「这里不是椎骨」的错误信号而且这种错误很难通过调参弥补。更隐蔽的是plan 阶段会统计每个类别的表面体积如果某个 label 只出现在少数几个 case 里它的表面统计数据会偏低影响裁剪大小的计算。3. 把脊柱影像整理成 nnU-Net 格式DICOM 转 NIfTI、标签校验与未标注区域处理3.1 用 dcm2niix 把脊柱 CT 序列转成 NIfTI医院给你的一般是 DICOM 目录一个文件夹对应一个序列。脊柱 CT 扫描通常包含定位像Scout、平扫序列和可能的增强序列定位像绝对不能转进训练集因为它层厚大、分辨率低、不是横断面重建转进来只会污染统计。按序列号区分即可平扫和增强如果要做多模态输入必须保证两个序列配准过否则宁愿只用平扫。转换命令用 dcm2niix 就可以了跨平台且不依赖图形界面dcm2niix -o ./nifti_raw -f %s_%d -z y -b n /data/dicom_series/patient001/参数含义-o指定输出目录-f %s_%d用序列描述加数字做文件名方便区分同一个病人的多个序列-z y输出压缩成.nii.gz直接匹配file_ending的声明-b n不生成.json侧车文件因为 nnU-Net 用不到里面的元数据。转完之后检查一遍 NIfTI 的 shape 和 spacing如果发现某个 case 的层数只有几十层多半是转到了定位像。3.2 标签制作规范整数标签、值与几何一个都不能错标注工具用 ITK-SNAP 或 3D Slicer 都行导出标签时有两个硬性要求一是标签文件必须保存为整数类型二是标签和影像必须共享同一个几何空间。浮点类型的标签文件里如果带着 0.2、0.8 这类小数preprocessing 阶段的重采样会把它们变成非整数值训练时的交叉熵直接按类别索引取数立刻报错。几何不一致更隐蔽影像和标签的 shape 一样但 spacing 不同训练照样能跑但分割结果整体偏移几个像素肉眼检查才能发现。我一般写完标签后先跑一个校验脚本再入集import nibabel as nib import numpy as np def check_label(path): img nib.load(path) data np.asanyarray(img.dataobj) vals np.unique(data) # 标签必须是整数且只含前景值和 0 assert np.allclose(vals, vals.astype(int)), flabel contains float values: {vals} # 背景 0 和有效类别之外的像素值都会让 nnU-Net 困惑 print(fvalid classes: {vals.tolist()}, fdtype: {data.dtype}, fspacing: {img.header.get_zooms()[:3]})脚本的作用是在数据入集前把三个最常踩的坑暴露出来浮点标签、类别值溢出、几何信息缺失。对于脊柱分割类别建议按节段连续编号比如背景 0、T1 为 1、T2 为 2依次排到 L5。如果椎间盘也参与分割在椎骨编号之后接着排不要穿插否则下游统计类别的代码会读得很痛苦。3.3 未标注区域怎么处理剔除优先于留空前面提到部分标注的脊柱数据会让网络把未标注椎骨学成背景。这类情况没有完美的自动补救方案。我的处理顺序是能补标的尽量补标哪怕粗标也比留空强补不了就把这些 case 直接挪出训练集实在数据少舍不得扔那就统一裁剪到标注覆盖的范围并记录裁剪偏移推理时再映射回去。对比三种方案的适用场景处理方式适用情形风险补标注数据量大且未标注区域集中标注成本高粗标引入噪声剔除 case未标注 case 占比 30%数据量减少类别分布更偏裁剪到标注范围未标注区域集中在首尾若干层推理时需处理边界容易出错4. 用整理好的 spine 数据跑通 nnU-Netplan、预处理与训练推理命令4.1 建立数据集目录v2 的三个环境变量与符号链接nnU-Net v2 的目录结构依赖三个环境变量nnUNet_raw、nnUNet_preprocessed、nnUNet_results。训练前要先把它们 export 到当前 shell或者写进.bashrc。raw 目录下新建Dataset120_SpineSeg里面放imagesTr和labelsTr。如果是 v1环境变量名对应的是nnUNet_raw_data_base、nnUNet_preprocessed和RESULTS_FOLDER目录命名也换成Task120_SpineSeg其余逻辑相同。把之前转换好的文件放进去时我建议用符号链接而不是复制。CT 原始数据动辄几百 MB复制一份就把磁盘空间翻倍符号链接节省空间且不影响 nnU-Net 读取for id in $(seq -w 1 120); do ln -sf $PWD/nifti_raw/${id}_0000.nii.gz \ nnUNet_raw/Dataset120_SpineSeg/imagesTr/${id}_0000.nii.gz ln -sf $PWD/nifti_raw/${id}.nii.gz \ nnUNet_raw/Dataset120_SpineSeg/labelsTr/${id}.nii.gz doneseq -w 1 120生成 001 到 120 的编号保证文件名位数一致避免case_2与case_11排序错乱。-sf中s指符号链接f指目标存在时覆盖。执行后再用第二章的comm -3命令复查一次配对关系。注意如果后续还要做交叉验证或留出测试集建议先按比例留好imagesTs不要等训练完了再从训练集里抽否则要重新跑预处理。4.2 跑 plan 与预处理命令参数和输出里要看什么plan 阶段是 nnU-Net 的「自我认知」过程它会遍历所有 case统计 spacing、shape、类别占比然后把结果写进plans.json。v2 命令是nnUNetv2_plan_and_preprocess -d 120 -c 3d_fullres -np 4参数说明-d指定数据集编号必须和目录名里的 120 对应-c指定要生成的配置脊柱 CT 数据量够的话直接上3d_fullres数据量少时可以先3d_lowres把粗分辨率结果也 plan 出来-np是并行进程数4 到 8 之间视 CPU 核数而定。v1 对应命令是nnUNet_plan_and_preprocess -t Task120_SpineSeg --verify_dataset_integrity。输出的最后几行有每个 case 的 spacing 和 median shape 统计扫一眼就能知道数据质量。输出里如果出现no foreground voxels in case ...的警告说明某个 case 的标签全是背景也就是全零文件。nnU-Net 不会因此停止但这个 case 在 preprocessing 阶段会被正常处理后续训练时它贡献的梯度大部分来自背景既浪费时间又干扰学习。遇到这种情况直接把这个 case 从imagesTr和labelsTr同时删掉然后改dataset.json里的numTraining重新跑 plan。4.3 训练与推理的最小命令几个不得不懂的参数预处理完成后训练命令很简单nnUNetv2_train 120 3d_fullres 0最后一个参数是 fold 编号范围 0 到 4。nnU-Net 默认把训练集分成 5 折fold 0 用其中 4/5 的数据训练1/5 做验证。如果只跑一折拿到的模型在推理时不会差太多但要得到论文级别的结果建议 5 折全跑完后用 ensemble 或直接平均 5 个模型的输出。训练中途看日志时关注验证集上的 Dice 变化趋势不用盯着 loss 数值。脊柱类别多有些类别占比极低Dice 在 0.1 上下波动是正常的连续 50 个 epoch 不升才需要停。推理命令nnUNetv2_predict -i ./input_nifti -o ./output_seg \ -d 120 -c 3d_fullres -f 0 1 2 3 4 -chk checkpoint_best.pth-f 0 1 2 3 4把五折模型全部加载输出前会做一个 softmax 平均比单折稳定得多。-chk指定权重文件checkpoint_best.pth 是验证集上表现最好的 epoch另一个 checkpoint_final.pth 是最后一个 epoch实践中前者几乎总是更好。5. 数据格式验收用自查脚本和预处理产物验证 spine 数据集5.1 直接检查预处理产物确认每个 case 都被正确处理nnUNet_preprocessed/Dataset120_SpineSeg/3d_fullres目录里每个 case 会生成一个.npy数据和对应的.pkl文件。数据文件保存的是归一化后的图像张量用 python 直接读出来比看日志快得多import numpy as np import pickle data np.load(preprocessed/case_001.npy, mmap_moder) with open(preprocessed/case_001.pkl, rb) as f: meta pickle.load(f) # 确认张量形状是 (C, D, H, W)CT 单通道则 C 1 print(data.shape) print(meta.keys())如果.npy的 C 维大于声明值说明某个 case 的模态文件比channel_names多或者配准后的多序列没分开如果发现某个 case 的文件不存在说明预处理阶段这个 case 就失败了通常伴随 4.2 节提到的 warning。真正的问题排查其实都集中在这一步后面训练出什么问题已经和「数据格式」没太大关系。5.2 多模态数据的格式取舍配准优先宁可少模态脊柱分割中多模态最常见的场景是平扫 CT 加增强 CT、或 CT 加 MR T1。放进 nnU-Net 时所有模态必须先做刚性配准确保每个体素对应同一解剖位置。未配准的数据直接喂进去网络会尝试学习两个模态间的错位映射输出结果会出现重影或边缘模糊。若配准效果不理想我的习惯是只用其中一个模态信息量少一点但数据干净。通道顺序在两个 case 之间不一致也是多头文件项目的常见错误imagesTr里一半 case 的 T1 是_0000、另一半是_0001preprocess 不会报错但最终模型会退化。这属于不报错的格式错误没有自查脚本很难发现。5.3 一个值得留存的自查函数每次都重新检查命名和值太累了可以把前三章提到的检查合并成一个脚本放在数据集的上一级目录换数据集时随手跑一下import json, glob, os import nibabel as nib def self_check(raw_dir): images sorted(glob.glob(f{raw_dir}/imagesTr/*_0000.nii.gz)) labels sorted(glob.glob(f{raw_dir}/labelsTr/*.nii.gz)) assert len(images) len(labels), image/label count mismatch with open(f{raw_dir}/dataset.json) as f: ds json.load(f) n_ch len(ds[channel_names]) for img, seg in zip(images, labels): assert nib.load(img).shape nib.load(seg).shape, img # 检查每个 case 的通道数是否与声明一致 for img in images: parts set(glob.glob(img.replace(_0000, _*))) assert len(parts) n_ch, img print(fOK: {len(images)} cases, {n_ch} channel(s)) self_check(nnUNet_raw/Dataset120_SpineSeg)脚本做三件事保证影像标签数量一致、检查每个 case 的几何 shape 相同、确认通道数和channel_names声明吻合。把这段脚本放在换数据集后的第一个命令位置能省掉后面几个晚上的排错时间。本文还有配套的精品资源点击获取