☰
SPINE MRI 3D分割实战:从nii.gz格式解析到nnU-Net训练避坑指南
2026/10/9 1:03:46 网站建设 项目流程

简介:面向医学图像分析与深度学习模型训练,这份资源提供了一套完整的磁共振人体脊椎分割数据集,适合从事医学影像分割算法研究的学生、科研人员及医学人工智能工程师使用。数据统一采用nii.gz格式存储,兼顾体积与读取效率,可直接作为常见三维分割框架的输入数据;压缩包共包含222个文件,其中220个为nii格式的影像图像与对应分割标注,另附dataset.json记录数据集划分与类别信息,以及txt格式的说明文档,便于快速理解数据组织。整个压缩包约374.58MB,下载解压后即可用于训练、验证与测试,目前已有299人浏览学习,具备一定参考价值。借助这份数据集,研究者可以省去医学影像收集、脱敏和预处理的繁琐环节,将更多精力投入到网络结构设计、损失函数优化和分割精度提升上;也可作为论文实验或课程设计的对照基准,帮助快速验证算法在真实医疗影像上的表现。

1. SPINE MRI 3D 分割数据集:拿到 nii.gz 之后先别急着训,把格式摸透再动手

做过医学影像分割的人应该都有同感:模型选型往往不是最耗时间的,反而是数据格式、方向信息、标注口径这些“脏活”最消磨耐心。这份脊椎 SPINE 分割数据集就是典型代表——数据格式为 nii.gz 的 3D MRI 人体脊椎分割数据集,训练目标是让模型逐体素地标出 MRI 图像里的脊椎结构。它不像自然图像那样子 PNG 就完事,NIfTI 格式自带方向、间距、原点等一堆头信息,处理不好直接训练全崩。这个资源适合两类人:一是刚入门 3D 医学分割、想拿真实 MRI 数据练手的同学;二是已经在用 nnU-Net 做其他器官分割、想快速适配到脊椎场景的工程师。本文会从数据解析讲到训练踩坑,把这份数据集从头到尾拆开。

2. 先读懂 nii.gz 与 dataset.json:这决定了你用什么框架、怎么预处理

2.1 NIfTI 格式的核心字段:方向余弦与 qform/sform

这份数据集里大量出现类似133.nii和133_0000.nii的文件,前者是标注 mask,后者是原始 MRI 图像。两者都是 NIfTI 格式,用 nibabel 读进来之后,你首先会接触到affine矩阵。这个矩阵 4×4,前 3×3 是方向余弦,描述的是体素坐标系到解剖坐标系的旋转关系;第 4 列是原点坐标。对于 MRI 来说,方向信息尤其重要——CT 一般横断面扫描,方向比较规整,而 MRI 可以有矢状位、冠状位、轴位多种采集体位,如果忽略 affine 直接做切片重采样,轻则图像左右翻转,重则空间对不齐。

我一般会先跑下面这段代码,把每个文件的affine、shape和pixdim(体素间距)打出来看:

import nibabel as nib import numpy as np import glob for path in sorted(glob.glob("*.nii")): img = nib.load(path) data = img.get_fdata() affine = img.affine pixdim = img.header.get_zooms() print(f"{path}: shape={data.shape}, pixdim={pixdim}") print(f" affine 前3行:\\n{affine[:3, :]}")

这段代码逐文件打印形状、体素间距和仿射矩阵。get_zooms()返回的是每个轴向上的实际物理间距,单位是毫米;affine[:3, :]则告诉你体素坐标系是怎么映射到解剖坐标系的。比如pixdim是(1.5, 1.5, 1.5)表示各向同性,(0.5, 0.5, 3.0)就说明层厚较大、层内分辨率高,这类数据在后续重采样策略上要有针对性。

2.2 dataset.json 是给 nnU-Net 准备的地图

这份数据集的文件命名方式是编号_0000.nii加编号.nii,这是 nnU-Net 的约定:_0000后缀表示第一个模态,没有后缀的文件是分割标签。dataset.json就是给这个数据集的声明文件,nnU-Net 训练之前会先读它,确认模态数、标签类别数、标签名。常见的dataset.json结构如下:

