资讯动态

深度学习医学影像分割实战:U-Net与V-Net的2D/2.5D/3D实现全解析

发布时间:2026/10/6 12:43:28 来源:尧图企业网站定制
简介基于深度学习的医学影像图像分割Python工程代码面向医学影像分析初学者、课程设计或需要复现U-Net系列分割实验的研究者提供一套从数据准备到模型训练、预测与查看结果的完整可运行流程。工程包含19个Python脚本压缩包整体仅35KB脚本按功能划分为数据预处理、2.5D/3D模型构建、训练、预测、后处理以及NII文件可视化等模块目录层级清晰便于按需取用。目前已有552人学习下载热度较好。代码中不仅包含基础U-Net还提供unet_3d、v_net等变体读者可据此对比不同网络在MRI分割上的表现同时给出训练集含10%验证的数据划分说明并提供批量分割脚本可对测试集和待预测图像生成结果并保存方便直观评估。资源附有环境依赖与运行顺序说明适合快速搭建环境、跑通全流程也可作为医学影像深度学习入门或课程项目复现的轻量实用代码库。1. 基于深度学习的医学影像图像分割这个 MRI 分割项目包里到底装着什么做医学影像图像分割的人大概都经历过这种场景手头有一批 MRI 的 .nii 文件标注已就绪但训练入口在哪、预处理要不要做、.nii 怎么读都是拦路虎。这个项目就是干这个的——它把 U-Net 和 V-Net 在 2D、2.5D、3D 三种维度下的实现、训练、预测、后处理全部串成一条可运行链路。data/train放训练集data/rest放测试集data/test放待预测数据三次独立预测结果也保留在目录里方便做稳定性对比这个细节我很喜欢。适合刚入门医学影像分割的学生、要做课程设计的本科生、以及想快速验证分割方案、不想从零搭网络的工程师。下面我从文件结构开始拆一步步说清楚怎么跑、怎么调参、踩过哪些坑。2. 文件结构与网络选型U-Net、V-Net 和 2.5D 方案怎么选2.1 项目文件布局训练链路核心文件与摘要映射解压MRI-Segmentation-main.rar之后目录大致是这样unet/ ├── data/ │ ├── train/ # 训练数据其中 10% 作为验证集 │ ├── rest/ # 测试数据含 predict/predict1/predict11 三次运行结果 │ └── test/ # 课程设计要求预测的数据 ├── two_half_d/ # 2.5D 方案脚本目录 ├── u_net_3d/ # 3D U-Net 相关实现 ├── create_train_data_25d.py ├── generator_25d.py ├── train_25d.py ├── preprocess_25d.py ├── network/ │ ├── unet_3d_nn.py │ ├── v_net_25d.py │ ├── unet_25d.py │ └── vnet_3d_nn.py ├── public_lib/ │ ├── utils.py │ ├── loss_function.py │ ├── pathvariable.py │ └── dataIO.py ├── predict.py # 对 data/test/image 分割 ├── predict_rest.py # 对 data/rest/image 分割 ├── train.py ├── preprocess.py ├── generator.py ├── postprocess.py ├── create_train_data.py └── environment.txt这个项目给我印象最深的是代码按阶段拆得很细每个脚本只负责一件事没有把预处理、训练、预测全塞进一个文件里。工程上这是好事——你可以单独替换某一段而不用动其他部分。比如你想换一种归一化策略只改create_train_data.py就够想换损失函数只动public_lib/loss_function.py。不过要注意摘要里写的运行方式是 python3 name.py并列举了data.py、unet_model.py、see.py这些名字实际压缩包里不是完全这么叫的。我整理了一张对应关系表照着这个找文件就不会懵摘要里的名称实际代码文件职责data.pycreate_train_data.py preprocess.py generator.py数据准备、预处理、在线增强unet_model.pynetwork/unet_25d.py、network/unet_3d_nn.pyU-Net 模型定义train.pytrain.py / train_25d.py训练主流程predict.pypredict.py / predict_rest.py对 test、rest 数据分割see.py查看 .nii 的工具脚本基于 dataIO 封装输入路径预览文件two_half_d和u_net_3d是两个独立的方案目录里面装的是 2.5D 和 3D 训练的专用脚本比如create_train_data_25d.py、train_25d.py。它们和根目录下主流程脚本形成对照作者等于把三条技术路线都铺开了这对做课程设计的人特别友好——你可以直接对比 2D、2.5D、3D 在同一批数据上的效果不用自己写三份代码。2.2 三种网络架构的定位差异2D、2.5D、3D 怎么选选择网络架构是医学影像分割里最核心的决策。2D U-Net 把每一层切片当独立图像处理输入形状是 (B, C, H, W)理解成本最低、显存占用最小但完全丢掉片间连续性。遇到层厚较大、目标在相邻层间形态突变的情况2D 的结果经常在层间出现锯齿状边界。2.5D 的思路是取当前切片并各带上下一层拼成 3 通道或 5 通道的输入我一般取上一层当前层下一层。这样网络虽然还是二维卷积却能通过通道维度感知到层间局部上下文。这个方案在 MRI 分割里很实用——很多数据集的层间距比平面内分辨率大3D 卷积在层间学到有效特征很费力而 2.5D 用很小的代价就把三维信息送进了网络。3D U-Net / 3D V-Net 直接以 (B, C, D, H, W) 体素卷积信息利用最充分但显存占用随深度线性增长训练时间也最长。当整个数据集只有几十个 case 时3D 网络很容易过拟合。这个项目同时实现三套相当于给不同数据条件留了三条路方案输入形状显存压力空间上下文适合场景2D U-Net(B, C, H, W)低层内快速验证、大数据量2.5D U-Net/V-Net(B, 3~5, H, W)中层间局部层间距大、显存有限3D U-Net/V-Net(B, C, D, H, W)高全局体积高分辨率厚数据、追求精度我实际跑这个项目时默认从 2.5D 入手因为它显存成本比 3D 低一个数量级又比纯 2D 稳。先跑通链路再根据数据厚度决定要不要切到 3D。如果数据只有几十层且各向异性明显2.5D 往往是效果和资源最平衡的选项。3. 数据准备与训练流程从 .nii 原始数据到分割模型3.1 环境安装与启动方式先跑通链路cd /path/to/unet pip3 install -r environment.txt python3 create_train_data.py python3 train.py python3 predict.py逻辑说明按顺序跑下来就是完整链路。cd到项目根目录是为了让相对路径生效这个项目把数据集路径写在public_lib/pathvariable.py里全部基于当前目录解析如果不在根目录运行第一行就会报 FileNotFoundError这是初学者最容易踩的一步。参数说明安装依赖建议加--user或用虚拟环境避免污染系统 PythonGPU 机器需要先确认 torch 版本与 CUDA 匹配常见做法是运行python3 -c import torch; print(torch.cuda.is_available())打印 True 说明显存可用。如果机器上同时有 Python 2 环境务必用pip3和python3否则依赖装错环境后 import 报错会浪费很多时间。提示先跑 create_train_data.py 再跑 train.py顺序不能反。create_train_data 输出的中间文件是 train.py 的输入漏跑一步在后面会直接读不到数据。3.2 create_train_data.py 与数据预处理create_train_data.py主要负责把 .nii 读进来、归一化、按比例切分训练集和验证集。下面是核心逻辑的示意代码和项目实现对齐# create_train_data.py 核心逻辑示意 import numpy as np from public_lib.dataIO import load_nii nii_files load_file_list(data/train/image) # 读入所有 nii 路径 images [] for nii_file in nii_files: img, affine load_nii(nii_file) # (D, H, W) img img.astype(np.float32) # 归一化MRI 灰度值范围大先 clip 再线性映射 lo, hi np.percentile(img, 1), np.percentile(img, 99) img np.clip(img, lo, hi) img (img - lo) / (hi - lo 1e-8) images.append(img) # 10% 做验证集 split int(len(images) * 0.9) train_list, val_list images[:split], images[split:] np.savez(data/train_ready.npz, traintrain_list, valval_list)逻辑说明clip 到 1%99% 分位是为了去掉 MRI 里极端高亮的噪声点。这种数据的灰度值经常有少数体素异常偏大直接用 max-min 归一化会被个别异常值带偏所以我一般用分位数截断而不是全范围截断。split 用 0.9 表示预留 10% 验证集这是项目作者预设的比例。参数说明如果你的数据来自不同扫描仪或不同序列灰度基线会差很多可以在归一化前加偏置场校正步骤。如果数据集很小并切各类别极不均衡可以把 split 调到 0.85 让训练数据多一些代价是验证集波动变大——小数据集上 10% 的验证集可能只有几例Dice 曲线看起来会很抖。这时候不是你模型不行是验证集太小要有这个心理准备。generator.py的作用是训练时按 batch 动态读取切片并做随机翻转、旋转等在线增强避免把所有增强结果一次性写进内存把磁盘撑爆。这是医学影像数据量小时的常规操作——在线增强等于变相扩容而且每个 epoch 看到的样本都不一样能有效抑制过拟合。3.3 train.py 训练参数与模型行为观察train.py负责加载数据集、初始化模型、迭代训练、保存最优权重。下面这些参数是训练时最需要盯的epochs 100 # 小数据集建议 100 起步看曲线再决定加不加 batch_size 8 # 2D/2.5D 可接受3D 要降到 2 或 1 learning_rate 1e-4 # 医学影像数据量小1e-3 太大1e-4 稳妥 optimizer torch.optim.Adam(model.parameters(), lrlearning_rate) criterion loss_function.dice_loss # 见 public_lib/loss_function.py逻辑说明医学影像分割正负样本严重失衡目标区域经常只占整个体积的百分之几直接用交叉熵会让网络倾向于输出全背景。Dice Loss 优化的是预测与标注的体素重叠程度天然对类别不均衡问题不敏感这是它在这个场景下被选为主损失函数的原因。参数说明学习率配 1e-4 是经验值如果 loss 震荡明显就降到 5e-5。batch_size 受显存约束2.5D 用 8 没问题但如果你把输入分辨率设得非常大比如 512×512×3先减半再观察显存余量。训练里另一个关键点是按验证集 Dice 保存最优权重# train.py 中保存最优权重的常见写法 best_dice 0.0 for epoch in range(epochs): train_loss run_epoch(model, train_loader, optimizer) val_dice evaluate(model, val_loader) if val_dice best_dice: best_dice val_dice torch.save(model.state_dict(), checkpoints/best.pth) print(fepoch{epoch} loss{train_loss:.4f} val_dice{val_dice:.4f})逻辑说明按验证集 Dice 决定是否保存当前权重而不是按训练 loss。训练 loss 只反映拟合程度验证集 Dice 才说明泛化能力——拿训练 loss 最小的一版权重去预测往往不是最好的。参数说明epochs 不要盲目跑满。我一般先观察 5 个 epoch如果 val_dice 没有上升趋势问题大概率出在数据准备环节而不是训练时间不够。检查路径通常是先看 create_train_data 的输出有没有问题再看生成器有没有把数据对齐。4. 预测与后处理实战predict.py 输出约定与结果修复4.1 predict.py 与 predict_rest.py 的输出约定predict.py针对data/test/image预测结果写到data/test/predictpredict_rest.py针对data/rest/image结果写到data/rest/predict。核心逻辑是这样# predict.py 核心循环示意 import torch from network.unet_25d import UNet25D from public_lib.dataIO import load_nii, save_nii model UNet25D(in_channels3, out_channels1) model.load_state_dict(torch.load(checkpoints/best.pth, map_locationcpu)) model.eval() # 切到推理模式关掉 BN/Dropout 的随机性 for case in test_cases: img, affine load_nii(case) # (D, H, W) pred predict_case(model, img) # 概率图 seg (pred 0.5).astype(np.uint8) # 二值化阈值可调整 save_nii(seg, affine, output_path)逻辑说明model.eval()那行是最容易漏的漏了预测结果会产生随机抖动。二值化阈值取 0.5 是常规默认实际应用中如果医生反馈分割边界偏小把阈值降到 0.30.4如果偏大提到 0.6 以上。阈值不是玄学它直接控制着查全率和查准率的取舍。参数说明predict_rest.py的数据流和 predict.py 相同只是输入换成data/rest/image输出换成data/rest/predict。这个项目把data/rest/predict、predict1、predict11三组运行结果都保留下来这就是标准的三次重复测试——同一模型多跑几遍看分割结果在体素级别稳不稳定。4.2 后处理连通域分析、阈值调整与结果修复网络预测出的原始 mask 有噪声是常态postprocess.py负责去掉零散假阳性# postprocess.py 后处理逻辑示意 from scipy import ndimage labeled, num_features ndimage.label(binary_pred 0) if num_features 0: volumes ndimage.sum(binary_pred 0, labeled, range(1, num_features 1)) # 只保留最大连通域或滤掉小于 min_volume 的区域 max_label volumes.argmax() 1 binary_pred (labeled max_label).astype(np.uint8)逻辑说明逐体素分类天然会产生碎片连通域分析是医学影像分割后处理的通用手段。这里用ndimage.label给每个连通域编号再用ndimage.sum统计体积把小于阈值的小块滤掉。最常见的设定是保留最大连通域——假设你不是在分割多发病灶这个操作很管用。参数说明min_volume要结合体素分辨率来定。假如体素是各向同性 1mm³目标是最小病灶直径 5mm那按 5³125 体素做下限是合理的。别小看这一步它经常能把 Dice 从 0.90 拉到 0.93 以上。边界毛糙时还可以做一次二值形态学闭运算填掉细缝和小空洞from scipy.ndimage import binary_closing binary_pred binary_closing(binary_pred, structurenp.ones((3, 3, 3)))注意闭运算的结构元素不要超过 5×5×5否则会吞掉真实解剖细节。我见过有人用 9×9×9 的结构做闭运算结果小病灶全被填没了这个参数是真的要谨慎。5. 避坑与排查训练不收敛、显存溢出与路径写死5.1 训练损失不下降现象dice_loss 在前 10 个 epoch 毫无变化打印出来像一条直线val_dice 也纹丝不动。原因最常见的是学习率偏大导致 loss 震荡但更隐蔽的是数据预处理没对齐——原始像素值范围是 03000没归一化直接进网络梯度数值不稳定网络根本没有有效更新。这种问题在 MRI 数据上特别常见因为不同序列的灰度范围差异极大。解决把 learning_rate 降到 1e-4 以下并回看 create_train_data.py 的归一化逻辑在 train.py 训练前打印一个切片的像素统计确认数值在 01 区间。我习惯在数据加载器里加一行断言值域不对直接报错省得训练半天才发现数据有问题。5.2 3D 网络显存溢出现象训练第二个 batch 时直接 OOM进程被系统杀掉或者报 CUDA out of memory。原因3D 卷积的中间特征图比 2D 大一个数量级。2D 下 batch_size8 没问题3D 下 batch_size4 都跑不进去显存被中间激活值瞬间占满。解决先降到 batch_size2 试跑看显存占用再逐步加。还不行就把输入统一裁剪成 64×64×64 的 patch训练时随机裁剪预测时重叠滑动窗口拼接。这个项目既然给了 2.5D 的完整脚本日常实验直接用 2.5D 最省事3D 留给数据厚度确实够大的场景。5.3 数据增强导致的信息泄露现象验证集 Dice 比训练集还高看起来像捡到宝仔细一查是数据流出了问题。原因如果在整个数据集上先做统一归一化再切分训练/验证或者增强时用了全数据的统计量验证集信息已经通过统计量泄露进训练过程验证指标失去意义。解决先切分再对训练部分单独计算归一化参数数据增强只作用于训练集。医学影像数据量小大家习惯先整体预处理再切分这个顺序其实是大坑。分割任务里尤其要小心因为很多预处理是全局的。5.4 路径写死的翻车现场现象代码在自己机器跑得好好的换个目录或换台机器就报 FileNotFoundError或者读到了旧数据、旧权重。原因public_lib/pathvariable.py里用的是相对路径但运行脚本时没有先切到项目根目录或者某个中间文件路径在多个脚本里被反复写死换环境就找不到。解决在程序入口打印os.getcwd()强制 cd 到项目根目录再运行路径集中到 pathvariable.py 里改不要散落在各脚本里改字符串。我这个教训很深曾经因为路径指到了旧数据目录用旧的训练集训了一个星期才发现。5.5 预测结果全是黑的或全白的现象分割结果 .nii 打开后全黑或者全白看不到任何解剖结构。原因大概率是保存时丢了 affine 信息或者把概率图当二值图保存了。全白通常是阈值设成了 0网络输出没有经过 sigmoid 就直接比较所有体素都被判成了正例。解决save_nii 时要传入 load_nii 时拿到的 affine确认网络输出层包含 sigmoid打开前用查看工具检查保存数组的数值分布是 0/1 还是 0~1 浮点。如果保存的是概率图需要做阈值判断后再存。看完第 6 节的查看工具你就知道怎么排查了。6. 结果验证技巧用 see.py 检查分割三次预测取一致性摘要里提到的see.py是查看 .nii 文件的小工具核心逻辑其实就是 load_nii 之后转成图像显示。我在这个项目上习惯不用 IDE直接写一个十几行的加载脚本来看结果# 查看单个 .nii 分割结果对应摘要里的 see.py import sys import numpy as np from public_lib.dataIO import load_nii img, affine load_nii(sys.argv[1]) slices img.shape[0] for i in range(0, slices, max(1, slices // 9)): # 均匀取 9 层预览 print(fslice {i}: min{img[i].min()} max{img[i].max()} mean{img[i].mean():.2f})配合 Dice 一致性检查更直观。这个项目保留了data/rest/predict、predict1、predict11三组运行结果我会对同一 case 两两算 Dice# 三次预测一致性检查 from public_lib.dataIO import load_nii def dice(a, b): return 2 * np.sum((a 0) (b 0)) / (np.sum(a 0) np.sum(b 0) 1e-8) p1, _ load_nii(data/rest/predict/case01.nii) p2, _ load_nii(data/rest/predict1/case01.nii) p3, _ load_nii(data/rest/predict11/case01.nii) print(frun1 vs run2: {dice(p1, p2):.4f}) print(frun1 vs run3: {dice(p1, p3):.4f}) print(frun2 vs run3: {dice(p2, p3):.4f})逻辑说明三次独立预测的 Dice 一致性是模型稳定性的硬指标。如果两两 Dice 低于 0.95说明预测对输入扰动敏感问题多半出在预处理或后处理环节而不是网络结构本身。再配合分层预览的像素统计就能快速定位是阈值问题还是连通域参数问题。参数说明检查脚本不挑数据把路径换到 test 目录同样适用。我还习惯把每组结果的总体素数打出来配合肉眼观察边界是否贴合解剖结构。数值和结构双重验证比只看 Dice 一个数可靠得多。有一次我赶时间只跑了一次预测就直接交结果和同事的算法一对比边界抖动非常明显那才意识到预测稳定性也是个必须盯的指标。从那以后我每次做完医学影像分割都强制跑三遍预测两两 Dice 低于 0.95 就回去查预处理和后处理这个习惯确实救了不少次。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