☰
肺部分割实战:胸部X光U-Net源码解析与避坑指南
2026/9/28 12:12:19 网站建设 项目流程

简介:这是一份面向医疗影像研究与深度学习开发者的肺部分割实战项目,针对胸部X光图像中的肺部区域识别与病理辅助定位问题,提供完整可运行的算法源码与训练流程。包内共19个文件、约161.85MB,包含3个Python核心脚本(数据加载、模型定义与评估指标)、2个Jupyter Notebook实验文档、2个预训练U-Net权重文件(pt)、1个数据划分文件(pk),以及8张训练过程/结果展示图片和说明文档,覆盖从数据预处理、模型训练到效果验证的主要环节。项目采用卷积神经网络实现像素级肺部区域分割,并引入数据增强与多种训练策略,帮助理解真实医疗影像任务中的模型构建与调优思路。已有65人学习下载,适合需要参考完整项目源码、快速上手医学图像分割实验或开展相关毕设与课题研究的用户。

1. 肺部分割算法实战:这份胸部X光的U-Net源码包解决了什么

一个很现实的场景:放射科医生每天要过几百张胸部X光片,肺实质区域在哪、边界是否清晰,直接决定后续病灶筛查的准确度。做深度学习辅助诊断的人,第一步通常不是检测结节,而是先把肺分割出来——这一步错了,后面的病变分析全跟着偏。这份基于胸部X光的肺部分割项目源码,把U-Net从数据组织、训练到评估的完整链路打包好了,还带了多个版本的权重和训练曲线,属于那种能直接上手的实战项目。适合刚入医学影像分割的开发者、做毕业设计的学生,以及想快速验证U-Net在肺分割上效果的研究人员。整体结构不复杂,三个Python文件加两个notebook,跑通一遍就能把加深度的流程摸透。

2. 项目源码拆解:从文件清单看懂U-Net肺分割项目的骨架

2.1 源码包内部结构:每个文件承担什么角色

zip解压后先别急着跑训练,把目录结构捋一遍,能省掉后面很多定位问题的时间。核心目录是lung-segmentation,按职责拆成src、models、images三块。src下只有metrics.py、models.py、data.py三个文件,分别管评估指标、模型结构、数据加载,这种拆分是医学影像深度学习的标准姿势。训练入口用main.ipynb串起来,notebook做实验记录比纯脚本直观——每次改参数留下一个单元格的输出,翻起来比终端日志清楚得多。sandbox.ipynb是草稿本,调试小功能用的,正式实验别写在里面。