{ "channel_names": { "0": "MRI" }, "labels": { "background": 0, "vertebrae": 1 }, "numTraining": 30, "file_ending": ".nii" }

channel_names里"0": "MRI"表示单模态 MRI;labels是标签映射,background对应 0,vertebrae对应 1,如果你的标注里每个椎体单独编号,这个字典会更大;numTraining告诉 nnU-Net 训练集有多少例。注意file_ending是.nii而不是.nii.gz,如果文件实际是.nii.gz而这里写了.nii,nnU-Net 会直接找不到文件。

2.3 从文件列表到可训练数据:整理目录与验证配对

拿到手第一步不是急着开训,而是把文件一一对应检查。常见做法是建一个imagesTr和labelsTr的目录结构,然后用脚本批量移动和配对校验。下面这段代码可以快速验证每个标签都有对应的图像:

import os import glob import shutil base_dir = "spine_mri" os.makedirs(f"{base_dir}/imagesTr", exist_ok=True) os.makedirs(f"{base_dir}/labelsTr", exist_ok=True) img_files = glob.glob("*.nii") labels = [f for f in img_files if "_0000" not in f] images = [f for f in img_files if "_0000" in f] for img in images: stem = img.replace("_0000.nii", "") label = f"{stem}.nii" if os.path.exists(label): shutil.move(img, f"{base_dir}/imagesTr/{img}") shutil.move(label, f"{base_dir}/labelsTr/{label}") print(f"配对成功: {img} <-> {label}") else: print(f"警告: {img} 缺少对应标签")

这段脚本把成对文件分别挪到imagesTr和labelsTr,同时检查有没有图像缺标签。输出里如果出现警告,说明这个数据集里某个样本没有对应标注,训练时要么剔除,要么手动补全。imagesTr与labelsTr的命名是 nnU-Net 的默认约定,dataset.json里的路径也会指向这两个目录。

3. 把 MRI 脊椎数据送进 nnU-Net:预处理、训练参数与模型适配

3.1 为什么选 nnU-Net 而不是直接堆 Vision Transformer

3D 医学分割圈子里,nnU-Net 依然是基线神器,尤其是对于椎骨这种边界相对清晰、但空间形态差异较大的目标。它的自适应机制会自动根据数据集的形态、间距、类别比例来推导预处理方案和网络拓扑。Vision Transformer 类方法不是不能用,但你的数据量只有几十例时,Transformer 的数据饥饿问题会非常明显,nnU-Net 这种带强归纳偏置的 U 型卷积结构反而稳。这份 SPINE 数据集用 nnU-Net 跑,基本是开箱即用的体验——它是按 nnU-Net 标准格式发布的,dataset.json、imagesTr/labelsTr都是现成的。

3.2 计划文件(plans)与预处理命令

nnU-Net 的一大特色是它会先跑一遍数据集分析,生成plans文件,里面记录着每种数据集的预处理方案:目标体素间距、重采样方式、归一化策略、patch size、batch size、网络结构深度等。对于 MRI 数据,默认的归一化策略采用的是全局统计的 z-score,因为在同一台设备、同一扫描序列下采集的 MRI,灰度分布相对稳定。不过脊柱 MRI 常有不同站点、不同权重序列混在一起的情况,这时需要考虑是否需要把归一化策略改成每例独立的 z-score。

跑预处理命令很直接:

nnUNetv2_plan_and_preprocess -d 1 --verify_dataset_integrity

-d 1指定 dataset.json 所在的 dataset ID;--verify_dataset_integrity会额外检查数据完整性,包括文件格式、标签值范围、图像与标签形状是否一致。预处理完成后会在nnUNet_results/Dataset001_SpineMRI下生成plans.json和预处理后的数据。plans.json里的关键参数可以打开看,你会看到类似这样的内容:

