基于Unet的LITS肝肿瘤分割:nii数据预处理与训练避坑指南

发布时间:2026/10/27 17:51:27
基于Unet的LITS肝肿瘤分割:nii数据预处理与训练避坑指南 简介基于Unet的肝肿瘤分割项目聚焦LITS数据集轴位面横断面切片任务mask中1为肝脏、2为肿瘤、0为背景适合医学图像分割学习者与科研人员复现和扩展。资源共2000个文件核心为8个Python脚本可完成训练、验证与推理1983张PNG切片提供完整训练/测试图像另有5个XML、3个TXT及1个README分别用于标注信息、类别配置和项目说明整包约680.5MB。训练仅10个epoch全局像素准确率已达0.988、mIoU达0.838若增加epoch有望进一步提升。代码自动将mask灰度值写入txt并动态定义Unet输出通道支持多类别扩展训练时随机缩放0.51.5倍实现多尺度增强学习率采用cos衰减run_results中可查看损失与IoU曲线训练日志含每类IoU、recall、precision及全局准确率等指标。已有302人学习适合希望快速上手肝肿瘤分割或借鉴Unet训练流程的读者。1. 基于Unet对肝肿瘤分割LITS切片的分割项目先看它解决什么、卡在哪基于Unet对肝肿瘤分割LITS切片的分割项目我在本地完整跑过一遍后最直接的感受是搭Unet网络只占整个工作量两成剩下八成全耗在LITS数据的读取、切片化、标签对齐和训练策略上。LITS这个医学影像语义分割数据集的标注里既有肝脏也有肿瘤原始CT是三维nii体数据必须切成网络能吃的二维切片同时保证标签不错位、肿瘤区域不被稀释这一步直接决定模型最终能不能用。标题里说包含数据集、完整代码、训练的结果文件所以这篇笔记按同样的交付结构拆数据读取与切片化怎么做Unet要针对小目标肿瘤做哪些改动训练时肝脏Dice和肿瘤Dice为什么要分开统计以及结果文件里哪些数值可以直接信、哪些要留个心眼。适合想在本地从零跑通一个LITS分割项目的从业者也适合准备拿医学影像数据做分割训练、但还没踩过nii预处理坑的读者。我用的是最常见的2D Unet加切片方案单卡12GB显存就能跑完整个训练流程显存需求没有想象中高。2. LITS数据读取与切片化把三维nii体数据切成可训练的二维切片2.1 先认清LITS的原始数据长什么样LITSLiver Tumor Segmentation挑战赛的公开训练数据每例包含两个nii文件一个是腹部CT平扫的原始体数据另一个是对应的标注文件。CT数据的值不是普通图像的0到255而是CT值HUHounsfield Unit范围动辄上千空气接近-1000骨骼能到几百上千。标注文件里每个像素是一个类别标签背景是0肝脏是1肿瘤是2。这里有个容易误判的点标注是“肝脏”和“肿瘤”两个目标同时存在的如果你只做肿瘤分割不能直接把标签改成“肿瘤1、其余0”因为肿瘤几乎不会脱离肝脏单独出现模型需要肝脏作为上下文信息。拿到数据后第一件事不是写网络而是先看体素间距。CT扫描的层厚经常不一致有的病例层厚1mm有的5mm如果你直接读进来按原始尺寸训练模型会把不同层间距离当成同样的空间关系泛化会出问题。常见的做法是把所有体数据重采样到各向同性分辨率比如1.0×1.0×1.0mm这样不同病例的切片在空间上才有可比性。另外要注意SimpleITK读取nii后数组顺序是(Z, Y, X)切片化沿Z轴切也就是横断面。方向搞反了后面所有环节都会跟着错。2.2 用SimpleITK读取nii并重采样到各向同性我一般用SimpleITK做这一步它读取nii不需要额外关心头文件里的方向信息直接读就行。重采样时有两个关键决策点CT图像用线性插值保留灰度过渡标注文件必须用最近邻插值否则会产生标签值1.5这种“伪标签”训练时类别索引直接越界。import SimpleITK as sitk import numpy as np def read_and_resample(vol_path, seg_path, target_spacing(1.0, 1.0, 1.0)): vol sitk.ReadImage(vol_path) seg sitk.ReadImage(seg_path) # 以CT图像为参考标注文件如果origin/direction和CT不一致必须对齐到CT上 # 否则重采样后标签和图像会发生几十像素级别的偏移 original_spacing vol.GetSpacing() original_size vol.GetSize() new_size [ int(round(original_size[i] * original_spacing[i] / target_spacing[i])) for i in range(3) ] resampler sitk.ResampleImageFilter() resampler.SetOutputSpacing(target_spacing) resampler.SetOutputSize(new_size) resampler.SetOutputOrigin(vol.GetOrigin()) resampler.SetOutputDirection(vol.GetDirection()) resampler.SetInterpolator(sitk.sitkLinear) vol_resampled resampler.Execute(vol) # 标注文件用最近邻线性插值会在类别边界产生0、1、2之外的值 seg_resampler sitk.ResampleImageFilter() seg_resampler.SetOutputSpacing(target_spacing) seg_resampler.SetOutputSize(new_size) seg_resampler.SetOutputOrigin(vol.GetOrigin()) seg_resampler.SetOutputDirection(vol.GetDirection()) seg_resampler.SetInterpolator(sitk.sitkNearestNeighbor) seg_resampled seg_resampler.Execute(seg) return sitk.GetArrayFromImage(vol_resampled), sitk.GetArrayFromImage(seg_resampled)这段代码里最值得注意的参数是target_spacing。把它设成1.0×1.0×1.0会让Z轴切片数量增加训练数据变多变慢设成2.0×2.0×2.0会损失小肿瘤的空间细节。我一般先用1.0×1.0×2.0折中保证XY平面精度Z方向别太密。New_size是反推出来的不需要手动指定但要注意如果原图spacing是(0.7, 0.7, 1.2)新size计算出来会带上小数必须round成整数否则SimpleITK会报错。2.3 把三维体数据切成二维切片CT值裁剪与空切片过滤重采样后得到的是三维数组接下来沿Z轴逐层切片。这里有两个必不可少的处理CT值裁剪和空标签过滤。CT值裁剪的意义是把动态范围压缩到模型能学的区间肝脏实质的HU值大约在40到60肿瘤区域由于坏死或脂肪变性往往更低裁剪到[-200, 200]可以同时去掉空气和骨骼这些无关信息。空标签过滤则是把所有像素都是背景的切片丢掉因为这些切片对训练毫无贡献却会显著加剧类别不平衡。def extract_slices(vol_arr, seg_arr, hu_min-200, hu_max200): slices, labels [], [] for z in range(vol_arr.shape[0]): # 这一层没有任何标注就跳过保留包含肝脏或肿瘤的层 if seg_arr[z].max() 0: continue # 先裁剪到肝脏/肿瘤对应的HU范围再线性归一化到0~1 img np.clip(vol_arr[z], hu_min, hu_max) img (img - hu_min) / (hu_max - hu_min) slices.append(img.astype(np.float32)) # 标签里0/1/2分别代表背景/肝脏/肿瘤直接作为类别索引保存 labels.append(seg_arr[z].astype(np.int64)) return np.stack(slices), np.stack(labels)切出来后你会得到一个很现实的数据分布包含肝脏的切片占总切片数的比例不高包含肿瘤的切片更少而且肿瘤往往只占单张切片的1%到5%像素。这个特征会直接影响后续损失函数设计和训练策略。另一个容易忽略的问题是标签中同时存在1和2训练时按三分类处理但计算Dice时肝脏和肿瘤要分开算不能混在一个平均Dice里——肿瘤区域的体积往往只有肝脏的十分之一混在一起算会被肝脏的高Dice掩盖。3. Unet结构与损失函数为什么肿瘤Dice老是上不去3.1 标准Unet的长处和LITS场景下的短板Unet网络结构图在网上能搜到很多版本核心就一句话编码器逐层下采样提取语义特征解码器逐层上采样恢复空间分辨率中间用skip connection把同尺度的编码器特征直接拼到解码器上。这个设计对医学影像分割非常友好因为医学图像里目标器官的位置相对固定、边界清晰低层特征边缘、纹理和高层语义特征都重要skip connection正好两者兼顾。在LITS这种单模态CT、结构相对规整的数据上标准Unet已经能到不错的肝脏Dice不需要一上来就换成Transformer或者加注意力模块。标准Unet真正的问题是在肿瘤这类小目标上。肿瘤在CT切片里经常只有几十个像素经过四层下采样后特征图缩小到原来的1/16小肿瘤在最高层特征图里可能只剩一两个像素点信息基本丢了。所以做LITS项目时不要追求过深的网络四层encoder是性价比最高的选择。我见过有人直接把encoder换成ResNet50的效果没提升多少显存占用倒是翻倍了。3.2 针对LITS的三个小改动类别数、输入尺寸、损失函数对Unet做改进要有边界。我在这个项目里只动三个地方第一输出通道数设为3对应背景、肝脏、肿瘤三个类别这样一次前向能同时得到肝脏和肿瘤分割肿瘤Dice单独统计第二输入切片尺寸统一缩放常见做法是保留CT原始分辨率附近的256×256或512×512不要随便缩到128×128那会让小肿瘤直接消失第三损失函数用多类别Dice损失而不是交叉熵这是整个网络改动里最关键的一环。交叉熵在LITS上的表现很糟因为肿瘤像素占比经常不到1%模型只要把所有像素预测成背景准确率就是99%。Dice损失直接优化分割目标对正负样本不敏感但它也有副作用训练初期梯度比较大loss曲线会震荡。常见做法是把Dice损失和Focal损失加权混合稳定性和小目标敏感度都能兼顾。我的经验是先只用Dice损失如果出现震荡再加Focal。3.3 多类别Dice损失一份可以直接抄的PyTorch实现下面这个实现按类别分别计算Dice然后取平均背景默认跳过。需要注意target是类别索引不是one-hot编码forward里先转成one-hot再按类别计算。import torch import torch.nn.functional as F class MulticlassDiceLoss(torch.nn.Module): def __init__(self, smooth1.0, ignore_backgroundTrue): super().__init__() self.smooth smooth self.ignore_background ignore_background def forward(self, logits, target): # logits: (B, C, H, W)target: (B, H, W)target取值0/1/2 probs F.softmax(logits, dim1) n_classes logits.shape[1] # 把target转为one-hotshape变为(B, C, H, W)方便按通道取 target_onehot F.one_hot(target.long(), num_classesn_classes) target_onehot target_onehot.permute(0, 3, 1, 2).float() dice_sum 0.0 count 0 start 1 if self.ignore_background else 0 for c in range(start, n_classes): p probs[:, c] t target_onehot[:, c] # smooth是数值稳定项防止除零 intersection (p * t).sum() dice_sum 1 - (2 * intersection self.smooth) / ( p.sum() t.sum() self.smooth ) count 1 return dice_sum / count这里的smooth参数建议设成1.0太小在训练初期会除零太大比如10.0会让Dice值失真损失值对分割质量的反馈变迟钝。ignore_background设为True因为背景像素占了绝大多数把背景也算进Dice会把损失值撑得很好看但实际肿瘤分割能力根本看不出来。如果你要严格复现LITS的评估方式还需注意Dice有两种统计口径按像素全局统计和按病例先算Dice再取平均LITS官方通常以病例级Dice为准和这个损失函数里按整批统计的方式不完全一样。4. 训练流程与结果文件从数据划分到权重保存的完整管线4.1 数据划分必须按病例切分不能按切片随机切训练自己的数据集时数据划分方式直接决定结果文件里的Dice是真实可信还是自欺欺人。LITS项目最常见的错误是按切片随机划分训练集和验证集同一个病例的相邻切片非常相似随机划分后训练集里出现了验证集的“近亲”验证Dice能到0.93但换一批新数据立刻掉到0.4以下。正确做法是先把病例编号列出来按病例划分保证同一个病例的所有切片只出现在训练集或验证集里。130例左右的数据我一般按8:2划分验证集留20例左右。还可以用5折交叉验证每折训一个模型五个权重文件取验证集Dice平均值评估。代价是训练时间五倍如果机器只够训一轮固定划分够用了。4.2 Dataset类与训练循环保存权重只看肿瘤Dice切片数据准备好后用一个轻量Dataset类把图像和标签包起来训练循环里最需要坚持的一条原则是保存模型的依据是验证集肿瘤Dice不是训练loss也不是平均Dice。理由前面说过肿瘤才是这个项目真正要分割的目标肝脏Dice再高不代表模型有临床价值。from torch.utils.data import Dataset, DataLoader class LitsSliceDataset(Dataset): def __init__(self, images, labels): self.images images self.labels labels def __len__(self): return len(self.images) def __getitem__(self, idx): img self.images[idx][None, :, :] # (1, H, W) label self.labels[idx] return img, label # 训练参数参考256x256输入、batch8、单卡12GB model UNet(in_channels1, n_classes3).cuda() optimizer torch.optim.Adam(model.parameters(), lr1e-3) scheduler torch.optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max80) loss_fn MulticlassDiceLoss() best_tumor_dice 0.0 for epoch in range(80): model.train() for imgs, labels in loader: imgs, labels imgs.cuda(), labels.cuda() logits model(imgs) loss loss_fn(logits, labels) optimizer.zero_grad() loss.backward() optimizer.step() scheduler.step() # 验证集单独统计肿瘤Dice tumor_dice, liver_dice evaluate(model, val_loader) if tumor_dice best_tumor_dice: torch.save(model.state_dict(), best_model.pth) best_tumor_dice tumor_dice print(fepoch {epoch}: saved, tumor dice {tumor_dice:.4f})上面UNet和evaluate两个函数是占位说明实际项目里需要自己实现。Adam的lr设1e-3在大多数情况下是安全的如果loss震荡可以降到5e-4。CosineAnnealing的T_max要和总epoch一致设80个epoch就让学习率从1e-3余弦下降到接近0。输入尺寸256、batch8是我在12GB显存卡上的参考值如果你的卡只有8GB把batch降到4或者输入缩到224肿瘤Dice的下降幅度在可接受范围内。4.2 训练结果文件里有什么权重、日志、验证Dice、预测nii标题里说包含训练的结果文件实践上这一部分通常有四个东西best_model.pth权重文件、训练日志、验证集评估表、以及所有验证病例的预测nii。权重文件没什么好说的PyTorch的state_dict格式。训练日志要记录每个epoch的训练loss、肝脏Dice、肿瘤Dice、学习率我见过太多人只记录loss结果训练完了根本解释不了为什么权重是好是坏。验证集评估表是判断训练结果可不可信的入口至少要有两列肝脏Dice和肿瘤Dice一张表格里列出每个验证病例的数值最后一行是平均值。只看一个整体平均值是不够的肿瘤Dice在不同病例上波动很大有的病例肿瘤边界模糊、和周围组织对比度低Dice可能只有0.2这才是模型真实的短板。预测nii则是把验证集每个病例的切片预测结果重新拼回三维体数据再用SimpleITK写回nii文件这样可以直接拖到医学影像软件里和原图、标注叠加检查肉眼检查往往能发现Dice统计发现不了的系统性偏差。4.3 训练参数参考表一组能直接开训的默认值参数推荐值说明输入尺寸256×256显存不足时可用224×224不建议低于192Batch Size812GB显存参考值梯度累积可用于更大batch优化器Adam收敛快医学分割项目够用初始学习率1e-3配合CosineAnnealing使用训练轮数80130例左右的数据量下足够过拟合损失函数MulticlassDiceLoss肿瘤目标过小交叉熵不适用模型保存策略按验证集肿瘤Dice不要看平均Dice会被肝脏掩盖这组参数是保守起点不是调优终点。数据增强我一般在训练时加随机水平翻转和10度以内旋转弹性形变虽然对小目标分割有帮助但会明显拖慢训练速度先不加。跑完一轮后再根据验证集表现决定要不要上更强的增强。5. LITS复现避坑指南5个最常见的翻车点5.1 翻车点一标签重采样后出现非整数标签值现象是训练时loss突然变成nan或者报“RuntimeError: Expected dtype int64 for target”之外更奇怪的索引错误。原因是标注文件重采样时用了线性插值标签边界上产生了0.5、1.5、2.5这类非法类别值。这类问题通常一两轮训练就引爆而不是立刻报错。解决的唯一正确办法是标注文件重采样强制用sitk.sitkNearestNeighbor这个没有任何妥协空间。如果你用了其他库做重采样也要确认是否支持最近邻模式。5.2 翻车点二CT值裁剪范围设错肿瘤在切片上根本看不见现象是训练loss正常下降肝脏Dice能到0.9但肿瘤Dice始终在0.1以下。此时把输入切片直接可视化检查如果看到的是整片灰蒙蒙、肝脏和周围组织对比度很低通常是HU裁剪范围没设对。有人为了保留全部信息把裁剪范围设到[-1000, 1000]结果肝脏和肿瘤的差值被压缩到极窄的灰度带里。CT值裁剪范围实际是在做特征选择[-200, 200]是经过大量LITS复现验证的区间肝脏、肿瘤、腹壁软组织的对比度都保留得不错。调整区间时记得归一化公式要同步改。5.3 翻车点三训练集和验证集混了同一个病例的切片现象最隐蔽验证Dice高达0.85但推理时模型对没见过的病例表现极差。原因前面提过按切片而不是按病例划分数据集。这个问题在LITS上尤为严重因为同一病例的相邻切片结构几乎一样模型等于直接“记住”了验证集答案。排查方法很简单写一行断言验证集里所有切片的病例编号不能出现在训练集病例编号集合里。这也是我对任何医学影像分割项目的第一检查项。5.4 翻车点四肿瘤像素太少模型把所有东西都预测成背景现象是epoch 1到epoch 80肿瘤Dice始终为0但准确率一直在99%以上。原因不是网络坏了是交叉熵损失被背景像素主导。解决路径是换Dice损失或Tversky损失同时在Dataset里做oversample——把包含肿瘤的切片重复采样让每个batch里肿瘤切片占比不低于50%。肿瘤区域确实少但整个切片层面它还是有相当数量的按切片级采样比按像素级采样简单得多。5.5 翻车点五显存不够就盲目缩小输入尺寸现象是输入缩到128×128后小肿瘤全部丢失肿瘤Dice直接掉到0.05。原因是肿瘤本身只有几十像素再缩到128×128就只剩几个像素语义信息彻底没了。如果显存吃紧优先降batch并用梯度累积模拟大batch其次考虑减小模型基础通道数比如从64降到32最后才考虑缩小输入尺寸而且下限设为192×192。这个优先级我吃过亏顺序反了会付出肿瘤Dice大幅下降的代价。提示模型保存和推理时的预处理参数必须完全一致包括HU裁剪范围、归一化公式、输入尺寸。不一致时的表现就是训练Dice高、推理结果烂。6. 推理与验证把训练好的权重变成可提交的nii分割结果训练完成后最容易被忽略的是推理和验证环节。推理阶段要写一个predict_volume函数输入是原始病例的CT体数据输出是和原始尺寸一致的预测nii。流程和训练时一模一样重采样、HU裁剪、逐层预测、按类别把预测结果映射回标签0/1/2、重采样回原始分辨率和体素间距。逐层预测时单张512×512的切片直接用模型跑就行显存没问题如果是更大尺寸的图需要用滑动窗口窗口大小建议256×256overlap设成窗口的四分之一避免窗口边界处的分割不连续。验证时不要只抽一张切片看效果那是最容易自我满足的做法。正确的做法是每个验证病例生成一张三维预测结果和标注在三个平面横断面、冠状面、矢状面上都叠加看一下。横断面效果好不代表冠状面连贯模型在Z轴方向经常有“闪断”——相邻切片预测的肿瘤区域突然变小再恢复。这种问题Dice数值上看不出来但医学影像软件里翻看切片时非常明显。我自己的习惯是最后跑一个按病例统计的Dice分布直方图把肿瘤Dice低于0.3的病例单独挑出来看原始CT。多数情况是这些病例的肿瘤和周围肝组织对比度极低标注本身也存在争议这是数据集的固有限制不是模型调参能解决的。另外保留一份推理时用的预处理配置到JSON文件三个月后回来看这个项目时不至于对着代码猜当初的裁剪范围是多少。这个项目真正值得投入的地方在于当你把LITS这套流程跑通换到其他CT影像分割任务时数据读取、预处理、训练策略、验证方式几乎全部可以直接复用这比单次分割精度的提升更值钱。希望帮到你。本文还有配套的精品资源点击获取

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询