news 2026/9/28 15:25:17

基于DenseUnet的CT切片左右肺分割实战:从训练到推理的完整指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
基于DenseUnet的CT切片左右肺分割实战:从训练到推理的完整指南

简介:本资源面向医学图像分割方向的初学者与算法工程师,提供一套基于DenseUnet的CT肺部左右肺分割完整实战方案,覆盖从数据准备到模型评估的全流程。压缩包共约2000个文件,以1984张png格式的CT切片与掩膜图像为主体,另含8个Python脚本、5个xml标注文件及2个txt说明文档,整体约236.76MB,目录结构清晰,便于按训练、验证、推理模块检索。其中train脚本可输出训练集与验证集的loss、IoU曲线、学习率衰减曲线及训练日志,evaluate脚本用于计算测试集的IoU、Recall、Precision与像素准确率,predict脚本则生成gt及gt叠加原图的掩膜结果,代码注释详尽,参照README即可快速训练自有数据。目前已有159人学习,适合希望掌握肺部多类别分割、理解DenseUnet网络结构与评估指标的读者参考实践。

1. 从一张 512×512 的 CT 切片说起:DenseUnet 左右肺分割到底能落地成什么

手里有一批新冠 CT 切片,文件名是coronacases_org_002_93.png这种格式,打开一看是灰度图,肺部和背景灰度接近,手动勾左右肺轮廓勾到怀疑人生。这个资源要解决的就是这件事:用 DenseUnet 把 CT 切片里的背景、左肺、右肺分成三类,输出掩膜,直接拿去做体积量化或者后续病灶分析。它包含完整代码和数据集,train、evaluate、predict 三个脚本各管一段,训练日志、loss 曲线、IoU 曲线、学习率衰减曲线、数据集可视化图都会自动生成,代码注释写得比较细,README 里写了怎么换成自己的数据。适合谁?做医学图像分割的算法工程师、需要快速搭一个肺部预处理流水线的研究生,以及想拿一个能跑通的多类别分割 baseline 来改的人。不适合指望开箱即用做临床诊断的——它是个工程起点,不是医疗器械。

2. DenseUnet 为什么适合做肺部分割:结构选型与数据组织

2.1 从 Unet 到 DenseUnet:密集连接解决了什么

Unet 的编码器-解码器加跳跃连接已经是分割任务的标配,但普通 Unet 在层数加深后容易出现梯度消失和特征复用不足。DenseUnet 把 DenseNet 的密集块塞进 Unet 的编码器和解码器里,每一层的输入来自前面所有层的输出拼接。对肺部 CT 这种目标边界模糊、左右肺之间只隔一条纵隔线的场景,密集连接让浅层的边缘纹理能更直接地传到深层,减少小目标(比如肺尖和肺底)被漏掉的情况。

具体到这份代码,编码器用 DenseBlock 堆叠,每个 DenseBlock 内部做BN → ReLU → Conv的瓶颈结构,块与块之间用过渡层做下采样。解码器对应地上采样,和编码器同分辨率层做跳跃拼接。最后 1×1 卷积输出 3 通道,对应背景、左肺、右肺。选 DenseUnet 而不是原始 Unet 的理由很实际:在数据量不大的情况下(这份数据集是几十张切片级别),密集连接带来的特征复用能缓解过拟合,同时参数量比同深度的 ResNet 编码器更可控。

2.2 数据目录怎么摆:图像和掩膜必须同名

拿到资源后第一件事不是跑训练,是确认目录结构。常见做法是把图像放一个文件夹,掩膜放另一个文件夹,文件名一一对应。这份资源里的切片命名是coronacases_org_002_93.png这种,掩膜通常同名但放在masks或labels目录下。如果掩膜是灰度图,像素值 0 是背景、1 是左肺、2 是右肺;如果是 RGB 彩色掩膜,需要先转成单通道索引图。

# 典型目录结构,先确认再动代码 dataset/ ├── images/ │ ├── coronacases_org_002_93.png │ ├── coronacases_org_002_123.png │ └── ... └── masks/ ├── coronacases_org_002_93.png ├── coronacases_org_002_123.png └── ...

逻辑说明:训练脚本一般会遍历images目录,按文件名去masks目录找对应掩膜。如果掩膜缺失或者文件名对不上,训练会在数据加载阶段报错或者静默跳过,导致实际训练样本数比预期少。参数上,图像尺寸建议统一 resize 到 512×512 或 256×256,前者保留更多肺尖细节,后者省显存。我一般会先写个脚本统计一下掩膜里 0、1、2 的像素占比,如果某一类占比低于 1%,说明数据不平衡,需要在 loss 里加权重。

2.3 训练脚本里那几个必须改的参数

