☰
nnUNet数据预处理全解析:规划、重采样与归一化实战指南
2026/10/22 3:23:39 网站建设 项目流程

1. 从“跑通模型”到“读懂代码”,为什么先啃数据预处理

很多朋友第一次接触 nnUNet,第一反应都是“这玩意儿怎么这么绕”。网络结构能看懂,损失函数能看懂,一跑到数据预处理就头大:又是裁剪又是归一化,又是重采样又是增强,前处理流程比训练本身还长。我当初也是这样,在某个医学影像分割项目里第一次用 nnUNet,脚本跑通了、指标也还行,但全流程像黑盒,出了 bug 根本不知道是数据问题还是网络问题。后来花了两周把nnUNet_preprocessed目录下的中间产物翻了个底朝天,终于把预处理的每一行逻辑都啃明白了,从那以后,这套框架对我就不再是个魔法黑盒,而是一套可以按需拆解、自定义改造的流水线。

这篇文章就专门聊聊 nnUNet 数据预处理部分——它到底在做什么、为什么这么做、每步的细节长什么样,以及你实际跑代码时最容易在哪几个环节踩坑。文章不会贴完整源码,但会把关键函数名、数据流向、重要参数掰开揉碎讲清楚。适合两类人看:一是想深入读源码、后续准备魔改 nnUNet 的进阶玩家;二是训练遇到玄学问题、想排查是不是预处理引入的 bug 的实践派。读完你能做到一件事:拿到任意 3D 医学影像分割任务,能独立判断 nnUNet 的预处理策略是否合理、中间产物该怎么检查、出问题时先从哪个环节下手。

2. 预处理到底在处理什么:一个完整的“数据进,张量出”流水线

要说清 nnUNet 的预处理,得先建立一张整体地图。它做的工作绝不只是把 NIfTI 读进来、转成 numpy 数组那么简单,而是把一份“非结构化”的医学影像数据,变成一套“标准化”的训练张量。全过程从nnUNet_plan_and_preprocess入口开始,大体分两阶段。

第一阶段是nnUNet_plan_and_preprocess里的规划(plan)过程。它遍历所有训练样本,统计每个模态的强度分布、每个样本的尺寸分布、体素间距分布,结合显存约束自动确定“目标中位数尺寸”“重采样策略”“归一化策略”。这一步的产物是nnUNetPlans相关的 json 文件,里面包含了几乎所有的预处理参数。第二阶段才是真正执行预处理,产出nnUNet_preprocessed目录下的数据文件。整个框架先做整套计算方案,再按方案批量处理数据,就算你换了数据集,也不需要手工改任何预处理参数——这恰恰是 nnUNet 自适应能力的关键。

为什么这么设计?因为医学影像数据太不规矩了。有的是 CT,有的是 MRI;CT 是绝对值上的物理量,MRI 是相对强度没有统一量纲;同一数据集里可能既有 1mm 各向同性采样的高分辨率图像,也有 5mm 层厚的粗采样图像;有的样本是 512×512×300,有的是 128×128×64。面对这种情况,靠手工配置预处理参数既费劲又容易出错,nnUNet 干脆自己先“观察”一遍数据,再制定一套专属流程,这是它与其他分割框架最本质的区别之一。

为了让理解有个抓手,我把整个预处理的流水线按顺序列一下,后面逐个拆:

  1. 读取原始 NIfTI 与分割标签,提取元数据(方向、间距、形状);
  2. 统计强度分布,决定是否需要归一化,以及用哪种归一化;
  3. 确定目标体素间距与目标图像尺寸(即“规划”核心);
  4. 按目标间距执行重采样(resample);
  5. 执行裁剪/填充,对齐到目标尺寸;
  6. 对每个模态执行归一化(标准化);
  7. 如果启用数据增强,后续在训练时再做在线增强,但预处理阶段不做;
  8. 保存为.npy/.npz格式的前处理结果,供训练直接加载。

