简介:本资源为胸部X线结节目标检测数据集,面向医学影像分析方向的算法工程师、研究生及YOLO实战学习者,用于训练和验证肺结节自动检测模型。数据集共约2000张已标注图像,采用YOLO标准标注格式,类别仅含结节一类,并已预先划分训练集与测试集,可直接接入YOLOv5等主流检测框架开展实验。压缩包内共2000个文件,以1999个txt标注文件与1个show.py可视化脚本为主,整体约120.23MB,运行show脚本即可快速查看标注框与图像对应效果,便于检查数据质量。目前已有44人学习下载。对于希望复现医学目标检测流程、验证改进网络在真实胸部X线数据上表现的读者,该资源提供了开箱即用的标注数据与可视化入口,可节省数据清洗与格式转换时间,将精力集中于模型结构改进与调参对比。
1. 胸部 X 线结节检测:2000 张 YOLO 标注数据能跑出什么
手里有一份约 2000 张的胸部 X 线结节图像数据集,已按 YOLO 格式标注好,这件事的价值不在于“数据量有多大”,而在于它把最耗人力的标注环节省掉了。做过目标检测的人都知道,公开数据集要么类别不对口,要么标注质量参差,真正能直接进训练脚本的干净数据少之又少。胸部 X 线里的肺结节检测属于医学影像里门槛适中的任务:目标小、对比度低、正负样本极不均衡,但图像是单通道灰度图,预处理链路比 CT 序列短得多。这份数据适合两类人:一类是想把 YOLO 系列从通用场景迁移到医学影像的工程师,另一类是手里有临床图像但缺标注、想先拿现成数据验证流程可行性的算法同学。2000 张不算多,但足够跑通一次完整的训练、验证、调参闭环,也能暴露小目标检测里最典型的几个坑。
2. 先搞清楚 YOLO 格式的胸部 X 线标注到底长什么样
2.1 目录结构与标签文件格式
YOLO 格式的数据集通常长这样:一个 images 目录放原图,一个 labels 目录放同名 txt 标签,外加一个 data.yaml 描述类别和路径。胸部 X 线结节数据一般只有一个类别,叫 nodule 或者 lung_nodule。每张图的标签文件里,每一行代表一个结节框,格式是class_id x_center y_center width height,后四个值都是归一化到 0 到 1 之间的浮点数。这里有个容易翻车的地方:归一化用的是原图宽高,不是缩放后的尺寸。如果你在预处理阶段把图 resize 了却没同步改标签,框就会整体偏移。
# 典型的 YOLO 数据集目录结构 dataset/ ├── images/ │ ├── train/ │ │ ├── patient_001.png │ │ └── patient_002.png │ └── val/ │ ├── patient_101.png │ └── patient_102.png ├── labels/ │ ├── train/ │ │ ├── patient_001.txt │ │ └── patient_002.txt │ └── val/ │ ├── patient_101.txt │ └── patient_102.txt └── data.yaml上面这个结构是 Ultralytics 系 YOLO 默认认的布局。images 和 labels 必须严格对应,文件名去掉扩展名后要一致。data.yaml 里写清楚 train、val 的路径和类别名。注意路径最好用绝对路径,相对路径在不同工作目录下启动训练时经常找不到文件,这是新手最常踩的坑之一。
2.2 标注质量自查:三行脚本筛掉坏框
拿到标注数据第一件事不是急着训练,而是查标签。医学影像标注里常见的问题包括:框超出图像边界、宽高为 0、坐标写成像素值没归一化、同一结节被标了两次。写个脚本扫一遍,比训练到一半发现 loss 不降要省时间得多。
import os import glob label_dir = "dataset/labels/train" bad_files = [] for txt_path in glob.glob(os.path.join(label_dir, "*.txt")): with open(txt_path, "r") as f: lines = f.readlines() for i, line in enumerate(lines): parts = line.strip().split() if len(parts) != 5: bad_files.append((txt_path, i, "字段数不对")) continue cls, x, y, w, h = parts x, y, w, h = map(float, [x, y, w, h]) # 归一化坐标必须在 0-1 之间,宽高必须大于 0 if not (0 <= x <= 1 and 0 <= y <= 1): bad_files.append((txt_path, i, "中心点越界")) if w <= 0 or h <= 0 or w > 1 or h > 1: bad_files.append((txt_path, i, "宽高异常")) print(f"发现 {len(bad_files)} 个可疑标注") for item in bad_files[:20]: print(item)这段脚本检查三件事:每行是否恰好 5 个字段、中心点是否在 0 到 1 之间、宽高是否为正且不超过 1。参数上,label_dir 换成你自己的标签目录即可。如果发现大量“宽高异常”,大概率是标注工具导出时没做归一化,需要拿原图尺寸重新除一遍。如果只是零星几个坏框,直接删掉对应行或者整张图移出训练集,别让脏数据进模型。
2.3 胸部 X 线特有的预处理:为什么不能直接套通用增强
通用目标检测的增强手段里,颜色抖动、HSV 变换、马赛克增强都很常见,但胸部 X 线是灰度图,颜色相关的增强要么无效要么引入伪影。更关键的是,医学影像里结节的形态和位置有临床意义,随机裁剪可能把结节切掉一半,随机旋转会让左右肺位置颠倒,这些都会让模型学到错误关联。常见做法是保留缩放、平移、亮度对比度微调,慎用大角度旋转和随机裁剪。马赛克增强在小目标上有时能提升召回,但胸部 X 线里结节本来就小,四张图拼一起后目标更小,反而可能拖累收敛。我一般会先关掉马赛克跑一版基线,再决定要不要开。
3. 用 YOLOv8 在 2000 张胸部 X 线上跑通训练闭环
3.1 环境配置与预训练权重选择
环境这块,Python 3.9 到 3.11 都比较稳,PyTorch 选和 CUDA 匹配的版本。Ultralytics 的包直接 pip 装就行,它会自动拉依赖。预训练权重建议从 yolov8s 或 yolov8m 起步,n 太小对医学小目标欠拟合,l 和 x 在 2000 张数据上容易过拟合。如果显卡显存只有 8G,yolov8s 配 imgsz 640 基本能跑,batch 设 8 或 16。显存够的话上 yolov8m,召回通常会好一截。
# 创建环境并安装依赖 conda create -n cxr_yolo python=3.10 -y conda activate cxr_yolo pip install ultralytics pip install opencv-python matplotlib # 验证安装和显卡可见性 python -c "import torch; print(torch.__version__, torch.cuda.is_available())"装完后先确认 torch.cuda.is_available() 返回 True,否则训练会默默跑在 CPU 上,2000 张图能跑到天荒地老。如果返回 False,先查显卡驱动和 CUDA 版本,别急着改代码。
3.2 data.yaml 配置与训练命令逐参数说明
data.yaml 是训练入口,写错一个路径后面全白搭。胸部 X 线结节只有一类,nc 设 1,names 写 nodule。
# data.yaml path: /abs/path/to/dataset train: images/train val: images/val nc: 1 names: 0: nodulepath 用绝对路径,train 和 val 相对于 path。如果验证集想单独指定,也可以写绝对路径。配置好后启动训练:
yolo detect train \ model=yolov8s.pt \ data=data.yaml \ epochs=150 \ imgsz=640 \ batch=16 \ lr0=0.01 \ patience=30 \ device=0 \ project=runs/cxr \ name=baseline逐个说参数:epochs 设 150 是因为医学小目标收敛慢,但 patience 30 会在验证指标不再提升时提前停,避免无效训练。imgsz 640 是精度和显存的平衡点,结节太小的话可以试 1024,但显存翻倍。lr0 初始学习率 0.01 对 yolov8s 偏大,如果 loss 震荡就降到 0.005。batch 根据显存调,16 不够就 8。device=0 指定第一块卡。project 和 name 决定输出目录,跑完在 runs/cxr/baseline 下能看到权重、曲线和混淆矩阵。
3.3 训练过程看什么:loss 曲线与验证指标
训练启动后重点盯三个东西:box_loss、cls_loss 和 mAP50。box_loss 管框回归,cls_loss 管分类,胸部 X 线只有一类,cls_loss 通常降得很快。如果 box_loss 降到某个值就不动了,可能是学习率太小或者标注框本身不一致。mAP50 是 IoU 0.5 下的平均精度,小目标任务里这个值能到 0.4 以上就算不错。mAP50-95 更严格,医学影像里通常比 mAP50 低不少,别拿通用数据集的预期来套。验证集 loss 开始上升而训练 loss 还在降,就是过拟合信号,该早停或者加增强。
from ultralytics import YOLO # 加载训练好的权重做验证 model = YOLO("runs/cxr/baseline/weights/best.pt") metrics = model.val(data="data.yaml", imgsz=640, batch=16) print(f"mAP50: {metrics.box.map50:.4f}") print(f"mAP50-95: {metrics.box.map:.4f}") print(f"精确率: {metrics.box.mp:.4f}") print(f"召回率: {metrics.box.mr:.4f}")这段代码加载 best.pt 在验证集上重新算指标。注意 val 默认用的置信度阈值和 NMS 参数可能和训练时不同,如果指标和训练日志对不上,先检查这两处。召回率低说明漏检多,可以降置信度阈值或者加数据;精确率低说明误检多,可以提阈值或者清理负样本。
4. 小目标结节检测的调参与踩坑记录
4.1 锚框与特征图尺度:为什么小节点容易漏
YOLOv8 是无锚框设计,靠特征图上的点直接回归框,但小目标依然依赖高分辨率特征图。P3 层 stride 8,对 640 输入来说每个格子对应 8 像素,结节如果只有十几像素,在 P3 上也就一两个格子,信息量很少。常见做法是加 P2 层,stride 4,但计算量上去了。另一个思路是把 imgsz 提到 1024,让结节在特征图上占更多格子。如果显存不够,可以只对含结节的图像做高分辨率训练,背景图降采样,但这需要改 dataloader,工程量不小。我一般先试 imgsz 1024 跑一版,看召回能涨多少,再决定值不值得加 P2。
4.2 正负样本失衡与损失函数表现
胸部 X 线里结节占比极低,一张图可能就一两个结节,剩下全是肺野和骨骼。YOLOv8 的分类损失用 BCE,框回归用 CIoU 加 DFL。正样本太少时,分类损失会被负样本主导,模型倾向于全预测背景。缓解手段包括:提高正样本采样权重、用 focal loss 替代 BCE、或者在数据层面过采样含结节的图。Ultralytics 默认没开 focal loss,要改得动源码。更简单的做法是在 data.yaml 里把含结节的图复制几份,文件名加后缀区分,让每个 epoch 见到的正样本更多。注意别复制太多,否则过拟合到那几张图。
4.3 避坑记录:胸部 X 线 YOLO 训练里最常见的五个翻车点
现象一:训练 loss 正常下降,但验证 mAP 一直是 0。原因通常是验证集标签路径不对,或者验证集图片和标签文件名不匹配。YOLO 找不到标签时会当背景处理,指标自然全零。解决:用 2.2 的脚本扫一遍验证集标签,确认每个 txt 都能对应到同名图片。
现象二:训练到一半 loss 突然变 NaN。医学影像里常见原因是学习率太大加上某批数据里有异常值。先降 lr0 到 0.001 重跑,如果还崩,检查输入图像是否有全黑或全白的坏图。另外混合精度训练在个别显卡上会不稳定,可以加amp=False关掉试试。
现象三:模型把肋骨、锁骨当成结节,误检一堆。这是负样本不够多样导致的。2000 张里如果背景图太少,模型没见过足够多的正常结构。解决:把不含结节的正常胸片也放进训练集,标签文件留空,让模型学会区分正常解剖结构和结节。
现象四:同一张图里相邻两个结节只检出一个。NMS 的 IoU 阈值默认 0.7,两个结节框重叠度高时会被合并。解决:把 NMS 的 IoU 阈值降到 0.5 或更低,在 val 和 predict 时都调。但降太低会引入重复框,需要权衡。
现象五:换用 yolov8m 后显存爆了,batch 降到 4 还是 OOM。除了降 batch,还可以开梯度累积,用nbs=64配合小 batch 模拟大 batch 效果。另外 imgsz 从 640 降到 512 也能省不少显存,但小目标召回会掉,慎用。
5. 从能跑到好用:推理部署与阈值调优的一个具体技巧
训练出 best.pt 只是第一步,真正落地时推理端的阈值调优往往比训练本身更影响体验。胸部 X 线结节检测里,置信度阈值和 NMS IoU 阈值是两个最关键的旋钮。默认 conf 0.25 在通用数据集上还行,但医学影像里模型对结节的置信度普遍偏低,很多真结节落在 0.1 到 0.2 之间。我一般会拿验证集跑一遍不同 conf 下的精确率-召回率曲线,找召回率还能接受、精确率开始陡降的那个点。具体做法是用 predict 模式批量跑验证集,把结果存成 json,再用 sklearn 算 PR 曲线。
from ultralytics import YOLO import json model = YOLO("runs/cxr/baseline/weights/best.pt") # 在不同置信度下跑推理,收集结果 for conf in [0.05, 0.1, 0.15, 0.2, 0.25, 0.3]: results = model.predict( source="dataset/images/val", conf=conf, iou=0.5, save=False, stream=True ) det_count = 0 for r in results: det_count += len(r.boxes) print(f"conf={conf}, 检出框总数={det_count}")这段代码不直接算 PR,而是先看不同 conf 下检出框数量的变化。如果 conf 从 0.05 提到 0.1 框数骤降,说明大量低置信度框是噪声;如果缓慢下降,说明模型对结节的确信度分布比较平滑,可以适当降 conf 保召回。iou 参数控制 NMS 合并力度,结节密集时调到 0.4 到 0.5,稀疏时 0.6 到 0.7。实际部署时我习惯把 conf 设得比验证集最优值再低 0.05,宁可多报几个让医生复核,也别漏掉真结节。这个习惯是从一次漏检教训里来的:验证集 mAP 看着不错,上线后连续漏了两个明显结节,回头查发现 conf 卡在 0.25 把 0.23 的真结节滤掉了。后来我把推理 conf 统一降到 0.15,误报多了些,但再没出过漏检。希望帮到你。
本文还有配套的精品资源,点击获取