多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

Unet医学影像分割完整工程:从标注到评估的流程闭环与避坑指南

Unet医学影像分割完整工程:从标注到评估的流程闭环与避坑指南 简介基于U-Net的医学影像分割系统是一套面向人工智能、计算机视觉方向学习者与毕业设计者的完整Python项目涵盖从数据准备到模型评估的全流程。压缩包共76个文件主要包含13个Python脚本模型定义、训练、预测、评估与数据预处理、10个JSON标签/配置文件、6个JPEG/JPG与19个PNG图像样本及分割结果图以及4个XML工程配置、依赖清单和U-Net论文原文PDF整体仅4.61MB结构清晰、体量轻量便于本地复现。目前已有217人学习下载对相关专业学生、教师及入门开发者具有参考价值。项目源自高分毕设答辩平均分96分代码测试通过可直接运行内部包含labelme标注转分割掩码、数据集生成、U-Net模型训练与推理、精度/召回率/mIoU等指标统计图表并附安装说明、使用文档与演示截图适合课程设计、毕业设计或作为理解医学影像分割原理的实践案例。1. 这个 Unet 医学影像分割项目真正值得下的是流程闭环如果只看代码量这个基于 Unet 的医学影像分割系统并不算庞大但它把「标注 → 数据清洗 → 划分数据集 → 训练 → 评估 → 预测 → UI 演示」整条链路全部跑通了。我第一次拆开源包时最直观的感受是该有的模块一个不缺从labelme2seg.py到utils_metrics.py每一步都有对应的脚本和输出文件甚至结果目录里Precision.png、mIoU.png、confusion_matrix.csv都生成好了这在课程设计和毕设场景里非常少见。适合两类人一是计算机视觉方向的学生需要一份能快速出结果的 Unet 完整工程二是想搞懂医学影像分割落地流程的工程师可以直接从这套代码里抄作业。它不是纯理论讲解而是用一个能跑通 ISIC 皮肤镜数据集分割的实例把 Unet 的前前后后讲明白了。2. Unet 结构拆解从 unet_parts.py 到 unet_model.py 的张量流动2.1 双路径结构编码器下采样与解码器上采样Unet 之所以适合医学影像核心是它 U 形的对称结构。左侧编码器通过卷积和池化不断下采样逐步提取高层语义特征右侧解码器通过上采样逐步恢复分辨率中间通过 skip connection 把编码器每一层的特征图拼接到解码器对应层弥补下采样丢失的细节信息。这套设计对于器官、肿瘤这类边界模糊的目标特别有效因为分割结果既要有全局上下文又要有精细的像素级边缘。项目里的unet_parts.py把这套结构拆得很清楚。其中的DoubleConv是基本积木块每次做两次卷积 批量归一化 ReLU 激活class DoubleConv(nn.Module): def __init__(self, in_ch, out_ch): super(DoubleConv, self).__init__() self.conv nn.Sequential( nn.Conv2d(in_ch, out_ch, 3, padding1), nn.BatchNorm2d(out_ch), nn.ReLU(inplaceTrue), nn.Conv2d(out_ch, out_ch, 3, padding1), nn.BatchNorm2d(out_ch), nn.ReLU(inplaceTrue) ) def forward(self, x): return self.conv(x)padding1保证卷积不改变特征图尺寸所以DoubleConv只改变通道数不改变空间大小。这里有个容易被忽略的细节BatchNorm2d在 batch size 很小时统计量不稳定如果你用单张图做预测模型要切到 eval 模式否则分割结果会出现奇怪的块状噪声。我在第一次跑预测时忘了调用model.eval()出来的 mask 全是雪花点排查了半天才发现是 BN 层在 train 模式下用了当前 batch 的统计量。unet_model.py里通过Down和Up类完成特征图的收缩和扩张class Down(nn.Module): def __init__(self, in_ch, out_ch): super(Down, self).__init__() self.mpconv nn.Sequential( nn.MaxPool2d(2), DoubleConv(in_ch, out_ch) ) def forward(self, x): return self.mpconv(x)MaxPool2d(2)把特征图尺寸减半对应编码器的每一层Up类则用转置卷积或双线性插值把分辨率翻倍然后把编码器对应层的输出在通道维度上拼接。拼接后通道数变成两倍所以Up里第一个卷积的输入通道要写成in_ch skip_ch的形式。以unet_model.py里的UNet主类为例默认输入n_channels3处理 RGB 医学图像输出n_classes1用 Sigmoid 做二分类前景背景分割这和 ISIC 皮肤镜数据集的 lesion 分割任务是匹配的。如果你要改成多类别分割比如肝脏的多个分区就需要把输出层改成多通道 Softmax同时损失函数从BCEWithLogitsLoss换成CrossEntropyLoss这属于最常见的改造方向。2.2 五层通道配置与显存代价这个项目默认把 Unet 的下采样深度设为 4 次输入 3 通道编码器各层输出通道为 64、128、256、512、1024。这是很标准的配置模型参数量大约在 3100 万左右单张 512×512 输入在 6GB 显存的显卡上可以跑训练但 batch size 只能开到 4 左右。关于通道数配置unet_model.py中有一段类似这样的逻辑self.inc DoubleConv(n_channels, 64) self.down1 Down(64, 128) self.down2 Down(128, 256) self.down3 Down(256, 512) self.down4 Down(512, 1024) self.up1 Up(1024, 512) self.up2 Up(512, 256) self.up3 Up(256, 128) self.up4 Up(128, 64) self.outc OutConv(64, n_classes)如果你显存不够最先砍的是down4的通道数比如 512 而不是 1024同时对称地调整up1的输入通道。因为down4层处理的特征图虽然尺寸最小原图的 1/16但通道数最厚占的显存反而最大。编码器前三层处理大尺寸特征图占用显存主要来自空间维度到最底层时空间小但通道多两者的乘积决定了实际显存占用。另外注意Up类里的上采样方式这个项目用的是转置卷积。转置卷积相比双线性插值多出一组可学习的权重表达能力更强但也更容易在训练初期产生棋盘格伪影。我实操时发现如果训练数据很少把Up里的转置卷积换成nn.Upsample(scale_factor2, modebilinear)会更稳定后期再换回转置卷积微调这是一个相当实用的策略。3. 数据准备闭环labelme 标注到 VOC 格式的转换与数据集划分3.1 从 JSON 标注到 PNG 灰度图labelme2seg.py 的作用医学图像分割项目的落地质量七成取决于数据准备的规范性。这个项目用的是 labelme 标注工具标注结果是一份 JSON 文件记录每个多边形的顶点坐标。模型训练需要的是像素级 mask所以第一步就是把 JSON 转成 PNG 灰度图。labelme2seg.py做的事就是读取 JSON 里的shapes对每个多边形做填充生成与原始图像等尺寸的 mask背景像素值为 0目标区域像素值为 255。这里有一个关键参数决定最终训练效果填充值。如果你打开labelme2seg.py会看到类似这样的核心逻辑mask np.zeros((height, width), dtypenp.uint8) for shape in data[shapes]: points np.array(shape[points], dtypenp.int32) cv2.fillPoly(mask, [points], color255)color255表示前景统一用 255 标记。这套代码的前提是二分类分割——目标区域和非目标区域。如果你的任务有多个类别强烈建议改成color1、color2之类的递增值而不是按 label 名称映射。因为 Pytorch 计算交叉熵时类别索引必须从 0 连续递增如果类别索引有跳号训练会直接报错或者指标计算失真。我从这套代码上踩过一次坑刚开始用类别名称首字母做索引比如把 sk 映射为 19训练时 loss 直接 Nan因为 19 号索引超过了模型输出通道数。label2png.py是另一个方向的工具它把 labelme 的可视化结果导出成叠加图方便你快速检查标注质量。实际项目流程中我会先跑一遍label2png.py把所有标注图叠加到原图上肉眼过一遍边界是否贴合病灶区域再进入训练。这一步能发现大量标注边界画歪、漏标的问题比训练完才看指标高效得多。3.2 data_remove_seg.py边界框裁剪与空标签过滤data_remove_seg.py这个文件名很有意思它在我见过的其他 Unet 项目里不常出现。它的作用是过滤那些没有有效分割区域的样本并做边界框裁剪。医学影像数据集里经常有大量图像只有很小的病灶区域或者根本没有标注如果直接用这种数据训练模型会严重偏向预测背景。# 核心逻辑示意剔除前景面积过小的样本 if foreground_ratio 0.01: print(fSkipping {image_name}, foreground ratio too low) continue这个0.01阈值很有讲究。如果你把阈值设得太高比如 0.05小病灶样本全部被过滤模型见不到小目标推理时对小病灶的召回率会非常差设得太低大量极端不平衡样本进入训练模型学到的 loss 被背景主导前景区域基本预测不出来。一般我会先看数据集中前景占比的直方图如果病灶区域占比集中在 0.5%~5% 之间阈值选 0.005 比较稳妥。3.3 数据集划分gen_split.py 与训练集验证集的组织方式医学影像分割比自然图像分割更讲究数据划分因为病灶样本通常稀缺。gen_split.py负责把数据划分为训练集和验证集。这个项目沿用 VOC 风格的目录组织即 JPEGImages 放原图SegmentationClass 放 mask 图ImageSets/Segmentation 放 train.txt 和 val.txt 两个索引文件。# gen_split.py 核心逻辑示意 with open(os.path.join(splits_dir, train.txt), w) as f: for img in train_imgs: f.write(img_name \n)这里有一个纯医学场景的血泪教训划分数据时一定要按患者 ID 而不是按图像切分。如果同一个患者的多张皮肤镜图像被分到训练集和验证集验证指标会虚高因为模型实际上在「背题」——它已经见过同一个皮肤区域的纹理特征了。我一般会在字典里维护每个患者对应的图像列表然后按患者维度做随机划分保证每个患者的图像只出现在一个集合里。这个坑在公开医学数据集上经常被研究论文踩到做课程设计时虽然没有那么严格的审稿压力但答辩时被问到「为什么你的验证集准确率这么高」会很难解释。4. 训练与评估train.py、utils_metrics.py 与结果目录里每个文件的价值4.1 训练主流程损失函数、优化器与训练节奏train.py是整个工程的核心入口。训练前先确认data/目录下有JPEGImages、SegmentationClass和ImageSets/Segmentation三个子目录然后修改train.py里的几个关键参数模型输入尺寸、batch size、epoch 数量、类别数。下面是训练脚本的关键配置段# train.py 关键参数配置 num_classes 1 # 二分类背景 病灶 input_size (512, 512) # 统一输入尺寸 batch_size 4 # 显存不够时先降这个值 epochs 100 # 医学小数据集建议配合早停 model UNet(n_channels3, n_classesnum_classes) criterion nn.BCEWithLogitsLoss() optimizer torch.optim.Adam(model.parameters(), lr1e-4)BCEWithLogitsLoss是二分类分割最常用的损失它内部把 Sigmoid 和 BCE 合并计算数值稳定性比手动先过 Sigmoid 再算 BCE 更好。学习率 1e-4 是 Unet 训练的保守起点如果你用了更大的 batch size 或者加了数据增强可以尝试 2e-4但我不建议超过这个值因为医学图像分割的监督信号相对稀疏学习率太大会让损失曲线剧烈震荡。训练节奏上有两个实操经验可以分享。第一是加载预训练编码器权重。这个项目的unet_model.py支持加载在 ImageNet 上预训练过的 VGG 或 ResNet 编码器如果你手头数据量小于 500 张强烈建议用预训练权重做迁移学习微调收敛速度和最终指标都会有明显提升。第二是启用早停机制train.py里的 epoch 循环可以加一个验证集 mIoU 的监控连续 15 个 epoch 不提升就停止并回滚到最优模型。医学数据标注成本高训练时间也是成本早停能帮你省下大量空跑时间。4.2 评估脚本输出的每一项指标都对应什么utils_metrics.py把这套评估体系做得相当完整训练结束后会在results/目录下生成Precision.png、Recall.png、mPA.png、mIoU.png和confusion_matrix.csv。很多人只关心 mIoU 一个数字但在答辩场景里能把每一项指标讲清楚其实是加分项。Precision.png展示的是精确率它回答的问题是「模型预测为病灶的像素里有多大比例真的是病灶」。数值高说明模型误报少但精确率高的模型可能过于保守漏掉了很多真实病灶。Recall.png对应召回率回答「真实的病灶像素里有多大比例被模型找出来了」。这两个指标在医学场景下存在天然的权衡你宁可多报几个假阳性区域也不能漏掉一个真病灶所以医生的视角通常更看重 Recall。mPA.png是平均像素准确率mIoU.png是平均交并比。IoU 是分割最核心的指标它计算预测区域和真实区域的交集除以并集同时惩罚了误检和漏检。confusion_matrix.csv则把每类的 TP、FP、FN、TN 数值都列出来方便你检查类别不平衡的影响。我见过一个有趣的翻车案例有人拿这套代码跑多类别任务训练日志显示 mIoU 有 0.87但打开confusion_matrix.csv发现模型把所有像素都预测成了背景因为背景类别占据 95% 以上的像素mIoU 被单个类别拉高了。只看均值不看分类别指标是评估阶段最容易踩的思维陷阱。4.3 requirements.txt 与运行环境的确定性requirements.txt这个文件虽然只有几行却是整个项目能否顺利复现的关键。项目依赖主要包括 Pytorch 1.x 系列、torchvision、opencv-python、numpy、PIL、labelme 和 tqdm 等。OpenCV 这里要特别注意版本pip install opencv-python会安装最新版但 Pytorch 1.x 对 OpenCV 的版本没有硬性要求反倒是 labelme 依赖的 PyQt 在 Python 3.10 以上版本容易安装异常。我在实际复现时踩过一次依赖冲突系统里预装的 numpy 是 2.0而这份代码里的某些函数用了np.float这类旧版 API直接报 AttributeError。解决方式是把 numpy 固定为 1.24.xpip install numpy1.24.3。项目根目录里的切换镜像.txt记录了使用国内镜像源安装依赖的命令常见做法是pip install -r requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple镜像源解决的是下载速度问题但依赖版本冲突必须靠 requirements.txt 或手动安装命令解决。我的操作习惯是新建一个干净的 conda 环境Python 版本用 3.8 或 3.9然后一次性安装全部依赖避免和系统环境里的其他包产生覆盖。5. 避坑指南从环境配置到模型训练的六个高发问题5.1 训练时 loss 为 NaN现象训练开始后前几个 step loss 还是正常数值突然变成 nan之后一直回不来。原因最常见的是学习率过大导致梯度爆炸其次是输入图像里有极端像素值比如全黑或全白的坏图经过归一化后仍然有异常第三个可能是 mask 标注里出现了超出类别范围的像素值。解决先看学习率Unet 配套的 Adam 优化器通常用 1e-4如果手动改到 1e-3 以上出 Nan 的概率会显著上升。然后检查输入图像的归一化方式确保图像矩阵除以 255 后在 0~1 之间。最后检查 mask 文件的像素值列表用np.unique(mask)看是否只包含 0 和 255 两个值如果出现 254、128 之类的中间值多半是标注转换时抗锯齿造成的需要进行二值化处理。5.2 预测图全黑或全白现象训练过程看起来一切正常loss 在下降验证集指标也不错但跑predict.py输出的分割图要么全黑要么全白。原因这是区分度最差的一个问题十有八九是模型输出和阈值处理不匹配。predict.py里对输出做 Sigmoid 然后把大于 0.5 的像素视为前景但如果你训练时用的是CrossEntropyLoss且num_classes1输出层就不是概率值阈值 0.5 完全不适用。解决先明确训练时用的损失函数。二分类用BCEWithLogitsLoss预测时torch.sigmoid(pred) 0.5多分类用CrossEntropyLoss预测时torch.argmax(pred, dim1)。另一个坑是模型加载时没有切到 eval 模式模型里的 Dropout 和 BN 层在训练模式下行为不同记得补上model.eval()和with torch.no_grad()。5.3 显存不足OOM现象训练 program 启动后直接报 CUDA out of memory程序退出。原因输入图像分辨率过高、batch size 过大、模型通道数过宽这三个因素叠加导致显存爆掉。512×512 输入加 batch size 4 已经比较极限如果你用 CPU 尝试跑完整训练大概率也要等待很久。解决优先把 batch size 降为 2 或 1同时启用梯度累积模拟更大的 batch。如果还不行把输入尺寸从 512×512 降到 448×448 或 384×384。医学分割场景下多数病灶对分辨率的要求没有高到不能缩小的程度。最后再考虑修改unet_model.py里的通道数把 64 打头的通道序列整体压缩一半。5.4 评估脚本报 KeyError现象utils_metrics.py运行时报错提示找不到某个 key通常是混淆矩阵构建时类别索引不存在。原因数据集的类别索引和模型输出的类别数不一致。典型场景是你下载的 mask 图里有 3 个像素值但你设置num_classes2评估时遍历到第 3 个类别索引就崩溃了。解决打开confusion_matrix.csv之前先跑一段脚本统计所有 mask 的唯一值import numpy as np from glob import glob masks glob(data/SegmentationClass/*.png) values set() for m in masks: values.update(np.unique(np.array(Image.open(m)))) print(sorted(values))把所有唯一的像素值打出来确认类别数后再设置num_classes。如果出现意外的中间值在转换脚本里强制二值化处理。5.5 验证集 mIoU 远低于训练集现象训练集上 mIoU 已经到 0.9验证集却只有 0.5差距悬殊。原因这是典型的过拟合加上验证集数据量太小或与训练集分布不同。医学数据集天然样本量少模型容易把训练集细节背下来尤其是病灶边缘的纹理特征被过度学习之后泛化到新图效果就会断崖式下跌。解决先做数据增强空翻、旋转 90 度、随机亮度对比度调整都加上。增强后训练集样本量扩了几倍过拟合会明显缓解。其次是引入预训练编码器做迁移学习让模型初始权重不是随机状态降低过拟合风险。如果增强后仍然有严重差距可以考虑用交叉验证或者扩大数据集但做课设的话前两种手段已经足够。5.6 切换镜像.txt 里的镜像源失效现象按照切换镜像.txt里的命令执行pip install后卡住或提示连接超时。原因国内镜像源偶尔会有服务波动或者你当前网络环境对该镜像源限速。这不代表代码有问题只代表网络环境变化。解决备选方案是换其他镜像源常见做法是执行下面的命令设置全局 pip 源pip config set global.index-url https://pypi.tuna.tsinghua.edu.cn/simple pip config set global.trusted-host pypi.tuna.tsinghua.edu.cn如果清华源也卡就换阿里源或中科大源。装 Pytorch 这类大包时优先用官方源或者项目的专属源因为 Pytorch 的 CUDA 版本依赖关系比较复杂普通 PyPI 源可能找不到对应的 wheel。6. 从预测脚本到 UI 演示验证模型的四种方式和一套组合技巧训练完模型后最担心的是模型只在指标上好看放到实际图像上却翻车。这个项目提供了predict.py、test.py和ui.py三个验证入口我建议首先跑一遍predict.py用单个样本做端到端的推理这是最高效的 smoke test。predict.py里默认从testdata目录读图然后调用utils.py里的预处理函数完成归一化和 resize最后把分割结果、原图和叠加可视化图并存到指定目录。test.py则是对整个测试集的批量评估方式。它的输出包括每张图像的 mIoU 值、平均推理时间以及失败样本的可视化。批量评估的价值在于找到模型失效的边界条件——比如暗光环境下的小病灶、被皮肤纹理遮挡的模糊病灶。每次看到test.py跑完的指标我习惯按 mIoU 从低到高排一下挨个打开末位那批样本看是标注问题还是模型问题这个习惯让我能快速定位数据集的短板。ui.py是可交互图形界面适合答辩演示场景。它用 Python 的 Tkinter 或 PyQt 实现提供一个按钮加载图像、一个按钮开始分割界面上并排显示原图、mask 和叠加结果。看起来简单但要保证界面交互不卡顿有两处实现细节第一模型必须在启动 UI 时加载一次放在全局变量里不要在每次点击分割时都重新加载权重第二推理时的预处理和后处理要写进单独的segment_one_image函数复用utils.py里的逻辑保证命令行和 GUI 得到的结果一致而不是写两套代码各跑各的。演示的时候还有一个很实用的小技巧把预测结果和原图做了半透明叠加处理alpha 值取 0.4 左右比较合适。太高了会遮住病灶边缘的细节太低了看不出分割边界。这个叠加图比只看黑白 mask 要直观得多答辩现场三秒钟就能让评委看清模型的能力。整个项目跑通以后我复盘了最想说的一条教训医学影像分割项目的成功与否从来不只是模型决定的。数据标注的规范度、数据集划分方式的合理性、验证指标的分析深度这三样东西占的比重比模型结构本身高得多。从那以后我每次拿到一个新的分割项目第一周永远不会碰模型代码先把数据清洗和验证协议跑通再回头改网络结构效果比之前直接调模型参数强不少。希望这套从标注到评估的完整链路能帮你在 Unet 医学影像分割这条路上少走一段弯路。本文还有配套的精品资源点击获取
返回列表