值得强调的是,第 7 步很多人容易混淆。nnUNet 的数据增强(旋转、缩放、弹性形变等)是在训练时在线做的,不在离线预处理阶段做。离线阶段只做“不变量”类的处理,让数据在进入网络之前具备规则形状和统一强度范围。明白这一点,你在排查问题时就不会把前处理和在线增强搞混。

2.1 版本与代码入口速览

如果你是第一次看 nnUNet 源码,建议不用管历史版本,直接从 v2 分支(或最新稳定版)入手。核心代码路径是nnunetv2.preprocessing,里面有一个preprocessors模块,定义了DefaultPreprocessor类;以及nnunetv2.planning,里面是规划策略相关代码。

实际运行时,你执行的命令:

nnUNet_plan_and_preprocess -d DATASET_ID -pl nns -t 1 -o OUTPUT_FOLDER

会先后调用规划器和预处理器。在代码层面,核心函数多集中在:

  • nnunetv2.planning.plan_and_preprocess:总入口
  • nnunetv2.planning.plan_training:生成 plan json
  • nnunetv2.preprocessing.preprocessors.default_preprocessor:执行重采样、归一化、保存
  • nnunetv2.preprocessing.resampling.default_resampling:负责所有重采样操作

建议读代码时按这个路径走,别一头扎进image_io这类底层读写模块,容易迷失。

3. 规划(Plan):nnUNet 的自适应灵魂

你没看错,提到预处理必须先从“规划”说起,因为 nnUNet 的所有预处理参数都不是写死的,而是 plan 出来的。plan 阶段的核心产出物是一份字典结构,包含:

  • dataset_properties:数据集的统计属性,包括各模态的强度百分位数、形状范围、间距范围、类别数、类别名称、前景体素占比等;
  • plans_per_stage:每个训练阶段的配置,包括目标尺寸、网络结构配置、batch size 等;
  • preprocessor_config:归一化方案、重采样方案等。

这些内容最终序列化为 JSON 保存在nnUNet_preprocessed/DATASET_ID/nnUNetPlans.json里。你每次训练都会读这份文件,所以如果训练效果不佳,首先应该打开它看看里面的关键数值是否合理。

3.1 规划阶段的核心统计量

在规划阶段,nnUNet 会遍历每一条训练数据,计算以下指标:

  • 每个模态的 intensity 分布的 0.5 和 99.5 百分位数(用于 CT 和 MRI 的归一化参考)
  • 所有模态的 shape(体素尺寸)
  • spacing(体素间距,单位 mm)
  • 类别列表与前景体素占比
  • 是否为 CT 模态的推断(通过是否有窗位窗宽元数据等)

这些指标用于确定三件事:目标图像尺寸、重采样策略、归一化方式。

为什么用百分位数而不是直接用 min/max?因为医学影像常存在极端异常值,比如 CT 中的金属伪影、MRI 中的噪声点,直接用 min/max 会让归一化结果被少数极端体素带偏。0.5 和 99.5 百分位就像一把剪子,把极端值剪掉后,中间 99% 的体素才主导强度映射。

3.2 目标尺寸是怎么算出来的

这一步是 nnUNet 的核心竞争力之一。它观察数据集中的所有样本,收集每个样本在“原始 spacing 下的形状”,然后取所有样本 shape 的中位数(严格讲是各维度的中位数),再结合显存限制、默认网络配置,推断一个“目标”形状。

举个例子,数据里有 100 个样本,大部分样本的尺寸都是 256×256×128,个别样本是 128×128×64。那目标尺寸大概率会选中位数附近,比如 256×256×128,而不会选最大值或最小值。选最大值会浪费显存、降低 batch size,选最小值会丢失细节信息。中位数既能代表主流数据形态,又不至于过于极端。这就是为什么 nnUNet 在 heterogeneous 数据集上效果依然稳定的原因。

同时,目标 spacing 的选择也和网络结构深度绑定。nnUNet 会根据你 GPU 显存、网络的最大下采样次数、每个 stage 的特征图数量等,综合算出各 stage 的目标尺寸。我见过不少初学者想直接改大目标尺寸,结果训练时显存溢出,然后误以为是网络设计问题。实际上,plan 阶段就已经考虑过这个约束,改尺寸要连带着把网络深度、batch size 一起改才行。

