简介:这是一份基于马萨诸塞道路遥感数据集整理而成的nnUNet训练与测试数据资源,面向从事遥感图像分析、道路提取以及深度学习语义分割的研究者和开发者。原始数据包含高分辨率航空影像与精确道路标注,并已按nnUNet标准格式进行划分和命名,可直接用于模型训练、验证与测试。资源共59个文件,含58张PNG格式图像与1个dataset.json配置,压缩包整体大小142.02MB。文件夹内imagesTr、labelsTr、imagesTs、labelsTs分别对应训练图像、训练标签、测试图像和测试标签,图像文件统一采用四位数编号及通道后缀,便于框架自动识别与加载。目前已有1772人学习使用。这份数据集省去了从影像预处理、标签格式转换到目录结构配置的大量工作,能帮助快速启动道路提取、遥感分割等实验,也适合作为学术论文中的公开基准数据集或算法效果对比数据。
1. nnunet训练测试数据集:为什么推荐用一套命令跑完
医学图像分割里,nnUNet 几乎成了“开箱即用”的代名词。拿到的数据既不是分类任务的 CSV,也不是检测任务的框标注,而是一堆 nii.gz 三维图像,这时候最容易踩的坑不是网络结构,而是“数据集怎么摆、预处理怎么跑、训练完怎么测”。这篇笔记就围绕 nnunet 训练测试数据集这件事,把从原始图像到分割结果的完整路径拆开讲:目录结构、预处理、五折训练、推理、评估,以及我实际跑项目时翻过车的几个地方。适合手里已经有一批标注数据、想用 nnUNet 快速出基线结果的读者,也适合被“找不到数据”“预测全黑”这类问题卡住的人。
2. 拆解数据集布局与预处理:从乱糟糟的原始图像到可训练的 nnUNet
nnUNet 对数据布局的要求极度严格,严格到第一次用的人会觉得“这也太矫情了”。但正是这种严格换来了后面的全自动配置。这一章先把目录、命名、标签和 dataset.json 讲透,再给出预处理命令。
2.1 目录结构与命名规则:nnUNet 的第一道门槛
nnUNetv2 的数据目录逻辑上分为三块:原始数据、预处理结果、训练结果。原始数据目录必须长成下面这样:
nnUNet_raw/ └── Dataset001_MyData ├── imagesTr │ ├── case_0000_0000.nii.gz │ └── case_0001_0000.nii.gz ├── imagesTs │ ├── test_0000_0000.nii.gz │ └── test_0001_0000.nii.gz ├── labelsTr │ ├── case_0000.nii.gz │ └── case_0001.nii.gz └── dataset.json这里最容易忽略的是命名里的“_0000”。imagesTr 里每个文件必须带_0000后缀,表示第 0 个模态;labelsTr 里的文件不能带这个后缀,但文件名主干必须和 imagesTr 一一对应。比如case_0000_0000.nii.gz对应case_0000.nii.gz。如果你的数据是多模态的,比如 T1 和 T2 两个序列,那就要写成case_0000_0000.nii.gz和case_0000_0001.nii.gz,最后一个数字表示模态序号。
Dataset001_MyData这个目录名也要注意:Dataset后必须跟三位数字 ID,从 001 开始,后面接任意名称。后面所有命令里用到的-d 1指的就是这里的 001。imagesTs 不是必须的,如果只是训练和交叉验证,可以暂时不建;但如果你要测试,就必须把测试图像按同样的_0000规则放进去。
我自己的习惯是写一个检查脚本,先确认 imagesTr 和 labelsTr 一一对应,再开始预处理,省得后面报“数据不匹配”才回头找问题:
import os images_dir = "nnUNet_raw/Dataset001_MyData/imagesTr" labels_dir = "nnUNet_raw/Dataset001_MyData/labelsTr" image_names = sorted(os.listdir(images_dir)) label_names = sorted(os.listdir(labels_dir)) # 去掉 imagesTr 里的 "_{模态号}" 后缀,再和 labelsTr 对比 image_stems = [f.replace("_0000.nii.gz", ".nii.gz") for f in image_names if f.endswith(".nii.gz")] assert image_stems == label_names, ( f"数量不一致: imagesTr {len(image_stems)} 个, labelsTr {len(label_names)} 个" ) print("imagesTr 和 labelsTr 配对正确")这段代码的核心逻辑是把 imagesTr 里的_0000尾巴去掉,然后和 labelsTr 做严格比对。实际项目里经常出现多了一张图、少了一个标注的情况,这种问题在预处理阶段才会炸出来,提前检查能省掉一次莫名其妙的报错。
2.2 dataset.json 和标签类别:最容易写错的两处
dataset.json 是整个数据集的身份证明。最小可用的内容如下:
{ "channel_names": { "0": "CT" }, "labels": { "background": 0, "liver": 1, "tumor": 2 }, "numTraining": 100, "file_ending": ".nii.gz" }逐个字段解释。channel_names里的 key 是模态序号,value 是你对这个模态的描述,比如 CT、MRI_T1、MRI_T2;value 本身不影响训练,但建议写清楚,方便查 plans.json 时能看懂。labels是最关键的部分:0 一定是 background,前景类别从 1 开始递增。这里的 key 是类别名,value 是标签图像里对应的像素值。numTraining是训练样本数量,可以写,也可以不写,nnUNet 会自动统计。file_ending必须是.nii.gz,如果你的数据是.nii或者别的格式,官网建议你转成.nii.gz,不要试图改这个字段去迁就数据。
标签的像素值处理和 YOLO 这类把标注转成 txt 的思路完全不同,nnUNet 使用的是原始像素级 mask。最常见的问题是标签图里除了 0、1、2 之外还有别的像素值,比如 255,或者把前景标成了 10、20。nnUNet 在训练时只会把 dataset.json 里声明过的 label 当作有效类别,遇到没声明过的像素值会直接忽略或导致 loss 计算异常。我的做法是在预处理前加一次标签重映射:
import SimpleITK as sitk import numpy as np import os input_dir = "raw_labels" output_dir = "labelsTr" os.makedirs(output_dir, exist_ok=True) for fname in os.listdir(input_dir): if not fname.endswith(".nii.gz"): continue img = sitk.ReadImage(os.path.join(input_dir, fname)) arr = sitk.GetArrayFromImage(img).astype(np.int16) # 重映射:把 255 和 200 这类杂散值统一到 1(前景),0 保持为背景 arr[arr == 255] = 1 arr[arr == 200] = 1 out_img = sitk.GetImageFromArray(arr) out_img.CopyInformation(img) # 保留 spacing/origin/direction sitk.WriteImage(out_img, os.path.join(output_dir, fname))注意最后那行CopyInformation,这是医学图像处理里最容易丢信息的操作。直接用sitk.GetImageFromArray(arr)写出来的文件会丢掉 spacing 和 direction,后面 nnUNet 做重采样时会出现错位。这种错位不会报错,但结果就是训练正常、预测结果空间位置不对。
2.3 跑 plan_and_preprocess:nnUNet 的自动配置到底做了什么
数据目录和 dataset.json 准备好之后,先设三个环境变量,再跑预处理:
export nnUNet_raw="/data/nnUNet/nnUNet_raw" export nnUNet_preprocessed="/data/nnUNet/nnUNet_preprocessed" export nnUNet_results="/data/nnUNet/nnUNet_results" nnUNetv2_plan_and_preprocess -d 1 -c 3d_fullres --verify_dataset_integrity三个环境变量分别指向原始数据、预处理输出、训练输出目录。这一步不做,后面所有命令都会报“nnUNet_raw is not set”之类的错。-d 1对应Dataset001_MyData;-c 3d_fullres表示只生成 3d_fullres 配置的预处理结果;--verify_dataset_integrity会检查图像和标签是否对齐、数据能否正常读取,我建议第一次跑必须带上。
预处理完成以后,在nnUNet_preprocessed/Dataset001_MyData/下会生成dataset_fingerprint.json和plans.json。你可以打开plans.json看几眼,里面的关键信息包括:每个类别的像素占比、中位数的 spacing、中位数的图像尺寸、以及 nnUNet 定的 target spacing 和 batch_size。这就是 nnUNet“自配置”的核心——它不靠人去调参,而是靠数据分布统计出来的一套方案。比如它发现你的数据 spacing 中位数是[0.5, 0.5, 3.0],层间距特别大,它可能会建议你用 2d 配置,或者在 3d 配置里做特殊处理。
有几个参数值得调整。-np 4可以开 4 个进程并行预处理,能显著加速,但因为预处理要读入整幅三维图像,内存占用也会跟着翻倍,小内存机器上反而会卡死。--verify_dataset_integrity在数据量大时比较慢,第二次跑可以去掉;不过我自己的习惯是每次换了新数据都留着,毕竟出错成本比等待成本高得多。
3. 训练:五折交叉验证与三个 U-Net 配置
预处理跑完,接下来进入真正的训练阶段。nnUNetv2 的训练命令和配置选择有固定的套路,这一章把命令参数、配置选型和资源估算讲清楚。
3.1 从 3d_fullres 开始:训练命令与显存控制
训练命令看起来非常简单:
export nnUNet_results="/data/nnUNet/nnUNet_results" CUDA_VISIBLE_DEVICES=0 nnUNetv2_train 1 3d_fullres 0这个命令的意思是:训练 Dataset001(-d 1隐含在命令参数的位置里),使用 3d_fullres 配置,跑第 0 折。这里nnUNetv2_train后面依次是数据集 ID、配置名、fold 编号。fold 0 表示把训练集切成五折,当前用其中 80% 训练、20% 验证;fold 可以取 0 到 4,也可以用all表示不切分、用全部数据训练。
训练过程中会看到类似Epoch 10/1000的输出,nnUNet 默认训练 1000 个 epoch,但实际远不需要跑满,它会根据验证集 loss 自动选最佳 checkpoint。我一般不会去改动这个默认值,因为 nnUNet 的 early stopping 策略已经足够可靠,手动调小反而容易欠拟合。
如果你是 16G 显存的卡,跑 3d_fullres 大概率会遇到 OOM。nnUNet 会自动根据显存推算 batch_size,但推算结果不一定保守。这时候有两条路:第一,改plans.json里的batch_size字段,从默认值往下调一半,重新训练;第二,在训练命令里限制数据加载线程数,因为有时 OOM 不是显存炸了,而是 CPU 预处理线程把内存吃满了。可以用:
export nnUNet_def_n_proc=4 CUDA_VISIBLE_DEVICES=0 nnUNetv2_train 1 3d_fullres 0nnUNet_def_n_proc控制数据加载进程数,默认会按 CPU 核心数开很多线程,在小数据集上线程开太多反而拖慢训练。
3.2 三个配置怎么选:3d_fullres、3d_lowres、2d 和 cascade
nnUNetv2 针对三维数据提供了几种配置,训练前必须想清楚用哪个。3d_fullres是全分辨率三维 U-Net,是绝大多数情况下的默认选择;3d_lowres是低分辨率版本,先把图像降采样再训练,适合图像尺寸特别大、目标器官也大的场景;2d是纯切片级训练,适合层间距特别大、层间信息不可靠的数据;3d_cascade_fullres是级联的第二阶段,先由 3d_lowres 产生粗分割,再把这个粗分割作为额外输入做精修。
我的选型经验是这样的:如果你的数据是常见的 CT 或 MRI,且目标器官在图像中占比不是极端小,直接用3d_fullres就跑出不错的结果了。只有当数据量极大、或者你的 GPU 实在跑不动全分辨率时,才考虑3d_lowres加3d_cascade_fullres的组合。2d 配置最容易被忽略,但遇到层间距 5mm 以上的数据时它往往比 3d 更稳,因为 3d 卷积在这种数据上会学到很多层间插值的假信息。
关于 fold 的理解,可以和目标检测里“训练集/验证集划分”对照着看。每条命令只能跑一个 fold,所以完整跑五折需要五条命令:
CUDA_VISIBLE_DEVICES=0 nnUNetv2_train 1 3d_fullres 0 CUDA_VISIBLE_DEVICES=0 nnUNetv2_train 1 3d_fullres 1 CUDA_VISIBLE_DEVICES=0 nnUNetv2_train 1 3d_fullres 2 CUDA_VISIBLE_DEVICES=0 nnUNetv2_train 1 3d_fullres 3 CUDA_VISIBLE_DEVICES=0 nnUNetv2_train 1 3d_fullres 4多卡机器可以同时开五条命令,每张卡跑一个 fold。
3.3 训练资源估算:别让训练时间变成翻车现场
训练资源是 nnunet 训练测试数据集这个方向里最容易被低估的部分。以常见的腹部 CT 分割任务为例,单折 3d_fullres 在 24G 显存、CPU 给足的情况下,通常要跑 12 到 24 小时;五折全跑完就是 3 到 5 天。如果你只有一张 8G 显卡,这个时间还要翻倍,这时候就得认真考虑是不是要放弃 3d_fullres。
几个实用控制手段:第一,前面说的nnUNet_def_n_proc,限制 CPU 线程能减少内存争抢。第二,在plans.json里调低batch_size,volume 不变的情况下显存占用会线性下降,但训练稳定性会略微变差。第三,只跑 3d_fullres 配置,不要默认把 3d_lowres 也一起训。很多新手以为 nnUNet 会自动训所有配置,实际上预处理时用-c 3d_fullres限制过了,训练时也只会跑你指定的那个配置,不会主动训 cascade。
这里还要提醒一个数据规模的问题。网上经常有人说 nnUNet “只用 50 例就能跑”,这个说法方向没错,但你要知道结果的含义。50 例的数据跑五折,每折只有 40 例训练、10 例验证,DSC 的波动会很大。我见过有人拿 30 例数据跑完五折,平均 DSC 0.85,但每一折的标准差接近 0.1,这个结果只能说明“方法可行”,还不能说明“模型稳定”。数据量越小,越要关注 5 折之间的一致性,而不是只看平均指标。
4. 测试与推理:把模型权重变成分割结果
训练完的权重是用来做推理的。nnUNet 的推理流程和训练一样高度命令行化,但有几个参数直接影响输出质量,值得单独展开。
4.1 单模型预测命令与输出说明
推理命令:
nnUNetv2_predict \ -i /data/nnUNet/nnUNet_raw/Dataset001_MyData/imagesTs \ -o /data/nnUNet/results/pred \ -d 1 \ -c 3d_fullres \ -f 0 1 2 3 4参数含义:-i是测试图像目录,里面的文件命名必须和训练时一致,带_0000后缀;-o是输出目录,不存在会自动创建;-d 1指定数据集;-c 3d_fullres指定配置;-f 0 1 2 3 4表示把五个 fold 的模型都加载并做预测,最后把五个 softmax 概率平均再取 argmax,这个操作本质是五折模型集成,比单折预测更稳。
输出目录里每个输入文件会对应一个同名的.nii.gz分割结果,像素值和 dataset.json 里定义的类别编号一致。注意 nnUNet 推理时会把输入图像自动重采样到训练时的 spacing,预测完再重采样回原始 spacing,所以输出的几何信息和输入图像是严格对齐的,不需要你自己做任何空间变换。
-step_size是另一个值得关注的参数,默认 0.5,表示滑窗推理时窗口之间有 50% 重叠。重叠越多,预测越平滑,但速度越慢。如果测试集比较大,可以调成 0.6 或 0.7 平衡一下;如果追求极致精度,可以调成 0.9,代价是推理时间可能翻倍。
4.2 五折平均与跨配置集成的区别
上面命令里的-f 0 1 2 3 4已经做了五折模型集成,这是 nnUNet 最推荐的推理方式。但如果你还想更进一步,把不同配置的模型结果也集成起来,比如把 3d_fullres 和 2d 的结果做融合,那就需要先保全概率,再做概率平均。做法是先在预测时加--save_probabilities,会额外输出.npz文件;然后使用 nnUNet 自带的概率平均命令做像素级平均,再转成最终标签。具体命令名不同小版本略有差别,建议先跑nnUNetv2 --help确认。
跨配置集成能带来稳定的收益,但收益通常在 1 到 3 个百分点的 DSC 之间,不是质变。我自己的习惯是:先只跑 3d_fullres 五折集成,如果发现某个类别特别难分,再针对性加 2d 或 cascade 配置的集成,而不是一上来就把所有配置都训一遍。
4.3 评估:DSC、NSD 以及怎么看预测结果
如果测试集有对应的金标准标签,用 nnUNet 自带的评估命令可以一次性算出多个指标:
nnUNetv2_evaluate_folder \ /data/nnUNet/results/pred \ /data/nnUNet/gt_labels \ -l 1 2-l 1 2表示只评估类别 1 和类别 2。输出会包含每个类别的 DSC、NSD(表面距离一致性)等指标。DSC 是日常最常看的,但 DSC 对大目标友好,对小目标或细长结构会“虚高”。比如一个器官占图像体积 20%,就算预测漏掉一半,DSC 也可能还有 0.7 以上。所以细长结构、小病灶一定要额外看 NSD,或者干脆把预测结果叠加在原图上做目检。
目检这一步别省。我一般会把预测 mask 和原图用 SimpleITK 读出来,用 matplotlib 叠一张切片图扫一眼,再去看指标。指标只能告诉你“好不好”,目检能告诉你“错在哪”。
5. 避坑:nnunet 训练测试数据集最常见的五个翻车现场
这一章写的是我实际跑 nnunet 训练测试数据集时踩过的坑,每条都是“现象→原因→解决”的完整记录。
5.1 预处理报错:明明有数据,却提示找不到文件
现象:执行nnUNetv2_plan_and_preprocess时,日志里出现No cases were found或imagesTr is empty,但用文件管理器看目录里明明有数据。
原因:最常见的是文件后缀不统一。nnUNet 只认.nii.gz,如果你的数据是从某些标注工具导出的.nii文件,它不会识别;还有一种情况是文件名里混了大写,比如Case_0000_0000.nii.gz,nnUNet 对大小写敏感,目录里明明有文件,但匹配时找不到。
解决:写一个小脚本统一改名。把.nii转成.nii.gz,把所有文件名转成小写,确认_0000后缀存在。改完以后重跑--verify_dataset_integrity,过了再继续。
5.2 训练正常但验证集 DSC 一直为 0
现象:训练 loss 在下降,但验证集每个类别的 DSC 始终是 0,或者某个类别的预测结果完全不存在。
原因:标签图像里出现 dataset.json 没有声明的像素值。最常见的是把前景标成了 255,但 dataset.json 里类别值是 1、2,这样 nnUNet 在训练时根本看不到这个类别的监督信号,模型学会的只有背景。
解决:预处理前做标签重映射,把 255 这类杂散像素值统一到合法类别里。重映射以后重新跑预处理,不要直接接着训练,因为plans.json里的类别统计已经错了。
5.3 OOM:显存不够,训练中途崩掉
现象:训练跑到第几个 epoch 时突然报CUDA out of memory,或者训练一开始加载模型就崩了。
原因:3d_fullres 全分辨率训练对显存的需求和数据体素量成正比。nnUNet 虽然会自动估计 batch_size,但估计逻辑偏向保守选大,遇到 spacing 特别不规则的数据时估计结果会失准。
解决:打开nnUNet_preprocessed/Dataset001_MyData/plans.json,找到batch_size字段,改成原来的一半,保存后重新训练。如果还是崩,就换 2d 配置或者 3d_lowres。同时检查是不是 CPU 线程开太多把系统内存吃满了,用nnUNet_def_n_proc=4限制一下。
5.4 训练时间过长:一周没跑完一折
现象:训练日志显示 epoch 推进极慢,一个 epoch 要好几分钟,照这个速度一折要跑十天。
原因:很可能你同时训了多个配置;也可能预处理时把-np开得过大,导致预处理阶段花了好几天;还有一种情况是数据量本身很大,比如几百例 512×512×300 的高分辨率 CT,全分辨率训练本来就慢。
解决:先只训 3d_fullres 一个配置;预处理阶段用-np 2或-np 4控制并行数;如果数据实在太大,把plans.json里的 target spacing 调大一点,比如从[0.5, 0.5, 0.5]改成[1.0, 1.0, 1.0],体素数量直接降到原来的八分之一,训练速度会快很多,代价是细节丢失一点。
5.5 预测结果全黑或全是背景
现象:推理完成,输出的 mask 里只有一个类别,或者整张 mask 全黑,DSC 惨不忍睹。
原因:这个坑至少有三种来源。一是测试图像的像素值范围不对,比如训练时是 CT 值范围,测试时输入的是归一化到 0 到 255 的 PNG 转的 nii.gz,分布变了模型直接失效;二是标签映射反了,预测时把背景当成前景;三是推理时忘了加-f 0 1 2 3 4,单折模型本身效果就差。
解决:先用--save_probabilities保存 npz,检查每个类别的概率分布是否正常,如果某个类别的概率始终接近 0,说明模型没学到这个类别的特征,问题大概率在训练数据或预处理阶段,而不是推理命令。然后检查测试图像和训练图像的 intensity 分布是否一致,最简单的方式是读一组训练图像和一组测试图像,对比灰度的 min、max、mean。
6. 把 nnunet 训练测试数据集用得更稳:增量训练、自定义配置和结果核对
训练一轮拿到基线结果以后,通常还有两个诉求:一是数据多了怎么办,二是结果怎么进一步提升和验证。
增量训练是最常见的需求。场景是这样的:你先用 50 例数据训了一版模型,两个月后又标了 30 例新数据,这时候不需要从零重新训练。常见做法是把新数据合并进同一数据集目录,重新跑预处理,然后直接加载旧 checkpoint 继续训练。nnUNetv2 的训练命令里提供了继续训练的参数,具体名称建议先看nnUNetv2_train --help确认。要注意:继续训练之前先备份旧模型,因为权重会被覆盖,没有后悔药。我的习惯是把nnUNet_results/Dataset001_MyData目录整个复制一份再动。
自定义配置是另一个实用方向。plans.json里的batch_size、target_spacing、num_processes都可以手动改。改完以后要重新跑预处理,因为预处理产物依赖于 plans 里的 spacing 设置。我自己改得最多的是batch_size,因为显存大小直接决定能不能跑起来;target_spacing我一般只在数据尺寸极端时才动,比如原始图像是 1024×1024 的大图,不改 spacing 的话全分辨率训练根本跑不动。
验证环节除了看 DSC,我每次都会加一步“切片目检”。用 SimpleITK 读预测结果和原图,挑几个代表性的切片叠在一起看,重点看边界是否平滑、小目标是否漏检、有没有出现训练集里没见过的奇怪伪影。指标是给人看的,但最终模型是给机器用的,边界上的系统性偏差只靠 DSC 是看不出来的。比如模型总把器官边界往外扩一圈,DSC 可能只掉两三个点,但下游手术规划根本不敢用。
最后说一个我这几年跑分割项目的习惯:任何新数据集,第一次一定是只跑 3d_fullres 五折,不做任何花哨操作;拿到评估指标后,先看 5 折标准差,再看每个类别的 NSD,最后目检十个切片。这三关过了才考虑跨配置集成、增量训练这些进阶操作。这个习惯帮我挡掉了至少一半的无效实验。希望帮到你。
本文还有配套的精品资源,点击获取