CT影像肝脏肿瘤像素级分割:从DICOM到掩膜的完整实战指南

发布时间:2026/10/2 3:13:03
CT影像肝脏肿瘤像素级分割:从DICOM到掩膜的完整实战指南 简介这份CT肝脏肿瘤像素级分割数据集面向医学影像AI研究者、计算机辅助诊断开发者及肝胆外科相关科研人员用于解决肝脏实质与肿瘤自动分割、肿瘤负荷定量分析等任务。数据涵盖1900例腹部增强CT包含门静脉期、动脉期等多期相覆盖肝细胞癌、转移瘤等多种肿瘤类型与尺寸由影像科与肝胆外科专家联合完成像素级标注并区分背景、肝脏实质、肝脏肿瘤三类目标。资源包共2000个文件以png、jpg图像为主另含1个txt说明与1个py分析脚本压缩包约32.06MB已做多期相配准、体素重采样与强度归一化并划分训练集与验证集影像与掩膜均为NIfTI格式。随附脚本支持肿瘤负荷统计、多期相对比、三维可视化及分割性能评估可直接用于nnUNet、3D U-Net等网络训练。已有38人学习适合开展肝脏肿瘤AI、手术规划与疗效监测研究。1. 肝脏 CT 里把肿瘤“抠”出来这套像素级分割资源到底能干什么如果你在影像科或手术规划团队待过大概见过这种场面放射科医生对着几十层腹部 CT 逐层勾画肝脏和肿瘤边界一套数据下来眼睛发花不同医生勾出来的范围还常常对不上。这套「CT 影像肝脏肿瘤像素级分割」资源解决的就是这件事——把肝脏区域和肿瘤区域从 CT 体数据里逐像素标出来输出和原图同尺寸的分割掩膜直接喂给三维重建、体积测算和手术切除范围评估。它面向的是做医学图像分割的算法工程师、影像组学研究者以及需要批量处理 CT 数据做术前规划的技术人员。核心任务就两个肝脏分割Liver和肿瘤分割Tumor像素级意味着输出不是方框而是逐像素标签边界精度直接决定后续体积估算靠不靠谱。资源本身是一套可复现的分割流程与配套代码不是单纯的理论讲解拿到手就能在自己的数据上跑起来。2. 像素级分割的技术底座从窗宽窗位到损失函数怎么选2.1 为什么肝脏肿瘤分割不能直接套自然图像那套腹部 CT 是单通道灰度体数据HU 值范围大致在 -1000 到 3000 之间肝脏实质大概落在 4070 HU肿瘤根据类型不同可能偏高或偏低和周围肌肉、血管的对比度远不如自然图像里猫和狗那么分明。更麻烦的是部分容积效应——肿瘤边缘那一圈像素往往是肝实质和肿瘤的混合HU 值介于两者之间人眼都难判模型更纠结。所以做这套分割第一步不是搭网络而是把窗宽窗位调对。常见做法是先用腹部窗窗宽 350、窗位 50把肝脏和肿瘤的对比拉出来再决定要不要做 HU 值截断。我一般会把输入截断到 [-200, 300] 再归一化到 [0,1]超出这个范围的骨头、气体对肝脏肿瘤分割基本是干扰。另一个坑是层厚。CT 数据层厚从 0.5mm 到 5mm 都有层厚越大Z 方向分辨率越差肿瘤上下边界越模糊。如果资源里带的数据层厚不统一训练前必须重采样到各向同性否则模型学到的边界是“糊”的。重采样这一步很多人偷懒跳过结果验证集 Dice 看着还行一到实际手术规划就发现肿瘤体积算出来偏大或偏小。2.2 网络结构选型2D 还是 2.5D 还是 3D这是被问最多的问题。纯 2D 分割逐层送进 U-Net显存友好、数据增强灵活但丢了层间连续性肿瘤在相邻层之间的形态变化学不到。纯 3D 网络如 3D U-Net、V-Net能利用空间上下文但对显存要求高小数据集容易过拟合。折中方案是 2.5D把相邻几层堆成多通道输入比如取当前层上下各 2 层共 5 通道网络还是 2D 的但有了局部 3D 感受野。我一般会这么选数据量少于 50 例优先 2.5D 强增强数据量上百例且显存够≥24G直接上 3D。这套资源里给的基线是 2.5D U-Net输入 5 通道输出 3 类背景、肝脏、肿瘤。选它的理由是复现门槛低单卡 12G 就能跑而且对层厚不敏感——因为相邻层堆叠本身就在做隐式的层间平滑。2.3 损失函数Dice 和交叉熵怎么配肝脏肿瘤分割有个天然难题肿瘤像素占比可能不到 1%背景占绝对多数。只用交叉熵模型会倾向于全预测背景准确率看着高Dice 惨不忍睹。所以必须上 Dice Loss 或它的变体。常见组合是 Dice Loss 交叉熵按权重相加比如 0.7 倍 Dice 加 0.3 倍 CE。Dice 负责拉高重叠度CE 负责稳定梯度。但 Dice Loss 对极小目标不稳定肿瘤特别小时梯度会剧烈波动。我一般会加一个 Tversky Loss 作为备选通过调节 alpha 和 beta 控制假阳和假阴的惩罚权重。肝脏肿瘤分割里假阴漏掉肿瘤比假阳把正常组织误判成肿瘤后果更严重所以 beta 设大一点比如 alpha0.3、beta0.7让模型对漏检更敏感。import torch import torch.nn as nn class DiceBCELoss(nn.Module): def __init__(self, weight_dice0.7, weight_ce0.3): super().__init__() self.weight_dice weight_dice self.weight_ce weight_ce self.bce nn.BCEWithLogitsLoss() def forward(self, pred, target): # pred: (B, C, H, W) logits, target: (B, C, H, W) 0/1 pred_sigmoid torch.sigmoid(pred) # 计算 Dice按通道独立算再平均 intersection (pred_sigmoid * target).sum(dim(2, 3)) union pred_sigmoid.sum(dim(2, 3)) target.sum(dim(2, 3)) dice (2. * intersection 1e-6) / (union 1e-6) dice_loss 1 - dice.mean() ce_loss self.bce(pred, target) return self.weight_dice * dice_loss self.weight_ce * ce_loss这段代码里weight_dice和weight_ce是最关键的两个参数。肿瘤分割任务里我通常把 Dice 权重调到 0.70.8CE 压到 0.20.3。1e-6是平滑项防止分母为零。注意BCEWithLogitsLoss内部已经做了 sigmoid所以传入的是 logits 不是概率别在外面又 sigmoid 一次否则梯度会出问题——这个坑我见过不止一个人踩。3. 从 DICOM 到掩膜数据预处理的完整链路3.1 DICOM 读取与 HU 值还原CT 数据一般以 DICOM 序列形式存储每个文件是一层。读取时不能直接拿像素值必须用RescaleSlope和RescaleIntercept转成 HU 值。常见做法是用 pydicom 或 SimpleITK。SimpleITK 更省事它能自动处理序列排序和方向信息。import SimpleITK as sitk import numpy as np def load_dicom_series(dicom_dir): reader sitk.ImageSeriesReader() series_ids reader.GetGDCMSeriesIDs(dicom_dir) if not series_ids: raise ValueError(未找到 DICOM 序列) # 取第一个序列多序列时需按需选择 dicom_names reader.GetGDCMSeriesFileNames(dicom_dir, series_ids[0]) reader.SetFileNames(dicom_names) image reader.Execute() # 转成 numpy注意 SimpleITK 顺序是 (z, y, x) volume sitk.GetArrayFromImage(image) # HU 值还原SimpleITK 读出来已是 HU但若用 pydicom 需手动换算 # hu pixel * RescaleSlope RescaleIntercept return volume, imageGetGDCMSeriesIDs返回的是序列 ID 列表一个患者目录下可能有平扫、增强多个序列必须确认取的是哪一期。增强期肿瘤边界更清晰但平扫也有其价值。GetArrayFromImage出来的数组维度是 (层数, 行, 列)和后续网络输入的 (通道, 高, 宽) 差一个维度后面要转置。HU 值还原这步如果用 pydicom 手动做记得先检查RescaleSlope是否存在有些设备默认 slope1、intercept0但增强扫描常常不是。3.2 窗宽窗位与归一化拿到 HU 体数据后先做窗宽窗位截断。腹部窗常用窗宽 350、窗位 50对应范围是 [50-175, 50175] [-125, 225]。但肝脏肿瘤分割我一般会放宽到 [-200, 300]给肿瘤低密度区留余量。def apply_window(volume, window_center50, window_width350): lower window_center - window_width // 2 upper window_center window_width // 2 volume np.clip(volume, lower, upper) # 归一化到 [0, 1] volume (volume - lower) / (upper - lower) return volume.astype(np.float32)window_center和window_width这两个参数直接决定输入对比度。窗宽太窄肝脏内部纹理被压平窗宽太宽肿瘤和肝实质的差异被稀释。我一般会在训练前抽几例可视化一下确认肿瘤边界肉眼可辨再定参数。归一化用 min-max 而不是 z-score因为 HU 值有物理意义z-score 会破坏窗宽窗位建立的对比关系。3.3 数据增强别把肿瘤“转”没了医学图像增强和自然图像不一样。水平翻转、旋转、缩放可以用但要注意左右肝叶的解剖差异——翻转后肝脏形态可能不符合解剖学不过对分割任务影响不大模型学的是纹理和边界。真正要小心的是弹性形变强度太大会把肿瘤边界揉糊反而有害。我一般用这些增强随机旋转 ±15 度、随机缩放 0.91.1、随机亮度对比度扰动、随机加高斯噪声。弹性形变只在数据量极少时用且 alpha 控制在 10 以内。另外增强必须同步作用于图像和掩膜用同一个随机种子否则图像转了掩膜没转模型直接学废。import random import numpy as np from scipy.ndimage import rotate, zoom def augment(image, mask): # 随机旋转 angle random.uniform(-15, 15) image rotate(image, angle, axes(1, 2), reshapeFalse, order1) mask rotate(mask, angle, axes(1, 2), reshapeFalse, order0) # 随机缩放 scale random.uniform(0.9, 1.1) image zoom(image, (1, scale, scale), order1) mask zoom(mask, (1, scale, scale), order0) # 裁剪或填充回原尺寸此处省略 return image, maskorder1是双线性插值用于图像order0是最近邻插值用于掩膜——掩膜不能插值出 0.5 这种值必须保持整数标签。这个细节很多人忽略结果掩膜里出现小数算 Dice 时直接报错。4. 训练与推理让模型在肝脏肿瘤上真正收敛4.1 数据划分与类别不平衡处理训练集、验证集、测试集一般按 7:1:2 划分但必须按患者划分不能按层划分。同一患者的相邻层高度相似如果按层随机分验证集里会出现训练集患者的“近邻层”Dice 虚高实际泛化能力被高估。这个坑在医学图像里太常见了血泪经验。类别不平衡除了损失函数还可以用加权采样。肿瘤层占比低可以在 DataLoader 里给含肿瘤的层更高采样权重比如含肿瘤层权重 3不含肿瘤层权重 1。但权重别设太高否则模型过拟合肿瘤层肝脏分割反而退化。from torch.utils.data import WeightedRandomSampler def make_sampler(labels): # labels: 每层是否含肿瘤的列表 weights [3.0 if l 1 else 1.0 for l in labels] sampler WeightedRandomSampler(weights, num_sampleslen(weights), replacementTrue) return samplerreplacementTrue表示有放回采样保证每个 epoch 样本数一致。权重 3:1 是我常用的起点如果验证集肿瘤 Dice 还是上不去可以调到 5:1但再高就容易过拟合。4.2 训练循环与学习率调度优化器用 Adam 或 AdamW初始学习率 1e-4配合余弦退火或 ReduceLROnPlateau。batch size 根据显存来2.5D 输入 5 通道 512×51212G 显存大概能跑 batch size 8。训练轮数一般 100200 epoch早停 patience 设 20。import torch from torch.optim import AdamW from torch.optim.lr_scheduler import ReduceLROnPlateau model UNet2_5D(in_channels5, out_channels3).cuda() optimizer AdamW(model.parameters(), lr1e-4, weight_decay1e-5) scheduler ReduceLROnPlateau(optimizer, modemax, factor0.5, patience10) criterion DiceBCELoss(weight_dice0.7, weight_ce0.3) for epoch in range(200): model.train() for image, mask in train_loader: image, mask image.cuda(), mask.cuda() pred model(image) loss criterion(pred, mask) optimizer.zero_grad() loss.backward() optimizer.step() # 验证 val_dice validate(model, val_loader) scheduler.step(val_dice) if early_stop(val_dice, patience20): breakweight_decay1e-5是轻量正则医学图像数据少正则太强会欠拟合。ReduceLROnPlateau的modemax因为监控的是 Dice越大越好。factor0.5表示每次衰减一半patience10是连续 10 个 epoch 没提升才衰减。注意验证指标要用肿瘤 Dice 而不是平均 Dice否则肝脏分割主导肿瘤被忽略。4.3 推理与后处理连通域和形态学推理时用滑动窗口或整图送入。整图送入简单但 512×512 的 CT 层直接送进去显存吃得消问题不大。输出是 3 通道 logits取 argmax 得到标签图。后处理常用两步保留最大连通域去掉零星假阳、闭运算填小洞。import numpy as np from scipy.ndimage import binary_fill_holes, label def postprocess(pred_mask): # pred_mask: (H, W) 0/1/2 tumor (pred_mask 2).astype(np.uint8) # 保留最大连通域 labeled, num label(tumor) if num 1: sizes np.bincount(labeled.ravel()) sizes[0] 0 largest sizes.argmax() tumor (labeled largest).astype(np.uint8) # 填洞 tumor binary_fill_holes(tumor).astype(np.uint8) pred_mask[tumor 1] 2 return pred_masklabel函数做连通域标记bincount统计各连通域大小argmax取最大。注意sizes[0]0是把背景排除掉。填洞用binary_fill_holes对肿瘤内部坏死区特别有用——有些肿瘤中心坏死模型可能预测成背景填洞能补回来。但填洞别过度否则会把肿瘤旁边的血管误填进去。5. 避坑与排查肝脏肿瘤分割里那些让人翻车的细节5.1 现象验证集 Dice 很高测试集一塌糊涂原因按层划分数据同一患者的相邻层同时出现在训练和验证集模型记住了患者特异性纹理而不是通用边界特征。解决严格按患者 ID 划分训练前打印一下训练集和验证集的患者列表确认无交集。5.2 现象肿瘤 Dice 始终在 0.3 以下肝脏 Dice 正常原因肿瘤像素太少Dice Loss 梯度被肝脏主导。解决给肿瘤通道单独加权或在损失函数里对肿瘤类乘一个系数比如肿瘤 Dice 权重是肝脏的 3 倍。另外检查一下掩膜里肿瘤标签是不是被错误映射成了背景。5.3 现象推理结果在肿瘤边缘出现一圈“毛刺”原因输入归一化时窗宽窗位设得太窄肿瘤边缘的混合像素被截断成同一值模型学不到渐变边界。解决放宽窗宽到 400 以上或改用多窗融合把不同窗宽下的图像堆成多通道输入。5.4 现象训练 loss 震荡剧烈Dice 忽高忽低原因学习率太大或 batch size 太小导致梯度噪声大。解决学习率降到 5e-5batch size 尽量提到 8 以上。如果显存不够用梯度累积模拟大 batch。5.5 现象后处理后肿瘤区域被“切碎”成多块原因连通域保留时只留了最大块但有些肿瘤本身就是多灶性的。解决不要无脑保留最大连通域改成保留面积大于阈值比如 50 像素的所有连通域或者根据临床先验判断是否多灶。6. 进阶技巧用 TTA 和模型集成把边界再抠细一点训练完一个模型只是起点。肝脏肿瘤分割对边界敏感单模型推理往往在肿瘤包膜处差几个像素。我一般会加测试时增强TTA对同一层做水平翻转、小角度旋转分别推理后再把结果反变换回来取平均。TTA 能把肿瘤 Dice 拉高 12 个百分点代价是推理时间翻几倍但手术规划场景对精度要求高这点时间值得花。def tta_predict(model, image): # image: (1, C, H, W) preds [] # 原始 preds.append(torch.softmax(model(image), dim1)) # 水平翻转 image_flip torch.flip(image, dims[3]) pred_flip torch.softmax(model(image_flip), dim1) preds.append(torch.flip(pred_flip, dims[3])) # 小角度旋转用 affine_grid 实现此处省略 return torch.mean(torch.stack(preds), dim0)torch.flip的dims[3]对应宽度维度翻转后推理再翻回来保证空间对齐。取平均用 softmax 后的概率图不要用 argmax 后的标签图否则投票没有意义。模型集成是另一个提升点。训练 35 个不同初始化的模型推理时概率图平均。集成对肿瘤边界的稳定性提升明显尤其是那些单模型预测忽大忽小的病例。但集成模型多了推理成本线性增长实际部署时我一般控制在 3 个。还有一个容易被忽略的技巧把肝脏分割结果作为肿瘤分割的先验。先跑肝脏分割把肝脏区域抠出来再在肝脏 ROI 内做肿瘤分割。这样肿瘤分割的搜索空间小了很多假阳把肝脏外组织误判成肿瘤大幅减少。代价是肝脏分割漏掉的区域肿瘤也找不回来所以肝脏分割的召回率要调高宁可多包含一点。验证分割质量不能只看 Dice。我习惯再算两个指标HD9595% 豪斯多夫距离和 ASSD平均对称表面距离。Dice 对内部填充敏感HD95 对边界更敏感。肿瘤分割里 HD95 超过 10mm 基本就不能用于手术规划了得回去查边界。另外体积误差也要看算一下预测肿瘤体积和标注体积的百分比差异超过 15% 就要警惕。从那以后我每次跑完训练都强制走一遍「按患者划分检查 → 肿瘤 Dice 单独看 → HD95 算一遍 → 后处理前后对比可视化」这个流程少一步都可能把有问题的模型放进规划环节。希望帮到你。本文还有配套的精品资源点击获取