3.3 归一化方案是怎么定的

nnUNet 主要分两类归一化:CT和non-CT(其实就是 MRI 类)。判定方式不复杂:读 DICOM 转 NIfTI 时是否带有窗位窗宽元数据,或者用户是否在 dataset json 中显式声明模态为 CT。

  • CT:使用全局的“窗宽窗位式”映射。具体做法是统计所有样本所有体素在 0.5~99.5 百分位,作为全局强度范围,再把该范围映射到 [0, 1],高于上限的截断,低于下限的截断。
  • MRI 或其他:使用每个样本的 z-score 归一化。即对每个样本,先减去自身均值,再除以自身标准差。这种做法消除了 MRI 扫描之间因设备参数不同带来的整体强度偏移。

你可能注意到,nnUNet 的 CT 归一化是“全局参数”,MRI 归一化是“样本级参数”,这背后是两种成像原理的根本差异。CT 值的物理意义是线性衰减系数,同一物质的 CT 值不同设备间基本一致,因此适合全局映射;MRI 的强度本身是相对量,受线圈、TR、TE 影响,无物理单位可言,因此只能做样本内的标准化。不了解这点,你可能会想当然地给 MRI 也套用全局百分位归一化,导致不同扫描间对比度不一致,模型泛化性下降。我在实际项目里曾经手滑给 MRI 配成 CT 预处理,结果验证集 Dice 直接掉了七八个点,查了半天才发现问题在预处理配置。

3.4 如何解读 plan json

当预处理跑完后,nnUNetPlans.json里有很多字段,我挑几个关键的说:

字段路径含义检查重点
dataset_properties.spacings所有样本 spacing 分布是否同质?如果差异巨大,重采样策略要谨慎
dataset_properties.shapes所有样本形状分布是否包含极端小样本?会影响中位数选择
dataset_properties.intensity_statistics各模态强度统计是否符合该模态已知范围
plans_per_stage[0].patch_size网络输入的 patch 尺寸一般和目标 shape 相关,显存溢出先看这里
plans_per_stage[0].batch_size每步样本数显存和收敛速度的平衡点
preprocessor_config.normalization_schemes各模态归一化方式CT 应该是CT,MRI 应该是nonCT

读 json 的时候,重点看normalization_schemes是不是符合预期,以及patch_size是不是你想要的。如果发现 plan 结果和数据集实际情况明显不符,多半是数据本身有脏样本(如全零图像、异常大间距),需要先清洗数据再重跑规划。

4. 核心环节拆解:重采样、裁剪与归一化的实战细节

规划只是建了个模板,真正干活的是DefaultPreprocessor。它逐样本处理,把原始 NIfTI 变成最终训练用的数组。这一章是全文的重点,建议对着源码一起看。

4.1 重采样:从“物理世界”到“规则网格”

重采样的目标是把不同 spacing 的图像统一到目标 spacing 上。例如原始图像是 512×512×30,spacing 为 (0.5, 0.5, 5.0),目标 spacing 是 (1.0, 1.0, 1.0),重采样后形状大约会变成 256×256×150。这个变换是整个预处理中最容易出问题的环节。

nnUNet 对图像和标签默认都使用三阶样条插值(sitkBSpline),但标签分割掩膜不能使用这种插值,因为分割标签是类别序号,插值会生成小数,破坏类别语义。因此 nnUNet 对 label 默认使用最近邻插值(sitkNearestNeighbor)。这是一个极其重要且容易忽略的细节。

为什么 label 不能用线性插值?举个简单的二分类例子:标签里 1 代表肝脏,0 代表背景。如果使用线性插值,边界处可能出现 0.5 这样的值,之后做argmax或阈值,会凭空产生一坨奇怪的边界伪影;更重要的是,多类别标签之间如果使用样条插值,类别之间的顺序关系完全没有物理意义,插值结果就是噪声。最近邻插值的本质是“把原位置的类别直接搬过去”,保证标签在重采样后依然只含有原始类别 ID。