{ "batch_size": 2, "patch_size": [128, 128, 128], "median_image_size_in_voxels": [160, 160, 160], "spacing": [1.5, 1.5, 1.5], "normalization": "zscore" }

patch_size是每个训练样本裁剪出来的三维块大小,受 GPU 显存制约;spacing是重采样后的目标间距,数据里的原始间距差异会被统一到这个值。如果你的 GPU 显存只有 12G,patch_size建议不要超过[96, 96, 96],否则容易 OOM。

3.3 训练命令与参数含义

预处理完以后,训练阶段的核心命令如下:

nnUNetv2_train 1 3d_fullres 0

这里1是 dataset ID,3d_fullres是配置名,0是 fold 序号(5 折交叉验证初中的第 0 折)。如果有 30 例训练数据,5 折交叉验证里每折训练 24 例、验证 6 例。训练过程中可以看几个关键输出:train_loss稳步下降说明模型在学习;validation loss与train loss的差距不断拉大要警惕过拟合;Dice值到 0.85 以上,在脊椎分割里算还不错的水平。过程中输出大概长这样:

Epoch 50: train_loss=0.234, val_loss=0.321, PseudoDice=0.812 Epoch 100: train_loss=0.108, val_loss=0.187, PseudoDice=0.883 Epoch 150: train_loss=0.071, val_loss=0.142, PseudoDice=0.906

PseudoDice是 nnU-Net 在训练过程中每隔若干 epoch 在验证集上算的 Dice 近似值,注意它是在裁剪 patch 上计算的,不代表最终全图推理结果。单卡训练 30 例左右的小数据集,大概 150 到 250 个 epoch 就能收敛,具体看数据复杂度和你预设的max_num_epochs。

3.4 数据增强参数:决定过拟合程度的关键

nnU-Net 默认会开启一组数据增强策略,包括随机旋转、缩放、弹性形变、加噪声、伽马校正等。3D MRI 数据里,最值得调整的是弹性形变的幅度。椎骨结构相对刚性,弹性形变太大容易把椎体形状拉得不像话,模型反而学到的是被扭曲过的形态。常见做法是在nnUNetTrainer子类里覆盖configure_rotation_augmentation或configure_elastic_deformation方法,把弹性形变的 sigma 从默认的 10 降到 5 左右。对于分割数据量只有 20 到 30 例的场景,数据增强强度直接决定模型是否过拟合,经验上是「轻度增强 + 早停」的收益最高。

4. 避坑与常见问题:从数据清洗到训练推理的五个血泪经验

4.1 方向信息丢失导致左右翻转

现象:训练出来的模型在推理时,分割结果整体镜像翻转,左椎弓成了右椎弓。

原因:某些预处理脚本或转换工具在读写 NIfTI 文件时把affine矩阵忽略了,或者用nib.load之后直接存成了单位矩阵,方向信息被覆盖。

解决:读写 NIfTI 时始终保持affine传递,不要自己构造新数组裸存。可以用nib.Nifti1Image(data, affine, header)来保存,其中affine从原始文件读出来原样传入。另外训练前跑一次nnUNetv2_plan_and_preprocess --verify_dataset_integrity会检测数据方向异常,这个检查值得养成习惯。

4.2 标签值与 dataset.json 对不上

现象:训练开始时 loss 异常高,或者验证集 Dice 一直是 0。

原因:标注文件的标签值不连续,比如只用了 1 和 3,但dataset.json里 labels 写的还是{0: background, 1: foreground},导致预处理时把所有非 0 区域全当成了背景。

解决:用代码扫描标签文件里所有出现的唯一值,再对照dataset.json。扫描方法很简单:

import nibabel as nib import numpy as np label = nib.load("133.nii").get_fdata() unique_vals = np.unique(label) print("标签值列表:", unique_vals)

如果发现标签值不连续或不在预期范围内,就用重映射函数把稀疏标签压缩成连续标签,再更新dataset.json中的labels字典。

4.3 显存不足(OOM)导致训练中断

现象:训练跑到第几轮之后直接报 CUDA out of memory,进程被杀。

