简介:本资源为面向医学影像分析与深度学习语义分割方向的数据集,适用于腹部多脏器自动分割的模型训练与算法验证,适合具备一定深度学习基础的研究生、算法工程师及医学影像研究者使用。数据集覆盖脾脏、左右肾、胆囊、食道、肝脏、胃、主动脉、下腔静脉、门静脉与脾静脉、胰腺及左右肾上腺共13类器官标签,具体类别可在classes文本中核对。资源包共约2000个文件,以png掩膜与jpg原始图像为主,另含1个py脚本和1个json配置,压缩包约74.85MB;运行show脚本即可查看gt在images上的掩膜叠加效果,数据已做对比度拉伸等图像增广。训练集约900张图像及对应mask,验证集约200张,划分清晰,便于直接开展分割实验。目前已有476人学习下载,可帮助读者快速搭建腹部多脏器分割流程、复现网络训练并对照掩膜结果排查问题。
1. 腹部多脏器语义分割数据集:从 CT 切片到可训练掩码的完整链路
拿到一份标注好的腹部 CT 数据集,把肝脏、脾脏、胰腺、左右肾这些器官从灰度切片里逐像素抠出来,是很多医学影像 AI 项目的起点。这件事听起来像是「跑个模型」那么简单,但真正卡住进度的往往不是网络结构,而是数据集本身——标签是不是多类、层厚是否统一、器官边界有没有漏标、训练时类别极度不均衡怎么办。人体腹部多脏器器官语义分割数据集要解决的,正是把原始 DICOM 或 NIfTI 影像和对应的多器官掩码整理成模型能直接吃的格式,让分割算法在肝脏、脾脏、肾脏、胰腺这些目标上稳定收敛。它适合两类人:一类是想入门医学图像语义分割、需要一份结构清晰的多类数据集练手的算法工程师;另一类是做辅助诊断、手术规划,需要快速验证某个分割模型在腹部场景下到底能不能用的从业者。下面我按自己实际处理这类数据集的顺序,把选型、预处理、训练、排错一条线讲透。
2. 多脏器语义分割的数据组织与标签体系:先搞清楚掩码里每个像素代表谁
2.1 多类语义分割和实例分割在腹部场景下的区别
很多人第一次接触这类数据集,会把「多脏器分割」和「多器官实例分割」混为一谈。语义分割给每个像素只打一个类别标签,肝脏的所有像素都是 1,脾脏都是 2,它不区分「这是第一个肾还是第二个肾」;实例分割则要在语义基础上再区分个体,左肾和右肾是两个独立实例。腹部多脏器数据集绝大多数是语义分割标注,因为临床关注的是「这个器官的边界在哪、体积多大」,而不是「这是第几个」。但这里有个容易翻车的点:如果左右肾在标签里被合并成同一类,模型输出的肾脏掩码就是连在一起的一坨,后续做分肾体积测量时还得自己写连通域拆分。所以拿到数据集第一件事,是确认标签映射表——每个整数对应哪个器官,左右肾是否分开,有没有背景类 0。
常见做法是维护一份label_map.json,把类别名和像素值绑定,训练、评估、可视化全部读这一份,避免代码里到处硬编码数字。下面是我一般会先跑的检查脚本,用来统计每个类别的像素占比和出现层数:
import numpy as np import nibabel as nib import json from pathlib import Path # label_map: {"background":0, "liver":1, "spleen":2, "pancreas":3, "rk":4, "lk":5} with open("label_map.json") as f: label_map = json.load(f) mask_dir = Path("masks") stats = {name: {"pixels": 0, "slices": 0} for name in label_map if name != "background"} for mpath in sorted(mask_dir.glob("*.nii.gz")): vol = nib.load(str(mpath)).get_fdata().astype(np.int16) for name, idx in label_map.items(): if name == "background": continue cnt = int((vol == idx).sum()) stats[name]["pixels"] += cnt # 统计包含该器官的层数,用于判断标注是否连续 stats[name]["slices"] += int(((vol == idx).sum(axis=(0, 1)) > 0).sum()) total = sum(v["pixels"] for v in stats.values()) for name, v in stats.items(): ratio = v["pixels"] / total if total else 0 print(f"{name:10s} pixels={v['pixels']:>12d} ratio={ratio:.4f} slices={v['slices']}")这段代码的逻辑是遍历所有掩码文件,按标签值统计像素总量和出现层数。参数上,astype(np.int16)是为了避免某些掩码存成 float 后相等比较出现精度问题;sum(axis=(0,1))把每层压成一个数,大于 0 就说明这层有该器官。跑完你会得到一张类别分布表,如果某个器官像素占比低于 0.5%,基本可以预判训练时它会被背景淹没,需要上加权损失或者重采样。这一步不做,后面 loss 不降你都不知道是模型问题还是数据问题。
2.2 影像与掩码的配准检查:层厚、方向和仿射矩阵
腹部 CT 数据集最隐蔽的坑是影像和掩码的几何信息不一致。NIfTI 文件头里有仿射矩阵(affine),它决定了体素坐标到真实世界坐标的映射。如果影像和掩码的 affine 不同,或者一个做了重采样另一个没做,你在屏幕上看着对齐,实际体素网格已经错位了。我见过最典型的翻车是:影像层厚 1mm,掩码被重采样到 5mm,直接叠上去器官边界整体偏移好几毫米,模型学出来的边界全是糊的。
检查方法很直接,读两个文件的 affine 和 shape 做对比:
import nibabel as nib import numpy as np img = nib.load("images/case_001.nii.gz") msk = nib.load("masks/case_001.nii.gz") print("image shape:", img.shape, "mask shape:", msk.shape) print("affine allclose:", np.allclose(img.affine, msk.affine, atol=1e-3)) print("zooms img:", img.header.get_zooms()) print("zooms msk:", msk.header.get_zooms()) # 若 shape 不一致,说明至少有一个被重采样过,需要统一到同一网格 if img.shape != msk.shape: print("WARNING: shape mismatch, need resample mask to image grid")get_zooms()返回体素物理尺寸,单位毫米。np.allclose用atol=1e-3容忍浮点误差。如果 shape 不一致,正确做法是把掩码用最近邻插值重采样到影像网格,千万别用线性插值——标签是整数类别,线性插值会造出 1.5 这种不存在的类别值。这一步确认完,才能进入下一步预处理,否则后面所有训练都是在错误对齐的数据上做无用功。
3. 从原始 NIfTI 到训练张量:窗宽窗位、归一化与数据增强的落地参数
3.1 腹部 CT 的窗宽窗位设置与 HU 值归一化
CT 像素原始值是 HU(Hounsfield Unit),空气约 -1000,水是 0,肝脏软组织大概在 40 到 60,骨头能到 1000 以上。直接把原始 HU 丢给网络,动态范围太大,模型很难学。医学影像的标准做法是加窗,把关注的组织范围映射到 0 到 1。腹部多脏器分割最常用的腹部窗是窗宽 400、窗位 50,也就是把 -150 到 250 这段映射到 0 到 1,肝脏、脾脏、肾脏、胰腺的对比度在这个窗口下最清晰。
import numpy as np def apply_window(volume, window_width=400, window_level=50): """将 HU 值按腹部窗映射到 [0,1]""" lower = window_level - window_width / 2 # -150 upper = window_level + window_width / 2 # 250 volume = np.clip(volume, lower, upper) volume = (volume - lower) / (upper - lower) return volume.astype(np.float32) # volume 为读出的 HU 数组 norm_vol = apply_window(volume, window_width=400, window_level=50)np.clip先把超出窗口的值截断,再做线性拉伸。参数上,窗宽决定对比度范围,窗位决定中心位置。如果你的数据集里胰腺分割效果特别差,可以试试窄一点的窗(窗宽 300、窗位 40),因为胰腺和周围脂肪的对比度在窄窗下更明显。但要注意,窗宽窗位是全局操作,不能每个器官单独加窗,否则输入就不一致了。归一化之后建议再算一遍全局均值和标准差做 z-score,很多分割网络对输入分布敏感,这一步能加快收敛。
3.2 针对小器官的采样策略与增强参数
腹部多脏器数据集有个天然的不均衡:肝脏体积大,胰腺又细又长,像素占比可能差几十倍。如果按整卷随机采样切片,胰腺出现的层数少,模型见到的正样本就少。我一般用两种策略组合:一是按器官出现层做加权采样,含胰腺的层提高采样概率;二是数据增强时对小器官做针对性处理。
import random import numpy as np from scipy.ndimage import rotate, zoom def weighted_slice_sample(mask_vol, pancreas_idx=3, p_pancreas=0.5): """含胰腺的层以更高概率被采样""" slices_with_pancreas = [i for i in range(mask_vol.shape[-1]) if (mask_vol[..., i] == pancreas_idx).any()] all_slices = list(range(mask_vol.shape[-1])) if random.random() < p_pancreas and slices_with_pancreas: return random.choice(slices_with_pancreas) return random.choice(all_slices) def augment_slice(img, mask): """随机旋转 + 缩放,图像用双线性,掩码用最近邻""" angle = random.uniform(-15, 15) scale = random.uniform(0.9, 1.1) img_aug = rotate(img, angle, reshape=False, order=1) mask_aug = rotate(mask, angle, reshape=False, order=0) img_aug = zoom(img_aug, scale, order=1) mask_aug = zoom(mask_aug, scale, order=0) return img_aug, mask_augweighted_slice_sample里p_pancreas=0.5表示一半概率优先抽含胰腺的层,这个值可以按你数据集的器官占比调,胰腺越稀少就调越高,但别超过 0.7,否则背景多样性不够。增强函数里最关键的是插值阶数:图像用order=1双线性,掩码必须用order=0最近邻,这是血泪经验,用错一次标签就废了。旋转角度控制在 ±15 度,腹部器官位置相对固定,转太多会造出不合理的解剖结构。缩放范围 0.9 到 1.1 模拟不同体型,再大就失真了。
4. 训练多脏器分割模型:损失函数选型与评估指标怎么看
4.1 Dice + CE 组合损失为什么比单用交叉熵稳
多脏器分割里交叉熵(CE)和 Dice 损失各有短板。CE 对每个像素一视同仁,背景像素多,梯度就被背景主导,小器官学不动;Dice 直接优化重叠度,对小器官友好,但训练早期梯度不稳定,容易震荡。常见做法是两者加权组合,我一般用0.5 * CE + 0.5 * Dice,这个配比在腹部多器官上比较稳。
import torch import torch.nn as nn import torch.nn.functional as F class DiceCELoss(nn.Module): def __init__(self, ce_weight=0.5, dice_weight=0.5, ignore_bg=False): super().__init__() self.ce_weight = ce_weight self.dice_weight = dice_weight self.ignore_bg = ignore_bg def forward(self, logits, target): # logits: [B, C, H, W], target: [B, H, W] ce = F.cross_entropy(logits, target, reduction="mean") probs = F.softmax(logits, dim=1) num_classes = logits.shape[1] dice = 0.0 start = 1 if self.ignore_bg else 0 for c in range(start, num_classes): p = probs[:, c] t = (target == c).float() inter = (p * t).sum() union = p.sum() + t.sum() dice += 1 - (2 * inter + 1e-5) / (union + 1e-5) dice = dice / (num_classes - start) return self.ce_weight * ce + self.dice_weight * diceignore_bg=True时跳过背景类算 Dice,因为背景占比太大,算进去会掩盖小器官的真实表现。1e-5是平滑项,防止分母为零。参数上,如果发现胰腺 Dice 一直上不去,可以把 dice_weight 提到 0.7,但 CE 别低于 0.3,否则训练不稳定。这个损失函数配合前面的加权采样,是我在腹部多器官任务上比较常用的一套组合。
4.2 Dice、HD95 和器官级评估的正确读法
评估不能只看一个平均 Dice。平均 Dice 会被大器官拉高,肝脏 Dice 0.95、胰腺 Dice 0.6,平均下来还有 0.85,看着不错,实际胰腺根本不能用。正确做法是逐器官报告 Dice,同时看 HD95(95% 豪斯多夫距离),它衡量边界最大偏差,对临床很关键。
| 指标 | 含义 | 腹部场景参考值 | 注意点 |
|---|---|---|---|
| Dice | 重叠度 | 肝脏>0.94,肾脏>0.90,胰腺>0.75 | 小器官单独看 |
| HD95 | 边界95%分位距离 | 肝脏<5mm,胰腺<15mm | 对边界敏感 |
| IoU | 交并比 | 与Dice趋势一致 | 类别不均衡时偏低 |
| 体积误差 | 预测体积与真值差 | 肝脏<5%,胰腺<15% | 临床关注 |
读表的时候注意,胰腺的 HD95 天然比肝脏大,因为胰腺边界本身就模糊,标注者之间的一致性也低,所以别拿肝脏的标准要求胰腺。如果某个器官 Dice 突然掉到 0.3 以下,先别调模型,回去查这个器官的标签是不是在部分病例里漏标了——漏标会让模型学到「有时这个器官不存在」,输出变得不稳定。
5. 多脏器分割的避坑与排查:标注、显存、过拟合的常见翻车现场
5.1 标签漏标与类别错位
现象:训练 loss 正常下降,但验证时某个器官的预测掩码时有时无,Dice 波动极大。原因:部分病例的该器官没有标注,模型把「未标注」当成了「不存在」,学到矛盾的监督信号。解决:训练前用第 2 章的统计脚本逐病例检查每个器官的像素数,对缺失器官的病例要么剔除,要么在损失里对该器官设 ignore 掩码,别让模型为没标注的器官背锅。
5.2 显存不够导致的 batch 和 patch 取舍
现象:想用 512×512 整层训练,batch 只能设 1,训练极慢且 BatchNorm 统计不稳。原因:腹部 CT 单层分辨率高,整层加多通道特征图显存吃紧。解决:改用 patch 训练,从每层随机裁 256×256 或 320×320,batch 能提到 8 到 16,BatchNorm 才有效。推理时用滑窗加重叠,重叠率设 0.5,再把各 patch 概率图平均,边界拼接处不会出现明显接缝。
5.3 过拟合:训练 Dice 0.95 验证 0.7
现象:训练集 Dice 很快冲到 0.95,验证集卡在 0.7 上不去。原因:数据量小、增强弱,模型记住了训练病例的解剖特征。解决:加强增强(弹性形变、随机亮度对比度),加 Dropout 或 DropBlock,早停按验证集器官平均 Dice 而不是总 loss。如果还是过拟合,考虑冻结编码器前几层用预训练权重,医学影像上 ImageNet 预训练虽然域差异大,但低层边缘特征还是能用的。
5.4 层间不连续导致的 3D 预测断裂
现象:2D 逐层预测再堆成 3D,器官在层与层之间出现锯齿或断裂。原因:2D 模型没有层间上下文,相邻层预测独立。解决:要么直接用 3D 网络(显存允许的话),要么在 2D 输出后做后处理,用 3D 连通域取最大连通块,把孤立的假阳性小区域去掉。这个后处理对脾脏和肾脏特别有效,能明显降假阳性。
5.5 窗宽窗位设错导致小器官消失
现象:胰腺在输入图像里几乎看不见,模型自然学不好。原因:用了默认的肺窗或骨窗,腹部软组织对比度被压没了。解决:确认用腹部窗(窗宽 400、窗位 50),可视化几张输入图确认胰腺和周围脂肪有区分度。这一步花五分钟,能省后面几小时的调参。
6. 把多脏器分割推到可用:滑窗推理、后处理与一个提点小技巧
模型训练完只是半成品,真正决定能不能用的是推理和后处理。腹部 CT 体积大,整卷塞不进显存,标准做法是滑窗推理。窗口大小跟训练 patch 一致,比如 320×320,步长设 160(重叠 50%),每个窗口输出 softmax 概率,累加到全图概率体里,最后按通道取 argmax。重叠区做平均能压掉边界伪影,这一步不做,拼接处会出现明显的方格状错位。
import torch import numpy as np def sliding_window_inference(model, volume, window=320, stride=160, num_classes=6): """volume: [D, H, W] 归一化后的 CT""" model.eval() D, H, W = volume.shape prob = np.zeros((num_classes, D, H, W), dtype=np.float32) count = np.zeros((D, H, W), dtype=np.float32) with torch.no_grad(): for d in range(0, D): for h in range(0, H - window + 1, stride): for w in range(0, W - window + 1, stride): patch = volume[d, h:h+window, w:w+window] t = torch.from_numpy(patch).unsqueeze(0).unsqueeze(0).float() out = torch.softmax(model(t), dim=1).squeeze(0).numpy() prob[:, d, h:h+window, w:w+window] += out count[d, h:h+window, w:w+window] += 1 count[count == 0] = 1 prob /= count return prob.argmax(axis=0)stride=160对应 50% 重叠,重叠越高边界越平滑但推理越慢,一般 0.5 够用。count记录每个体素被覆盖次数,做归一化。推理完拿到整数掩码后,做一步 3D 连通域后处理:对每个器官通道,只保留最大连通块,去掉零散假阳性。这个操作对肾脏和脾脏提升明显,因为这两个器官形态紧凑,假阳性通常是孤立小点。
再分享一个提点小技巧:如果胰腺 Dice 始终差一口气,试试在损失里给胰腺单独加一个类别权重,或者在采样时把含胰腺的 patch 再上采样一倍。胰腺是腹部多脏器分割里公认最难的目标,边界模糊、体积小、形态变异大,别指望它和肝脏一个水平。我自己的习惯是每次换数据集先跑一遍第 2 章的统计脚本,把器官占比和层数分布打印出来贴在实验记录里,后面任何指标异常都回头对这张表。这个习惯帮我省了无数次瞎调参的时间。希望帮到你。
本文还有配套的精品资源,点击获取