实操时,你可以这样检查重采样是否有问题:取一个样本的原始 label 和预处理后的 label,对比其唯一值集合是否一致。如果多了奇怪的数字,说明插值方式错了;如果少了某个类别,说明采样过程中类别丢失,很可能是凑整导致面积小于一个体素的细小结构被抹掉了。这类问题在小目标分割任务中尤其常见,要结合自己的类别频率去看。

另外,nnUNet 里有个强制的“裁剪前景”步骤:通过 label 的连通区域(或者前景 mask)计算出包围盒,然后只保留包围盒附近的一部分背景区域,作为重采样的范围。这一步既减少计算量,也避免背景区域过大导致有效前景占比过低。不过要注意,这不代表 nnUNet 将所有样本都裁剪成紧凑的包围盒,而是在包围盒周围额外保留一定边距,具体边距大小由 plan 阶段根据经验自动设置。如果你的目标器官很小,想保留更多上下文信息,可以手动增大这个边距参数,代价是显存占用增加。

4.2 处理“不同模态配准”问题

多模态数据(比如 T1 和 T2 加权 MRI、CT 和 PET)在预处理时要保证各模态在相同坐标位置、相同尺寸上对齐。nnUNet 的预处理是基于 NIfTI 的几何信息(affine 矩阵)进行坐标重采样的,也就是说,无论原始各模态 spacing 差多少,最终都会在统一的世界坐标系下重采样到同一网格。这一步看起来简单,实际如果原始多模态图像之间没有配准,单靠重采样是无法对齐的。nnUNet 假设输入的模态已经互相配准,如果你自己制作数据集时没有配准,预处理过程会“看起来正常”,但训练出来的模型必然在模态融合上出问题。

一个快速验证多模态是否对齐的小技巧:在预处理前,把两个模态叠加显示,观察解剖结构边界是否重合。别等训练跑完再后悔,这个问题在预处理阶段就要排除。

4.3 裁剪与填充:强制形状一致性

重采样到目标 spacing 后,不同样本的最终形状可能仍有细微差异。比如目标 spacing 是 1×1×1,经过重采样后,一个样本是 256×256×128,另一个是 254×258×132。为了能放进同一个 batch,nnUNet 会把它们统一到“中位数形状”(或 plan 中的目标形状)附近。

统一方式有两种:裁剪或填充。若样本某维大于目标尺寸,则中心裁剪;若小于目标尺寸,则零填充(或常量填充,取决于数据模态)。中心裁剪比随机裁剪更稳定,因为它保留了解剖结构的中心区域,而绝大多数器官都偏向中心;对脑部、腹部等结构尤其有效。填充值的选择在 CT 数据上一般用 0,因为 CT 归一化后 0 通常接近空气背景;MRI 上如果用 z-score,填充 0 其实等价于把该区域当成均值强度的背景,神经网络的注意力会被弱化,这是可以接受的。有个坑是如果某个样本的解剖结构不在中心,中心裁剪时可能把器官切掉。这也是为什么 nnUNet 在裁剪前先做前景包围盒裁剪的原因——它先把前景区域包进来,再做尺寸统一。理解这一层逻辑,你就能解释很多“为什么我的器官在边界上训练效果差”的问题。

4.4 强度归一化的实际代码观察点

如果你想在代码层面确认归一化细节,看preprocessor_config和具体实现函数。CT 归一化代码大致思路是:

# 伪代码示意 whole_ct = collect_all_ct_intensities() lower, upper = np.percentile(whole_ct, [0.5, 99.5]) data = np.clip(data, lower, upper) data = (data - lower) / (upper - lower) data = data.astype(np.float32)

MRI 的 z-score 则类似:

mean = data.mean() std = data.std() data = (data - mean) / max(std, 1e-8)

