简介这份资源面向医学图像处理与深度学习方向的初学者及研究者聚焦肺部CT影像中的肺结节自动检测问题提供了一套可复现的完整项目代码。包内共26个文件以11个Python脚本和4个Jupyter Notebook为核心辅以8张结果示意图、说明文档与依赖清单压缩包约5.7MB。代码覆盖数据预处理、模型定义、训练与可视化全流程包含U-Net、Inception、VGG、ResNet、DenseNet等多种网络实现并配有分割与分类两套训练脚本便于对比不同骨干网络在结节检测任务上的表现。预处理模块涉及灰度化、归一化、降噪与数据增强评估环节可借助ROC、AUC及平均IOU等指标衡量模型性能。目前已有231人学习适合希望快速上手医疗影像分割、理解肺结节检测流程并积累实战经验的读者参考借鉴。1. 从一份 CT 影像到结节坐标这个项目到底在做什么一份肺部 CT 扫描通常有几百张切片每张 512×512医生要逐层翻找几毫米的结节眼睛盯久了漏诊率会明显上升。这个项目要解决的就是这件事把原始 CT 序列喂进去让模型自动圈出可疑结节的位置和概率。它不是一个跑在云端的成品软件而是一套完整的训练与推理代码骨架包含预处理、分割、分类、可视化四个环节模型层面给了 U-Net、VGG、ResNet、DenseNet、Inception 五种实现。适合两类人一类是想入门医学图像分割的开发者拿它当模板改数据就能跑另一类是做医疗 AI 方向的学生或工程师需要一套能复现、能对比不同骨干网络效果的基线代码。项目里既有.ipynb交互式 notebook也有.py脚本说明作者既想让人快速看效果也想让人能批量跑实验。2. 目录结构与技术选型先看清骨架再动手2.1 文件清单背后的模块划分拿到压缩包解压后根目录下大致是这么几类文件。第一类是配置与入口config.py管路径和超参README.md是说明.gitignore过滤数据缓存。第二类是数据层preprocess.py做预处理generators.py负责训练时的批量数据生成。第三类是模型层model_UNet.py、model_VGG.py、model_ResNet.py、model_DenseNet.py、model_Inception.py各自独立。第四类是训练脚本train_segmentation.py做分割train_classification.py做分类train_ipynbs目录下还有三个 notebook 对应 U-Net、DenseNet、VGG 的交互式训练。第五类是工具visual_utils.py画图assets目录放的是网络结构示意图和预处理效果图比如VGG.png、ResNet.png、small-tumor.png、preprocess-cover-ratio.png。这种拆法有个好处模型定义和训练流程解耦。你想换骨干网络只改config.py里的模型名字训练脚本不用动。常见做法是把模型工厂写成一个字典键是字符串值是类这样命令行传参就能切换。2.2 技术栈与依赖推断requirements.txt里大概率是 PyTorch 或 TensorFlow 二选一从文件命名风格model_UNet.py这种下划线加驼峰和 notebook 的用法看PyTorch 的可能性更大。配套的库包括 NumPy、Pillow 或 OpenCV 做图像读写Matplotlib 做可视化scikit-learn 算 AUC 和 ROC。医学图像格式如果是 DICOM还会用到 pydicom如果是 NIfTI则是 nibabel。预处理里提到的灰度化、归一化、降噪对应的是cv2.normalize、高斯滤波或中值滤波。提示先打开requirements.txt确认框架版本再决定要不要建虚拟环境。医学图像项目对版本敏感尤其是 CUDA 和 cuDNN 的匹配。2.3 配置文件的组织方式config.py通常长这样一个Config类里面用类属性存路径、图像尺寸、batch size、学习率、epoch 数。下面这段是常见写法我按项目场景补全了字段# config.py class Config: # 数据路径 data_root ./data/LIDC # 原始 CT 序列目录 mask_root ./data/masks # 分割标签目录 cache_dir ./cache # 预处理缓存避免重复计算 # 图像参数 img_size 256 # 统一缩放到 256×256 in_channels 1 # CT 是单通道灰度 num_classes 2 # 背景 结节 # 训练超参 batch_size 8 lr 1e-4 epochs 100 optimizer adam loss bce # 二分类交叉熵 # 模型选择 backbone unet # 可选 unet / vgg / resnet / densenet / inception pretrained True # 分类模型是否加载预训练权重参数说明img_size决定显存占用256 是医学图像分割的常用折中值in_channels1是因为 CT 灰度图不需要三通道backbone这个字段就是切换模型的开关。改batch_size时注意同步改学习率线性缩放规则是 batch 翻倍、lr 翻倍。3. 预处理与数据生成把 CT 变成网络能吃的张量3.1 preprocess.py 里的关键步骤原始 CT 有两个坑一是像素值范围是 HUHounsfield Unit跨度从 -1024 到 3000 以上二是不同设备的层厚和分辨率不一致。preprocess.py要做的就是统一。典型流程是读 DICOM 序列转 HU做肺窗截断窗宽 1500、窗位 -600 左右归一化到 [0,1]再缩放到固定尺寸。项目 assets 里的preprocess-cover-ratio.png和preprocess-diameter-mm.png说明作者还统计了结节覆盖比例和直径分布用来指导正负样本划分。# preprocess.py 核心片段 import numpy as np import cv2 import pydicom def load_dicom_series(series_dir): 读取一个 CT 序列按层厚排序后堆成 3D 体数据 slices [pydicom.dcmread(f) for f in sorted(os.listdir(series_dir))] slices.sort(keylambda s: float(s.ImagePositionPatient[2])) volume np.stack([s.pixel_array for s in slices]).astype(np.float32) # HU 转换斜率乘像素值再加截距 volume volume * slices[0].RescaleSlope slices[0].RescaleIntercept return volume def lung_window(volume, level-600, width1500): 肺窗截断把无关组织压到边界 low, high level - width // 2, level width // 2 volume np.clip(volume, low, high) volume (volume - low) / (high - low) # 归一化到 [0,1] return volume def resize_volume(volume, size256): 逐层缩放到统一尺寸 return np.stack([cv2.resize(s, (size, size)) for s in volume])逻辑说明load_dicom_series先按ImagePositionPatient的 z 坐标排序保证切片顺序正确否则 3D 重建会错位。lung_window把 HU 压到肺窗范围这一步能去掉骨头和脂肪的干扰。resize_volume逐层缩放注意不要用 3D 插值否则层间信息会混。参数level和width可以按数据集调整肺窗常用 -600/1500纵隔窗是 40/400。3.2 generators.py 与训练时的数据流generators.py负责把预处理后的体数据切成 patch 或整层送进网络。分割任务通常按层送分类任务按结节 patch 送。数据增强在这里做随机旋转、翻转、弹性形变。医学图像增强有个原则——不能改变解剖结构所以水平翻转要谨慎因为左右肺是对称的翻转后标签也得跟着翻。# generators.py 核心片段 import torch from torch.utils.data import Dataset class LungDataset(Dataset): def __init__(self, volume_list, mask_list, augmentTrue): self.volumes volume_list self.masks mask_list self.augment augment def __len__(self): return len(self.volumes) def __getitem__(self, idx): img self.volumes[idx] # 已经是 [0,1] 的 float32 mask self.masks[idx] if self.augment: # 随机水平翻转图像和标签同步 if np.random.rand() 0.5: img np.fliplr(img).copy() mask np.fliplr(mask).copy() # 随机亮度扰动模拟不同设备差异 img img * np.random.uniform(0.9, 1.1) img np.clip(img, 0, 1) # 增加通道维度[H,W] - [1,H,W] img torch.from_numpy(img).unsqueeze(0).float() mask torch.from_numpy(mask).unsqueeze(0).float() return img, mask参数说明augment在验证集要设成 False否则评估指标会虚高。np.fliplr是左右翻转对应肺部的左右对称标签同步翻转才不会错。亮度扰动范围 0.9 到 1.1 是经验值太大反而让模型学偏。3.3 预处理阶段的验证方法跑完预处理别急着训练先可视化几张图确认。visual_utils.py里应该有show_slice或plot_overlay这类函数把原图和 mask 叠在一起看。重点检查三件事肺窗截断后肺实质是否清晰、结节区域是否还在、缩放后小结节有没有被抹掉。assets 里的small-tumor.png就是用来对照小结节是否保留的。如果发现小结节消失把img_size提到 512或者改用 patch 训练。4. 模型实现与训练五种骨干网络怎么选、怎么跑4.1 U-Net 分割网络的结构要点model_UNet.py是分割任务的主力。U-Net 的经典结构是编码器下采样、解码器上采样、跳跃连接把浅层特征拼到深层。医学图像里小结节只有几个像素跳跃连接能保住这些细节。下面是一个精简实现# model_UNet.py 核心片段 import torch.nn as nn class DoubleConv(nn.Module): def __init__(self, in_ch, out_ch): super().__init__() self.net 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.net(x) class UNet(nn.Module): def __init__(self, in_ch1, out_ch1): super().__init__() # 编码器每层通道翻倍尺寸减半 self.enc1 DoubleConv(in_ch, 64) self.enc2 DoubleConv(64, 128) self.enc3 DoubleConv(128, 256) self.pool nn.MaxPool2d(2) # 解码器转置卷积上采样后与编码器特征拼接 self.up2 nn.ConvTranspose2d(256, 128, 2, stride2) self.dec2 DoubleConv(256, 128) self.up1 nn.ConvTranspose2d(128, 64, 2, stride2) self.dec1 DoubleConv(128, 64) self.out nn.Conv2d(64, out_ch, 1) def forward(self, x): e1 self.enc1(x) e2 self.enc2(self.pool(e1)) e3 self.enc3(self.pool(e2)) d2 self.dec2(torch.cat([self.up2(e3), e2], dim1)) d1 self.dec1(torch.cat([self.up1(d2), e1], dim1)) return torch.sigmoid(self.out(d1))逻辑说明DoubleConv是两次卷积加 BN 加 ReLUBN 在医学图像里很重要因为不同设备对比度差异大。torch.cat就是跳跃连接把上采样后的特征和编码器同层特征拼起来。最后一层用 sigmoid 输出 [0,1] 的概率图配合 BCE 损失。参数in_ch1对应灰度 CTout_ch1是二分类分割。4.2 分类模型VGG、ResNet、DenseNet、Inception 的差异model_VGG.py、model_ResNet.py、model_DenseNet.py、model_Inception.py是分类分支用来判断一个 patch 是不是结节。VGG 结构简单但参数多ResNet 用残差连接解决深层退化DenseNet 把每层都连到后面所有层Inception 用多尺度卷积核并行。assets 里的VGG.png、ResNet.png、Inception.png、DenseNet.png就是这些网络的结构图。选哪个我的经验是数据量小几千个 patch用 ResNet18 或 VGG16 加预训练数据量大且显存够上 DenseNet121想要多尺度特征Inception 更合适。config.py里的backbone字段就是切换开关pretrainedTrue会加载 ImageNet 权重但注意第一层卷积要改成单通道。# model_ResNet.py 核心片段 import torchvision.models as models import torch.nn as nn def build_resnet(in_ch1, num_classes2, pretrainedTrue): model models.resnet18(pretrainedpretrained) # 把第一层三通道卷积改成单通道权重取平均 old_conv model.conv1 model.conv1 nn.Conv2d(in_ch, 64, kernel_size7, stride2, padding3, biasFalse) if pretrained: model.conv1.weight.data old_conv.weight.data.mean(dim1, keepdimTrue) model.fc nn.Linear(model.fc.in_features, num_classes) return model参数说明old_conv.weight.data.mean(dim1, keepdimTrue)把 RGB 三通道权重平均成单通道这是迁移学习到灰度图的常用技巧。num_classes2是结节/非结节二分类多分类就改这个数。4.3 训练脚本的执行流程train_segmentation.py和train_classification.py是命令行入口。典型流程是解析参数、建 dataset 和 dataloader、实例化模型、定义损失和优化器、循环 epoch、每个 epoch 后在验证集上算指标、保存最优权重。# 分割训练 python train_segmentation.py --backbone unet --epochs 100 --batch_size 8 --lr 1e-4 # 分类训练 python train_classification.py --backbone resnet --epochs 50 --batch_size 32 --lr 1e-3损失函数方面分割用 BCE 或 Dice Loss分类用交叉熵。优化器 Adam 起步学习率 1e-4 到 1e-3。评估指标分割看 IoU 和 Dice分类看 AUC 和 ROC。visual_utils.py里应该有画损失曲线和 ROC 曲线的函数训练完调一下就能出图。注意如果验证集 loss 一直不降先检查 mask 是不是全黑或全白预处理阶段归一化有没有把标签也归一化了。5. 避坑与排查训练不收敛、显存爆了怎么办5.1 现象loss 震荡不下降AUC 卡在 0.5原因通常是学习率太大或数据标签有问题。先打印一个 batch 的标签分布如果正样本比例低于 1%就是极端类别不平衡。解决办法是加pos_weight到 BCE Loss或者用 Focal Loss。学习率方面Adam 配 1e-3 在分割任务上偏大降到 1e-4 试试。5.2 现象显存溢出batch_size 降到 1 还报错原因可能是图像尺寸太大或模型没释放中间变量。先确认img_size是不是 512改成 256 能省四分之三显存。如果还不行用梯度累积batch_size4但每 4 个 batch 才optimizer.step()一次等效 batch 16。另外检查torch.no_grad()有没有加在验证循环里忘了加会一直建计算图。5.3 现象预处理后图像全黑或全白原因是窗宽窗位设错或者归一化时除了零。CT 的 HU 值有负数如果直接除以最大值负数会被截断。正确做法是先np.clip到肺窗范围再减最小值除以范围。检查preprocess.py里lung_window的level和width是不是匹配你的数据集。5.4 现象notebook 能跑脚本报路径错误原因是 notebook 的工作目录和脚本不一致。notebook 默认在文件所在目录脚本默认在终端当前目录。解决办法是在config.py里用os.path.dirname(os.path.abspath(__file__))拼绝对路径别用相对路径。另外.gitignore可能把cache目录忽略了换机器跑要重新生成缓存。5.5 现象验证集指标很高测试集一塌糊涂原因是数据泄漏。同一个病人的切片可能同时出现在训练集和验证集模型记住了病人特征而不是结节特征。解决办法是按病人 ID 划分数据集而不是按切片随机分。这个坑在医学图像里非常常见血泪经验就是划分前先按病人分组。6. 二次开发换数据、换模型、加指标的具体改法拿到这套代码最常做的改造是换自己的数据集。假设你有一批标注好的 CT 和 mask第一步改config.py里的data_root和mask_root确保目录结构是data_root/病人ID/切片.dcm和mask_root/病人ID/切片.png。第二步改preprocess.py里的读取逻辑如果你的数据是 NIfTI 格式把pydicom.dcmread换成nibabel.loadHU 转换那步可以跳过因为 NIfTI 通常已经是 HU。第三步改generators.py里的__getitem__如果 mask 是多分类背景、结节、血管把out_ch改成对应类别数损失换成多分类交叉熵。换模型更简单config.py里backbone改成densenet或inception训练脚本会自动实例化对应模型。但要注意分类模型和分割模型的输入输出不一样U-Net 输出分割图ResNet 输出类别概率别混用。如果你想加一个新的骨干网络在model_xxx.py里定义一个类然后在训练脚本的模型工厂字典里注册一下就行。加评估指标的话visual_utils.py里加一个compute_metrics函数输入预测和标签输出 Dice、IoU、敏感度、特异度。敏感度在医学场景比准确率重要因为漏诊代价高。下面是一个 Dice 和敏感度的实现# visual_utils.py 补充指标 import numpy as np def compute_metrics(pred, target, threshold0.5): pred 和 target 都是 [0,1] 的概率图 pred_bin (pred threshold).astype(np.uint8) target_bin (target 0.5).astype(np.uint8) # Dice 系数 inter (pred_bin target_bin).sum() dice (2 * inter) / (pred_bin.sum() target_bin.sum() 1e-6) # 敏感度真阳性 / 实际阳性 tp inter fn ((pred_bin 0) (target_bin 1)).sum() sensitivity tp / (tp fn 1e-6) # IoU union (pred_bin | target_bin).sum() iou inter / (union 1e-6) return {dice: dice, iou: iou, sensitivity: sensitivity}参数说明threshold0.5是分割二值化的默认阈值实际部署时可以调低到 0.3 提高敏感度代价是假阳性变多。1e-6防止除零。这个函数可以直接塞进验证循环每个 epoch 打印一次。还有一个实用改造是加早停。医学图像训练经常过拟合验证集 Dice 连续 10 个 epoch 不涨就停省时间也省显存。实现方式是在训练循环里维护一个best_dice和patience计数器不涨就patience 1到阈值就break。最后说个我自己的习惯每次改完预处理或模型结构先拿 10 个样本过一遍完整流程确认输入输出形状对得上再开全量训练。这个习惯帮我省过好几次通宵跑完才发现标签错位的后悔药。希望帮到你。本文还有配套的精品资源点击获取 SEO 优化官网定制响应式建站教育培训建站