拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

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

基于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_classes3 是硬约束背景、左肺、右肺 model DenseUnet(num_classes3).cuda() train_dataset LungDataset(root_dirdataset, splittrain, img_size512) val_dataset LungDataset(root_dirdataset, splitval, img_size512) # batch_size 根据显存调8G 显存从 4 开始试 train_loader DataLoader(train_dataset, batch_size4, shuffleTrue, num_workers2) val_loader DataLoader(val_dataset, batch_size4, shuffleFalse, num_workers2) # 交叉熵损失如果类别不平衡可以加 weight 参数 criterion torch.nn.CrossEntropyLoss() optimizer torch.optim.Adam(model.parameters(), lr1e-4)逻辑说明LungDataset负责读图、resize、归一化和标签映射。CrossEntropyLoss默认对每个像素做 softmax 后算交叉熵适合多类别分割。如果左肺和右肺像素数远少于背景可以在CrossEntropyLoss里传weighttorch.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_classes3): 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 和 gtimage 掩膜怎么用predict 脚本用于推理图像会生成 gt 以及 gtimage 的掩膜图像。gt 是真实标签的彩色掩膜gtimage 是把预测掩膜叠加在原图上的效果图。这两张图是给你做定性检查的打开 gtimage看预测的左右肺边界是不是贴合真实轮廓有没有把气管或者肋骨误分进去。如果发现某几张切片效果特别差把它们挑出来单独看通常是切片本身对比度低或者有伪影。# 推理脚本常见调用方式 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 memorybatch_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, splittrain, img_size512): 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, dtypenp.float32) / 255.0 mask np.array(mask, dtypenp.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 张切片挑出来看 gtimage 叠加图确认是模型问题还是标注问题。第二个拿一张训练集里没出现过的病人切片做推理如果效果明显差于验证集说明模型泛化不够需要加数据或者加正则。我一般会在训练结束后强制走一遍这两个检查有一次就是靠看低 IoU 切片发现掩膜标注把气管标成了肺改完标注后 IoU 直接涨了 4 个点。从那以后我每次换数据集都先抽查 10 张掩膜再开训。希望这份资源和这些踩坑记录能帮你少走点弯路。本文还有配套的精品资源点击获取
返回列表