注意一个细节:CT 归一化中,如果某个样本的强度范围在整体范围之外,clip后它可能变成大量 0 或 1 的饱和区域,这是预期的。而 MRI z-score 中,如果样本背景占比极大,导致均值和标准差被背景主导,前景对比度会被压缩。多数情况下 z-score 能工作,是因为 nnUNet 在 z-score 前一般也会把背景区域裁剪到合理范围,保证前景在统计中占据一定比例。这个处理顺序很重要:先裁剪,后归一化。如果你自己写预处理流程,别颠倒了,否则归一化统计量会被大片空气或背景干扰。

4.5 数据类型与存储格式

预处理完成的图像数据会以.npy格式保存,对应的标签一般以.npz格式保存(或分开保存为独立 npy)。图像数据使用float32,标签使用整数类型(如int8、uint8),因为类别数量通常很少。这里有一个隐性收益:float32 的医学影像数据在 GPU 上可以直接被网络读取,无需额外转换,训练效率更高。但 float32 也意味着文件体积大,如果你的预处理目录磁盘紧张,要注意预留足够的空间。一个 512×512×150 的 float32 数组大约 150MB,几百个样本就要几十 GB,这在大型数据集中是常事。

存储格式还与后续“数据加载”相关。nnUNet 的 dataloader 会直接从.npy文件通过内存映射读入,而不会像传统方案那样每次从 NIfTI 重采样,这节省了训练前的重复计算开销。所以一旦预处理完成,训练过程几乎与原始 NIfTI 无关,所有折腾都在前面一次性做完。

5. 实操过程:用一个小数据集完整走一遍预处理

我说再多理论,不如带你自己动手走一遍。下面以一个虚构的小型 CT 分割数据集为例,演示从原始文件到预处理产物的全流程。数据集假设有 8 个训练样本和 2 个验证样本,单模态 CT,标签类别的值是 0(背景)、1(器官A)、2(器官B)。

5.1 数据集原始结构

nnUNet v2 要求的数据集目录结构如下:

Dataset123_OrganSeg/ ├── imagesTr/ │ ├── case_0000_0000.nii.gz │ ├── case_0001_0000.nii.gz │ └── ... ├── labelsTr/ │ ├── case_0000.nii.gz │ ├── case_0001.nii.gz │ └── ... ├── imagesTs/ │ ├── case_0002_0000.nii.gz │ └── ... └── dataset.json

注意_0000后缀表示这是第一个模态(如果多模态则是_0000、_0001……)。dataset.json里定义了模态名称、标签类别等信息,最少需要包含:

{ "modality": { "0": "CT" }, "labels": { "background": 0, "organA": 1, "organB": 2 }, "numTraining": 8, "file_ending": ".nii.gz" }

这一步很多人会栽在标签类别 ID 的连续性和命名上。nnUNet 虽然允许类别 ID 不连续,但玩具级数据集最好保持 0~N 连续,否则某些后处理或评估阶段会出现意想不到的编号 mismatch。

5.2 执行预处理命令

环境变量设置完毕后,运行:

nnUNet_plan_and_preprocess -d 123 -pl nns -t 1 -o nnUNet_preprocessed

其中-t 1指定线程数,-o指定输出根目录。命令执行后,终端会打印每个阶段的进度。如果数据集没问题,几分钟后就完成了。

整个过程会经历这些关键日志:

  • “Running planning…” 阶段:分析每个样本的形状与间距
  • “Running preprocessing…” 阶段:一个样本一个样本地处理
  • “Saving plans…” 阶段:写出 plan json

跑完之后,nnUNet_preprocessed/Dataset123_OrganSeg/下会出现:

  • nnUNetPlans.json
  • dataset.json(复制过来的)
  • splits_final.json(自动划分的训练验证集)
  • 每个 case 对应的.npy与.npz文件

5.3 检查关键中间产物

我强烈建议你养成检查中间产物的习惯,别直接跳去训练。首先看nnUNetPlans.json,重点关注:

"preprocessor_config": { "normalization_schemes": ["CT"], "resampling_schemes": ["ct"], ... }