原因:预处理生成的plans.json里patch_size设置过大,或者 batch size 大于 GPU 可容纳的容量。

解决:修改plans.json里的patch_size,比如从[128, 128, 128]降为[96, 128, 128]或者[96, 96, 96];也可以把 batch size 从 2 降到 1,虽然训练速度变慢但稳定。对于这个只有几十例的小数据集,宁可缩小 batch size 也不要大幅缩小 patch size,因为感受野变小会伤害分割精度。

4.4 图像与标签空间位置不对齐

现象:预测结果里脊椎位置和真实 MRI 图像上的脊椎位置错位,肉眼看起来分割结果像是「平移了一下」。

原因:预处理过程中图像和标签经过了不同的重采样流程,或者原始数据的图像和标签物理空间坐标本来就不一致。

解决:用nib.load分别读图像和标签,对比两者的affine,如果相差很大说明原始数据就有问题。正常配对数据两者的affine应该一致。如果确实不一致,需要对标签做重采样到图像的物理空间,操作是用nibabel.progress或SimpleITK的Resample。这个数据集本身是配好的,但如果后续你自己合并外部数据,务必要做这一步。

4.5 验证集 Dice 高但实际效果差

现象:交叉验证时 Dice 0.9,推理新数据却发现漏分割和小椎体直接没分出来。

原因:数据划分有问题,比如同一病人的多个扫描被分到了训练集和验证集;或者验证集的 Ground Truth 本身就有标注漏标。

解决:检查数据划分是否正确——脊椎 MRI 数据集里同一个病人的多次扫描要放到同一个 fold,不允许跨 fold。做法是提前建一个数据划分表,按病人 ID 而不是按文件序号划分。

5. 推理与结果评估:滑动窗口、Dice 计算和 3D 可视化验证

训练完成后你要用 nnU-Net 的推理命令对新样本进行分割预测,它会自动应用滑动窗口策略,把大体积图像切成重叠块推理再融合,避免单次推理显存溢出。命令如下:

nnUNetv2_predict -i /path/to/input_images -o /path/to/output_masks -d 1 -c 3d_fullres -f 0 1 2 3 4

-f 0 1 2 3 4表示同时使用 5 折交叉验证训练出的全部模型做集成推理,通常能把预测稳定性提升一个档次。如果只训了第 0 折,就只写-f 0。输出文件是.nii.gz格式,可以直接加载查看。

推理完成后建议自己算一次 Dice 而不是只看 nnU-Net 输出的日志。用SimpleITK或nibabel计算验证集上的 Dice,逻辑如下:

import nibabel as nib import numpy as np def dice_score(pred_path, label_path): pred = nib.load(pred_path).get_fdata() > 0.5 label = nib.load(label_path).get_fdata() > 0.5 intersection = np.sum(pred & label) return 2.0 * intersection / (np.sum(pred) + np.sum(label)) print("Dice:", dice_score("output_masks/133.nii.gz", "labelsTr/133.nii"))

这个脚本把预测结果和 Ground Truth 都转成二值 mask,计算体素级别的 Dice。有一点要注意:get_fdata() > 0.5是因为输出是概率图或软化后的 logits,需要阈值化。如果预测结果是多分类,要对每个类别分别计算 Dice 再取平均。

评估完了以后,最好把结果叠加到原始 MRI 上做一次 3D 可视化。医学影像的评估不能只看数值——边缘有没有毛刺、相邻椎体有没有粘连,这些都要眼睛确认。可以用itkwidgets或napari加载 NIfTI 和预测 mask 叠加显示,检查几个典型的轴向切片,确认分割边界贴合椎体边缘而不是溢出到椎间盘。

我现在的习惯是:每跑完一组实验,先打印测试集全体的 Dice 分布,再看至少三个病人的三维叠加结果,确认没有系统性偏差后再定模型。从那以后我再也没有被「看着 Dice 很高、实际效果一塌糊涂」的错觉坑过。希望这份数据集和这套流程能帮你在 SPINE MRI 3D 分割上少走几段弯路。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询