
简介本资源为面向医学影像与目标检测方向的YOLO肺结节检测数据集适合计算机视觉学习者、医学AI研究者及需要真实场景数据开展课程设计或论文实验的人员使用。数据均来自真实场景场景丰富经labelimg标注标注框质量较高并同时提供voc(xml)、coco(json)与yolo(txt)三种格式标签分别存放于不同文件夹可直接接入YOLO系列模型训练。压缩包共约2000个文件以1986个xml标注文件为主另含html教程、txt列表与py脚本整体约77.56MB。资源还附赠环境搭建、训练教程及数据集划分脚本可按需生成训练集、验证集与测试集并输出ImageSets下的划分文件。目前已有403人学习下载能帮助读者省去数据整理与格式转换环节快速完成从数据准备到模型训练的全流程实践。1. 从一张肺部 CT 说起YOLO 肺结节目标检测数据集到底解决什么问题肺结节筛查这件事真正卡住大多数团队的从来不是模型结构而是数据。你拿到一批 DICOM 影像想训一个 YOLO 肺结节目标检测模型第一步就会撞上标注格式这道墙影像科医生给的是 XML 或者一堆坐标文本YOLO 要的是归一化后的 txt中间还夹着 VOC 和 COCO 两套生态。这个数据集的价值就在这儿——5000 张图片同时给出 VOC、COCO、YOLO 三种格式标签外加划分脚本和训练教程等于把「数据准备」这段最耗人力的脏活提前干完了。它适合两类人一类是想快速验证肺结节检测思路的算法工程师另一类是手上只有零散标注、想找一套规范流程照着复现的学生和初级开发者。下面我按自己实际跑这类数据集的顺序把格式转换、划分脚本、训练参数和踩坑点讲清楚。2. 三种标签格式的差异与转换逻辑VOC、COCO、YOLO 到底该用哪个2.1 三种格式的坐标系差异VOC 格式用 XML 存绝对像素坐标xmin/ymin/xmax/ymax四个值直接对应图像上的像素位置好处是可读性强坏处是图像尺寸一变就得重算。COCO 用 JSONbbox是[x, y, width, height]的绝对像素另外还有category_id和image_id做索引适合多类别、大规模数据集管理。YOLO 格式最精简每行class_id x_center y_center width height全部归一化到 0 到 1 之间一个 txt 对应一张图。肺结节检测里结节通常只有一个类别所以 YOLO 格式的class_id基本都是 0。但要注意肺结节在 CT 上尺寸差异极大小结节可能只有十几个像素归一化之后 width 和 height 会小到 0.01 以下这时候如果标注精度不够训练时很容易被忽略。2.2 从 VOC 转 YOLO 的脚本与参数数据集里虽然给了三种格式但实际训练 YOLO 时我一般还是自己再转一遍确保归一化逻辑和图像实际尺寸对齐。下面这段脚本处理 VOC 到 YOLO 的转换import xml.etree.ElementTree as ET import os from PIL import Image def voc_to_yolo(xml_dir, img_dir, out_dir, class_map): xml_dir: VOC 标注文件夹 img_dir: 对应图片文件夹用于读取真实宽高 out_dir: 输出 YOLO txt 文件夹 class_map: {nodule: 0} 类别映射 os.makedirs(out_dir, exist_okTrue) for xml_file in os.listdir(xml_dir): if not xml_file.endswith(.xml): continue tree ET.parse(os.path.join(xml_dir, xml_file)) root tree.getroot() # 用图片真实尺寸不要用 XML 里的 size有些标注工具会写错 img_name root.find(filename).text img_path os.path.join(img_dir, img_name) with Image.open(img_path) as im: w, h im.size lines [] for obj in root.findall(object): cls_name obj.find(name).text if cls_name not in class_map: continue cls_id class_map[cls_name] bbox obj.find(bndbox) xmin float(bbox.find(xmin).text) ymin float(bbox.find(ymin).text) xmax float(bbox.find(xmax).text) ymax float(bbox.find(ymax).text) # 归一化并做边界裁剪防止标注越界 x_center max(0, min(1, (xmin xmax) / 2.0 / w)) y_center max(0, min(1, (ymin ymax) / 2.0 / h)) bw max(0, min(1, (xmax - xmin) / w)) bh max(0, min(1, (ymax - ymin) / h)) lines.append(f{cls_id} {x_center:.6f} {y_center:.6f} {bw:.6f} {bh:.6f}) out_file os.path.join(out_dir, xml_file.replace(.xml, .txt)) with open(out_file, w) as f: f.write(\n.join(lines)) voc_to_yolo(annotations_xml, images, labels_yolo, {nodule: 0})这段代码的关键点有三个。第一宽高从图片实际读取不用 XML 里的size字段因为很多标注工具在图像被裁剪或缩放后不会同步更新这个字段用错了会导致所有框偏移。第二归一化后做了max(0, min(1, ...))裁剪肺结节标注里偶尔会出现 xmax 超过图像宽度的情况不裁剪训练时直接报错。第三保留六位小数小结节的宽高本身很小精度不够会丢信息。2.3 COCO 格式在肺结节场景下的取舍COCO 格式的优势在于生态兼容性好很多评估脚本和可视化工具默认吃 COCO。但肺结节数据集用 COCO 有个坑COCO 的area字段是width * height小结节的 area 可能只有几十某些评估库会按 area 过滤掉小目标导致你明明标注了却算不进指标。我的做法是保留 COCO 格式用于可视化验证训练时统一走 YOLO 格式避免两套逻辑打架。提示转换完成后一定要抽查几张图用可视化脚本把框画回去确认没有整体偏移或镜像翻转。我见过因为图像和标注文件名大小写不一致导致一半数据没配上标签的情况训练 loss 看着正常实际模型什么都没学到。3. 划分脚本怎么用训练集、验证集、测试集的比例与分层策略3.1 为什么不能直接随机划分肺结节数据集有个特点阳性样本和阴性样本比例可能不均衡而且不同患者的结节大小分布差异大。如果直接random.shuffle然后按 8:1:1 切很可能出现验证集里全是小结节、训练集里全是大结节的情况指标波动会非常大。更稳妥的做法是按结节尺寸分层抽样保证三个集合的尺寸分布一致。3.2 分层划分脚本实现import os import random import shutil from collections import defaultdict def stratified_split(label_dir, img_dir, out_dir, ratios(0.8, 0.1, 0.1), seed42): 按结节面积分层划分数据集 ratios: 训练/验证/测试比例 random.seed(seed) # 按面积分桶小、中、大 buckets defaultdict(list) for txt_file in os.listdir(label_dir): if not txt_file.endswith(.txt): continue with open(os.path.join(label_dir, txt_file)) as f: lines [l.strip() for l in f if l.strip()] if not lines: buckets[empty].append(txt_file) continue # 取最大结节面积作为分层依据 max_area 0 for line in lines: parts line.split() if len(parts) 5: _, _, _, bw, bh parts max_area max(max_area, float(bw) * float(bh)) if max_area 0.005: buckets[small].append(txt_file) elif max_area 0.05: buckets[medium].append(txt_file) else: buckets[large].append(txt_file) splits {train: [], val: [], test: []} for bucket, files in buckets.items(): random.shuffle(files) n len(files) n_train int(n * ratios[0]) n_val int(n * ratios[1]) splits[train].extend(files[:n_train]) splits[val].extend(files[n_train:n_train n_val]) splits[test].extend(files[n_train n_val:]) # 按 YOLO 目录结构复制 for split, files in splits.items(): img_out os.path.join(out_dir, images, split) lbl_out os.path.join(out_dir, labels, split) os.makedirs(img_out, exist_okTrue) os.makedirs(lbl_out, exist_okTrue) for txt_file in files: base txt_file.replace(.txt, ) # 图片可能是 jpg 或 png这里按实际扩展名匹配 for ext in [.jpg, .png, .jpeg]: src_img os.path.join(img_dir, base ext) if os.path.exists(src_img): shutil.copy(src_img, os.path.join(img_out, base ext)) break shutil.copy(os.path.join(label_dir, txt_file), os.path.join(lbl_out, txt_file)) print({k: len(v) for k, v in splits.items()}) stratified_split(labels_yolo, images, dataset_split)分层逻辑按最大结节面积分三桶小于 0.005 算小结节0.005 到 0.05 算中等大于 0.05 算大结节。这个阈值不是固定的你可以根据自己数据集的面积分布调整。seed固定保证可复现团队协作时大家用同一个 seed 划分结果一致避免「你训的和我训的不是同一份数据」这种扯皮。3.3 划分后的目录结构校验划分完别急着开训先跑一遍校验每个 split 下 images 和 labels 的文件名是否一一对应有没有空标签文件有没有图片没有对应标签。我一般用下面这几行快速检查for split in train val test; do echo $split img_count$(ls dataset_split/images/$split | wc -l) lbl_count$(ls dataset_split/labels/$split | wc -l) echo images: $img_count, labels: $lbl_count # 找出没有标签的图片 for img in dataset_split/images/$split/*; do base$(basename $img | sed s/\.[^.]*$//) [ -f dataset_split/labels/$split/$base.txt ] || echo missing label: $base done done数量对不上或者有 missing label先解决再训练。空标签文件在 YOLO 里是合法的负样本但肺结节数据集里如果大量空标签说明阳性样本太少需要考虑过采样或者调整 loss 权重。4. 训练教程落地从 data.yaml 到第一个 baseline 的完整命令4.1 data.yaml 的写法与路径陷阱YOLO 训练入口是data.yaml肺结节数据集通常只有一个类别写法如下path: /abs/path/to/dataset_split train: images/train val: images/val test: images/test nc: 1 names: [nodule]path一定用绝对路径相对路径在不同工作目录下会找不到数据。nc是类别数肺结节只有一类就写 1。names的顺序要和标注里的class_id对应如果转换时把结节标成了 0这里第一个就是 nodule。4.2 启动训练的命令与关键参数yolo detect train \ datadata.yaml \ modelyolov8n.pt \ epochs100 \ imgsz640 \ batch16 \ lr00.01 \ patience20 \ device0 \ projectruns/nodule \ namebaselinemodel选 yolov8n 还是更大的版本取决于你的显存和精度要求。肺结节检测里小结节多输入分辨率imgsz建议不低于 640有条件可以上 1024但显存占用会明显增加。patience20表示 20 个 epoch 验证指标不提升就早停避免过拟合。lr00.01是初始学习率如果 loss 震荡厉害可以降到 0.001。4.3 训练过程中该盯哪些指标YOLO 训练输出里重点看三个box_loss是否稳定下降mAP50是否在验证集上提升precision和recall是否严重失衡。肺结节检测常见的问题是 recall 高但 precision 低模型把血管截面、胸膜增厚都当成结节。这时候可以适当提高置信度阈值或者在数据里补充一些难负样本。注意如果训练前几个 epoch loss 就变成 nan大概率是标注里有非法值比如坐标超出 0 到 1 范围或者空行格式不对。回头用校验脚本扫一遍标签文件。5. 避坑与排查肺结节 YOLO 训练里最容易翻车的 5 个点5.1 现象训练 loss 正常下降但验证集 mAP 始终为 0原因标签类别映射错了。转换脚本里class_map写的是{nodule: 0}但 XML 里实际类别名可能是Nodule或者lung_nodule大小写或拼写不一致导致所有框被跳过生成的 txt 全是空的。模型在学负样本验证集自然没有正例命中。解决转换后统计每个 txt 的行数如果大量为 0先检查类别名。用grep -r object annotations_xml | head看实际标签名再统一映射。5.2 现象训练到一半突然报 CUDA out of memory原因batch设太大或者imgsz从 640 调到 1024 后没同步降 batch。肺结节 CT 图像如果是 16 位灰度转 8 位单通道数据在 YOLO 里会被复制成三通道显存占用比自然图像高。解决先把batch降到 8 或 4用yolo detect train ... batch4跑通再逐步加。也可以用ampTrue开启混合精度显存能省不少。5.3 现象验证集可视化框位置整体偏移原因VOC 转 YOLO 时用了 XML 里的size字段而不是图片真实尺寸标注工具记录的宽高和实际图片不一致。或者图片在标注后被统一 resize 过但 XML 没更新。解决转换脚本里强制用PIL.Image.open读实际宽高不要信任 XML 的 size。转换后抽 10 张图用yolo detect predict跑一遍把预测框和原标注叠一起看。5.4 现象小结节全部漏检大结节正常原因YOLO 默认的 anchor 或损失函数对小目标不友好归一化后宽高小于 0.01 的结节在特征图上只剩不到一个像素。另外数据增强里的 mosaic 和随机缩放会把小结节进一步缩小。解决提高imgsz到 1024关闭或减弱 mosaic 增强mosaic0.5或close_mosaic10在 loss 里增加小目标的权重。如果还不行考虑用切片推理把大图切成小块分别检测再合并。5.5 现象训练完模型在测试集上指标很好实际推理时一堆误报原因测试集和真实场景分布不一致。数据集里的负样本可能太少模型没见过足够多的血管、胸膜、钙化灶等干扰项。另外测试集如果和训练集来自同一批患者存在数据泄漏指标虚高。解决划分时按患者 ID 划分同一患者的切片不能同时出现在训练和测试集。补充难负样本把误报高的区域裁出来加进训练集重新训。6. 进阶技巧用切片推理和 TTA 把小结节召回率再提一档肺结节检测里小结节漏检是最头疼的问题。整图直接推理时640 或 1024 的输入下一个 5 毫米的结节可能只占几个像素模型很难响应。我常用的做法是切片推理加测试时增强具体分三步。第一步把原始 CT 切片按 512×512 窗口、步长 256 滑窗切块每块单独送 YOLO 推理。这样每个小结节在局部块里的相对尺寸变大更容易被检测到。切块之间保留 50% 重叠避免结节正好落在切缝上被切掉。第二步对每个切块做水平翻转和垂直翻转的 TTA三次推理结果做加权框融合。YOLO 本身支持augmentTrue参数开启 TTA但内置 TTA 对医学图像提升有限我一般自己写融合逻辑def merge_boxes(boxes_list, iou_thr0.5): boxes_list: 多次推理的框列表每个元素 [x1,y1,x2,y2,score] 按得分排序后做 NMS 融合 all_boxes sorted([b for boxes in boxes_list for b in boxes], keylambda x: x[4], reverseTrue) keep [] while all_boxes: best all_boxes.pop(0) keep.append(best) all_boxes [b for b in all_boxes if iou(best, b) iou_thr] return keep def iou(a, b): xx1 max(a[0], b[0]); yy1 max(a[1], b[1]) xx2 min(a[2], b[2]); yy2 min(a[3], b[3]) w max(0, xx2 - xx1); h max(0, yy2 - yy1) inter w * h area_a (a[2]-a[0]) * (a[3]-a[1]) area_b (b[2]-b[0]) * (b[3]-b[1]) return inter / (area_a area_b - inter 1e-6)第三步把切块坐标映射回原图再做一次全局 NMS。这一步的iou_thr我一般设 0.3 到 0.4比训练时的 NMS 阈值低因为切片重叠区域会产生大量重复框阈值高了去不干净。这套流程跑下来小结节召回率通常能比整图推理高 10 到 15 个百分点代价是推理时间变成原来的 4 到 6 倍。实际部署时可以根据场景取舍筛查场景优先保召回可以全量切片随访场景对速度敏感可以只对整图推理置信度低的区域做局部切片。最后说个我自己的习惯每次拿到新数据集先不急着训模型花半小时把标签可视化一遍随机抽 20 张图把框画出来看。这半小时能省掉后面至少两天的排查时间。肺结节数据尤其如此标注质量参差不齐肉眼过一遍比任何指标都直接。希望帮到你。本文还有配套的精品资源点击获取