如果这里新出现"nonCT"或"simple",说明你的数据没被识别为 CT,需要查dataset.json里的 modality 设置。

然后挑一个样本,用代码快速检查数据范围:

import numpy as np data = np.load('nnUNet_preprocessed/Dataset123_OrganSeg/case_0000.npy') label = np.load('nnUNet_preprocessed/Dataset123_OrganSeg/case_0000.npz')['data'] print(data.shape, data.dtype, data.min(), data.max()) print(np.unique(label), label.shape)

正常情况下:

  • 图像 shape 和 plan 中的patch_size不会差太远(如果差距太大,说明重采样或裁剪环节有问题)
  • 数据范围应该在 0~1 附近(CT 归一化后)
  • 标签唯一值应该还是 {0,1,2}

如果发现标签出现了 0.0、1.0 之外的如 0.5、1.5 等数值,极大可能是 label 使用了非线性插值,这种问题一般在默认配置下不会出现,但如果你改了源码或自定义了预处理器,就要重点查。

5.4 一个小型批处理的完整代码参考

如果你想跳过命令行,直接在 Python 里调用预处理功能(比如你想批量处理大量数据集时),可以参考以下最小代码:

from nnunetv2.planning.plan_and_preprocess import experiment_planner, preprocess # 假设所有配置已在 dataset.json 中 dataset_id = 123 experiment_planner(dataset_id, output_folder='nnUNet_preprocessed', plans_identifier='nnUNetPlans') preprocess(dataset_id, output_folder='nnUNet_preprocessed', plans_identifier='nnUNetPlans', num_processes=4)

这段代码和命令行完成的事情实质相同。但你直接调用函数时,对 plan 和 preprocess 两个阶段的独立执行更清晰,适合你后续自定义某一步时插入调试代码。

5.5 自定义预处理参数的推荐做法

nnUNet 的“全自动”不代表“不可改”。如果你确实需要干预,官方支持的方式是在 plan json 生成后、训练开始前,手动修改 json 中的参数。比如想把 patch size 调大,直接改plans_per_stage[0].patch_size,然后训练时会自动读取新值。注意,如果修改了 patch size,显存占用会变化,batch size 要随之调整。

我自己的习惯是:先跑一遍默认 plan,看一眼 json,再综合显存和目标结构大小决定要不要调整。这不等于否定 nnUNet 的规划能力,而是因为实际项目往往有自己的特殊性,比如目标器官特别大、或者数据具有强各向异性,默认的中位数策略未必最优。在简单数据集上完全默认是可以的,但遇到复杂场景,花几分钟改一下参数,收益往往很明显。

6. 常见问题与排查技巧实录

这一部分全是实战中真实踩过的坑,我按出现频率从高到低整理。

6.1 预处理后显示 “ERROR: No foreground in labels”

这个报错的意思是,某些样本的标签全是背景,没有前景体素。出现原因通常有三种:一是标签文件本身就是空的,可能转换时漏了;二是标签文件与图像文件的坐标方向不一致,导致标签全部落在图像范围之外;三是标签的类别 ID 全为 0,而 dataset.json 又把 0 当成前景(这类情况较少,因为 nnUNet 约定 0 是背景)。

排查方式:用 ITK-SNAP 或 SimpleITK 加载该样本的 label,观察是否有前景。如果肉眼可见有前景,但仍报错,多半是坐标系统问题。可以用 SimpleITK 打印图像和标签的 origin、direction、spacing,检查两者是否一致。多模态和标签之间如果坐标参考不一致,重采样时前景就找不到位置。处理方式是用sitk.Resample把标签重采样到图像坐标空间,再做一次配准检查。

6.2 所有样本最终形状都一样,但某个器官却丢失了

这多半是中心裁剪导致的边界裁剪问题。虽然 nnUNet 有前景包围盒保护,但如果某个样本的器官特别靠近图像边缘,而包围盒计算时使用的阈值或形态学膨胀不足,裁剪后器官的一部分被切掉。另一个可能原因是该样本是极端异常形状,中位数目标尺寸对它来说太小,导致整个器官被压缩或裁切。

