首页 / 资讯中心 / 文章详情

基于Python的3D-CT肺结节检测源码实战:从DICOM预处理到两阶段检测全流程

基于Python的3D-CT肺结节检测源码实战:从DICOM预处理到两阶段检测全流程 ★ FEATURED ARTICLE
简介这份资源是面向计算机、人工智能、自动化等专业学生与从业者的3D-CT影像肺结节检测算法项目源自个人毕业设计答辩评审分达98分代码经调试测试可稳定运行适合作为期末大作业、课程设计或毕业设计的参考方案也便于基础较好的学习者在此基础上修改扩展功能。压缩包共64个文件约9.61MB以38个Python源码文件为核心辅以csv标注与候选数据、npy数组文件、png示意图、ipynb演示笔记及md说明文档整体覆盖数据预处理、检测器与分类器网络、训练与测试脚本等模块目录结构清晰。目前已有57人学习关注。读者可从中获得完整的肺结节检测流程实现包括DICOM转raw预处理、候选提取、模型训练与推理评估等环节并借助项目说明快速理解各模块职责为复现实验、撰写论文或二次开发提供可借鉴的工程范例。1. 从一份 98 分毕设拆起3D-CT 肺结节检测这套源码到底能跑出什么肺结节检测这个方向很多人第一次接触都是在毕设选题表上看到「基于深度学习的医学影像检测」几个字然后一头扎进 LUNA16 数据集最后卡在「DICOM 读进来是一坨 HU 值怎么变成网络能吃的张量」这一步。这套基于 Python 的 3D-CT 肺结节检测源码恰好把这条链路完整走通了从 DICOM 原始序列到预处理、从候选结节提取到假阳性分类两个阶段串成一条可运行的流水线。它不是那种只丢一个模型权重的空壳而是带了 detector 和 classifier 两套网络、配套的预处理脚本、训练配置和一份 demo notebook。适合谁计算机、人工智能、自动化方向做期末大作业或毕业设计的同学以及想找一个完整医学影像 3D 检测工程模板的从业者。下面我按「这套东西怎么组织、怎么跑起来、哪里会翻车」的顺序拆一遍。2. 工程结构与两阶段检测原理为什么是 detector classifier2.1 目录里藏着一条完整流水线把压缩包解开第一眼看到的是一堆.py和.zbak混在一起容易懵。其实按功能分就三块preprocessing/负责把 DICOM 转成统一尺度的体素数据detector/是第一阶段候选结节检测classifier/是第二阶段假阳性过滤。根目录下的main.py、config_training.py、run_training.sh是训练入口test_detect.py、test_classifier.py、prediction.csv是推理和提交产物。.zbak是编辑器或备份工具留下的副本比如config_submit.py.zbak、net_detector_3.py这类功能上和去掉后缀的文件重复。真正要跑的时候认准不带.zbak的版本别改错文件——这是血泪经验改了半天发现改的是备份。目录/文件作用是否核心preprocessing/DICOM 转 raw、肺部分割、重采样核心detector/第一阶段 3D 候选检测网络核心classifier/第二阶段假阳性分类网络核心config_training.py训练超参与路径配置核心run_training.sh训练启动脚本核心*.zbak备份副本忽略detection result demo.ipynb结果演示参考2.2 两阶段检测为什么比单阶段靠谱肺结节在 CT 里是典型的「小目标 高假阳性」场景。一个扫描里可能有几十个候选但真正结节往往只有一两个。如果直接用一个网络做端到端检测召回率上去了假阳性会爆炸医生根本没法用。所以主流做法是拆成两阶段第一阶段用高召回策略把可疑位置全捞出来宁可错杀第二阶段用分类网络对每个候选做二分类把假阳性压下去。这套源码的net_detector.py和net_classifier.py就是这个思路。detector 负责在 3D 体素上滑窗或锚点生成候选classifier 拿候选的小 patch 判断是不是真结节。常见做法是 detector 用类似 3D RPN 的结构classifier 用 3D ResNet 做 patch 分类。这样拆的好处是每一阶段的目标单一调参时知道该动哪一边——召回不够调 detector误报多调 classifier。2.3 3D 卷积和 2D 切片的取舍有人会问为什么不直接把 CT 当 2D 切片逐张检测因为结节是立体的一个结节在连续几层里都有体现单看一层容易和血管断面混淆。3D 卷积能利用层间上下文这是它相对 2D 方案的核心优势。代价是显存吃紧所以源码里预处理阶段做了重采样和裁剪把体素间距统一到各向同性控制输入尺寸。这个取舍在full_prep.py和step1.py里能看出来。3. 环境搭建与预处理把 DICOM 变成网络能吃的体素3.1 依赖安装与版本选择这套代码是 Python 写的深度学习部分依赖 PyTorch 或类似框架看layers.py的写法。医学影像读取离不开pydicom或SimpleITK数值处理靠numpy图像增强可能用到scipy。建议用 Python 3.8 建虚拟环境太新的版本某些医学影像库轮子不全。# 建虚拟环境Python 3.8 兼容性最稳 python -m venv venv source venv/bin/activate # Windows 用 venv\Scripts\activate # 核心依赖版本按实际报错微调 pip install numpy scipy pydicom SimpleITK pip install torch torchvision # 有 GPU 装对应 CUDA 版本 pip install pandas scikit-learn pip install jupyter # 要跑 demo notebook 才需要逻辑说明先隔离环境避免污染全局。pydicom和SimpleITK二选一即可前者轻量读单张后者处理序列和几何信息更省心。torch的 CUDA 版本要和本机驱动匹配装错了会在torch.cuda.is_available()返回 False训练直接掉到 CPU 上慢到怀疑人生。参数说明numpy建议 1.21 以上pydicom2.x 对压缩 DICOM 支持更好。如果pip install卡在编译优先找预编译 wheel。3.2 预处理三步走转 raw、分割肺、重采样preprocessing/下的dicom2raw.py、full_prep.py、step1.py是预处理主力。典型流程是读 DICOM 序列 → 按 HU 值阈值分割肺部 → 裁剪到肺区域 → 重采样到统一间距 → 归一化存成 npy 或 raw。import SimpleITK as sitk import numpy as np def load_and_resample(dicom_dir, target_spacing(1.0, 1.0, 1.0)): # 读取 DICOM 序列SimpleITK 会自动处理层间几何 reader sitk.ImageSeriesReader() series_ids reader.GetGDCMSeriesIDs(dicom_dir) files reader.GetGDCMSeriesFileNames(dicom_dir, series_ids[0]) reader.SetFileNames(files) image reader.Execute() # 重采样到各向同性间距保证 3D 卷积输入尺度一致 original_spacing image.GetSpacing() original_size image.GetSize() new_size [ int(round(original_size[i] * original_spacing[i] / target_spacing[i])) for i in range(3) ] resampler sitk.ResampleImageFilter() resampler.SetOutputSpacing(target_spacing) resampler.SetSize(new_size) resampler.SetInterpolator(sitk.sitkLinear) resampled resampler.Execute(image) # 转 HU 值数组后续按阈值做肺分割 arr sitk.GetArrayFromImage(resampled).astype(np.float32) return arr逻辑说明GetGDCMSeriesIDs处理一个目录里可能有多组序列的情况取第一组。重采样用线性插值因为 CT 是连续灰度线性比最近邻更平滑。GetArrayFromImage出来的维度顺序是 (z, y, x)和 DICOM 的 (x, y, z) 相反后面写网络时要注意对齐。参数说明target_spacing设 (1,1,1) 是常见选择间距越小分辨率越高但显存越吃紧。如果显存不够可以放宽到 (1.5,1.5,1.5)。插值方式在掩膜上要用最近邻否则标签会被插出小数。3.3 肺分割的阈值玄学肺实质在 CT 里 HU 值大概在 -1000 到 -400 之间空气是 -1000 左右血管和结节偏正。常见做法是先阈值分割出肺再做形态学闭运算填掉小结节造成的空洞最后取最大连通域去掉背景。from scipy import ndimage def segment_lung(arr, hu_threshold-400): # 阈值分割低于阈值的算肺实质候选 binary arr hu_threshold # 闭运算填补结节和血管造成的空洞 binary ndimage.binary_closing(binary, structurenp.ones((3,3,3))) # 取最大连通域去掉体外的空气区域 labeled, num ndimage.label(binary) if num 0: return binary sizes ndimage.sum(binary, labeled, range(1, num 1)) largest np.argmax(sizes) 1 return labeled largest逻辑说明阈值 -400 是经验值能把大部分肺实质框进来。闭运算的结构元用 3x3x3 是为了在三个方向都做形态学处理只用 2D 结构元会漏掉层间空洞。取最大连通域是关键一步否则体外的空气会被误当肺。参数说明hu_threshold对不同设备可能要在 -450 到 -350 之间微调。结构元太大容易把肺门血管也吞进去太小填不上空洞3 是折中。4. 训练与推理detector 和 classifier 怎么分别调4.1 配置文件里改什么config_training.py是训练的总控路径、batch size、学习率、epoch 数都在这里。第一次跑之前必须改的是数据路径和输出路径其他超参可以先按默认跑通再说。# config_training.py 里典型需要改的字段 config { data_path: /your/path/to/preprocessed, # 预处理后的数据目录 output_path: /your/path/to/output, # 权重和日志输出 batch_size: 8, # 3D 网络显存吃紧从小开始 lr: 1e-3, # 初始学习率 epochs: 100, # 训练轮数 patch_size: (32, 32, 32), # classifier 的 patch 尺寸 num_workers: 4, # 数据加载线程 }逻辑说明batch_size在 3D 网络里是最容易翻车的参数8 已经算激进显存不够就降到 4 或 2。patch_size决定 classifier 看多大范围太小看不到结节全貌太大引入无关背景32 是常见起点。参数说明num_workers在 Windows 上设太大会报多进程错误设 0 或 2 更稳。lr用 1e-3 配 Adam 是常规组合训练不收敛再降到 1e-4。4.2 启动训练与日志观察run_training.sh是 shell 入口里面通常就是一行 python 调用。Linux 下直接跑Windows 下把命令抠出来手动执行。# Linux 下赋予执行权限后运行 chmod x run_training.sh ./run_training.sh # 或者直接看脚本内容手动跑 python main.py --config config_training.py --mode train逻辑说明先看run_training.sh里到底调了哪个入口、传了什么参数别盲目执行。训练开始后重点盯 loss 曲线detector 的 loss 通常包含分类损失和回归损失两部分classifier 就是二分类交叉熵。参数说明如果 loss 一直不降先检查数据路径对不对、标签有没有读进来。如果 loss 震荡剧烈把学习率降一个数量级。如果显存 OOM降 batch size 或 patch size。4.3 推理与结果输出test_detect.py和test_classifier.py分别对应两阶段的推理最终结果汇总到prediction.csv。这个 csv 通常包含结节坐标、直径、置信度是提交或评估用的。# 先跑检测阶段生成候选 python test_detect.py --config config_submit.py # 再跑分类阶段过滤假阳性 python test_classifier.py --config config_submit.py逻辑说明两阶段必须按顺序跑classifier 的输入依赖 detector 的输出候选。config_submit.py是推理专用配置和训练配置分开别混用。参数说明推理时的置信度阈值决定最终输出多少结节阈值高漏检多阈值低误报多要在验证集上卡一个平衡点。5. 避坑与常见问题排查那些让我重跑一整晚的坑5.1 现象训练 loss 是 nan几轮后直接崩原因3D 医学影像的 HU 值范围跨度大-1000 到 3000如果没做归一化直接喂网络梯度爆炸是必然的。另外学习率设太大也会导致 nan。解决在预处理阶段把 HU 值截断到 [-1000, 400] 再归一化到 [0,1] 或减均值除标准差。学习率从 1e-4 起步确认稳定后再往上调。检查data_detector.py里的归一化逻辑有没有生效。5.2 现象显存 OOMbatch size 降到 1 还是爆原因3D 卷积的显存占用和 patch 尺寸是立方关系32x32x32 看着不大乘上通道数和 batch 就爆了。另外数据加载时如果一次性把整个 CT 读进内存也会爆。解决先把 patch 降到 24x24x24 试或者用梯度累积模拟大 batch。数据加载改成按需读取别在__init__里全load。num_workers调小也能省内存。5.3 现象DICOM 读出来方向反了结节位置全错原因不同设备的 DICOM 坐标系和层间顺序不一致SimpleITK 读出来的数组方向可能和预期相反。预处理时如果没统一方向后面所有坐标都是错的。解决读完后用sitk.DICOMOrient统一到标准方向如 LPS或者在 numpy 层面做 flip 对齐。验证方法是拿一个已知结节的病例看预处理后的坐标和原始标注能不能对上。5.4 现象classifier 准确率 99% 但实际全是假阳性原因候选样本里正负极度不平衡真结节可能只占 1%模型全预测成负样本也能有 99% 准确率。这是医学影像检测最经典的陷阱。解决看召回率和 AUC别只看准确率。训练时用加权损失或重采样让正负平衡。data_classifier.py里如果有采样逻辑确认它真的在起作用。5.5 现象.zbak文件和正式文件改混了原因目录里备份文件太多config_training.py和config_training.py.zbak长得几乎一样编辑器里切错标签就改错了。解决动手前先把所有.zbak挪到一个备份文件夹眼不见心不烦。改配置前用git diff或文件修改时间确认改的是哪个。这个坑不致命但极耗时间。6. 进阶玩法把两阶段拆开单独调以及怎么验证结果可信跑通整条流水线只是第一步真正要拿这份源码做点东西得学会把两阶段拆开单独调。detector 阶段的核心指标是召回率你希望它尽量别漏classifier 阶段的核心指标是假阳性率你希望它尽量别误报。这两个目标是对抗的所以要分开评估。一个实用技巧是先把 detector 的候选全导出来人工看一批统计漏检和误报的分布。如果漏检集中在某类形态的结节比如贴着胸膜的说明 detector 的锚点尺寸或数据增强需要针对这类样本加强。如果误报集中在血管断面那是 classifier 的 patch 特征不够可以加大 patch 或引入多尺度。验证结果可信度有个笨办法但很有效拿几个公开数据集里有标注的病例把预测结果和标注做匹配算每个结节的 IoU 或中心点距离。如果大部分预测都能和标注对上说明流水线是通的如果预测位置系统性偏移八成是预处理的方向或间距没对齐。import numpy as np def match_predictions(preds, gts, dist_threshold5.0): # preds/gts 格式: [(z, y, x, diameter), ...] matched 0 for gt in gts: gt_center np.array(gt[:3]) dists [np.linalg.norm(np.array(p[:3]) - gt_center) for p in preds] if dists and min(dists) dist_threshold: matched 1 recall matched / len(gts) if gts else 0 return recall逻辑说明用中心点欧氏距离做匹配阈值 5mm 是常见设定因为结节本身直径可能就 5-10mm。这个函数只算召回精确率要反过来遍历预测。实际评估时两个都要看。参数说明dist_threshold根据结节大小调整小结节用 3mm大结节可以放宽到 8mm。坐标单位要统一别一个用体素一个用毫米。从那以后我每次拿到这类医学影像项目都强制先跑一遍「读一张 DICOM → 打印形状和方向 → 可视化中间层」的检查确认数据管道没问题再动模型。这个习惯帮我省了无数个通宵。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?
咨询建站