简介本资源为面向医学影像分析与深度学习语义分割方向的数据集适用于腹部多脏器自动分割模型的训练与验证可支撑脾脏、肝肾、胆囊、食道、胃、主动脉、下腔静脉、门静脉与脾静脉、胰腺及左右肾上腺等13类器官的像素级标注任务。包内共约2000个文件以png掩膜与jpg原始图像为主另含1个py脚本和1个json配置压缩包约74.85MB训练集约900张图像及对应mask验证集约200张并已做对比度拉伸等图像增广运行show脚本即可查看gt在images上的掩膜叠加效果。目前已有476人学习下载适合医学图像分割入门与进阶读者可快速搭建训练验证流程、复现分割网络并对照掩膜结果排查问题。1. 腹部多脏器语义分割数据集从标注格式到训练落地的完整拆解腹部 CT 的多脏器分割是医学影像 AI 里最容易被低估的一类任务。肝脏、脾脏、肾脏、胰腺、胃这些器官在轴位切片上互相挤压、边界模糊胰腺这种细长结构还经常被周围脂肪和肠道干扰用普通的分割网络直接跑Dice 卡在 0.6 上不去是常态。这份「人体腹部多脏器器官语义分割」数据集解决的就是从零搭建训练管线时最耗时的两件事拿到带像素级标注的腹部 CT 切片以及搞清楚多类器官标签怎么映射成网络能吃的 mask。它适合正在做医学影像分割的算法工程师、准备毕设或课题的硕博生以及想把 nnU-Net、U-Net 这类模型跑通真实临床数据的人。数据集本身不挑框架PyTorch、TensorFlow、PaddleSeg 都能接关键在于标签体系和预处理流程要对齐。2. 标签体系与数据组织多脏器 mask 到底怎么读拿到一份腹部多脏器数据集第一件事不是急着写 DataLoader而是把标签的编码方式搞清楚。医学分割数据集常见的坑是标注文件里每个器官用不同的灰度值表示但不同来源的灰度映射表不一样直接按 0/1/2/3 去读很可能把脾脏当成肝脏。这一章先把数据组织方式和标签映射讲透再落到具体的读取代码。2.1 多类语义标签的两种常见组织方式腹部多脏器分割数据集在磁盘上一般有两种存法。第一种是每张切片对应一个单通道 PNG 或 NIfTI maskmask 里每个像素的整数值代表器官类别比如 0 是背景、1 是肝脏、2 是脾脏、3 是左肾、4 是右肾、5 是胰腺、6 是胃。第二种是每个器官单独一个二值 mask 文件训练时再拼成多通道。前者省空间、读取快后者方便做单器官的独立评估。我一般优先选第一种因为多类 mask 直接喂给交叉熵损失就行不用自己拼通道。但要注意PNG 是 8 位存储最多 256 个类别腹部器官一般不超过 10 类够用如果标注是 NIfTI 格式像素值可能是 16 位甚至浮点读取时要用nibabel而不是cv2.imread否则灰度会被截断。判断数据集属于哪种组织方式最快的办法是看目录结构和文件后缀。常见结构是images/放原图、masks/放标签文件名一一对应。如果masks/下每个病例有多个文件比如liver.png、spleen.png那就是第二种。2.2 用 Python 读取并校验标签映射下面这段代码是我每次拿到新数据集都会先跑的校验脚本目的是确认标签值分布和器官映射是否和文档一致。import numpy as np import cv2 from pathlib import Path from collections import Counter # 数据集根目录按实际路径改 mask_dir Path(data/abdominal_multi_organ/masks) # 统计所有 mask 里出现过的像素值 label_counter Counter() for mask_path in mask_dir.glob(*.png): mask cv2.imread(str(mask_path), cv2.IMREAD_GRAYSCALE) if mask is None: print(f读取失败: {mask_path}) continue # 统计本张图出现的唯一值 unique, counts np.unique(mask, return_countsTrue) for u, c in zip(unique, counts): label_counter[int(u)] int(c) # 按像素值排序输出 for label in sorted(label_counter.keys()): print(f标签值 {label}: 像素数 {label_counter[label]})这段代码的逻辑很直接遍历所有 mask用np.unique拿到每张图的唯一像素值和计数再累加到全局计数器。跑完之后你会看到类似标签值 0: 像素数 12000000、标签值 1: 像素数 800000这样的输出。如果出现文档里没写的标签值比如突然冒出一个 255那多半是标注工具留下的边界填充或者未标注区域训练前必须处理掉否则网络会学出一个无意义的第 N 类。参数上唯一需要改的是mask_dir指向你的标签目录。如果数据集是 NIfTI 格式把cv2.imread换成nibabel.load(...).get_fdata()并注意 dtype 转换。校验通过后把标签映射写成一个字典常量后续 Dataset 类直接引用避免到处硬编码。2.3 训练集 / 验证集划分的注意事项腹部 CT 数据集划分不能按切片随机分。同一个病人的相邻切片高度相似如果随机打散验证集里会出现训练集病人的邻近切片Dice 虚高模型实际泛化能力被高估。正确做法是按病例patient-level划分一个病人的所有切片要么全在训练集要么全在验证集。常见做法是先把病例 ID 列表拿出来用sklearn.model_selection.GroupShuffleSplit或者手动按 8:2 切分再根据病例 ID 把切片分配到对应集合。如果数据集本身已经给好了 train/val 目录那就直接用不要自己再打散。这一点在医学分割里是血泪经验按切片随机分导致验证指标好看、上线翻车的案例太多了。3. 从 mask 到训练张量预处理管线与增强策略标签读对了只是第一步腹部 CT 的预处理直接决定模型能不能收敛。CT 的 HU 值范围从 -1000 到 3000不同设备的成像参数还不一样不做窗宽窗位归一化网络第一层就饱和了。这一章把预处理管线和增强策略拆开讲每一步都给可复现的代码。3.1 CT 窗宽窗位与强度归一化腹部软组织的最佳观察窗是窗宽 400、窗位 40对应 HU 范围 -160 到 240。把原始 HU 裁剪到这个区间再归一化到 [0,1]是腹部分割的标准操作。如果数据集已经是 PNG 格式说明原图已经做过窗宽窗位转换这时候只需要做 min-max 归一化不要再按 HU 处理。import numpy as np def normalize_ct(image, window_center40, window_width400): 对 CT 图像做窗宽窗位裁剪并归一化到 [0,1] image: numpy array, 原始 HU 值或已转换的灰度 window_center: 窗位腹部软组织常用 40 window_width: 窗宽腹部常用 400 lower window_center - window_width / 2 upper window_center window_width / 2 image np.clip(image, lower, upper) image (image - lower) / (upper - lower) return image.astype(np.float32)window_center和window_width是两个关键参数。腹部多脏器用 40/400 是通用起点但如果你的数据集侧重肝脏可以试 60/160侧重胰腺窗宽可以收窄到 300 突出软组织对比。归一化后的图像范围固定在 [0,1]后续做标准化时减 0.5 除 0.5 即可。注意如果原图已经是 8 位 PNGnp.clip的上下界要改成 0 和 255否则会把有效像素裁掉。3.2 几何增强与类别不平衡处理腹部多脏器数据集的类别极不平衡肝脏、脾脏像素占比高胰腺、肾上腺这类小器官可能只占千分之几。不做处理网络会倾向于预测背景和大器官小器官的 Dice 惨不忍睹。几何增强方面常用的有随机旋转±15°、随机缩放0.9~1.1、随机弹性形变。弹性形变对腹部器官特别有效因为器官本身有自然的形变空间。但要注意图像和 mask 必须用同一组变换参数否则标签会错位。用albumentations库可以保证这一点。import albumentations as A train_transform A.Compose([ A.RandomRotate90(p0.5), A.HorizontalFlip(p0.5), A.ShiftScaleRotate( shift_limit0.05, scale_limit0.1, rotate_limit15, border_mode0, p0.7 ), A.ElasticTransform(alpha1, sigma50, p0.3), A.RandomBrightnessContrast(p0.3), ]) # 验证集只做必要的 resize不做随机增强 val_transform A.Compose([ A.Resize(height256, width256), ])ShiftScaleRotate的border_mode0表示填充背景值对 CT 来说填充 0 相当于空气合理。ElasticTransform的alpha控制形变强度sigma控制平滑度腹部器官用 alpha1、sigma50 是比较温和的设置再大就可能把器官形状扭曲得不真实。类别不平衡的另一个手段是损失函数。交叉熵配 Dice Loss 是分割任务的标配Dice Loss 对小器官更敏感。如果胰腺 Dice 还是上不去可以试 Tversky Loss通过调整 alpha/beta 参数加大对假阴性的惩罚。3.3 构建可复用的 Dataset 类把前面的读取、归一化、增强串起来就是一个标准的 PyTorch Dataset。下面这个类可以直接抄改一下路径和标签映射就能用。import torch from torch.utils.data import Dataset import cv2 import numpy as np class AbdominalOrganDataset(Dataset): def __init__(self, image_paths, mask_paths, transformNone): self.image_paths image_paths self.mask_paths mask_paths self.transform transform def __len__(self): return len(self.image_paths) def __getitem__(self, idx): # 读图CT 灰度图用 IMREAD_GRAYSCALE image cv2.imread(str(self.image_paths[idx]), cv2.IMREAD_GRAYSCALE) mask cv2.imread(str(self.mask_paths[idx]), cv2.IMREAD_GRAYSCALE) # 归一化到 [0,1] image normalize_ct(image, window_center40, window_width400) # 增强注意 albumentations 要求 image 是 HWC 格式 if self.transform: augmented self.transform(imageimage, maskmask) image augmented[image] mask augmented[mask] # 转成 tensor图像加通道维mask 保持 long 类型 image torch.from_numpy(image).unsqueeze(0).float() mask torch.from_numpy(mask).long() return image, mask这个类的关键点有三个。第一cv2.imread读灰度图保证 image 和 mask 都是单通道。第二normalize_ct在增强之前调用因为增强库的亮度变换是在归一化空间里操作的。第三mask 转 tensor 时用.long()因为交叉熵损失要求标签是 int64。如果 mask 里有未标注的 255要在__getitem__里把它映射成 0 或者忽略索引否则训练直接报错。4. 模型训练与评估多脏器分割的收敛技巧数据管线搭好之后模型选型和训练策略决定最终指标。腹部多脏器分割不需要从零设计网络U-Net 系列和 nnU-Net 已经足够强关键是把训练细节调对。这一章讲模型选择、损失函数配置和评估指标的正确用法。4.1 模型选型U-Net、nnU-Net 还是 TransformerU-Net 是医学分割的基线编码器-解码器加跳跃连接的结构对器官边界恢复很有效。如果数据集规模在几百张切片以上U-Net 配 ResNet 编码器就能跑出不错的基线。nnU-Net 是自动配置框架会根据数据集的 spacing、器官大小自动决定网络深度、patch size 和训练轮数省去大量调参时间代价是训练资源要求高一些。Transformer 类模型比如 Swin-UNet 在腹部多脏器上也有应用长距离依赖对胰腺这种细长结构有帮助但需要更多数据才能发挥优势。如果数据集只有几十个病例我一般先用 U-Net 把基线跑出来再考虑换模型。选型时还要看输入尺寸。腹部 CT 切片常见 512×512直接整图训练显存吃不消通常随机裁 256×256 的 patch。nnU-Net 会自动处理这个手写管线的话patch size 设 256、batch size 设 8 是 8GB 显存下的稳妥配置。4.2 损失函数与优化器配置多类分割的标准损失是交叉熵加 Dice Loss权重各 0.5。交叉熵负责像素级分类Dice Loss 负责区域重叠度两者互补。如果某个器官特别小可以在 Dice Loss 里给每个类别加权小器官权重调高。import torch import torch.nn as nn class DiceLoss(nn.Module): def __init__(self, num_classes, class_weightsNone): super().__init__() self.num_classes num_classes self.class_weights class_weights def forward(self, logits, targets): # logits: [B, C, H, W], targets: [B, H, W] probs torch.softmax(logits, dim1) targets_one_hot torch.nn.functional.one_hot( targets, self.num_classes ).permute(0, 3, 1, 2).float() dice_per_class [] for c in range(self.num_classes): p probs[:, c] t targets_one_hot[:, c] intersection (p * t).sum() union p.sum() t.sum() dice (2 * intersection 1e-6) / (union 1e-6) dice_per_class.append(dice) dice_per_class torch.stack(dice_per_class) if self.class_weights is not None: w torch.tensor(self.class_weights, devicelogits.device) dice_per_class dice_per_class * w return 1 - dice_per_class.mean()class_weights是每个类别的权重列表长度等于类别数。背景权重可以设 0.1大器官设 1.0胰腺这种小器官设 3.0 到 5.0。1e-6是平滑项防止分母为零。优化器用 AdamW学习率 1e-4配余弦退火调度训练 100 到 200 轮。如果验证集 Dice 在 50 轮后还在震荡把学习率降到 1e-5 再跑。4.3 评估指标Dice、IoU 与 HD95 的正确解读Dice 系数是最常用的分割指标但对小器官不敏感。胰腺的 Dice 到 0.7 已经算不错肝脏能到 0.95。看指标时要分器官看不能只看平均值否则大器官会把小器官的问题掩盖掉。IoU 和 Dice 是单调对应的报告一个就行。HD9595% 豪斯多夫距离衡量边界误差对器官轮廓要求高的场景更有参考价值。如果 Dice 高但 HD95 也高说明区域重叠好但边界毛糙可能需要后处理或者换损失函数。评估时还要注意mask 里的背景类要不要算进平均。常见做法是只对前景器官求平均背景单独报告。如果数据集有未标注区域评估前要把它从计算中排除否则指标没有意义。5. 避坑与排查多脏器分割训练中的五个真实翻车点这一章记录的是我在腹部多脏器分割任务里实际踩过的坑每条按现象、原因、解决来写。新手照着排查能省不少时间熟手可以对照检查自己的管线。5.1 验证集 Dice 很高但推理结果全是背景现象训练日志里验证 Dice 到 0.9但拿模型去推理新数据输出几乎全是背景类。原因验证集和训练集按切片随机划分同一病人的相邻切片同时出现在两个集合里模型记住了病人特征而不是器官特征。解决改成按病例划分确保一个病人的所有切片只出现在一个集合。如果数据集没给病例 ID从文件名里提取通常文件名前缀就是病例号。5.2 胰腺 Dice 始终低于 0.3现象肝脏、脾脏 Dice 都正常唯独胰腺和肾上腺这类小器官指标极低。原因类别不平衡加上小器官在 patch 里出现概率低随机裁剪时很多 patch 根本不含胰腺网络没见过足够样本。解决用器官感知的 patch 采样保证每个 batch 里至少有一定比例的 patch 包含小器官。或者在损失函数里给小器官加权重Tversky Loss 的 alpha 设 0.7、beta 设 0.3加大对漏检的惩罚。5.3 训练 loss 震荡不收敛现象loss 在前几十轮下降之后开始剧烈震荡Dice 也跟着波动。原因学习率太大或者 batch size 太小导致梯度噪声大。腹部 CT 的 patch 之间差异大小 batch 的梯度方向不稳定。解决把学习率从 1e-3 降到 1e-4加梯度裁剪torch.nn.utils.clip_grad_norm_max_norm 设 1.0。如果显存允许batch size 从 4 提到 8 或 16。还可以用梯度累积模拟大 batch。5.4 mask 读取后标签值错位现象训练时 loss 正常下降但推理结果把脾脏预测成肝脏器官类别整体偏移。原因标注工具的灰度映射和代码里的类别定义不一致。比如标注里肝脏是 1、脾脏是 2但代码里按 0 背景、1 肝脏、2 脾脏读看起来对实际标注文件里 0 是未标注、1 是背景、2 才是肝脏。解决跑 2.2 节的校验脚本打印所有出现过的标签值和数据集文档逐一对照。确认后把映射写成常量字典所有地方统一引用。5.5 数据增强后图像和 mask 不对齐现象训练时 loss 能降但验证指标比不加增强还低可视化发现 mask 和图像错位。原因图像和 mask 用了不同的增强参数或者增强库的输入格式不对。比如 albumentations 要求 image 是 HWC、mask 是 HW如果 image 传成了 CHW变换会作用在错误维度上。解决确保 image 和 mask 在同一个A.Compose里调用不要分开做。检查 image 的 shape 是 (H, W) 或 (H, W, C)mask 是 (H, W)。增强后立即可视化几张确认对齐再开始训练。6. 进阶技巧用 nnU-Net 快速复现与自定义器官扩展如果你不想从零写训练管线nnU-Net 是目前腹部多脏器分割最省事的方案。它自动处理预处理、网络配置和训练调度你只需要把数据整理成指定格式。这一章讲怎么把这份数据集转成 nnU-Net 格式以及怎么扩展到自定义器官。nnU-Net 要求的数据格式是每个病例一个文件夹里面放_0000.nii.gz原图和.nii.gz标签标签的像素值从 1 开始连续编号。转换脚本的核心是把 PNG 或 NIfTI 切片按病例重组并生成dataset.json描述文件。import json import numpy as np import nibabel as nib from pathlib import Path # nnU-Net 数据集根目录 nnunet_raw Path(nnUNet_raw/Dataset001_AbdominalOrgans) imagesTr nnunet_raw / imagesTr labelsTr nnunet_raw / labelsTr imagesTr.mkdir(parentsTrue, exist_okTrue) labelsTr.mkdir(parentsTrue, exist_okTrue) # 假设原始数据按病例组织每个病例一个文件夹 src_root Path(data/abdominal_multi_organ) case_dirs sorted([d for d in src_root.iterdir() if d.is_dir()]) for case_dir in case_dirs: case_id case_dir.name # 读取该病例的所有切片并堆叠成 3D 体积 slices sorted(case_dir.glob(images/*.png)) volume np.stack([ cv2.imread(str(s), cv2.IMREAD_GRAYSCALE) for s in slices ], axis-1) # 形状 [H, W, D] label_slices sorted(case_dir.glob(masks/*.png)) label_volume np.stack([ cv2.imread(str(s), cv2.IMREAD_GRAYSCALE) for s in label_slices ], axis-1) # 保存为 nnU-Net 要求的 NIfTI 格式 nib.save( nib.Nifti1Image(volume.astype(np.int16), np.eye(4)), str(imagesTr / f{case_id}_0000.nii.gz) ) nib.save( nib.Nifti1Image(label_volume.astype(np.uint8), np.eye(4)), str(labelsTr / f{case_id}.nii.gz) ) # 生成 dataset.json dataset_info { channel_names: {0: CT}, labels: { background: 0, liver: 1, spleen: 2, left_kidney: 3, right_kidney: 4, pancreas: 5, stomach: 6 }, numTraining: len(case_dirs), file_ending: .nii.gz } with open(nnunet_raw / dataset.json, w) as f: json.dump(dataset_info, f, indent2)这段脚本的关键在于把 2D 切片按病例堆成 3D 体积nnU-Net 的预处理会自动处理 spacing 和归一化。labels字典里的编号必须从 1 开始连续背景固定为 0。channel_names里 CT 用 CT如果是 MRI 就写 MRInnU-Net 会根据这个选择不同的归一化策略。转换完成后跑nnUNetv2_plan_and_preprocess -d 001生成预处理计划再跑nnUNetv2_train 001 3d_fullres 0开始训练。3d_fullres 配置对腹部器官效果最好但显存要求高显存不够可以用 2d 配置先跑基线。扩展到自定义器官时只需要改dataset.json里的labels字典把新器官加进去并分配编号同时确保标签 mask 里对应像素值正确。如果新器官和已有器官有重叠nnU-Net 不支持多标签需要拆成多个数据集或者用多通道输出。从那以后我每次拿到新的腹部分割数据集都强制先跑一遍标签校验和病例划分检查再开始写训练代码。这两个步骤花不了半小时但能避免后面几天甚至几周的无效训练。希望帮到你。本文还有配套的精品资源点击获取