文件/目录作用什么时候用
src/data.py读图、预处理、生成训练/验证张量改数据流程或者查预处理逻辑
src/models.pyU-Net结构定义,含1~6版演进想加深网络、换backbone时
src/metrics.pyDice、IoU等分割指标计算评估阶段统一调用
main.ipynb主流程:加载数据、训练、保存权重复现的主入口
sandbox.ipynb实验草稿临时验证某个想法
splits.pk训练/验证/测试划分,pickle格式保证多次实验数据划分一致
models/unet-6v.pt第6版U-Net权重推理和迁移首选
models/unet-2v.pt第2版权重做版本对比
images/*.png各版训练历史曲线和最终效果图分析训练过程和结果

readme.md和train-log.txt是必看的两个文本文件。readme告诉你数据放哪、命令怎么跑,train-log记录了每次迭代的loss变化,能快速判断收敛节奏。splits.pk这个文件容易被忽略,但它的价值在于固定了数据划分——没有它,换台机器或者重开session,随机划分一变,训练集和验证集就不一样了,后面的指标没法横向比。我一般拿到项目先看train-log和六张训练曲线,再决定要不要从头训练。

2.2 为什么选U-Net而不是SegNet:医学小数据集的现实选择

项目正文里提到SegNet是备选架构,但源码实际跑的是U-Net系列,这个选择本身值得展开。SegNet的编码器-解码器结构用池化索引恢复位置信息,结构轻,但对边界细节的还原能力一般。U-Net在每层编码器和对应解码器之间加了跳跃连接,把高分辨率特征直接拼到解码器对应层,这对肺分割是决定性的。X光片里肺部与心脏、肋骨、纵隔的灰度接近,深层语义特征只能告诉你“这里是胸腔”,分辨不了肺的精确边界,必须靠浅层高分辨率特征把边缘勾出来。

从工程成本看,U-Net参数量适中,单张消费级显卡就能训练,这对医疗影像项目是刚需。项目里unet-1v到unet-6v的六次迭代,本质上是在调编码器深度、初始通道数和下采样路径。每版对应一份独立权重和训练曲线,对比起来一目了然。我自己跑这类项目时也习惯这种版本号递增的写法——同一个模型文件反复覆盖,最后连哪个参数组合出了什么结果都说不清,这种血泪经验相信不少人都有过。

2.3 损失函数与评估指标:交叉熵和Dice怎么配合

肺分割是像素级二分类,每个像素要么是肺、要么不是肺。最常见的是交叉熵和Dice损失的组合。交叉熵对每个像素独立计算梯度,优化稳定,但X光片里肺区域占比通常不到三分之一,背景像素远多于前景,纯交叉熵会让模型偏向预测背景,分割结果偏保守。Dice损失直接最大化预测与真实mask的重叠区域,对前景小、背景大的场景更友好,但它对区域整体度量,单独用时收敛曲线容易抖动,尤其训练初期。

Dice = 2×|A∩B|/(|A|+|B|),IoU = |A∩B|/|A∪B|,这两个都衡量区域重合度。Dice对边界小误差更宽容,医学分割论文里用Dice居多,因为临床关注的是病变区域有没有被完整覆盖。loss里通常要给交叉熵和Dice配一个权重,常见做法是0.5对0.5起步,看验证集表现再调,Dice权重大一点会让mask更完整。metrics.py里实现的就是这两个指标的批量计算,跑完训练再看images/obtained-results.png,原图、真实mask、预测mask三列并排对比,比盯数字更能说明模型行不行。

3. 复现训练流程:从数据预处理到加载unet-6v权重推理

3.1 环境准备:CUDA确认与依赖安装

复现第一步是装环境。打开requirements.txt,核心依赖是PyTorch、OpenCV、scikit-image、Jupyter。先用conda创建一个干净环境,别直接装在base里,后面版本冲突有你受的。

conda create -n lung_seg python=3.8 -y conda activate lung_seg pip install -r requirements.txt python -c "import torch; print(torch.__version__, torch.cuda.is_available())"

第一行创建隔离环境,第二行激活,第三行按清单安装依赖。最后一行确认PyTorch版本和CUDA是否可用——如果打印False,说明装了CPU版或者CUDA没配对,后面训练慢得让人怀疑人生。常见做法是PyTorch用官方命令单独装,匹配你本机的CUDA驱动版本,requirements.txt里的torch版本可能偏保守。装完之后把数据目录和项目根目录的路径关系确认好,readme里一般会写,别把路径写死成自己机器上的绝对路径。

3.2 数据加载与预处理:灰度图、归一化与mask插值

data.py里核心是两件事:读图、预处理。读X光片用OpenCV按灰度模式读,然后做统一尺寸、归一化、mask二值化。这里有一个很多人都会翻车的细节:mask的resize插值方式必须和图像不同。

import cv2 import numpy as np def load_pair(img_path, mask_path, size=(256, 256)): # 图像用面积插值缩小,保留灰度层次 img = cv2.imread(img_path, cv2.IMREAD_GRAYSCALE) img = cv2.resize(img, size, interpolation=cv2.INTER_AREA) img = img.astype(np.float32) / 255.0 # 归一化到[0,1] # mask必须用最近邻插值,避免边缘出现中间灰度 mask = cv2.imread(mask_path, cv2.IMREAD_GRAYSCALE) mask = cv2.resize(mask, size, interpolation=cv2.INTER_NEAREST) mask = (mask > 127).astype(np.float32) # 统一二值化阈值 return img, mask

cv2.INTER_AREA在缩小图像时能保留更多灰度层次,适合X光原图这种大尺寸输入;归一化到[0,1]是为了配合U-Net最后sigmoid输出。mask用INTER_NEAREST是因为分割标注是硬标签,线性插值会产生0.5这种中间值,训练时这些边界像素的损失会一直震荡,模型始终学不稳。

注意:mask的resize必须用INTER_NEAREST,用线性插值产生的中间灰度会让Dice指标虚高,但实际分割边界是糊的。

3.3 训练参数与版本演进:unet-1v到unet-6v改了什么

main.ipynb里训练流程是固定的:加载splits.pk划分数据,构建对应版本的U-Net,选Adam优化器,算交叉熵加Dice的组合损失,循环跑epochs。unet-1v到unet-6v的差异,从权重文件名和训练曲线能看出来是逐步加深网络、调整初始通道数。train-log.txt里能看到每轮loss的变化,通常第二个epoch开始明显下降,到第40轮左右进入平台期。

参数常见取值为什么这么设
输入尺寸256×256或512×512512细节多但显存翻倍,小显存先用256
batch size8~16受显存限制,坑见避坑章节
学习率1e-3起步,余弦衰减Adam适合分割任务,后期降到1e-5
轮数50~100以验证Dice不再上升为准,不是固定值
数据增强旋转±10°、水平翻转、微小缩放解剖结构有约束,旋转大了会失真

数据增强这块项目正文提到了旋转、缩放、平移。对胸部X光,水平翻转是安全的,左右肺基本对称;垂直翻转完全不可用,解剖位置被颠倒,模型学到的先验就是错的。随机旋转建议控制在±10度以内,角度太大肺尖和肋膈角的形态被扭曲,训练损失看着降了,验证集上边界却是锯齿状。

3.4 加载预训练权重:unet-6v.pt推理一条龙

如果不想从头训练,models/unet-6v.pt可以直接拿来推理。注意torch.load默认把权重加载到保存时的设备,跨机器使用要指定map_location。

import torch from src.models import UNet # version=6对应unet-6v.pt,网络结构必须和权重匹配 model = UNet(version=6) ckpt = torch.load('models/unet-6v.pt', map_location='cpu') if 'state_dict' in ckpt: model.load_state_dict(ckpt['state_dict']) else: model.load_state_dict(ckpt) model.eval() # 对单张图推理 img, _ = load_pair('data_example.png', 'dummy_mask.png') with torch.no_grad(): prob = torch.sigmoid(model(img.unsqueeze(0).unsqueeze(0)))

先实例化对应版本的网络结构,再加载权重。很多项目会把checkpoint包成字典,里面包含state_dict、epoch、optimizer状态,判断一下键名最稳妥。推理必须包在torch.no_grad()里,否则会建计算图,显存白白消耗。model.eval()也别忘了,训练模式的dropout和BatchNorm统计会让推理结果不稳定,这是新手最容易漏的一步。

4. 肺部分割避坑指南:五个最容易翻车的细节与排查

4.1 坑一:mask二值化阈值不统一,Dice虚高

现象:训练时用(mask > 127)转二值,测试评估时却用了(mask > 0)或者直接拿原始mask算指标,Dice比实际高出好几个点,看起来效果很好,部署就露馅。

原因:mask数据里像素值可能是0/255,也可能是0/1,甚至两种格式混合。不同脚本、不同session里各写各的阈值,标准就飘了。评估脚本和训练脚本如果不共用同一个预处理函数,这种不一致几乎是必然的。

解决:在data.py里统一封装一个二值化函数,训练、验证、测试全部走同一个入口。阈值固定写死127,输出统一成float32的0/1张量,不要在notebook里到处手写一遍。

4.2 坑二:数据增强过头,肺边缘失真

现象:训练加了±30度旋转和0.8倍缩放,训练集loss降得很快,验证Dice却上不去,预测mask边缘呈锯齿状,甚至肺尖位置飘了。

原因:胸部X光片里肺的解剖位置相对固定,旋转和缩放一旦超过生理范围,模型学到的增强域和真实分布偏离了。数据增强的初衷是模拟拍摄角度差异,不是制造解剖变异。

解决:旋转控制在±10度,缩放±0.1,只保留水平翻转和轻微平移。弹性形变这类增强在肺分割里慎用,除非你的训练数据量大到能覆盖这种形变,否则就是硬生生把一个规则问题变成无规则问题。

4.3 坑三:训练和验证的预处理不一致

现象:训练做了归一化和增强,验证时直接读原图送进模型,验证集Dice高得离谱,换到真实场景效果崩塌。

原因:模型见过的输入分布和实际输入分布不一致,验证变成了一种作弊。归一化参数、图像尺寸、通道顺序,任何一处不一致都会让验证指标失真。

解决:把图像读取、缩放、归一化封装成同一个函数,训练、验证、推理全部调用它。这也是为什么data.py要集中管理预处理,而不是在main.ipynb里各处写一遍。我见过有人训练用BGR、验证用RGB,模型验证指标照样能看,但一部署就废。

4.4 坑四:显存不够直接砍batch size,BatchNorm失效

现象:换了个更深的版本号,显存爆了,把batch size从16砍到2,结果loss跳来跳去,指标越训越差。

原因:BatchNorm在batch维度上计算均值和方差,batch太小统计量噪声大。肺分割单张图里背景占比高,小batch下统计量更容易漂移,尤其训练后期。

解决:优先用梯度累积模拟大batch,比如batch size=2跑8步再更新一次,等效于16。或者把BatchNorm换成GroupNorm,U-Net在group size设8左右时,小batch训练稳定得多。显存不够时先缩输入尺寸,再考虑动batch size,这个顺序别搞反。

4.5 坑五:只盯loss曲线,不看分割效果

现象:loss单调下降,以为训练一切正常,结果预测mask左右肺连成一片,或者内部有大量孔洞,区域统计完全没法用。

原因:交叉熵加Dice的组合损失,数值下降不必然代表空间上的分割合理。连通性、解剖结构约束这些信息,从数值曲线里看不出来。

解决:每个epoch结束保存几张验证图,原图、真实mask、预测mask拼在一起看,images下的obtained-results.png就是这么来的。训练脚本里加一段可视化代码耗不了几行,它比任何指标都早暴露问题。

5. 分割结果落地:mask后处理与Dice/IoU定量验证

5.1 mask后处理:去噪、连通域筛选与空洞填补

模型输出的概率图直接二值化,通常会有两类问题:远离肺区的孤立噪声点,以及肺内部的小空洞。这些不影响肉眼观察,但会影响后续基于mask的病变检测——噪声点会被算成病灶面积,空洞会导致特征统计失真。我一般做三步后处理。

from scipy import ndimage import numpy as np def postprocess_mask(prob, min_area=500): # 第一步:二值化,阈值和训练时保持一致 binary = (prob > 0.5).astype(np.uint8) # 第二步:连通域分析,剔除小于min_area的孤立区域 labeled, num = ndimage.label(binary) sizes = ndimage.sum(binary, labeled, range(1, num + 1)) keep = [i + 1 for i, s in enumerate(sizes) if s >= min_area] clean = np.isin(labeled, keep).astype(np.uint8) # 第三步:闭运算填补内部小空洞 clean = ndimage.binary_closing(clean, iterations=3).astype(np.uint8) return clean

ndimage.label给每个连通区域编号,ndimage.sum统计每个区域像素数,小于min_area的直接剔除。min_area按原图面积比例取,512×512的图取500像素左右的噪声块比较合适。binary_closing迭代3次,把mask内部小空洞填掉,同时不改变肺的整体轮廓。

注意:闭运算迭代次数别贪多。迭代过头会把左右肺在纵隔区域焊起来,或者把肋膈角填平,解剖结构就错了。

后处理阶段还有一个更激进的约束:只保留面积最大的两个连通域,对应左右肺。这个约束在正常X光片上非常有效,但遇到单侧肺不张、胸腔积液占据部分区域的病例,会误删真实肺区,部署前要根据你的病例分布决定用不用。

5.2 定量验证:Dice和IoU评估脚本与测试集组织

后处理完要量化验证,别再用肉眼定结论。把预测mask和真实mask逐像素比对,计算Dice和IoU。这个脚本要对整个测试集跑,输出平均值和标准差,不是只算一张图。

def dice_iou(pred, target, smooth=1e-6): pred = (pred > 0.5).float() inter = (pred * target).sum() dice = (2 * inter + smooth) / (pred.sum() + target.sum() + smooth) iou = (inter + smooth) / (pred.sum() + target.sum() - inter + smooth) return dice.item(), iou.item()

smooth防止预测和target都为空时除零。pred先二值化再算,标准统一。跑完测试集后把每张图的Dice存下来,算均值和标准差——均值反映整体水平,标准差反映稳定性。哪个case的Dice明显低于均值,把原图和预测图单独拉出来,通常能发现是胸腔积液、起搏器植入这类极端情况。这些case对做临床落地反而是最有价值的分析对象,直接决定你的模型能覆盖到什么程度。

测试集的组织直接复用splits.pk里的test划分,不要自己重新随机抽。用同一个划分才能和项目原版的指标对比,也才能判断unet-6v.pt相比unet-2v.pt在测试集上到底提升多少。

6. 进阶调优:用六版训练曲线定位瓶颈,三个可马上试的改进点

images目录下从unet-1v到unet-6v的六张训练历史曲线,是这个项目里最值钱的分析素材。把六张图放在一起看,能判断瓶颈在模型容量还是数据量。如果前几版loss降不动、验证Dice一直低,加深网络后明显改善,说明容量不足;如果loss降了但验证指标停滞,说明过拟合,该加正则化或者数据增强,而不是继续加层。

基于这个判断,三个改进点可以马上动手。第一,测试时增强(TTA):推理时把输入做水平翻转和微小旋转,对预测概率取平均再做二值化。肺分割上TTA通常能稳定提升零点几个Dice点,代价是推理时间成倍增加。

提示:TTA对分割不是每次都有正收益,跑一轮对比,没效果就撤掉,别为了炫技拖慢推理。

第二,把后处理的连通域约束做成可配置项,正常病例保留最大两个连通域,极端病例放开限制。跑通评估脚本后对比两组结果,能直观看到这个约束的价值。第三,换预训练编码器——把U-Net的编码器换成ImageNet预训练过的EfficientNet或ResNet系列。医学图像数据集普遍小,迁移学习的收益往往比单纯加深网络更明显。

验证这三个改进有没有用,不要凭感觉,回到第5章的评估脚本跑一遍测试集,对比baseline的Dice均值和标准差。从那以后我每次跑分割项目都强制走一遍这个流程:先看训练历史曲线判断瓶颈,再做后处理,最后用统一脚本出指标。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询