SLIVER07肝脏CT分割与三维重建:从数据预处理到模型训练全流程
发布时间:2026/9/28 23:40:43来源:尧图网络
简介基于sliver07公开数据集的肝脏CT图像分割与三维重建Python源码聚焦医学影像分析中的器官分割与可视化任务面向计算机视觉、人工智能、生物医学工程等专业的在校学生、科研人员与算法爱好者也适合作为毕业设计、课程设计或科研练手项目。压缩包共122个文件大小约105.1MB42个py脚本覆盖数据预处理、模型训练与推理、三维重建全流程42张png图片直观展示各阶段分割结果与重建效果15个pyc为编译缓存9个txt含说明与训练日志5个tar存放原始或中间数据2个xml配置环境参数另包含GIF演示动画、VTK三维模型与说明文档。当前已有449人学习下载。借助该资源可系统掌握基于公开数据集的肝脏CT分割流程快速复现从图像输入到三维可视化的完整链路文档与示例图便于对照调试亦可在此基础上替换数据、调整网络结构迁移至其他医学影像分割任务。1. 一个 sliver07 肝脏分割源码包真正值钱的是数据处理和重建管线拿到一个标题带「基于sliver07数据的肝脏CT图像分割及三维重建python源码.zip」的压缩包很多人第一反应是直奔模型文件。我拆过几个同类项目后发现网络结构反而是整条链路里最不用操心的部分——真正决定分割 Dice 和重建效果上限的是数据预处理和三维重建这两条管线。sliver07严格拼写是 SLIVER07来自 MICCAI 2007 肝脏分割挑战赛腹部 CT 加肝脏金标准几十例数据常被当作 CT 图像分割和三维重建的入门基准。这套源码做的事是把 CT 序列变成肝脏概率图再变成一个能旋转、能导出 STL 的肝脏网格。适合医学影像算法验证、影像组学预处理和手术规划前的形态学研究也适合作为第一个完整跑通的医学图像深度学习项目。2. 数据准备决定分割上限sliver07 的格式、HU 换算与重采样解开源码包先别急着训练第一步永远是看数据目录长什么样。我见过太多人把数据加载写死成一种格式换台机器换份数据就全线崩盘。sliver07 的数据来源比较杂常见的有三种形态原始 DICOM 序列、转好的 mha/mhd 文件、已经切成 nii/nii.gz 的预处理版本。标签ground truth一般是一份与 CT 同尺寸的二值 mask像素值 0 表示背景、1 表示肝脏。你首先得搞清楚手里是哪一种再决定读取代码怎么写。2.1 常见数据构成与统一读取入口一个典型的数据目录长这样文件名和层级可能不同但构成基本逃不出这几类内容常见格式说明CT 原始序列DICOM 文件夹每个病例一个目录里面几十到几百张横断位切片CT 单文件.mha / .mhd / .nii经常是别人预处理好的spacing 信息在文件头里肝脏标签.mhd / .nii / .nrrd和 CT 同尺寸二值图划分文件.txt / .csv有些源码包自带 train/val/test 列表不管哪种格式统一用 SimpleITK 读取最省事它能同时吃 DICOM 序列、mha 和 nii。DICOM 序列路径指向文件夹即可不需要自己手动排序拼接。import SimpleITK as sitk def load_volume(path): # path 可以是 .mha/.nii也可以是包含 DICOM 序列的文件夹 image sitk.ReadImage(path) arr sitk.GetArrayFromImage(image) # 返回 shape: (z, y, x) spacing image.GetSpacing() # 返回 (x, y, z) 三个方向像素间距 return arr, spacing, image注意GetArrayFromImage出来的 shape 顺序是(z, y, x)而GetSpacing()的顺序是(x, y, z)。后面做重采样和三维重建时这个顺序不对齐会出现网格横着躺、z 轴拉伸这类诡异问题。先把这行注释刻在脑子里数组轴序和 spacing 轴序是反的。2.2 HU 换算与窗宽窗位裁剪不转这一步后面全是噪声DICOM 文件里存的像素值不是 CT 值HU它是设备相关的整数需要通过RescaleSlope和RescaleIntercept换算成 Hounsfield Unit。很多源码包把这一步省了直接把原始像素送进网络结果训练出来的模型换个设备采集的数据就失效。肝脏的 CT 值范围大致在 0~150 HU 附近和肌肉、脾脏重叠和骨骼、空气差得很远。常规做法是先做窗宽窗位裁剪把动态范围压到肝脏区域再做归一化。import numpy as np def load_ct_with_window(path, window150, level30): arr, spacing, image load_volume(path) # 如果原始数据是 DICOM需要先做 HU 换算 # hu pixel_array * RescaleSlope RescaleIntercept # 这一步通常在 SimpleITK 读取时已自动完成但用 pydicom 读原始值时必须手动做 # 窗宽窗位裁剪: 只保留 level ± window/2 的范围 hu_min level - window / 2.0 hu_max level window / 2.0 arr np.clip(arr, hu_min, hu_max) # 线性归一化到 [0, 1] arr (arr - hu_min) / (hu_max - hu_min) return arr.astype(np.float32), spacing, image肝脏 CT 显示常用的窗宽窗位是 W150/L30但这不等于训练时最优。有些源码包用的是 W200/L50因为这样能把肝内血管和胆管的对比度保留更多。我的建议是先看一眼训练数据的值分布如果你发现 arr 的最小值不是 -1024 而是 0说明数据已经被前一个人做过裁剪或 8bit 量化此时不要再套窗宽窗位直接归一化即可。判断依据很简单看像素值范围和肝脏边缘的锐利度。2.3 各向同性重采样mask 必须用最近邻插值sliver07 各病例的层厚不统一有的 1mm有的 3mm甚至同一例内部层间距都有变化。直接拿原始分辨率训练模型会把层厚当成特征学进去导致泛化差。常见做法是把所有数据重采样到各向同性比如 1mm×1mm×1mm这样 z 方向和 xy 方向的空间分辨率一致三维重建时网格比例才正确。def resample_to_spacing(image, new_spacing(1.0, 1.0, 1.0), is_maskFalse): original_spacing image.GetSpacing() original_size image.GetSize() new_size [ int(round(original_size[i] * original_spacing[i] / new_spacing[i])) for i in range(3) ] resampler sitk.ResampleImageFilter() resampler.SetOutputSpacing(new_spacing) resampler.SetSize(new_size) resampler.SetOutputOrigin(image.GetOrigin()) resampler.SetOutputDirection(image.GetDirection()) # 关键: mask 用最近邻, CT 用线性 if is_mask: resampler.SetInterpolator(sitk.sitkNearestNeighbor) else: resampler.SetInterpolator(sitk.sitkLinear) return resampler.Execute(image)SetOutputOrigin和SetOutputDirection这两行很多人会漏。漏掉之后图像内容没变但空间坐标原点跑偏后面三维重建导出 STL 再叠加到原始 CT 上时网格和切片位置对不上肉眼看不出来一量距离全是错的。mask 之所以必须用最近邻是因为线性插值会在肝脏边缘产生介于 0 和 1 之间的过渡像素二值图变成灰度图后面取连通域、算体积全被污染。注意重采样完成后检查一次 mask 的取值集合是否还是 {0, 1}。出现 0.3、0.7 这类小数说明插值器选错了没有后悔药只能重新跑。2.4 数据划分与增强验证集不要动测试集20 例左右的数据量划分方式直接决定实验结论可不可信。如果源码包没给划分文件按病人 ID 划分而不是按切片划分。把同一个病人的切片同时放进训练集和验证集验证 Dice 会虚高因为在切片级别已经泄漏了病人信息。我一般按 16:2:2 或者严格一点 15:3:2保证测试集病人完全没参与训练。数据增强方面翻转和旋转够用弹性形变我一般不用在肝脏上——肝脏是实体器官过度形变会产生不符合解剖结构的样本模型学到的是扭曲特征而不是肝脏特征。# 训练时对 CT 和 mask 做同样变换 # 注意: CT 用线性插值, mask 用最近邻, 旋转角度控制在 ±10° 内3. 分割模型选型与训练2D U-Net 的结构、损失和关键参数数据管线通了之后才轮到模型。sliver07 这类数据量在几十例级别的任务模型选型的核心不是追新而是控制过拟合、保证可复现。我推荐先用 2D U-Net 打底不是因为 3D 模型不好而是因为 20 例数据喂 3D 网络参数量和数据量完全不成比例训练成本还高。先把 2D 跑通拿到基线再决定要不要上 3D。3.1 为什么先选 2D U-Net 而不是 3D2D U-Net 的输入是一张 512×512 的横断位切片一个病例几十到几百张切片20 个病例能凑出几千张训练样本。3D U-Net 的输入是整个体积一个病例就是一个样本20 个样本训练一个深度网络过拟合几乎是必然的。3D 模型的显存占用也是问题一个 128×128×64 的 patch 就能吃掉大半张卡个人电脑很难跑起来。折中方案是 2.5D取相邻三张切片作为三个通道输入中间那张是目标。这个改动能让模型感知到层间连续性成本只有 2D 的零头。很多源码包不敢上 3D最后就是用 2.5D 把 Dice 提到 0.93 以上。3.2 输入尺寸与 patch 策略512 到底要不要缩512×512 直接进网络不是不行但 batch size 会被压得很小训练不稳定。常见做法是随机裁剪 256×256 的 patch配合翻转旋转增强。推理时用滑窗拼接滑窗重叠 32 像素重叠区域取平均避免拼接边界出现明显的分割缝。有一个容易翻车的细节裁剪时不要把 CT 和 mask 分别随机裁必须用同一个裁剪偏移量。def random_crop(ct_slice, mask_slice, crop_size256): h, w ct_slice.shape y np.random.randint(0, h - crop_size 1) x np.random.randint(0, w - crop_size 1) return ct_slice[y:ycrop_size, x:xcrop_size], mask_slice[y:ycrop_size, x:xcrop_size]偏移量随机生成一次CT 和 mask 共用这个错误低级但非常常见我自己也踩过。3.3 DiceBCE 混合损失权重怎么定才不打架肝脏分割是典型的前景占比极小任务肝脏像素占整张 CT 切片的比例大概 5%~15%用纯 BCEbinary cross entropy会让网络倾向于预测全背景因为全背景的 loss 已经很低了。Dice Loss 天然对类别不平衡不敏感但单独用 Dice Loss 训练后期容易震荡。最常见也是我最推荐的做法是 Dice 和 BCE 加权求和Dice 权重 0.8BCE 权重 0.2。import torch import torch.nn.functional as F class MixLoss(torch.nn.Module): def __init__(self, dice_weight0.8, bce_weight0.2): super().__init__() self.dice_weight dice_weight self.bce_weight bce_weight def forward(self, pred_logits, target): # 注意: pred 是 logits, sigmoid 放在 loss 内部做 pred torch.sigmoid(pred_logits) smooth 1.0 intersection (pred * target).sum() dice (2.0 * intersection smooth) / (pred.sum() target.sum() smooth) bce F.binary_cross_entropy(pred, target) return self.dice_weight * (1.0 - dice) self.bce_weight * bce两个细节值得说。第一模型最后一层不要自带 sigmoid让 loss 函数内部做数值稳定性更好预测阶段再手动加 sigmoid。第二Dice 权重拉太高时训练初期 loss 会剧烈震荡如果发现震荡把 dice_weight 降到 0.7 试试。3.4 训练参数表与评估指标照着抄也能复现的配置一套我验证过多次的默认训练参数参数值说明优化器AdamW比 Adam 泛化略好weight_decay 用 1e-5初始学习率1e-4用得最多的起点太大会训飞batch size8针对 256×256 patch再大容易 OOMepoch100配合早停一般 40~60 轮收敛早停 patience15连续 15 轮 val dice 不涨就停学习率调度Cosine Annealing最后阶段比 StepLR 稳增强RandomFlip Rotate±10°不做弹性形变评估指标只看两个Dice 和 ASD平均表面距离。Dice 衡量体素重叠率ASD 衡量表面贴合度对三维重建来说 ASD 比 Dice 更有参考价值——一个网格表面全偏移 2mmDice 可能只掉 0.02但模型的实用价值已经大打折扣了。如果源码包只给了 Dice 计算脚本建议自己补一份 ASD。4. 三维重建Marching Cubes、网格平滑与 STL 导出分割模型输出的是一张概率图要变成能旋转、能测量的肝脏三维网格得走 Marching Cubes 这条最经典的路。这一步的代码量不大但参数选错重建出来的肝脏要么全是毛刺要么表面坑坑洼洼。我见过有人在三维重建这一步反复折腾了几天最后发现是 spacing 传错了。4.1 分割结果先做后处理连通域与填洞模型输出的概率图经过阈值 0.5 得到二值 mask这个时候直接重建噪声会逼疯你。腹部 CT 里和肝脏灰度接近的组织不少模型偶尔会在脾脏、胃壁附近输出小块假阳性。常规做法是先取最大连通域再做填洞这两个操作能消掉绝大多数杂散区域。import scipy.ndimage as ndi def clean_liver_mask(pred_binary): # pred_binary: shape (z, y, x), dtype uint8, 取值 0/1 lab, num ndi.label(pred_binary) # 找出所有连通区域 if num 0: return pred_binary sizes ndi.sum(pred_binary, lab, range(num 1)) keep int(np.argmax(sizes)) # 取最大连通区域 cleaned (lab keep).astype(np.uint8) # 填洞: 肝脏内部血管、胆管在低分辨率下可能形成空洞 cleaned ndi.binary_fill_holes(cleaned).astype(np.uint8) return cleanedndi.label默认按 26 连通判断对三维体数据来说比 6 连通更符合解剖连续性。填洞这一步要谨慎如果分割结果本身在肝门区域有真实解剖结构肝门静脉进入肝脏的位置填洞会把它们一并填掉造成体积偏大。我的习惯是先填洞再人工抽检几个断面确认没有把真实凹陷抹平。4.2 Marching Cubes 提取等值面level 和 spacing 是两个关键参数scikit-image的marching_cubes是使用最广泛的三维重建入口。两个关键参数level和spacing。level0.5是因为二值 mask 只有 0 和 1等值面取中值spacing必须按数组轴序传入(z_spacing, y_spacing, x_spacing)。from skimage import measure # cleaned: shape (z, y, x)已处理后二值 mask # spacing 来自重采样后的 new_spacing但注意轴序要反一下 # SimpleITK 返回 (x, y, z)这里传入必须 (z, y, x) z_spacing, y_spacing, x_spacing reversed(new_spacing) verts, faces, _, _ measure.marching_cubes( cleaned, level0.5, spacing(z_spacing, y_spacing, x_spacing), )spacing不传或传错网格的 xyz 比例就错。常见翻车现场重建出来的肝脏像一个被压扁的气球或者竖着拉长。检查方法很粗暴量一下重建网格在 x、y、z 三个方向的物理尺寸和原始 CT 的切片范围对比。原始 CT 的 z 方向长度 层数 × 层厚对不上就说明 spacing 传错了。4.3 网格平滑与导出 STL用 trimesh 一把梭Marching Cubes 出来的网格表面通常比较粗糙因为分割 mask 是体素化的重建网格会有明显的阶梯感。用 trimesh 做一次拉普拉斯平滑再导出 STL就能得到能直接进 Meshlab、Blender 或 3D 打印的模型。import trimesh mesh trimesh.Trimesh(verticesverts, facesfaces, processFalse) # processFalse 很关键: trimesh 默认会做顶点合并和修复 # 但有时会把本来正确的表面搞变形, 关闭它最保险 mesh mesh.simplify_quadric_decimation(face_count200000) mesh.export(liver.stl)simplify_quadric_decimation用来减面。原始网格可能有几百万面推进切片软件直接卡死减到 20 万面在精度和流畅度之间比较平衡。如果 trimesh 版本报错找不到这个方法说明版本太老升级一下或者跳过减面直接导出STL 一样能用就是文件大点。注意导出前检查mesh.is_watertight。如果返回 False说明网格有破口体积计算会偏小此时不要急着导出先做一次mesh.fill_holes()再检查。4.4 别急着上 NeRF体数据三维重建的选型判断热词里那个 NeRF神经辐射场不适合这个场景先说清楚为什么。NeRF 解决的是从多张二维照片恢复三维场景的问题输入是稀疏视角的图像输出是辐射场。而 CT 本身就是完整的三维体数据每个体素都有真实的密度值不需要用神经网络去猜缺失视角。直接 Marching Cubes 提取等值面拿到的就是物理意义上的组织表面。NeRF 在医学 CT 上用武之地很小除非你想做的不是表面重建而是某种渲染效果。这个方向不要被热词带偏体数据就用体数据的方法。5. sliver07 肝脏分割避坑4 个高频翻车点与排查路径整个流程跑下来最容易翻车的不是模型结构而是下面这四个位置。每条都按「现象 → 原因 → 解决」写都是我实际排查过的问题。5.1 预测结果全黑或全白先查数据归一化不是查网络现象训练 loss 正常下降但验证集预测输出全黑或全白偶尔有零星噪点。原因训练时数据经过了窗宽窗位裁剪和归一化推理时加载的测试数据没有做同样的处理。比如训练数据是裁剪后归一化到 [0,1] 的推理时直接拿原始 HU 值喂进网络范围差了几百倍sigmoid 输出自然被压到 0 或顶到 1。解决把训练和推理共用一个预处理函数严格保证统计口径一致。检查方法是在推理前打印输入数据的 min、max、mean和训练集对比。如果训练集是 [0,1]推理输入是 [-1024, 2000]那问题一目了然根本不用怀疑网络结构。5.2 肝脏和脾脏粘连最大连通域救不了你现象重建出来的三维网格在肝脏下缘和脾脏连成一体怎么也分不开。原因肝脏和脾脏在 CT 灰度上接近模型输出概率图在两者交界处没有清晰的分界线阈值 0.5 后两个器官被一个细颈连在一起最大连通域把它们当成一个整体。解决在做连通域之前先用形态学开运算先腐蚀再膨胀把细颈断开。腐蚀几轮看效果我一般腐蚀 3~5 轮就够。腐蚀会损失边缘精度所以只在取连通域时用腐蚀效果取完最大连通域后再用原始二值 mask 和这个连通域做交集保住原始边缘。5.3 mask 重采样后撕裂最近邻插值和数据类型一起检查现象重采样后的 mask 上肝脏边缘出现锯齿状断口甚至整片缺失。原因两个。一是插值器没有设成sitkNearestNeighbor线性插值在薄壁区域会把 0 和 1 的边界糊成中间值阈值回 0/1 时造成断裂二是 mask 在重采样前被转成了 float 类型配合线性插值糊得更严重。解决重采样函数里is_maskTrue时强制SetInterpolator(sitk.sitkNearestNeighbor)并且sitek.GetArrayFromImage之后立即检查np.unique(cleaned) {0, 1}。如果冒出来小数不是插值问题就是读入时已经损坏重跑。5.4 Loss 下降但 Dice 不涨前景占比太低导致的假收敛现象训练 loss 缓慢下降验证 Dice 一直在 0.2~0.5 之间波动模型预测结果几乎全是背景。原因肝脏前景占比太低BCE 被背景主导。虽然用了 DiceBCE 混合损失但 Dice 权重不够网络依然学成「全背景最优」的懒惰解。另一个常见原因是 batch size 太小一个 batch 里全是肝脏占比极低的切片梯度被背景淹没。解决把 dice_weight 拉到 0.9 试试或者把损失函数临时改成纯 Dice 训练 10 轮看看曲线。如果纯 Dice 能涨说明混合损失里 BCE 干扰过大。还有一个笨办法但很有效统计每个 batch 的前景像素比例发现低于 1% 时强制重采一个 batch保证每个 batch 里至少有 2~3 张肝脏占比高的切片。5.5 验证 Dice 虚高按病人划分而不是按切片划分现象训练集 Dice 0.90验证集 Dice 0.89自信满满。换一批外部数据一测Dice 掉到 0.75。原因数据划分是按切片随机分的。同一个病人的相邻切片内容高度相似如果训练集和验证集混入了同一病人的切片验证就等于开卷考试。这是最隐蔽的坑因为训练曲线一切正常只有上真实场景才暴露。解决强制按病人 ID 划分数据集代码里用一组唯一的病例 ID 做 split保证同一个病人的切片只会出现在一个集合里。20 例数据划分成 16:2:2测试集虽然只有 2 例但结论可信度完全不同。6. 验证分割与重建质量Dice、切片叠置与网格断面检查训练结束不算完验证阶段才见真章。我最后做三件事算指标、叠切片、切网格。算指标不要只用 Dice。Dice 对整体重叠敏感对小边缘误差不敏感因此补一个 ASD平均表面距离。不依赖 medpy 的轻量实现用scipy.ndimage的距离变换就能算from scipy.ndimage import distance_transform_edt, binary_erosion def surface_distance(pred, target, spacing(1.0, 1.0, 1.0)): pred_edge pred.astype(bool) ~binary_erosion(pred.astype(bool)) target_edge target.astype(bool) ~binary_erosion(target.astype(bool)) dt distance_transform_edt(~target_edge, samplingspacing) dist dt[pred_edge] return dist.mean(), dist.max()这个函数计算预测表面每个点到真实表面的最短距离均值和最大值都报出来。如果均值 2mm基本可以认为表面贴合度达到了可接受水平如果均值 5mm说明预测整体偏了Dice 再高也救不了。叠切片是最直观的定性检查。把三个图并排摆在一起原始 CT 切片、预测 mask 轮廓、真实 mask 轮廓一眼就能看出边界偏移集中在哪个方向import matplotlib.pyplot as plt fig, axes plt.subplots(1, 3, figsize(12, 4)) axes[0].imshow(ct_slice, cmapgray) axes[1].imshow(ct_slice, cmapgray) axes[1].contour(pred_slice, levels[0.5], colorsr) axes[2].imshow(ct_slice, cmapgray) axes[2].contour(true_slice, levels[0.5], colorsg) plt.savefig(check_slice.png, dpi150)最后一步是网格断面检查。重建的三维网格导出 STL 后常用软件里切一刀看截面。具体做法在 Meshlab 或 PyVista 里加载 liver.stl同时加载一份原始 CT 切片作为背景旋转视角比较网格外轮廓是否贴合 CT 上的肝脏边缘。这个检查能暴露 spacing 传错、坐标轴搞反、平滑过度导致表面收缩这几类问题。网格比 CT 大一圈或小一圈基本都是重采样或 Marching Cubes 的 spacing 参数错的回头查第 2.3 和第 4.2 两步别去调网络。我自己的习惯是训练完先跑一遍验证脚本指标达标后抽三个病人各挑三层切片做叠置图再对重建网格做一次截面检查。这套流程走完不到半小时但能挡住后面好几天的返工。希望帮到你。本文还有配套的精品资源点击获取
网站建设高端定制企业官网