简介一套面向医学影像分割与深度学习算法验证的腹部多脏器器官语义分割数据集适用于需要真实人体CT/腹部影像数据进行模型训练与评估的研究者、开发者。数据划分为训练集、验证集训练集约900张图像及对应掩膜验证集约200张图像及对应掩膜标签覆盖脾脏、左右肾、胆囊、食道、肝脏、胃、主动脉、下腔静脉、胰腺、肾上腺等13个腹部器官类别具体编号可对照classes文本查看。资源共2000个文件以1141个png掩膜图与857个jpg原图为主另含1个Python脚本与1个json配置文件运行show脚本即可查看掩膜叠加在原始图像上的分割结果且数据已做对比度拉伸等图像增广处理。压缩包约74.85MB下载与使用轻便。目前已有477人浏览学习适合直接用于分割模型训练、评估与算法验证省去自行标注数据的大量时间成本。1. 人体腹部多脏器语义分割数据集先搞清楚它到底给了你什么做腹部CT脏器分割的人应该都有同感卡住你的往往不是模型而是标注。人体腹部多脏器语义分割数据集把肝脏、脾脏、左右肾、胰腺这些常见脏器的像素级标注直接给到手里省掉了从影像科老师那里反复求标注的漫长等待。它面向的是一类具体任务用深度学习模型做腹部CT图像的语义分割输入是原始CT切片输出是每个像素的脏器类别。适合正在入门语义分割的研究生、刚接手医学影像分割项目的算法工程师以及想做迁移学习但缺带标签数据的从业者。拿到这套数据之后你不需要再从零标注把数据格式、预处理、训练评估这条线跑通就能开始自己的实验。2. 数据格式与标签体系读NIfTI之前先确认三件事这类腹部多脏器语义分割数据集最常见的打包格式是NIfTI文件后缀是.nii.gz用医学影像处理库可以直接读。不少新手第一步就栽在维度顺序上SimpleITK读出来的是(z, y, x)而不是(x, y, z)如果直接按第三个维度切片当横断面处理就会把整个数据方向搞反。所以拿到数据之后别急着训练先把标签编码、体素间距、HU分布三件事确认清楚。2.1 文件组织与标签编码先看unique再写数据处理解压之后一般是image和label两个目录或者同一个目录下的成对文件。label文件里的每个像素值是一个整数0通常表示背景1、2、3分别对应不同脏器具体对应关系不同数据集定义不完全一样。稳妥做法是先用脚本把类别枚举出来再决定训练时算多少个类别而不是凭文件名猜。import SimpleITK as sitk import numpy as np image sitk.ReadImage(data/image_001.nii.gz) label sitk.ReadImage(data/label_001.nii.gz) # SimpleITK 读入后转数组维度顺序是 (z, y, x) img_arr sitk.GetArrayFromImage(image) lab_arr sitk.GetArrayFromImage(label) print(图像尺寸 (z, y, x):, img_arr.shape) print(体素间距 (mm):, image.GetSpacing()) print(方向矩阵:, image.GetDirection()) print(CT值范围:, img_arr.min(), img_arr.max()) print(标签里的类别值:, np.unique(lab_arr))这段代码输出的三行信息直接决定后续所有脚本。标签unique若出现小数或负数说明数据本身有问题别继续往下跑。print出来的标签类别数量决定模型输出通道数比如unique结果是0到5那分类头就是6个类别其中0是背景。GetSpacing返回的是每个体素在x、y、z三个方向的物理尺寸单位mm不同医院扫描设备出来的spacing差异很大这就是为什么要做重采样。2.2 重采样与HU预处理两个影响精度的前置步骤CT数据集里同一器官在不同病例中的体素间距往往不一致比如肝脏在A病例里占300个体素在B病例里只占200个体素如果不重采样模型学到的是“像素计数”而不是“物理尺寸”。常见做法是把所有数据统一到1.0 x 1.0 x 1.0 mm或某个固定层厚。图像部分用线性插值标签部分必须用最近邻插值否则器官边界会出现小数标签训练时直接报错。def resample_to_spacing(sitk_img, new_spacing(1.0, 1.0, 1.0), is_labelFalse): original_spacing sitk_img.GetSpacing() original_size sitk_img.GetSize() # 新尺寸 原尺寸 * 原间距 / 新间距四舍五入取整 new_size [int(round(o * s / ns)) for o, s, ns in zip(original_size, original_spacing, new_spacing)] resampler sitk.ResampleImageFilter() resampler.SetOutputSpacing(new_spacing) resampler.SetSize(new_size) resampler.SetOutputOrigin(sitk_img.GetOrigin()) resampler.SetOutputDirection(sitk_img.GetDirection()) # 标签必须最近邻图像用线性 resampler.SetInterpolator(sitk.sitkNearestNeighbor if is_label else sitk.sitkLinear) return resampler.Execute(sitk_img)new_spacing这个参数值得反复调。设1.0 mm各向同性细节保留好但体素数量成倍增加显存压力大如果只做2D切片训练可以把层间spacing放到2.0甚至2.5 mm轴向切片数量减少一半以上训练速度快很多对Dice影响通常很小。ResampleImageFilter要同时设置OutputSpacing、Size、Origin、Direction四项漏掉Origin和Direction会导致重采样结果坐偏。is_label这个开关是封装时最常写错的地方标签一定走最近邻Dice结果差0.02往往就是这里。接着是HU值处理。原始CT值的动态范围是-1024到3071直接做归一化的话肝脏、脾脏这些软组织只占其中很小一段对比度被压缩得非常厉害。腹部软组织通常用窗宽400、窗位40左右的参数来观察换算成裁剪范围大约是-160到240我自己更习惯用-100到300这个区间对肝脏和肾脏边界都够用。def preprocess_hu(image_arr, lower-100, upper300): # 裁剪到腹部软组织窗范围 image_arr np.clip(image_arr, lower, upper) # 映射到 [0, 1] image_arr (image_arr - lower) / (upper - lower) return image_arr.astype(np.float32)clip这一步相当于数字版本的窗宽窗位把范围外的CT值全部截断。lower和upper不是玄学参数可以先打印数据集的HU直方图再定如果肝脏区域的中位数偏移明显就把窗口往那个方向平移。归一化到[0,1]之后如果之后要加载ImageNet预训练权重还可以再按ImageNet的均值和标准差做一次标准化但注意那只对RGB三通道输入有效2D灰度图需要把单通道复制成三通道再做。3. 把体数据切成可训练的切片切法、存法与增强2D语义分割模型读的是二维切片所以要把重采样后的三维数组拆成一摞二维图。腹部CT的横断面轴向分辨率最高器官在每一层上的轮廓是完整的冠状面和矢状面因为层间距大直接当2D输入会有明显锯齿。常见做法是按z轴均匀切每隔一层或隔两层采一次既能减少冗余也能加快训练。3.1 沿轴向切片与空白过滤切片脚本要解决两件事一是怎么切二是切完怎么存。如果直接把每一层都保存成png多类别标签的索引很容易被图像库改写所以我更推荐用npy直接落盘后面Dataset读取也快。from pathlib import Path import numpy as np def slice_and_save(img_arr, lab_arr, out_dir, z_step1, min_fg_pixels20): # img_arr 传入预处理后的数组lab_arr 是原始标签 out_dir Path(out_dir) (out_dir / image).mkdir(parentsTrue, exist_okTrue) (out_dir / label).mkdir(parentsTrue, exist_okTrue) for z in range(0, img_arr.shape[0], z_step): label_slice lab_arr[z] # 过滤掉几乎全是背景的空层 if int((label_slice 0).sum()) min_fg_pixels: continue np.save(out_dir / image / fcase_{z:04d}.npy, img_arr[z]) np.save(out_dir / label / fcase_{z:04d}.npy, label_slice)这个脚本里z_step控制采样密度。如果数据层厚已经是2.5 mmz_step取1就行如果是亚毫米重建的高分辨率数据z_step取2或3能省一半显存而且相邻切片差异很小取太密对Dice提升有限。min_fg_pixels用于过滤腹部上下两端全是空气的层这类样本如果混进训练集模型会把大量注意力花在学“预测背景”上对器官分割没有帮助。我一般取20到50过滤量大约在5%到10%之间。保存格式用npy而不是png多类别标签用png保存有被压缩工具改索引值的风险虽然png是无损但很多库默认会做色彩空间转换npy直接按数组dtype落盘最稳。3.2 Dataset类与同步增强切完切片后要写一个PyTorch Dataset。这里的关键是两个约定标签数组要和图像数组在文件名上一一对应数据增强必须同步作用到图像和标签上否则器官位置和标签就对不上了。我一般用albumentations做增强它对mask参数有内置同步处理比手动写numpy变换省事很多。import torch from torch.utils.data import Dataset from pathlib import Path import numpy as np import albumentations as A class AbdomenSliceDataset(Dataset): def __init__(self, image_dir, label_dir, transformNone): self.image_dir Path(image_dir) self.label_dir Path(label_dir) self.image_names sorted(p.name for p in self.image_dir.glob(*.npy)) self.transform transform def __len__(self): return len(self.image_names) def __getitem__(self, idx): image np.load(self.image_dir / self.image_names[idx]) label np.load(self.label_dir / self.image_names[idx].replace(image_, label_)) if self.transform: aug self.transform(imageimage, masklabel) image, label aug[image], aug[mask] # 图像加通道维标签保持长整型 image torch.from_numpy(image).float().unsqueeze(0) label torch.from_numpy(label).long() return image, label train_transform A.Compose([ A.HorizontalFlip(p0.5), A.RandomRotate90(p0.5), A.ElasticTransform(alpha1, sigma50, p0.3), ])transform放在__getitem__里而不是提前做离线增强等于每个epoch读到的是同一张切片的随机变形结果样本的有效多样性被放大对小数据集尤其关键。ElasticTransform是医学分割里常用的形变增强alpha控制位移幅度sigma控制形变平滑度alpha取1对腹部这类近似刚体的器官是安全的取太大器官形状会扭曲到不真实。RandomRotate90只做90度的整数倍旋转这样标签不需要插值不会出现旋转后标签边缘错位的问题。验证集不要加transform只保留归一化否则验证集每次评估的结果都会抖。4. 训练与评估从FCN换到UNet时的损失与指标模型选型上不用一上来就追新网络。FCN是最经典的语义分割模型把全连接层换成卷积层用反卷积上采样恢复分辨率理解语义分割的原理从FCN入手非常合适。但直接拿FCN分割腹部多脏器胰腺这类小器官很容易丢原因是反卷积上采样只做了粗糙恢复没有把浅层的高分辨率特征接回去。4.1 模型选型FCN的局限与UNet/DeepLabV3的取舍UNet通过跳跃连接把编码器每一层特征直接拼给解码器对边界细节友好很多DeepLabV3用ASPP模块做多尺度空洞卷积对大小差异很大的多脏器场景也比较稳。所以我通常的选型思路是先跑通UNet拿到基准Dice再换DeepLabV3看小器官有没有提升最后再决定要不要上3D模型。模型适合场景训练成本腹部多脏器上的常见表现FCN入门语义分割、验证pipeline最低胰腺等小器官易漏UNet医学影像2D分割基准低边界稳定推荐作为首跑DeepLabV3多尺度器官并存中小器官召回率更高显存略大4.2 损失函数Dice Loss与BCE的组合腹部脏器分割里背景占比通常在95%以上如果用纯交叉熵模型学到的是“都预测背景也能拿到低loss”Dice系数上不去。Dice Loss直接优化目标指标对类别不平衡天然不敏感。常见做法是Dice Loss和交叉熵各占一半前者稳住指标后者提供更平滑的梯度。def dice_loss(pred, target, num_classes6, smooth1e-5): # pred: (B, C, H, W)target: (B, H, W) pred torch.softmax(pred, dim1) target_onehot torch.nn.functional.one_hot(target, num_classes) target_onehot target_onehot.permute(0, 3, 1, 2).float() intersection (pred * target_onehot).sum(dim(2, 3)) union pred.sum(dim(2, 3)) target_onehot.sum(dim(2, 3)) dice (2 * intersection smooth) / (union smooth) # 忽略背景类只对器官类求平均 return 1 - dice[:, 1:].mean()smooth取1e-5防止某个器官在切片上完全没出现时出现除零。dice[:, 1:]这里把背景类去掉因为背景占比太大算进去会把loss稀释训练时模型对胰腺这类小器官的梯度会被背景掩盖。如果发现训练过程中某几个类的Dice始终偏低可以改成按类别加权平均给胰腺这类小器官更大的权重这是调loss最直接的一步。组合使用时loss dice_loss(pred, target) * 0.5 torch.nn.functional.cross_entropy(pred, target) * 0.5。4.3 训练参数与按病例划分验证集训练参数方面我倾向于把Adam的学习率设1e-4而不是默认的1e-3医学图像分割的标签噪声偏大学习率太大会在Dice曲线上看到明显震荡。weight_decay设1e-4到5e-4即可相当于给深层网络加一点L2正则。batch_size取决于切片尺寸512x512一般8到16256x256可以到32。epochs给100配合每15个epoch不提升就早停的机制一般50到80轮收敛。参数取值说明lr1e-4Adam默认1e-3偏大weight_decay1e-4等价于L2正则batch_size16 (512x512)显存不足时先降通道schedulerCosineAnnealing后半程降lrearly stoppingpatience15防过拟合验证集划分要按case来而不是按切片随机抽# 假设文件名格式是 case001_z0000.npy case_ids sorted({name.split(_)[0] for name in all_images}) train_cases case_ids[:int(len(case_ids) * 0.8)] val_cases case_ids[int(len(case_ids) * 0.8):] train_names [n for n in all_images if n.split(_)[0] in train_cases] val_names [n for n in all_images if n.split(_)[0] in val_cases]这一步是决定验证集Dice可信度的关键。如果随机从所有切片里抽10%做验证同一个病人的上下相邻切片会同时出现在训练集和验证集测试出来的Dice会虚高不少。按case_id划分训练和验证来自不同病人结果才是真实泛化水平。评估指标我一般算每个器官的Dice和mDSC所有器官Dice平均IoU可以在Dice基础上换算IoU Dice / (2 - Dice)。def compute_dice(pred_mask, target_mask, num_classes6): dice_list [] for c in range(1, num_classes): # 跳过背景 inter ((pred_mask c) (target_mask c)).sum() union (pred_mask c).sum() (target_mask c).sum() dice (2 * inter 1e-6) / (union 1e-6) dice_list.append(dice) return dice_list # 顺序对应每个器官类别这段代码在验证或测试阶段用先把网络输出用argmax转成类别掩码再逐类算Dice。看结果时不要只盯mDSC要把每一类的Dice单独列出来多数情况下胰腺最低如果胰腺的Dice比肝脏低0.1以上说明小器官还是被忽略了。5. 腹部器官分割避坑五条实测翻车记录这些坑都是我在实际数据集上踩过的每一条都会让你的Dice看起来正常但实际结果不可信或者训练根本跑不起来。按预处理和训练两个阶段分开说。5.1 预处理与标签侧的三个高频坑坑一重采样后标签边缘出现灰色值。现象重采样完打开标签发现器官边缘出现1.5、2.7这种小数标签训练时模型直接报错或loss变成nan。原因重采样时把图像和标签用了同一个插值器标签被线性插值成了小数。这是新手最容易犯的错因为图像用线性插值效果很好顺手就套到标签上了。解决标签重采样强制用最近邻。上面的resample_to_spacing里is_labelTrue这个分支就是为了拦这个问题调用时分开传image和label不要共用一个interpolator。改完之后再print一次np.unique(label)确认标签集合里只有0到N的整数。坑二HU没有裁剪直接归一化。现象训练曲线正常但可视化输入时CT图像灰蒙蒙器官边界看不清验证Dice一直上不去。原因原始CT范围-1024到3071软组织只占其中很小一段不裁剪就归一化会把软组织对比度压到极低模型能看到的器官边缘信息非常弱。解决先看数据集的HU直方图确定腹部软组织窗口我用[-100, 300]做默认裁剪。如果某个脏器增强后明显偏亮或偏暗按直方图平移窗口再试一轮这个调整通常能带来0.01到0.03的Dice提升。每次换数据集都要重新确认窗口不要拿着上一个数据集的参数直接套。坑三切片后大量纯背景样本混进训练。现象训练loss正常下降但验证时模型在器官边缘过度平滑小器官召回率低。原因腹部CT的顶层和底层都是空气或身体其他组织按固定间隔切片时这些空层全被保存并参与训练模型花了大量参数拟合背景。解决切片时用min_fg_pixels过滤或以切片上器官面积占比为准低于某个阈值直接跳过。验证集统计一下过滤前后有效切片数一般能省掉5%到15%的数据训练时间也缩短。这个过滤逻辑要同时用在训练和测试切片生成上否则两边分布不一致。5.2 训练与验证侧的两个高频坑坑四loss一直降Dice停在0.7不动。现象训练loss从0.6降到0.2但mDSC始终在0.7附近胰腺类尤其拉胯。原因背景占比超过95%交叉熵主导梯度模型本质上在学“哪里是背景”器官边缘的梯度被淹没。loss下降的快感是假象它对Dice的改善远不如Dice Loss直接。解决换Dice Loss与交叉熵各半的组合或对背景类别降采样/降权。评估时报告每类Dice而不只报告全局准确率全局准确率在这个任务里没有参考价值——全预测背景也能到95%以上。坑五验证集Dice虚高换新数据就崩。现象验证集Dice 0.85换一台设备或新病例只有0.6。原因划分验证集时按切片随机切同一个case的相邻层分去了两边模型相当于见过目标病人泛化成绩被高估。这是分割任务里最隐蔽的坑因为训练和验证loss看起来都正常。解决严格按case_id分组。划分后再检查一下有没有重叠的case id这一步看起来简单但漏掉的话后面跑出来的所有结论都不稳定。我从那以后每个数据集第一件事就是先列case清单再划分这个习惯救了不少实验。6. 进阶从2D切片到2.5D输入先小批量验证再全量训练2D训练跑通之后想进一步提升Dice最直接的不是换3D UNet而是2.5D输入——把当前切片和前后几张切片一起作为多通道输入让模型看到层间连续性。胰腺这类在轴向上变化明显的器官对层间信息很敏感2.5D通常能带来0.02到0.04的mDSC提升显存开销比3D小得多。6.1 2.5D输入的构造与验证节奏构造2.5D输入的核心代码很薄就是把z轴附近的切片堆到通道维。def build_volume_input(img_arr, z, radius3): zs [z offset for offset in range(-radius, radius 1)] # 越界时用边界切片代替而不是丢弃 zs [min(max(z_, 0), img_arr.shape[0] - 1) for z_ in zs] return np.stack([img_arr[z_] for z_ in zs], axis0) # (2*radius1, H, W)radius3表示取前后各3层总共7个通道。模型第一层卷积的输入通道要从1改成7如果加载预训练权重常见做法是把预训练conv1的权重在通道维上做平均复制或者只把前3个通道用预训练值初始化剩下的重新初始化两种方式都能跑第一种收敛更快。z越界的处理要统一我习惯用边界复制而不是粗暴丢弃否则靠近首尾的切片数量变少数据集里最上面和最下面的切片丢失信息不一致。换任何新方案我都不会直接全量训练。常见做法是先从切片池里随机抽50张做一个小验证集在这个小集上跑20个epoch看Dice曲线的斜率和震荡幅度确认提升方向正确再上全量数据。以前我吃过一次亏直接把2.5D输入套到全量数据集上训练了一个晚上第二天发现mDSC反而降了0.01原因是radius设得太大层间信息里混入了与当前层相距过远的切片等于给输入加了噪声。从那以后我每次换预处理、换损失、换模型都要先跑小验证用20个epoch的Dice曲线说话不被一次全量训练的结果带偏。希望帮到你。本文还有配套的精品资源点击获取