
简介本资源是面向医学图像AI初学者与计算机视觉实践者的YOLOv5肺结节检测实战项目聚焦CT影像中单类别肺结节目标检测任务解决医学影像标注数据稀缺、模型调优门槛高等实际问题。压缩包共704个文件含285张标注CT切片JPG、250个对应YOLO格式标签TXT、52个配置文件YAML/YML、51个核心脚本PY覆盖训练/推理/评估全流程以及训练权重PT、可视化结果PNG、Docker部署文件及教程Notebook等结构完整、开箱即用。资源包大小47.71MB项目已迭代100个epoch验证集mAP0.5达0.89附带混淆矩阵、PR曲线、F1曲线等分析结果runs/detect目录提供全部推理效果图。目前已有408人学习下载配套两篇详细参数解析博文涵盖训练与推理脚本的实操要点显著降低复现难度。1. 为什么肺结节CT图像目标检测不能直接套用YOLOv5默认配置你手上有几十例低剂量CT扫描序列每例含200~500张横断面图像标注文件是JSON或XML格式的结节中心坐标长径/短径单位mm想用YOLOv5快速跑通检测流程——结果训练loss不降、验证AP几乎为0、推理时满屏误检。这不是模型不行而是CT图像和自然图像存在三重根本性错配像素值非RGB、目标尺度极小3~10mm在512×512图像中仅占3~10像素、病灶对比度极低结节与肺实质HU值差常50。YOLOv5原生设计面向COCO这类高对比、大目标、RGB三通道数据直接迁移等于让赛车手开拖拉机犁地。本项目核心不是“怎么装YOLOv5”而是如何把CT图像的物理特性HU值范围、层厚、重建核映射成YOLOv5能理解的视觉信号再构建适配医学影像特性的数据集流水线。适合两类人刚接触医学影像的CV工程师需避开放射科术语陷阱以及有标注数据但卡在模型落地的临床AI团队需可复现的预处理链路。全文所有步骤均基于真实肺结节项目沉淀已通过LIDC-IDRI和NLST公开数据集验证不依赖任何商业标注平台或私有工具链。2. 从DICOM到YOLOv5可用图像CT数据预处理四步法CT原始数据是DICOM序列直接转PNG会丢失关键信息。必须经过HU值校准、窗宽窗位适配、尺寸归一化、通道重构四个不可跳过的环节。常见错误是用PIL.Image.open()强行读取DICOM——这会丢弃所有DICOM元数据导致后续无法还原真实尺寸。正确做法是用pydicom精确解析再用opencv做无损转换。2.1 提取HU值并校正CT物理量纲DICOM文件中的像素值PixelData不是绝对HU值需结合RescaleIntercept和RescaleSlope字段计算import pydicom import numpy as np def dicom_to_hu(dicom_path): ds pydicom.dcmread(dicom_path) # 关键必须用DICOM标准公式还原HU值 intercept ds.RescaleIntercept if RescaleIntercept in ds else 0 slope ds.RescaleSlope if RescaleSlope in ds else 1 pixel_array ds.pixel_array.astype(np.float32) hu_array pixel_array * slope intercept return hu_array, ds # 示例验证某张图HU值范围 hu_img, ds dicom_to_hu(patient_001/000001.dcm) print(fHU range: {hu_img.min():.1f} ~ {hu_img.max():.1f}, SliceThickness: {ds.SliceThickness}mm) # 输出HU range: -1024.0 ~ 3071.0, SliceThickness: 1.25mm提示RescaleIntercept/Slope在不同设备厂商GE/Siemens/Philips中可能缺失若报错需手动补全常规CT设intercept-1024, slope1低剂量CT建议实测校准。2.2 窗宽窗位WW/WL映射到8位灰度HU值范围达-1024~3071远超8位图像0~255。直接截断会丢失结节细节。必须按肺窗WW1500, WL-600或纵隔窗WW350, WL50线性映射def hu_to_grayscale(hu_array, window_width1500, window_level-600): # 肺窗参数突出肺实质和结节抑制血管和骨骼 img_min window_level - window_width // 2 img_max window_level window_width // 2 # 截断并归一化 hu_array np.clip(hu_array, img_min, img_max) hu_array (hu_array - img_min) / (img_max - img_min) * 255.0 return hu_array.astype(np.uint8) # 应用肺窗 gray_img hu_to_grayscale(hu_array, window_width1500, window_level-600) cv2.imwrite(lung_window.png, gray_img) # 保存为标准PNG参数说明window_width1500控制对比度值越小对比越强但易丢失低密度结节window_level-600控制亮度负值增强肺实质显示结节HU值通常在-200~100血泪经验同一病例不同层厚1mm vs 5mm需统一窗宽窗位否则模型认为是不同类别。2.3 尺寸归一化与切片筛选CT序列中大量切片无结节如膈顶、锁骨区域直接全量输入YOLOv5会稀释正样本。需结合Spacing信息剔除无效层def filter_slices(hu_array_list, spacing_list, min_spacing0.5, max_spacing2.5): # 排除层厚异常切片如重建伪影层 valid_indices [i for i, s in enumerate(spacing_list) if min_spacing s max_spacing] return [hu_array_list[i] for i in valid_indices] # 实际项目中我们只保留HU值方差100的切片结节区域纹理更复杂 def select_candidate_slices(hu_array_list, variance_threshold100): selected [] for i, hu in enumerate(hu_array_list): if np.var(hu) variance_threshold: selected.append(i) return selected逻辑说明spacing_list从DICOM的SliceThickness和ImagePositionPatient推导非简单取相邻切片Z轴差方差筛选比阈值法更鲁棒结节区域因组织异质性像素值波动显著大于正常肺组织2.4 构建YOLOv5兼容的三通道输入YOLOv5默认接收BGR三通道但单通道CT图像直接复制三份会引入冗余噪声。更优方案是生成伪彩色增强def create_three_channel(gray_img): # 方法1灰度图复制三份baseline简单但有效 bgr_img cv2.cvtColor(gray_img, cv2.COLOR_GRAY2BGR) # 方法2CLIP风格增强提升小目标对比度 clahe cv2.createCLAHE(clipLimit2.0, tileGridSize(8,8)) enhanced clahe.apply(gray_img) bgr_img cv2.cvtColor(enhanced, cv2.COLOR_GRAY2BGR) return bgr_img # 保存为YOLOv5要求的JPEG格式非PNG bgr_img create_three_channel(gray_img) cv2.imwrite(000001.jpg, bgr_img) # 注意YOLOv5 train.py默认读JPEG关键点必须用cv2.cvtColor(..., cv2.COLOR_GRAY2BGR)而非np.stack([gray]*3, axis2)避免OpenCV读取时通道错位JPEG压缩比PNG更适合YOLOv5的随机裁剪PNG无损压缩导致块效应放大3. 标注文件转换从放射科JSON到YOLOv5 TXT的硬编码规则肺结节标注常以JSON格式存储如LIDC-IDRI包含结节中心(x,y,z)、长径/短径(mm)、恶性概率等字段。YOLOv5要求每张图对应一个TXT文件每行class x_center y_center width height归一化到0~1。直接写脚本易出错核心在于HU值空间到像素坐标的精确映射。3.1 解析DICOM物理坐标系CT标注中的(x,y,z)是世界坐标mm需转为图像像素坐标。关键参数来自DICOMImagePositionPatient: 图像左上角在世界坐标系中的位置x,y,zPixelSpacing: 每像素对应的实际距离dx, dySliceThickness: 层厚dz用于z轴映射def world_to_pixel(world_coord, image_position, pixel_spacing, slice_thickness): # world_coord: [x_world, y_world, z_world] # image_position: [x0, y0, z0] 图像左上角世界坐标 # pixel_spacing: [dx, dy] x_pixel (world_coord[0] - image_position[0]) / pixel_spacing[0] y_pixel (world_coord[1] - image_position[1]) / pixel_spacing[1] # z轴不参与2D检测但可用于筛选切片 z_slice int(round((world_coord[2] - image_position[2]) / slice_thickness)) return x_pixel, y_pixel, z_slice # 示例将标注点转为像素坐标 ip ds.ImagePositionPatient # [x0, y0, z0] ps ds.PixelSpacing # [dx, dy] st ds.SliceThickness # dz x_px, y_px, z_idx world_to_pixel( world_coord[-120.5, 45.2, -230.1], image_positionip, pixel_spacingps, slice_thicknessst )3.2 生成YOLOv5格式TXT文件注意YOLOv5要求坐标归一化且宽度/高度为包围盒尺寸非结节直径def generate_yolo_label(json_ann, dicom_dir, output_dir): # json_ann: {nodule_list: [{x: -120.5, y: 45.2, z: -230.1, diameter_mm: 6.2}, ...]} for nodule in json_ann[nodule_list]: # 1. 找到对应切片索引 x_px, y_px, z_idx world_to_pixel( [nodule[x], nodule[y], nodule[z]], ip, ps, st ) # 2. 计算包围盒结节直径→像素宽度假设球形 diameter_px nodule[diameter_mm] / ps[0] # 用x方向pixel spacing # 3. 归一化到0~1YOLOv5强制要求 img_h, img_w 512, 512 # 假设已resize到512x512 x_norm x_px / img_w y_norm y_px / img_h w_norm diameter_px / img_w h_norm diameter_px / img_h # 结节近似圆形wh # 4. 写入TXTclass_id0因只有结节一类 label_path os.path.join(output_dir, f{z_idx:06d}.txt) with open(label_path, a) as f: f.write(f0 {x_norm:.6f} {y_norm:.6f} {w_norm:.6f} {h_norm:.6f}\n) # 调用示例 generate_yolo_label(json_data, dicom_dir/, labels/)参数说明class_id0肺结节检测通常为单类任务多类如良/恶性需扩展为class_id1/2diameter_px用ps[0]x方向间距计算因CT图像x/y分辨率通常一致z方向不参与2D检测避坑重点若DICOM的ImageOrientationPatient非标准如旋转扫描需额外做坐标系旋转校正否则定位偏差5px3.3 处理多结节与重叠标注临床标注常存在同一层多个结节或不同医生标注同一结节位置偏差。YOLOv5对重叠框敏感需去重def deduplicate_nodules(nodule_list, distance_threshold_px10): # 按欧氏距离合并相近结节同一层内 kept [] for n1 in nodule_list: is_duplicate False for n2 in kept: dist np.sqrt((n1[x]-n2[x])**2 (n1[y]-n2[y])**2) if dist distance_threshold_px: is_duplicate True break if not is_duplicate: kept.append(n1) return kept逻辑说明distance_threshold_px10对应实际距离约1.5mm按0.15mm/px计算覆盖标注误差范围不建议用NMS后处理替代此步训练前去重能减少label噪声提升收敛稳定性4. YOLOv5训练配置调优针对CT图像的超参数重设YOLOv5默认超参数如anchor尺寸、学习率针对COCO优化直接用于CT结节检测会导致召回率暴跌。必须根据结节物理尺寸重设anchor并调整数据增强策略。4.1 重新聚类anchor尺寸CT结节在图像中尺寸集中于10~40像素512×512下远小于COCO的200像素目标。需用k-means重新计算anchor# 步骤1生成所有标注框尺寸像素单位 python tools/cluster_anchors.py \ --dataset-dir datasets/lung_nodule \ --img-size 512 \ --num-clusters 9 \ --output anchors_ct.txtcluster_anchors.py核心逻辑# 读取所有TXT标签提取width/height像素值 boxes [] for label_file in glob.glob(labels/*.txt): with open(label_file) as f: for line in f: cls, cx, cy, w, h map(float, line.strip().split()) # 归一化坐标转像素尺寸 w_px w * 512 h_px h * 512 boxes.append([w_px, h_px]) # k-means聚类使用IOU距离而非欧氏距离 from scipy.cluster.vq import kmeans wh np.array(boxes) cluster_centers, _ kmeans(wh, 9, iter20) print(New anchors:, cluster_centers.astype(int)) # 输出示例[[12,12], [18,18], [25,25], [32,32], [40,40], ...]关键点--img-size 512必须与训练时resize尺寸一致否则anchor比例失真聚类结果需写入models/yolov5s.yaml的anchors:字段替换默认值4.2 修改数据增强策略CT图像增强需规避自然图像常用操作禁用ColorJitterCT灰度值具有物理意义颜色扰动破坏HU一致性降低Mosaic概率结节常位于肺野中央Mosaic拼接易产生虚假边缘启用CLAHE增强提升低对比结节可见度# models/yolov5s.yaml 中修改 train_augment: hsv_h: 0.0 # 禁用色调扰动 hsv_s: 0.0 # 禁用饱和度扰动 hsv_v: 0.0 # 禁用明度扰动改用CLAHE mosaic: 0.5 # 从1.0降至0.5 mixup: 0.1 # 从0.1保持但mixup图像需同为CT窗位实操技巧在datasets/lung_nodule.py中重写__getitem__对每张图应用CLAHEdef apply_clahe(img): clahe cv2.createCLAHE(clipLimit2.0, tileGridSize(8,8)) yuv cv2.cvtColor(img, cv2.COLOR_BGR2YUV) yuv[:,:,0] clahe.apply(yuv[:,:,0]) return cv2.cvtColor(yuv, cv2.COLOR_YUV2BGR)4.3 学习率与优化器调整CT数据量通常较小1000例过大学习率导致震荡。推荐配置# train.py 参数 lr0: 0.01 # 初始学习率COCO默认0.01→此处保持 lrf: 0.2 # 最终学习率比例0.2→0.1更保守衰减 warmup_epochs: 3 # 预热期避免初期梯度爆炸 optimizer: SGD # SGD比Adam更稳定Adam在小数据易过拟合 momentum: 0.937 # 保持默认 weight_decay: 0.0005 # L2正则防止过拟合参数依据lrf0.1训练50epoch时学习率从0.01→0.001避免后期loss平台期optimizer: SGD在LIDC-IDRI子集测试中SGD比Adam提升AP0.5达3.2%5. Dockerfile构建与部署隔离医学影像环境的最小可行方案本地环境常因OpenCV/PIL版本冲突导致CT读取失败。用Docker封装可确保复现性且便于部署到医院GPU服务器。本Dockerfile专为CT影像优化体积1.2GB。5.1 基础镜像选择与CUDA适配# 使用nvidia/cuda:11.3.1-cudnn8-runtime-ubuntu20.04 # 避免conda环境启动慢用aptpip混合安装 FROM nvidia/cuda:11.3.1-cudnn8-runtime-ubuntu20.04 # 安装系统依赖 RUN apt-get update apt-get install -y \ libglib2.0-0 \ libsm6 \ libxext6 \ libxrender-dev \ rm -rf /var/lib/apt/lists/* # 安装Python3.8及pip RUN apt-get update apt-get install -y python3.8 python3.8-venv python3.8-dev \ ln -sf /usr/bin/python3.8 /usr/bin/python \ ln -sf /usr/bin/pip3.8 /usr/bin/pip # 创建工作目录 WORKDIR /workspace选型理由cuda:11.3.1匹配RTX 3090/A100主流显卡cudnn8提供最佳YOLOv5推理性能ubuntu20.04避免ubuntu22.04的glibc版本过高导致pydicom兼容问题5.2 医学影像专用库编译安装# 安装pydicom需指定版本防DICOM解析崩溃 RUN pip install pydicom2.3.0 # 编译安装opencv-python-headless无GUI节省空间 RUN pip install opencv-python-headless4.7.0.72 # 安装YOLOv5依赖固定版本防API变更 RUN pip install torch1.10.2cu113 torchvision0.11.3cu113 -f https://download.pytorch.org/whl/torch_stable.html RUN pip install numpy1.21.6 pandas1.3.5 matplotlib3.5.3 # 复制YOLOv5代码建议用git submodule管理 COPY yolov5/ /workspace/yolov5/ WORKDIR /workspace/yolov5关键点pydicom2.3.0修复了2.4版本中对某些GE设备DICOM的解析崩溃opencv-python-headless避免X11依赖适配无显示器的服务器环境5.3 构建与运行命令# 构建镜像耗时约8分钟 docker build -t lung-yolov5:v1 . # 运行训练挂载数据集和输出目录 docker run --gpus all -it --rm \ -v $(pwd)/datasets:/workspace/datasets \ -v $(pwd)/runs:/workspace/runs \ lung-yolov5:v1 \ bash -c cd /workspace/yolov5 \ python train.py \ --data ../datasets/lung_nodule.yaml \ --weights yolov5s.pt \ --img 512 \ --batch-size 16 \ --epochs 100 \ --name lung_exp1 # 导出ONNX模型供医院PACS系统集成 docker run --gpus all -it --rm \ -v $(pwd)/weights:/workspace/weights \ lung-yolov5:v1 \ bash -c cd /workspace/yolov5 \ python export.py \ --weights weights/best.pt \ --include onnx \ --imgsz 512避坑指南--gpus all必须显式声明否则容器内CUDA不可见-v挂载路径需绝对路径相对路径在Docker中失效ONNX导出时--imgsz 512必须与训练尺寸一致否则PACS系统推理报错6. 验证与临床可用性评估超越mAP的三个硬指标YOLOv5训练完看到mAP0.50.75就收工在肺结节检测中这是危险信号。临床真正关心的是能否检出3mm微小结节假阳性是否集中在血管交叉区模型对低剂量CT是否鲁棒这些需定制化评估脚本。6.1 微小结节召回率专项测试def evaluate_small_nodules(model, test_loader, size_threshold_px15): # size_threshold_px15 对应3mm结节按0.2mm/px计算 tp_small, fn_small 0, 0 for imgs, targets in test_loader: preds model(imgs.cuda()) # 解析preds获取检测框 for i, pred in enumerate(preds): # 筛选置信度0.3的预测框 conf_mask pred[:, 4] 0.3 boxes pred[conf_mask, :4] # 计算每个预测框的像素尺寸 for box in boxes: w_px box[2] - box[0] h_px box[3] - box[1] size_px max(w_px, h_px) # 匹配GTIoU0.5 gt_boxes targets[i] matched False for gt in gt_boxes: iou calculate_iou(box, gt) if iou 0.5 and gt[2] size_threshold_px: # GT是小结节 tp_small 1 matched True break if not matched and gt[2] size_threshold_px: fn_small 1 recall_small tp_small / (tp_small fn_small 1e-6) return recall_small # 调用 small_recall evaluate_small_nodules(model, test_loader, size_threshold_px15) print(fRecall for 3mm nodules: {small_recall:.3f})临床意义size_threshold_px15对应3mm结节是肺癌筛查关键阈值若recall_small 0.6即使mAP0.75也需回溯anchor或增强策略6.2 假阳性热点区域分析def analyze_fp_regions(model, test_loader, lung_mask_dir): # 加载肺野掩膜由radiologist提供或U-Net生成 fp_locations [] for imgs, targets in test_loader: preds model(imgs.cuda()) for i, pred in enumerate(preds): # 获取FP框未匹配任何GT gt_boxes targets[i] for p in pred: if p[4] 0.3: continue is_fp True for gt in gt_boxes: if calculate_iou(p[:4], gt) 0.5: is_fp False break if is_fp: # 投影到肺野掩膜统计坐标分布 x, y (p[0]p[2])/2, (p[1]p[3])/2 mask_path os.path.join(lung_mask_dir, f{i:06d}.png) mask cv2.imread(mask_path, 0) if mask[int(y), int(x)] 0: # 在肺野外如血管/骨骼 fp_locations.append(outside_lung) else: fp_locations.append(inside_lung) # 统计FP分布 from collections import Counter counter Counter(fp_locations) print(FP location distribution:, counter) # 输出{outside_lung: 127, inside_lung: 23} → 说明模型过度依赖肺野外伪影行动指南若outside_lung占比80%需加强肺野裁剪在预处理中mask掉非肺区域可视化FP热力图定位高频误检区域如主动脉弓、肋骨交界处6.3 低剂量CT鲁棒性测试def test_ldct_robustness(model, ldct_dataset): # ldct_dataset含不同管电流50mA/80mA/120mA的CT序列 ap_list [] for mA in [50, 80, 120]: subset ldct_dataset.filter_by_mA(mA) ap evaluate_ap(model, subset) ap_list.append(ap) print(fAP0.5 at {mA}mA: {ap:.3f}) # 计算鲁棒性得分AP下降幅度 robust_score 1.0 - (ap_list[0] - ap_list[-1]) / (ap_list[-1] 1e-6) print(fRobustness score (50→120mA): {robust_score:.3f}) return robust_score # 调用 robust_score test_ldct_robustness(model, ldct_testset) # 临床接受阈值robust_score 0.85参数说明mA管电流决定辐射剂量50mA为典型低剂量扫描robust_score 0.85表示模型在低剂量下AP下降15%满足临床部署要求我坚持在每次新项目启动时先用这三套验证脚本跑一遍基线模型——哪怕多花两天。因为见过太多团队在mAP达标后才进医院测试结果发现3mm结节漏检率40%、血管伪影误报率70%最后返工重训。技术可以炫酷但临床交付必须诚实。希望帮到你。本文还有配套的精品资源点击获取