资讯动态

脊柱MRI分割实战:nnUNet预处理与20类标签体系解析

发布时间:2026/9/11 2:11:31 来源:尧图企业网站定制
简介面向MRI影像中的脊柱结构分割任务这份数据集适合医学图像处理研究者及具备一定深度学习基础的学生使用。数据源于脊椎MRI成像包含原始图像与对应mask模板标签覆盖背景、L5至T9椎骨、脊髓管及L5/S1至T8/T9椎间盘等区域解剖层次清晰。为便于直接训练已基于nnUNet完成预处理与增强累计约4000余张图像并划分好训练集与验证集。压缩包共2000个文件以PNG格式图像为主另有1个Python脚本和1个JSON配置整体体积111.12MB解压后即可接入常见分割框架。目前已有530人学习使用可省去数据清洗、格式转换等繁琐步骤用于模型训练、验证与论文复现都很高效。1. 脊椎 MRI 分割4K 张切片背后的 20 类标签体系收到一套脊椎数据 MRI 成像 医学图像分割资源包解压后不是权重文件而是一堆 PNG 切片和一个 dataset.json2620.png、3692.png、1391.png……初看容易发懵。细读 JSON 会发现这是一套标准的脊柱 MRI 分割监督数据集像素标签从背景、L5 到 T9 椎体、椎管一直排到 L5/S1 至 T8/T9 椎间盘共 20 个类别。整套资源约 4K 张图像与 mask 模板作者已用 nnUNet 做过预处理和增强训练集、验证集划分完毕。价值在于把 MRI 成像中最易混淆的椎体、椎管、椎间盘拆成分层标签让模型学到解剖结构顺序预处理也已对齐 nnUNet 输入规范省掉重采样、归一化的脏活。适合刚入 3D UNet 医学图像分割的工程师也适合椎间盘退变、椎管狭窄方向的预研。2. 标签体系与数据组织dataset.json 和切片文件的对应关系2.1 0 到 19椎体、椎管与椎间盘的空间顺序这份数据集的语义类别一共 20 个定义非常明确标签值解剖结构数量说明0Background1背景1-9L5 至 T9 椎体9自下而上编号10Spinal Canal1椎管11-19L5/S1 至 T8/T9 椎间盘9从 L5/S1 向上计数这个顺序不是随意排的。1 到 9 对应矢状位上自下而上的 9 个椎体L5、L4、L3、L2、L1、T12、T11、T10、T911 到 19 对应 9 个椎间盘底部从 L5/S1 开始顶部到 T8/T9。把椎体和椎间盘分开标而不是合成一个 spine 标签是因为两者在 MRI 成像上的信号模式完全不同T2 序列里椎间盘髓核呈高信号、纤维环呈低信号椎体则表现为皮质骨低信号包绕骨髓高信号。混在一个类里模型学到的是两种信号的均值边界会系统性偏移。注意椎管标签 10是单独一类的。它是一条贯穿椎体的低信号通道解剖上连续但在单张切片里只是很窄的一条。这种小目标类别在后面做类别权重时要单独处理否则训练时它几乎不参与 loss 更新Dice 会一直垫底。2.2 从 dataset.json 读取元信息拿到资源第一步先把 dataset.json 打开看字段。nnUNet 有两种数据集命名风格——v1 是 Task 开头v2 是 Dataset 开头——但 dataset.json 里的打底字段基本一致。用下面这段脚本读出来import json with open(dataset.json, r, encodingutf-8) as f: meta json.load(f) print(模态定义:, meta[modality]) print(训练样本数:, meta[numTraining]) print(文件后缀:, meta[file_ending]) for idx, name in meta[labels].items(): print(f标签 {int(idx):2} - {name})这里modality声明输入图像是单通道还是多通道MRI 灰度图一般是[MRI]或{0: MRI}numTraining是 imagesTr 里的样本对数不等于图像总张数验证集不计入file_ending决定读取器用哪种 IO.png会走 Pillow 路径。labels的 key 在 JSON 里天然是字符串打印时用int()转换避免后面按整数索引查标签时类型对不上。提示如果labels里出现 255 这类不在 0-19 范围内的 key说明标注导出时把背景或未标注区域写成了白色训练会被当成第 21 个类别必须提前处理。2.3 切片与 mask 的配对和完整性检查2620.png、3692.png 这类编号看不出规律配对约定是同一 basenameimagesTr/2620.png对应labelsTr/2620.png。我不会直接拿去训练而是先跑一段配对校验排除标注工具导出时的坑import numpy as np from PIL import Image def verify_pair(img_path, mask_path): img np.array(Image.open(img_path).convert(L)) mask np.array(Image.open(mask_path)) assert img.shape mask.shape, f尺寸不一致: {img.shape} vs {mask.shape} labels np.unique(mask) bad set(labels.tolist()) - set(range(20)) assert not bad, f出现合法范围外标签: {bad} print(f{img_path}: OK, {len(labels)} 类, 尺寸 {img.shape}) verify_pair(imagesTr/2620.png, labelsTr/2620.png)convert(L)把可能的三通道伪彩图转成单通道灰度nnUNet 默认不接受三通道输入除非在 dataset.json 里把 modality 声明成三通道。校验标签值域这步能过滤两类典型问题一是 255 残留二是 mask 和图像尺寸错位后者通常是 resize 时把 mask 也按 RGB 三通道插值导致的。同一个脚本把 imagesVal/labelsVal 也跑一遍。关于训练集验证集的划分常见做法是 8:2 或按病例切分。4K 张切片如果来自几十个病例的连续扫描按文件随机划分会把同一个椎体的相邻切片同时分进训练和验证造成数据泄漏验证集 Dice 虚高。这份资源声称已经划分好但我一般会再确认验证集和训练集的文件名前缀有没有重叠稳妥起见。3. nnUNet 预处理与增强把 4K 张 MRI 切片整理成可训练格式3.1 为什么脊柱 MRI 数据集默认选 nnUNetMRI 脊柱分割有两个让通用分割模型失效的特点。第一MRI 信号强度没有统一单位同一组织在不同设备、不同线圈、不同序列下亮度差异巨大CT 的窗宽窗位那套完全用不上。第二矢状位切片的重建分辨率通常是各向异性的层间距可能远大于层内像素间距直接堆叠成 3D volume 会得到变形的解剖结构。nnUNet 的 planner 会自动统计数据集中所有样本的形状、间距和强度分布然后决定三件事重采样目标间距、patch size、batch size。它把每个新数据集都得手动调一遍预处理参数这件事自动化了这也是 3D UNet 医学图像分割里 nnUNet 几乎成为默认基线的原因。模型结构本身没有太多花哨改动赢在数据管线和训练策略的确定性上。3.2 目录布局v1 和 v2 的差异nnUNet v2 的原始数据目录约定如下nnUNet_raw/ └── Dataset500_Spine/ ├── dataset.json ├── imagesTr/ # 训练图像命名唯一即可 ├── labelsTr/ # 训练标签与 imagesTr 同名 ├── imagesVal/ # 验证图像可选 └── labelsVal/ # 验证标签v1 则要求放在nnUNet_raw_data_base/nnUNet_raw_data/Task500_Spine/并把验证集统一放在 imagesTs/labelsTs。版本差异不大但环境变量不同v2 用nnUNet_raw、nnUNet_preprocessed、nnUNet_results三个路径v1 用nnUNet_raw_data_base和nnUNet_preprocessed两个。先确认环境里的 nnUNet 是哪个版本再决定目录结构。Dataset 或 Task 后的数字是数据集唯一编号建议用 500 的段位避免和官方示例冲突。3.3 planner 的预处理决策和参数含义预处理阶段planner 会对每张图像做强度标准化和重采样。默认策略和它对脊柱 MRI 的实际意义如下预处理项nnUNet 默认策略对脊柱 MRI 的意义强度裁剪0.5% ~ 99.5% 百分位裁剪去掉极端高信号保留软组织对比度归一化逐样本 z-score消除扫描间整体亮度差异重采样按中位间距对齐目标 spacing统一椎体体素尺寸避免训练时形状抖动patch 采样含前景概率加权保证每个 batch 里都有椎体和椎间盘样本前景权重按类别频率反比缓解椎管和椎间盘的小目标劣势需要特别注意MRI 不能直接套 CT 的固定强度窗口。nnUNet 的百分位裁剪对大多数 MRI 分割任务是够用的但如果序列里出现金属植入物伪影高信号会被裁剪到 99.5% 以内z-score 仍然可能偏移。我一般会在预处理前先看一眼全数据集的强度直方图如果尾部有长尾把上界百分位从 99.5 降到 99.0。3.4 增强配置里对脊椎有效的几个变换nnUNet 默认增强包括随机旋转、缩放、弹性形变、gamma 校正和镜像翻转。对脊柱场景优先级是不同的弹性形变模拟软组织在扫描时的微小位移对椎间盘这种柔性结构帮助最大沿矢状面的小角度旋转±10° 左右模拟患者摆位差异gamma 校正是对 MRI 亮度不均匀的鲁棒化处理。镜像翻转要谨慎——椎体左右对称可以翻但矢状面里前后方向不能翻脊髓在背侧、椎体在腹侧翻了就违背解剖位置。提示如果训练 loss 下降正常但验证 Dice 波动很大先检查增强里是否开了 coarse dropout。脊椎结构连续性很强大块随机 dropout 会让模型去拼纹理而不是学结构对这种数据弊大于利。4. 训练与验证nnUNetv2 命令、显存控制与类别权重4.1 从预处理到训练的完整命令链环境变量和命令如下export nnUNet_raw/data/nnUNet_raw export nnUNet_preprocessed/data/nnUNet_preprocessed export nnUNet_results/data/nnUNet_results # 规划并预处理 Dataset500 nnUNetv2_plan_and_preprocess -d 500 --verify_dataset_integrity # 训练 3D full resolutionfold 0 nnUNetv2_train 500 3d_fullres 0-d 500指定数据集编号--verify_dataset_integrity会在预处理前检查 imagesTr/labelsTr 配对、间距信息是否齐全前面章节的校验脚本在这里会被重复验证一遍。训练命令里3d_fullres是配置名0是 fold 编号。医学图像分割做五折交叉验证是惯例每个 fold 一个模型推理时把五个模型的概率平均。显存小的机器先只跑 fold 0 验证流程确认无误再补剩余 fold。如果 4K 张切片来自连续矢状位扫描可以把同一病例的切片按 z 轴叠成 3D volume 后跑 3d_fullres如果切片之间没有严格的空间连续性就选 2d 配置把命令里的3d_fullres换成2d即可。判断依据是看文件名或 DICOM 里有没有层位置信息而不是凭感觉。4.2 显存不够时优先动哪个参数预处理完成后planner 把 patch size、batch size 等写进了 plans.json。训练时如果 OOM按下面顺序调整参数默认策略显存不足时的调整batch size通常 2先降为 1patch size按显存自动取最大手动减半或编辑 plans.jsonbase_num_channels32降为 24 或 16deep supervision开启保持开启不要关这里有个容易踩的坑很多人先降 patch size但 patch 变小意味着感受野变小椎体这种大目标更容易被预测碎。优先降 batch size 到 1再不够才动 patch size。改 plans.json 后要重新运行预处理或者手动把改动同步到nnUNet_preprocessed下的对应文件否则训练时读到的还是旧参数改了半天没生效。4.3 类别不平衡椎管和椎间盘的 Dice 上不去的解法20 个类别里椎管标签 10在矢状面单张切片上只占几十到几百个像素椎间盘是薄片结构这两类在默认 loss 下的 Dice 通常会拖后腿。nnUNet 默认 soft Dice CE 已经按频率做了补偿但对这种极端不平衡还不够。常见做法是按标签频率计算反比权重再加一个开方压缩import numpy as np from PIL import Image def compute_class_weights(mask_paths, num_classes20, power0.5, clip10.0): 统计训练集 mask 的类别频率生成反比权重。 power 取 0.5 做开方压缩避免椎管类权重过大导致训练初期梯度爆炸 clip 限制最大倍率保留背景类在 loss 里的参与度。 counts np.zeros(num_classes, dtypenp.float64) for p in mask_paths: mask np.array(Image.open(p)) vals, cnt np.unique(mask, return_countsTrue) counts[vals] cnt freq counts / counts.sum() weights 1.0 / np.clip(freq, 1e-6, None) weights (weights / weights.max()) ** power return np.clip(weights, 1.0, clip) weights compute_class_weights(mask_paths) # mask_paths 为全部训练 mask 路径 print(weights)直接反比会让椎管类权重放大几十上百倍训练初期 loss 直接起飞开方和 clip 把权重压到 10 倍以内。使用方式是在自定义 trainer 里把torch.Tensor(weights, devicedevice)传给 loss 的 class_weight 参数。对椎间盘类除了加权更有效的是在推理后处理里用椎体位置做约束具体做法看下一章。5. 推理后处理连通域过滤与椎体顺序校验5.1 五折集成推理nnUNetv2_predict -i /data/input -o /data/output \ -d 500 -c 3d_fullres -f 0 1 2 3 4-f 0 1 2 3 4把五个 fold 的概率平均后再取 argmax比单 fold 稳定很多不确定用哪个配置时先跑nnUNetv2_find_best_configuration -d 500对比验证集 Dice 再决定。5.2 连通域过滤推理结果最常见的噪声是背景上的零星预测集中在椎管和椎间盘类。按类别保留最大连通域from scipy import ndimage def keep_largest_component(seg, class_id): mask (seg class_id) lbl, n ndimage.label(mask) if n 1: return mask sizes ndimage.sum(mask, lbl, range(1, n 1)) return lbl (int(np.argmax(sizes)) 1)注意这个操作只对解剖上应该连续的结构有效椎体、椎管、椎间盘都是连续的可以用如果某个病例有椎体融合或术后改变硬套会误删需要给ndimage.sum的结果加一个面积阈值只删掉远小于最大分量的碎片。5.3 解剖先验校验最实用的校验是检查椎体中心点顺序。L5 到 T9 在矢状面上自下而上排列中心点坐标应该单调def verify_vertebra_order(seg, class_idsrange(1, 10)): centers [] for c in class_ids: ys, xs np.where(seg c) if len(ys) 0: continue centers.append((c, float(ys.mean()), float(xs.mean()))) diffs np.diff([c[1] for c in centers]) return bool(np.all(diffs 0)), centers如果返回 Falsecenters 列表会直接告诉你哪两个相邻椎体发生了错位或标签粘连比单独看整体 Dice 更能定位问题切片。这套 20 类标签数据最值得利用的先验就在这里——标签顺序本身就是解剖顺序把顺序校验加进数据质量检查流程能直接定位到出错的那张切片。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价