解决办法:建议先用nnUNet_plot_overlay或者自己写个可视化脚本,将原始 label 和预处理后的 label 叠加在原图上,逐层滑动检查。如果确认是边界裁剪,可以在 plan json 中增大前景裁剪边距(源码中有对应参数),或者把目标尺寸调大以保留更多边缘信息。但我必须提醒一句:调大目标尺寸会连带增加显存压力,需要平衡。

6.3 训练出来的模型对某个类别完全失效

这类问题往往和预处理阶段标签类别不均衡有关。虽然 nnUNet 默认使用带权损失函数来缓解类别不均衡,但如果某个极小类在“重采样+裁剪”过程中被过度压缩,比如一个直径只有 3 体素的血管结构,在目标 spacing 较大时可能被直接抹成 0 个前景体素。还有一种可能是该类别没有出现在某个样本中,导致该样本的 label 漏掉该类,这会让模型在训练时根本没机会学习该类的特征。

我的建议:预处理完成后,统计每个类别的体素总数,看有没有“几乎为零”的类别。如果存在,要么提高该类别的采样权重,要么调整重采样参数,让目标 spacing 更小以保留细节。这类问题越早发现越好,等训练跑完再回头查数据,时间成本太高。

6.4 NIfTI 方向不一致导致预处理后的图像被翻转/旋转

医学影像的 NIfTI 文件里,direction 矩阵可以各不相同。nnUNet 使用的 SimpleITK 重采样基于世界坐标,按理说可以处理不同方向,但如果你在数据格式转换阶段不小心丢了方向信息(比如直接把 numpy 数组转成 NIfTI 而没设置方向),预处理就会在错误的空间关系下重采样。

排查技巧:预处理后,用切片工具检查轴位、冠状位、矢状位的解剖结构是否正常。一个简单粗暴的方法是找一个样本,把原始图像用 ITK-SNAP 打开,再打开预处理后的 npy 重建的 NIfTI,对比同一解剖层面的位置。如果方向不一致,多半需要回到数据转换阶段,确保每个 NIfTI 头文件的 direction 和 spacing 正确。这个坑在从公开数据集下载并私自重转格式时特别常见。

6.5 预处理极慢,怎么加速

预处理速度主要卡在重采样和 I/O。如果样本数量大,建议多线程并行处理;如果单个样本非常大,可以先把前景包围盒算出来再重采样,nnUNet 默认也是这么做的。但有时你重写了自定义预处理,忽略了先裁包围盒这一步,速度会慢一个数量级。我见过有人直接对整个 512×512×500 的体积做三阶样条重采样,一次要好几分钟,而加上包围盒裁剪后,只需要处理 200×200×200 的大小,速度提升显著。

另一个加速技巧是把中间结果缓存为内存映射格式(.npy 本身就是这样),避免反复压解 NIfTI 的开销。如果你多次跑同一套预处理,可以直接复用前面生成的缓存,不必每次从头跑。

7. 个人建议与一个可以立马上手的小技巧

这篇文章写了这么多,核心观点只有一个:nnUNet 的预处理不是“无脑的全部自动”,而是“可解释、可检查、可干预”的流程。你只有亲手打开过那个nnUNetPlans.json,亲眼看一两个样本的.npy数据,真正踩过一两次坑,才算是把预处理部分学会。

最后分享一个我每次处理新数据集都会做的小事:预处理完成后,写一个 10 行左右的可视化脚本,随机选 3~5 个样本,把原始图像、预处理后的图像、预处理后的标签放在一起,用最大密度投影或单层切片扫一遍。这比看任何日志都管用。哪怕只是花五分钟,也能拦下大部分“数据方向错了”“标签跑位了”“前景被裁没了”的问题。很多跑训练时出现的玄学低分,根子都在数据预处理上,这个前哨步骤能帮你省下的,绝不仅仅是一次重跑的时间。

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

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

立即咨询