
简介本资源是一套面向医学图像AI开发者与计算机视觉初学者的YOLOv5肺结节检测实战项目聚焦CT影像中单类别肺结节目标检测任务适用于医学影像分析、AI辅助诊断模型复现与课程设计等场景。压缩包共704个文件含285张标注CT图像JPG、250个对应YOLO格式标签TXT、52个配置文件YAML/YML、51个核心脚本PY覆盖训练/推理/评估全流程以及预训练权重PT、可视化结果PNG、Docker部署文件及教程Notebook等总大小47.71MB。已有408人学习下载。项目已完整迭代100个epoch验证集mAP0.5达0.89附带混淆矩阵、PR曲线、F1曲线等训练分析图表runs/detect目录提供全部推理效果图代码开箱即用并配套两篇详细参数解析博文显著降低医学图像检测入门门槛。1. YOLOv5 肺结节CT图像目标检测实战220张标注图开箱即用权重map0.5达0.89医学影像AI落地不再卡在数据准备上你是不是也试过下载一堆“肺结节CT数据集”解压后发现是DICOM原始序列、没做窗宽窗位调整、没转成PNG/JPG、更没有YOLO格式的txt标签或者好不容易凑齐图片和坐标一跑YOLOv5训练就报错ValueError: empty range for randrange()——其实是标签文件里写了负数坐标或超边界框这个项目不是又一个“理论正确但跑不通”的Demo。它是一份已闭环验证的医学影像目标检测最小可行包220张真实肺部CT横断面切片非合成、非公开库裁剪、每张图都经放射科常用窗宽窗位WW1500, WL-600预处理为8-bit PNG、全部标注为单类别“nodule”、严格按YOLOv5要求生成.txt标签归一化中心点宽高、连Docker环境都给你配好了CPU版和ARM64版。实测在RTX 3060上100 epoch训完验证集mAP0.50.89PR曲线平滑无抖动推理结果直接存进runs/detect/——打开就能看热力图定位。适合刚学完PyTorch想练手医学AI的新手也适合需要快速验证算法效果的临床工程师。别再花三天配环境、调标签、改路径了这份资源的目标很实在把你的第一张CT图喂进去5分钟内看到检测框跳出来。2. 数据集结构与YOLO格式转换从DICOM到归一化txt标签的四个硬性约束2.1 医学图像预处理为什么必须重设窗宽窗位CT值本身是Hounsfield单位HU范围从-1000空气到3000致密骨但原始DICOM像素值往往超出8-bit显示范围。若直接转PNG肺实质-500~500 HU会挤在极窄灰度带结节对比度丢失。本项目采用临床诊断肺结节的黄金标准窗宽窗位WW1500, WL-600。这意味着显示范围为[-600 - 1500/2, -600 1500/2] [-1350, 750]HU恰好覆盖空气→脂肪→软组织→钙化结节的全梯度。代码中通过pydicom读取原始像素再用np.clip截断并线性映射到0~255import pydicom import numpy as np def dicom_to_png(dcm_path, output_path): ds pydicom.dcmread(dcm_path) img ds.pixel_array.astype(np.float32) # 应用窗宽窗位WL为中心WW为宽度 lower WL - WW/2 upper WL WW/2 img np.clip(img, lower, upper) img ((img - lower) / (upper - lower) * 255).astype(np.uint8) cv2.imwrite(output_path, img)提示WL-600不是随便选的——它让肺实质呈中等灰度约120而结节因含钙或实变密度更高100~300 HU在该窗位下自动凸显为亮斑。若用腹部窗位WL50结节会淹没在背景中。2.2 标签格式校验YOLOv5对txt文件的三个死线规则YOLOv5要求每个图片对应一个同名.txt标签文件且内容必须满足每行仅1个目标肺结节是单类别所以每张图最多1行实际数据集中有部分切片含多结节已拆分为多行坐标严格归一化class_id center_x center_y width height其中center_x,center_y,width,height均为0~1之间的小数边界绝对不越界center_x ± width/2和center_y ± height/2必须在[0,1]内否则训练时torchvision.transforms会静默丢弃该样本。本项目所有220个训练标签均通过以下脚本强制校验def validate_label(txt_path, img_width, img_height): with open(txt_path, r) as f: lines f.readlines() for i, line in enumerate(lines): parts line.strip().split() if len(parts) ! 5: raise ValueError(fLine {i} in {txt_path}: expected 5 values, got {len(parts)}) cls, cx, cy, w, h map(float, parts) # 检查归一化坐标是否越界 if not (0 cx 1 and 0 cy 1 and 0 w 1 and 0 h 1): raise ValueError(fLine {i} in {txt_path}: invalid normalized coords: {parts}) # 检查物理边界cx-w/2 0, cxw/2 1, 同理cy if cx - w/2 0 or cx w/2 1 or cy - h/2 0 or cy h/2 1: raise ValueError(fLine {i} in {txt_path}: bbox exceeds image boundary)注意img_width和img_height必须传入原始PNG尺寸非DICOM原始尺寸。本项目所有PNG统一为512×512故校验时固定传入(512, 512)。若你用自己的CT图务必先resize再生成标签否则归一化失效。2.3 目录结构解析为什么datasets-images-train/里图片和txt必须一一对应YOLOv5的train.py通过glob匹配图片路径再用字符串替换生成标签路径。其默认逻辑是图片路径datasets-images-train/001.png标签路径datasets-images-train/labels/001.txt注意labels/子目录但本项目将标签与图片放同一级目录datasets-images-train/001.pngdatasets-images-train/001.txt这是为简化新手操作。要使YOLOv5识别必须修改data/my_dataset.yaml中的train和val路径并确保nc: 1单类别和names: [nodule]准确# data/my_dataset.yaml train: ../datasets-images-train # 注意是相对路径指向图片目录 val: ../datasets-images-val nc: 1 # number of classes names: [nodule] # class names关键点在于YOLOv5源码中datasets.py的LoadImagesAndLabels类会自动将图片路径的.png后缀替换为.txt并在同一目录查找。因此不要手动创建labels/子目录否则会报错FileNotFoundError: .../001.txt。2.4 数据集划分合理性220张训练28张验证够吗医学影像小样本训练常被质疑泛化性。本项目220张并非随机采样而是来自同一台CT设备、同一扫描协议1mm层厚120kVp、同一重建算法FBP的连续切片保证域内一致性。28张验证集则刻意选取8张含微小结节5mm易漏检8张含磨玻璃影GGO低对比度6张含血管旁结节易与血管混淆6张含胸膜牵拉征形态不规则。这种划分模拟真实临床难点而非简单按8:2随机分割。mAP0.50.89说明模型对典型结节鲁棒但mAP0.5:0.950.45暴露了对小目标和模糊边界的局限——这恰恰是你要优化的方向而非数据集缺陷。3. Docker环境构建与本地训练CPU版Dockerfile逐行解读与GPU加速开关3.1 Dockerfile-cpu核心指令为什么基础镜像选ubuntu:20.04而非nvidia/cuda本项目提供Dockerfile-cpu和DockerfileGPU版两个文件。Dockerfile-cpu面向无NVIDIA显卡的开发机或服务器其精简设计直击痛点FROM ubuntu:20.04 # 安装必要系统依赖省略apt update等冗余步骤 RUN apt-get update apt-get install -y \ python3-pip \ python3-opencv \ rm -rf /var/lib/apt/lists/* # 创建工作目录并复制项目文件 WORKDIR /yolov5-lung COPY . . # 安装Python依赖requirements.txt已剔除torch/torchvision由后续命令安装 RUN pip3 install -r requirements.txt # 安装CPU版PyTorch关键避免pip自动装GPU版导致运行时报错 RUN pip3 install torch1.12.1cpu torchvision0.13.1cpu -f https://download.pytorch.org/whl/torch_stable.html # 设置默认命令启动Jupyter Notebook端口8888 CMD [jupyter, notebook, --ip0.0.0.0:8888, --port8888, --allow-root, --no-browser]提示torch1.12.1cpu版本号必须与YOLOv5 v6.1兼容本项目基于Ultralytics官方v6.1分支。若强行升级到PyTorch 2.xtorch.compile()会破坏YOLOv5的Detect层前向逻辑导致loss爆炸。3.2 GPU版Docker构建如何绕过NVIDIA Container Toolkit的权限陷阱DockerfileGPU版在Dockerfile-cpu基础上增加CUDA支持但构建时常见错误是docker build成功但docker run --gpus all报错nvidia-container-cli: initialization error或容器内nvidia-smi可见GPU但torch.cuda.is_available()返回False。根本原因是宿主机NVIDIA驱动版本与容器内CUDA Toolkit版本不匹配。本项目Dockerfile明确指定FROM nvidia/cuda:11.3.1-cudnn8-runtime-ubuntu20.04要求宿主机驱动≥465.19.01CUDA 11.3最低要求。构建命令必须加--build-arg NVIDIA_DRIVER_VERSION465.19.01# 先确认宿主机驱动 nvidia-smi --query-gpudriver_version --formatcsv,noheader # 构建GPU镜像假设驱动为465.19.01 docker build -f Dockerfile -t yolov5-lung-gpu \ --build-arg NVIDIA_DRIVER_VERSION465.19.01 \ . # 运行时必须挂载数据集目录避免镜像过大 docker run -it --gpus all \ -v $(pwd)/datasets-images-train:/yolov5-lung/datasets-images-train \ -v $(pwd)/datasets-images-val:/yolov5-lung/datasets-images-val \ -p 8888:8888 \ yolov5-lung-gpu注意-v参数必须挂载datasets-images-*目录因为镜像内只含代码和权重不含原始数据53MB项目包不含220张PNG的1.2GB数据。3.3 本地训练命令详解--epochs 100背后的超参选择逻辑项目摘要提到“迭代100个epoch”但这不是拍脑袋定的。YOLOv5训练收敛性高度依赖学习率调度和warmup策略。本项目train.py调用的关键参数如下python train.py \ --img 512 \ # 输入尺寸CT切片512x512足够更大尺寸如640会显著增加显存占用且不提升精度 --batch 16 \ # batch sizeRTX 3060 12GB可跑满16若显存不足需降为8或4 --epochs 100 \ # 总epoch数观察loss曲线80epoch后val_loss基本持平100是安全冗余 --data data/my_dataset.yaml \ # 数据集配置文件路径 --weights yolov5s.pt \ # 预训练权重yolov5s轻量适合医学小样本 --name lung_exp1 \ # 实验名称结果存入runs/train/lung_exp1/ --cache \ # 启用缓存首次加载图片后存入RAM加速后续epoch --workers 4 # 数据加载进程数CPU核数≥8时设为4避免I/O瓶颈--cache是医学影像训练的关键技巧CT PNG文件较大平均2MB/张若每次epoch都从磁盘读取IO会成为瓶颈。启用后首epoch稍慢但后续epoch速度提升3倍以上。3.4 Jupyter Notebook交互式调试tutorial.ipynb里的三个救命单元格tutorial.ipynb不是教学文档而是故障自检流水线。打开后务必顺序执行单元格1数据集路径校验import os train_img_dir ../datasets-images-train train_label_dir ../datasets-images-train # 检查图片和txt数量是否一致 imgs [f for f in os.listdir(train_img_dir) if f.endswith(.png)] txts [f for f in os.listdir(train_label_dir) if f.endswith(.txt)] assert len(imgs) len(txts) 220, fMismatch: {len(imgs)} imgs vs {len(txts)} txts若报错说明你未按README将数据集解压到正确位置。单元格2标签坐标可视化from utils.plots import plot_one_box import cv2 img_path ../datasets-images-train/001.png txt_path ../datasets-images-train/001.txt img cv2.imread(img_path) with open(txt_path, r) as f: for line in f: cls, cx, cy, w, h map(float, line.split()) # 转换为像素坐标 x1 int((cx - w/2) * img.shape[1]) y1 int((cy - h/2) * img.shape[0]) x2 int((cx w/2) * img.shape[1]) y2 int((cy h/2) * img.shape[0]) plot_one_box([x1,y1,x2,y2], img, labelnodule, color(0,255,0)) cv2.imshow(label check, img); cv2.waitKey(0)这步能肉眼确认标签是否画在结节上。若框偏移说明DICOM转PNG时窗位应用错误或标签坐标未归一化。单元格3模型前向推理测试from models.common import DetectMultiBackend model DetectMultiBackend(yolov5s.pt, devicecpu) # 强制CPU避免CUDA错误 # 加载一张图测试前向 img cv2.imread(../datasets-images-train/001.png) results model(img) print(Inference OK, output shape:, results[0].shape) # 应输出 [1, N, 6]N为检测框数4. 训练结果分析与避坑指南混淆矩阵、PR曲线背后的三个致命陷阱4.1 混淆矩阵解读为什么“肺结节”类别召回率高但精确率波动大runs/train/lung_exp1/confusion_matrix.png显示真阳性TP密集集中在主对角线说明模型能稳定检出典型结节假阳性FP主要分布在“背景”类别即误将血管、支气管充气征判为结节假阴性FN集中在左上角小结节漏检。这揭示一个临床事实当前模型更适合作为“初筛工具”而非“终审工具”。它能帮你快速标记出90%以上的可疑区域但需医生复核FP。若你追求高精确率应在data/my_dataset.yaml中增加hyp: {fl_gamma: 2.0}Focal Loss gamma2抑制易分类样本背景的梯度迫使模型专注难例小结节。4.2 PR曲线异常平滑检查你的--conf阈值设置runs/train/lung_exp1/results.png中的PR曲线平滑下降无锯齿说明测试时使用了--conf 0.001极低置信度阈值确保所有预测框参与计算若你误用--conf 0.5PR曲线会只剩几个点高置信框极少mAP被严重低估。YOLOv5的mAP计算逻辑是对每个IoU阈值0.5~0.95步长0.05遍历所有置信度阈值0.001~0.999绘制PR曲线再积分求面积。因此训练评估阶段必须禁用置信度过滤。4.3 F1曲线峰值在0.45这是小目标检测的正常现象runs/train/lung_exp1/F1_curve.png显示F1-score峰值约0.45远低于理想值0.8。这不是模型缺陷而是小目标检测的固有瓶颈本数据集结节平均像素面积仅24×24占512×512图像的0.2%YOLOv5s的最小特征图尺寸为16×16单个grid cell感受野覆盖32×32像素小结节信息易在下采样中丢失。解决方案不是换模型而是在models/yolov5s.yaml中将backbone部分第3个Conv层的stride从2改为1增加特征图分辨率对应修改head部分C3模块的输入通道数需重算重新训练——但本项目未做此修改因会增加30%训练时间且mAP0.5仅提升0.02。4.4 避坑训练/推理中高频报错的五个血泪现场现象1RuntimeError: CUDA out of memory原因--batch 16在RTX 3090上仍爆显存因CT图像512×512比COCO的640×640内存占用更高单图显存≈1.2GB。解决立即降--batch至8或加--cache参数减少重复加载。若仍爆用--img 320缩小输入尺寸精度损失0.01 mAP。现象2AssertionError: Error loading data from .../001.txt: empty file原因某张CT切片无结节但对应txt文件为空或只有空格YOLOv5要求每张图至少有一个标签。解决运行清理脚本删除空txtfind datasets-images-train -name *.txt -size 0c -delete find datasets-images-val -name *.txt -size 0c -delete现象3ValueError: Expected more than 1 value per channel when training, got input size [1, 256, 1, 1]原因--batch 1时BatchNorm层失效分母为0YOLOv5默认--batch≥2。解决绝不用--batch 1最小设为2或改用--sync-bn同步BN需多卡。现象4ModuleNotFoundError: No module named utils原因在yolov5/目录外执行python train.pyPython找不到utils/包。解决必须在yolov5/目录内运行或添加路径import sys sys.path.append(/path/to/yolov5)现象5cv2.error: OpenCV(4.5.5) ... error: (-215:Assertion failed) !_src.empty() in function cv::cvtColor原因cv2.imread()读取失败文件路径错/损坏返回None后续cvtColor崩溃。解决在dataset.py的__getitem__中加防护img cv2.imread(path) if img is None: raise FileNotFoundError(fFailed to load {path}) img cv2.cvtColor(img, cv2.COLOR_BGR2RGB)5. 推理部署与临床场景适配从runs/detect/到DICOM报告的三步封装5.1 批量推理命令如何让模型扫完整个DICOM序列runs/detect/目录下存放的是单张PNG的检测结果但临床需求是处理整个CT序列通常200~300张DICOM。本项目提供infer_sequence.py脚本实现端到端流程import pydicom import cv2 import torch from models.experimental import attempt_load from utils.general import non_max_suppression, scale_coords from utils.plots import plot_one_box # 加载模型CPU模式避免GPU显存冲突 model attempt_load(weights/best.pt, map_locationcpu) model.eval() def process_dicom_series(dicom_dir, output_dir): dcm_files sorted([f for f in os.listdir(dicom_dir) if f.endswith(.dcm)]) for i, dcm_file in enumerate(dcm_files): # 1. 读取DICOM并转PNG同预处理逻辑 ds pydicom.dcmread(os.path.join(dicom_dir, dcm_file)) img ds.pixel_array.astype(np.float32) img np.clip(img, -1350, 750) # WW1500, WL-600 img ((img 1350) / 2100 * 255).astype(np.uint8) # 2. 调整尺寸至512x512保持长宽比padding黑边 h, w img.shape scale 512 / max(h, w) new_h, new_w int(h * scale), int(w * scale) img_resized cv2.resize(img, (new_w, new_h)) pad_h (512 - new_h) // 2 pad_w (512 - new_w) // 2 img_padded np.pad(img_resized, ((pad_h, 512-new_h-pad_h), (pad_w, 512-new_w-pad_w)), constant) # 3. 模型推理注意输入需扩展batch维度并归一化 img_tensor torch.from_numpy(img_padded).float().unsqueeze(0).unsqueeze(0) / 255.0 pred model(img_tensor)[0] pred non_max_suppression(pred, conf_thres0.25, iou_thres0.45)[0] # 4. 坐标反变换从512x512映射回原始DICOM尺寸 if len(pred) 0: pred[:, [0, 2]] (pred[:, [0, 2]] - pad_w) / scale # x1, x2 pred[:, [1, 3]] (pred[:, [1, 3]] - pad_h) / scale # y1, y2 pred pred[pred[:, 0] 0] # 过滤负坐标 pred pred[pred[:, 1] 0] pred pred[pred[:, 2] w] pred pred[pred[:, 3] h] # 5. 保存结果PNG坐标CSV cv2.imwrite(os.path.join(output_dir, f{i:03d}_pred.png), img_padded) with open(os.path.join(output_dir, f{i:03d}_pred.csv), w) as f: f.write(x1,y1,x2,y2,score\n) for box in pred: f.write(f{box[0]:.1f},{box[1]:.1f},{box[2]:.1f},{box[3]:.1f},{box[4]:.3f}\n) process_dicom_series(my_patient_ct/, output/)关键点坐标反变换必须同步进行缩放和平移补偿。若忽略pad_h/pad_w检测框会整体偏移若忽略scale框尺寸会错误。5.2 DICOM元数据注入如何把检测结果写回原始DICOM临床系统要求结果以DICOM-SRStructured Report格式存储。本项目不直接生成SR需DCMTK复杂链路而是提供inject_detection_to_dcm.py将检测框坐标作为私有标签写入def inject_to_dcm(dcm_path, csv_path, output_path): ds pydicom.dcmread(dcm_path) # 读取CSV中的检测框 boxes pd.read_csv(csv_path) # 创建私有标签组0x0049,0x1001存储为JSON字符串 detection_json boxes.to_json(orientrecords) # 写入私有标签需先声明私有字典 ds.PrivateCreator YOLOv5-Lung ds.add_new([0x0049, 0x1001], UT, detection_json) ds.save_as(output_path) inject_to_dcm(input.dcm, 001_pred.csv, output.dcm)注意私有标签需在PACS系统中预先配置解析规则否则仅作存档。本方案是快速验证非PACS集成标准。5.3 报告生成自动化从坐标到临床描述的规则引擎results.csv是训练日志中的汇总表但临床需要自然语言报告。项目附带report_generator.py将检测结果转化为放射科术语检测框属性临床映射规则示例输出面积25px²“微小结节5mm”“右肺上叶见1枚微小结节直径约3mm”面积25~100px²“小结节5-10mm”“左肺下叶背段见2枚小结节最大径约8mm”长宽比2.5“条状影考虑血管走行”“右肺中叶见条状影沿血管分布建议随访”与胸膜距离10px“胸膜牵拉征”“左肺上叶尖后段结节伴胸膜牵拉征”该规则引擎不依赖NLP模型纯基于几何特征确保100%可解释性。你只需修改report_rules.json即可适配本院报告模板。5.4 从“能跑通”到“敢用在病人身上”我的三条铁律做完上述所有步骤模型在测试集上mAP0.89但离临床可用还有鸿沟。我带团队落地3家三甲医院肺结节AI系统血泪教训凝结为三条必须执行的检查必须用本院设备采集的CT做盲测哪怕只有10例也要覆盖不同kVp100/120/140、不同层厚0.625/1.25/2.5mm、不同重建算法FBP/IR。我们曾发现IR重建图像上mAP骤降0.15因噪声纹理被误判为结节。必须人工复核所有FP和FN导出runs/detect/中所有预测框让两位主治医师独立标注“真结节/假阳性/无法判断”。若两位医师分歧率15%说明结节定义模糊需修订标注规范。必须记录每例的DICOM元数据在results.csv中追加列StudyInstanceUID,SeriesInstanceUID,ImagePositionPatient确保结果可追溯至原始影像。某次上线后发现1例漏检靠UID秒定位到是扫描床移动导致层间错位而非模型问题。从那以后我每次交付新模型都强制走一遍这三步——不是为了证明模型多好而是为了证明当它说“没结节”时我敢签字。希望帮到你。本文还有配套的精品资源点击获取