简介这份资源面向医学图像处理与深度学习方向的学习者和研究者聚焦肺部CT影像中的肺结节自动检测问题提供了一套可复现的完整项目代码。包内共26个文件以11个Python脚本和4个Jupyter Notebook为核心辅以8张PNG结果示意图、说明文档与依赖清单压缩包约5.7MB结构清晰便于按模块查阅。代码覆盖数据预处理、模型定义、训练与可视化全流程预处理脚本负责灰度化、归一化与降噪模型部分实现了U-Net、Inception、VGG、ResNet与DenseNet等多种网络训练脚本区分分割与分类任务可视化工具则用于展示损失曲线与分割效果。读者可据此理解医学影像从原始CT到结节定位的完整链路掌握交叉熵损失、Adam优化器、数据增强及ROC、AUC、平均IOU等评估指标的落地方式并借助Notebook快速复现实验。目前已有231人学习适合具备一定机器学习基础、希望切入医疗影像分析的中高级开发者参考。1. 从一份 CT 影像到结节坐标这个项目把检测链路拆成了可跑的模块拿到一份肺部 CT 序列先别急着上模型。真正卡住大多数人的是 DICOM 读进来之后那一堆窗宽窗位、像素间距、结节直径换算——这些没对齐后面 ResNet 再深也白搭。这个项目干的事就是把「预处理 → 分割 → 分类 → 可视化」整条链路拆成独立脚本和 notebook让你能一段一段跑通、一段一段改。它适合两类人一是想入门医学图像深度学习、需要一份能跑起来的完整代码的开发者二是手里有 CT 数据、想快速搭个肺结节检测 baseline 的工程同学。项目里 U-Net、VGG、ResNet、DenseNet、Inception 五种模型实现齐全预处理和可视化工具也单独抽了出来不是那种只丢一个 train.py 的玩具仓库。2. 项目结构与技术选型五种骨干网络加一条预处理流水线2.1 目录里到底有什么先把文件按职责分个类不然后面找代码会绕晕。类别文件作用配置config.py路径、超参、图像尺寸集中管理预处理preprocess.py、preprocess.ipynbCT 读取、归一化、肺实质分割、结节候选提取数据生成generators.py训练时的 batch 生成与增强模型model_UNet.py、model_VGG.py、model_ResNet.py、model_DenseNet.py、model_Inception.py五种骨干网络训练入口train_segmentation.py、train_classification.py分割与分类两条训练线Notebooktrain-UNet.ipynb、train_VGG.ipynb、train_DenseNet.ipynb交互式调试与结果展示可视化visual_utils.py图像叠加、损失曲线、分割掩码展示素材assets/下若干 png网络结构图与预处理效果图这个划分方式很工程化模型定义和训练逻辑分离预处理独立成脚本notebook 只负责调参和看结果。你改模型结构时不用动训练循环换预处理策略时也不用碰网络定义。2.2 为什么是 U-Net 做分割、ResNet 做分类肺结节检测本质是两个子任务串起来的先在 CT 里把疑似结节区域圈出来分割再判断这个区域是不是结节、恶性概率多大分类。U-Net 的编码器-解码器加跳跃连接结构在医学图像分割里几乎是默认选择——它能把浅层的高分辨率特征和深层的语义特征拼起来对小目标分割特别友好。项目里model_UNet.py就是这个思路的实现。分类这边给了 VGG、ResNet、DenseNet、Inception 四个选项不是让你全跑一遍而是让你根据数据量和算力选。数据少、想快速验证VGG 够用要压榨精度且显存允许ResNet 或 DenseNet 更稳。config.py里通常会有一个MODEL_NAME之类的字段来控制加载哪个模型这样训练脚本不用改。2.3 预处理流水线从原始 CT 到模型输入preprocess.py是这个项目里最值得细看的部分。医学图像和自然图像最大的区别在于CT 值是有物理意义的HU 单位不能直接除以 255。常见做法是先把 HU 值截断到肺窗范围比如 -1000 到 400再归一化到 [0,1]。项目里preprocess-cover-ratio.png和preprocess-diameter-mm.png这两张图大概率是在展示结节覆盖比例和直径分布的统计结果——这是做候选筛选时的重要依据。# preprocess.py 中典型的 CT 预处理片段按项目场景补全 import numpy as np import SimpleITK as sitk def load_ct_volume(path): 读取 CT 序列返回 HU 值数组和 spacing image sitk.ReadImage(path) volume sitk.GetArrayFromImage(image) # shape: (z, y, x) spacing image.GetSpacing() # (sx, sy, sz) 单位 mm return volume, spacing def hu_window(volume, level-600, width1500): 肺窗截断把无关组织压掉 lower level - width // 2 upper level width // 2 volume np.clip(volume, lower, upper) volume (volume - lower) / (upper - lower) # 归一化到 [0,1] return volume.astype(np.float32)load_ct_volume用 SimpleITK 读序列关键是拿到spacing——后面算结节真实直径毫米全靠它。hu_window里的level和width是肺窗参数不同数据集可能微调但 -600/1500 是常见起点。归一化之后的数据才能喂给网络否则 HU 值范围跨度太大训练直接发散。注意如果你的数据是 DICOM 而非 NIfTI读取方式要换成pydicom逐切片读再按ImagePositionPatient排序别直接用文件名排顺序错了整个 volume 就废了。3. 跑起来环境、数据组织与训练入口3.1 依赖安装与版本坑requirements.txt里通常包含 torch、torchvision、numpy、SimpleITK、matplotlib、opencv-python 这些。安装本身不复杂但有几个版本组合容易翻车。# 建议先建虚拟环境别在 base 里折腾 python -m venv venv source venv/bin/activate # Windows 用 venv\Scripts\activate # 按 requirements 装但 torch 建议单独指定 pip install torch torchvision --index-url https://download.pytorch.org/whl/cu118 pip install -r requirements.txtCUDA 版本和 torch 版本对不上是最常见的报错来源。如果你机器是 CUDA 11.8就装对应 wheel不确定的话先跑nvidia-smi看驱动支持的 CUDA 上限。SimpleITK 一般不会有问题但如果和 numpy 版本冲突优先保 numpy 的兼容性。3.2 数据目录怎么摆项目没有强制规定数据路径但config.py里一般会有DATA_ROOT之类的变量。按分割和分类两条线建议这样组织data/ ├── raw/ # 原始 CT 序列 │ ├── patient_001/ │ └── patient_002/ ├── masks/ # 分割标签和 raw 对应 │ ├── patient_001.npy │ └── patient_002.npy └── candidates/ # 分类用的结节候选 patch ├── positive/ └── negative/分割训练读raw和masks分类训练读candidates。generators.py里的 Dataset 类会按这个结构去索引。如果你数据格式不同改generators.py里的__getitem__就行不用动模型。3.3 分割训练从 train_segmentation.py 入手分割是第一步先把肺实质和结节区域分出来。python train_segmentation.py --config config.py --model unet --epochs 50 --batch_size 8train_segmentation.py的主循环大致长这样# train_segmentation.py 核心训练循环按项目场景补全 import torch from model_UNet import UNet from generators import SegmentationDataset def train(): device torch.device(cuda if torch.cuda.is_available() else cpu) model UNet(in_channels1, out_channels1).to(device) dataset SegmentationDataset(data_rootcfg.DATA_ROOT, splittrain) loader torch.utils.data.DataLoader(dataset, batch_sizecfg.BATCH_SIZE, shuffleTrue) criterion torch.nn.BCEWithLogitsLoss() # 二分类分割常用 optimizer torch.optim.Adam(model.parameters(), lrcfg.LR) for epoch in range(cfg.EPOCHS): model.train() for img, mask in loader: img, mask img.to(device), mask.to(device) pred model(img) loss criterion(pred, mask) optimizer.zero_grad() loss.backward() optimizer.step()BCEWithLogitsLoss比先 sigmoid 再 BCELoss 更稳数值上不容易溢出。in_channels1是因为 CT 是单通道灰度别改成 3。如果显存不够把batch_size降到 4 甚至 2同时可以把输入 patch 尺寸调小。3.4 分类训练换模型就是换一行分类这条线在train_classification.py切换骨干网络通常只需要改配置或传参。python train_classification.py --model resnet --epochs 30 --lr 1e-4model_ResNet.py里一般会保留 torchvision 的 ResNet 结构只改第一层卷积的输入通道和最后的全连接输出。# model_ResNet.py 关键改动点 import torchvision.models as models import torch.nn as nn def build_resnet(num_classes2, pretrainedTrue): model models.resnet18(pretrainedpretrained) # CT 是单通道把第一层改成 1 通道输入 model.conv1 nn.Conv2d(1, 64, kernel_size7, stride2, padding3, biasFalse) # 二分类输出 model.fc nn.Linear(model.fc.in_features, num_classes) return modelpretrainedTrue在医学图像上其实有争议——ImageNet 预训练权重是 RGB 三通道的改成单通道后第一层权重没法直接用。常见做法是把三通道权重求平均压成单通道或者干脆从头训。数据量小的时候前者通常收敛更快。4. 避坑与排查那些让训练不收敛的细节4.1 损失不降先查标签和输入是否对齐现象训练几个 epochloss 在 0.69 附近晃二分类的随机水平准确率不动。 原因分割任务里 mask 和 image 的尺寸或方向不匹配或者分类任务里正负样本标签反了。 解决写个临时脚本把dataset[0]的 image 和 mask 同时用matplotlib画出来叠一起看。分类的话打印几个 batch 的 label 分布确认正样本不是 0 个。4.2 显存溢出不一定是 batch_size 太大现象报CUDA out of memory但 batch_size 已经降到 2。 原因U-Net 在解码器部分特征图分辨率高中间激活值占显存或者数据加载时没有及时释放 CPU 上的大数组。 解决先把输入 patch 从 512×512 降到 256×256 试再检查generators.py里有没有在__getitem__里做重操作能放到预处理阶段就放过去。PyTorch 的话加torch.cuda.empty_cache()只是缓解根治还是减尺寸。4.3 验证集指标虚高可能是数据泄漏现象验证集 AUC 0.98换一批数据掉到 0.6。 原因同一个病人的切片同时出现在训练集和验证集里模型记住了病人特征而非结节特征。 解决按病人 ID 划分数据集不是按切片随机分。generators.py里如果有split逻辑确认它是按 patient 维度切的。4.4 预处理后的图像全黑或全白现象可视化出来一片黑模型输入全是 0 或 1。 原因HU 窗宽窗位设错或者归一化时除数为 0。 解决打印预处理前后数组的 min/max/mean。正常肺窗处理后应该在 [0,1] 内有分布如果全是 0检查level和width是不是反了如果全是 1检查np.clip的上下界。4.5 notebook 里跑得好脚本跑就报错现象train-UNet.ipynb能跑通train_segmentation.py报路径或模块找不到。 原因notebook 的工作目录和脚本执行目录不一致相对路径失效。 解决在config.py里用os.path.dirname(os.path.abspath(__file__))拼绝对路径别用./data这种写法。notebook 里也建议先os.chdir到项目根目录。5. 二次开发换数据、换模型、加评估的实操思路5.1 换数据集时改哪几个文件如果你手里是别的肺部 CT 数据集改动集中在三处preprocess.py的读取函数DICOM 换 NIfTI 或反之、config.py的路径和图像尺寸、generators.py的 Dataset 索引逻辑。模型和训练循环基本不用动。我一般会先拿 5 个病例跑一遍预处理用visual_utils.py把中间结果画出来确认没问题再全量跑。5.2 加一个新骨干网络项目里已经有五种模型加第六种比如 EfficientNet的套路是在model_XXX.py里定义build_XXX函数保持输入输出接口和其他模型一致输入[B,1,H,W]输出[B,num_classes]或[B,1,H,W]然后在train_classification.py的模型选择分支里加一个 case。关键是别改训练循环的接口否则 notebook 里的代码也要跟着改。5.3 评估指标补全项目摘要里提到 ROC、AUC、平均 IOU但代码里不一定都实现了。补评估的推荐做法是单独写一个evaluate.py加载 checkpoint 后在验证集上跑推理用sklearn.metrics.roc_auc_score算 AUC用torchmetrics或手写算 IOU。# evaluate.py 核心评估逻辑 import numpy as np from sklearn.metrics import roc_auc_score def evaluate_classification(model, loader, device): model.eval() all_probs, all_labels [], [] with torch.no_grad(): for img, label in loader: img img.to(device) logit model(img) prob torch.softmax(logit, dim1)[:, 1] # 正类概率 all_probs.extend(prob.cpu().numpy()) all_labels.extend(label.numpy()) auc roc_auc_score(all_labels, all_probs) return aucroc_auc_score要求输入是概率值不是 logits所以先过 softmax。分割的 IOU 计算要注意预测的 mask 要先二值化阈值 0.5再和标签做交并比别直接拿概率算。5.4 一个我踩过的坑有次我图省事直接在 notebook 里改了config.py的参数然后跑训练结果忘了同步回文件第二天用脚本跑的时候参数全是旧的白白浪费一晚上算力。从那以后我每次改配置都强制走一遍git diff config.py确认改动落盘了再启动训练。另外 checkpoint 命名一定带上模型名和 epoch不然跑多了根本分不清哪个是哪个。希望这些能帮到你少走点弯路。本文还有配套的精品资源点击获取 SEO 优化官网定制响应式建站教育培训建站