train 脚本会生成训练集和验证集的 loss、IoU 曲线、学习率衰减曲线、训练日志和数据集可视化图像。跑之前重点看这几个参数:batch_size、lr、num_epochs、num_classes。num_classes必须是 3,对应背景、左肺、右肺,改成 2 或者 4 都会让输出通道和标签对不上。lr初始值常见做法是 1e-3 或 1e-4,配合余弦退火或者 StepLR。如果 loss 曲线在前几个 epoch 就炸成 NaN,先把 lr 降到 1e-4 再试。

# 训练脚本里数据加载和模型初始化的关键片段(示意) import torch from torch.utils.data import DataLoader from dataset import LungDataset from model import DenseUnet # num_classes=3 是硬约束:背景、左肺、右肺 model = DenseUnet(num_classes=3).cuda() train_dataset = LungDataset(root_dir='dataset', split='train', img_size=512) val_dataset = LungDataset(root_dir='dataset', split='val', img_size=512) # batch_size 根据显存调,8G 显存从 4 开始试 train_loader = DataLoader(train_dataset, batch_size=4, shuffle=True, num_workers=2) val_loader = DataLoader(val_dataset, batch_size=4, shuffle=False, num_workers=2) # 交叉熵损失,如果类别不平衡可以加 weight 参数 criterion = torch.nn.CrossEntropyLoss() optimizer = torch.optim.Adam(model.parameters(), lr=1e-4)

逻辑说明:LungDataset负责读图、resize、归一化和标签映射。CrossEntropyLoss默认对每个像素做 softmax 后算交叉熵,适合多类别分割。如果左肺和右肺像素数远少于背景,可以在CrossEntropyLoss里传weight=torch.tensor([0.1, 1.0, 1.0])这种,把背景权重压低。num_workers在 Windows 上设 0 更稳,Linux 上可以设 2 到 4。训练日志里重点看验证集 IoU,如果训练集 IoU 一直涨但验证集不涨,就是过拟合,加数据增强或者减模型深度。

3. 评估与推理:IoU、Recall、Precision 怎么读,掩膜怎么出

3.1 evaluate 脚本输出的四个指标分别意味着什么

evaluate 脚本用于评估模型,计算测试集的 IoU、Recall、Precision、像素准确率。这四个指标在医学分割里各有侧重。IoU 是交并比,衡量预测掩膜和真实掩膜的重叠程度,对边界敏感。Recall 是召回率,衡量真实肺部像素里有多少被找出来了,漏检多的时候 Recall 低。Precision 是精确率,衡量预测为肺部的像素里有多少是真的,误检多的时候 Precision 低。像素准确率是整体像素分类正确的比例,在背景占大头的时候会虚高,不能单独看。

# 评估指标计算的核心逻辑(示意) def compute_metrics(pred_mask, true_mask, num_classes=3): ious, recalls, precisions = [], [], [] for cls in range(1, num_classes): # 跳过背景类 pred_cls = (pred_mask == cls) true_cls = (true_mask == cls) intersection = (pred_cls & true_cls).sum().item() union = (pred_cls | true_cls).sum().item() ious.append(intersection / union if union > 0 else 0) recalls.append(intersection / true_cls.sum().item() if true_cls.sum() > 0 else 0) precisions.append(intersection / pred_cls.sum().item() if pred_cls.sum() > 0 else 0) return ious, recalls, precisions

逻辑说明:这里跳过背景类,只算左肺和右肺。如果测试集里某张切片没有左肺(比如切片位置偏上),true_cls.sum()为 0,除法会出问题,所以加了判断。实际跑的时候,evaluate 脚本会把所有测试样本的指标平均。如果 Recall 明显低于 Precision,说明模型偏保守,漏检多,可以试着降低分类阈值或者增加正样本权重。如果 Precision 低,说明误检多,可能是掩膜标注噪声或者模型把纵隔误判成肺。

3.2 predict 脚本生成的 gt 和 gt+image 掩膜怎么用

predict 脚本用于推理图像,会生成 gt 以及 gt+image 的掩膜图像。gt 是真实标签的彩色掩膜,gt+image 是把预测掩膜叠加在原图上的效果图。这两张图是给你做定性检查的:打开 gt+image,看预测的左右肺边界是不是贴合真实轮廓,有没有把气管或者肋骨误分进去。如果发现某几张切片效果特别差,把它们挑出来单独看,通常是切片本身对比度低或者有伪影。

# 推理脚本常见调用方式 python predict.py --checkpoint checkpoints/best_model.pth --input dataset/images --output results/

逻辑说明:--checkpoint指向训练保存的最优权重,--input是待推理图像目录,--output是输出目录。推理时模型处于 eval 模式,不计算梯度。输出掩膜一般用调色板映射:背景黑色、左肺红色、右肺绿色,方便肉眼区分。如果输出全黑,先检查权重路径对不对,再检查输入图像归一化方式是不是和训练时一致。常见坑是训练用了 ImageNet 均值方差归一化,推理时忘了做,导致输入分布偏移,预测全错。

3.3 训练曲线怎么判断模型有没有收敛

train 脚本生成的 loss 曲线、IoU 曲线、学习率衰减曲线是判断训练状态的直接依据。loss 曲线正常是训练 loss 和验证 loss 都下降,最后趋于平稳。如果训练 loss 降但验证 loss 升,是过拟合。如果两条都降不下去,是欠拟合或者学习率太小。IoU 曲线看验证集 IoU,一般在前 20 到 50 个 epoch 快速上升,然后缓慢爬升。学习率衰减曲线用来确认调度器有没有生效,如果 lr 一直是一条水平线,说明调度器没配好。

提示:训练日志里会记录每个 epoch 的 loss 和 IoU,建议用tail -f train.log实时看,比等训练完再看曲线更早发现问题。

4. 避坑与排查:从数据到推理的五个血泪经验

4.1 掩膜像素值不是 0/1/2,训练直接崩

现象:训练启动后 loss 一直是 NaN,或者 IoU 始终为 0。原因:掩膜图像保存时被 JPEG 压缩或者用 RGB 保存,像素值变成了 0/128/255 或者 0/85/170 这种,不是连续的 0/1/2。解决:用 PIL 或 OpenCV 读掩膜后先打印唯一值,确认是 0、1、2 再做标签映射。如果是 0/128/255,写个映射表转成 0/1/2。

import numpy as np from PIL import Image mask = np.array(Image.open('masks/coronacases_org_002_93.png')) print(np.unique(mask)) # 先看唯一值 # 如果是 [0, 128, 255],做映射 mask = np.where(mask == 128, 1, mask) mask = np.where(mask == 255, 2, mask)

4.2 训练集和验证集按切片随机划分,同一病人的切片泄漏

现象:验证集 IoU 高得离谱,测试集一跑就掉。原因:同一病人的 CT 切片被随机分到了训练集和验证集,切片之间高度相似,等于变相泄漏。解决:按病人 ID 划分,同一病人的所有切片只出现在一个集合里。这份资源的文件名里coronacases_org_002就是病人标识,按这个前缀分组再划分。

4.3 推理时忘了做和训练一致的归一化

现象:predict 输出的掩膜全是背景或者全是肺。原因:训练时对图像做了(img - mean) / std归一化,推理时直接喂原始像素。解决:把训练时的归一化参数存下来,推理时复用。常见做法是在 dataset 类里写一个preprocess函数,训练和推理都调它。

4.4 显存不够,batch_size 降到 1 还是 OOM

现象:CUDA out of memory,batch_size 降到 1 依然报错。原因:输入图像尺寸太大(比如 1024×1024),或者模型在验证阶段没加torch.no_grad(),梯度图一直累积。解决:先把输入 resize 到 512×512,验证和推理包在with torch.no_grad():里,再不行就换小一点的 DenseUnet 配置(减少 DenseBlock 层数)。

4.5 学习率衰减曲线是直线,模型后期不收敛

现象:训练后期 loss 震荡,IoU 上不去。原因:学习率调度器没生效,lr 一直保持初始值。解决:检查 scheduler 的step()调用位置,常见错误是写在 epoch 循环外面或者忘了调。如果是余弦退火,确认T_max设成了总 epoch 数。

5. 换自己的数据怎么改:从 README 到进阶调参的实操路径

5.1 替换数据集的三步走

README 里写了怎么训练自己的数据,核心就三步。第一步,把自己的图像和掩膜按同名规则放进images和masks目录。第二步,改 dataset 类里的num_classes和标签映射,如果你的数据只有背景和肺(不区分左右),num_classes改成 2,标签映射改成 0 和 1。第三步,改 train 脚本里的数据路径和输出目录,跑一遍确认数据加载没问题。

# 自定义数据集类的最小改动点 class MyLungDataset(Dataset): def __init__(self, root_dir, split='train', img_size=512): self.img_dir = os.path.join(root_dir, 'images') self.mask_dir = os.path.join(root_dir, 'masks') self.img_size = img_size self.files = sorted(os.listdir(self.img_dir)) # 按病人 ID 划分训练验证,避免切片泄漏 self.files = self._split_by_patient(self.files, split) def __getitem__(self, idx): img = Image.open(os.path.join(self.img_dir, self.files[idx])).convert('L') mask = Image.open(os.path.join(self.mask_dir, self.files[idx])) img = img.resize((self.img_size, self.img_size)) mask = mask.resize((self.img_size, self.img_size), Image.NEAREST) img = np.array(img, dtype=np.float32) / 255.0 mask = np.array(mask, dtype=np.int64) return torch.from_numpy(img).unsqueeze(0), torch.from_numpy(mask)

逻辑说明:convert('L')把 CT 转成单通道灰度,resize对掩膜用NEAREST避免插值出小数标签。_split_by_patient按文件名前缀分组,保证同一病人的切片不跨集合。归一化用简单的/255.0,如果效果不好再换成 ImageNet 均值方差。

5.2 提升 IoU 的几个调参方向

如果 baseline 的 IoU 在 0.85 左右想往上推,可以试这几个方向。损失函数从交叉熵换成 Dice Loss 或者交叉熵加 Dice 的混合损失,Dice 对类别不平衡更鲁棒。数据增强加随机旋转、弹性形变、灰度扰动,肺部 CT 的形变增强效果比较明显。模型层面可以把 DenseUnet 的编码器换成预训练的 DenseNet121,用 ImageNet 权重初始化,小数据集上收敛更快。后处理加连通域分析,把面积小于阈值的预测区域去掉,减少孤立误检。

调参方向具体操作预期影响
损失函数交叉熵 + Dice Loss 按 0.5:0.5 加权边界更贴合,小目标召回提升
数据增强随机旋转 ±15°、弹性形变、灰度 ±10%验证集 IoU 提升 1~3 个点
预训练编码器DenseNet121 ImageNet 权重前 20 epoch 收敛更快
后处理去除面积小于 50 像素的连通域减少孤立误检,Precision 提升

5.3 验证模型有没有真正学到肺部特征

训练完别只看指标,做两个验证。第一个,把验证集里 IoU 最低的 5 张切片挑出来,看 gt+image 叠加图,确认是模型问题还是标注问题。第二个,拿一张训练集里没出现过的病人切片做推理,如果效果明显差于验证集,说明模型泛化不够,需要加数据或者加正则。我一般会在训练结束后强制走一遍这两个检查,有一次就是靠看低 IoU 切片发现掩膜标注把气管标成了肺,改完标注后 IoU 直接涨了 4 个点。从那以后我每次换数据集都先抽查 10 张掩膜再开训。希望这份资源和这些踩坑记录能帮你少走点弯路。

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

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/28 15:24:56

GitHub Trending日榜筛选:DLSS版本管理与Claude Code Skills落地

周四早上,我把GitHub Trending的日榜过了一遍,实话实说,2026-09-24这期日榜的信息量比平时大不少。挂在前面几位的仓库不再是清一色的新AI框架,反而是一大批"让工具真正能被用起来"的项目:游戏玩家在翻DLSS版…

作者头像 李华
网站建设 2026/9/28 15:23:24

樱花内网穿透:专为《我的世界》联机优化的零配置隧道方案

1. 为什么“樱花”成了《我的世界》联机玩家的首选穿透方案?在《我的世界》Java版联机实践中,最常被卡住的不是红石电路,也不是末地折跃门坐标,而是——你的服务器明明开着,朋友却连不上。输入IP和端口后,客…

作者头像 李华
网站建设 2026/9/28 15:23:04

JavaWeb文章管理系统源码解析:从环境搭建到软删除与恢复

简介:这是一套基于JavaWeb的文章管理系统完整源码与数据库,面向计算机相关专业学生及企业开发者,可用于课程设计、毕业设计、大作业或初期项目立项演示。系统区分用户与管理员两种登录角色,支持用户发布新文章、查看文章详情、修改…

作者头像 李华
网站建设 2026/9/28 15:22:13

RSTSM 2026国际学术会议:SPIE出版+EI检索投稿指南

每年年初我都会收到大量学术会议的征稿邮件,这个时节最让人纠结的就是"要不要投、该投哪个"。如果你正在做遥感、测绘、地理信息相关的研究,想找一个相对靠谱的渠道把手头成果发表出去,这篇分享值得看完。第三届遥感技术与测量测绘…

作者头像 李华
网站建设 2026/9/28 15:21:39

Jev接入实战:从密钥配置到Codex集成与Python API调优

上个月我接了个活:给一个跑了好几年的遗留系统做性能排查。那代码写得跟迷宫似的,我对着日志一行行啃,效率低得离谱。同事看我抓狂,说了一嘴:你试试Jev?我一开始还以为是某个新的前端框架,结果研…

作者头像 李华
网站建设 2026/9/28 15:18:56

OpenClaw启动失败排查:从Gateway到依赖环境的全链路攻略

1. 先搞清楚:OpenClaw靠什么启动1.1 启动链路大致是怎样的OpenClaw 这类网关式 Agent 项目,启动不是敲一条命令就完事的。它内部通常有好几个进程:入口 Gateway、会话管理、Agent Worker、外部服务连接器,再加上后端依赖的数据库和…

作者头像 李华