简介:本资源为YOLO肺结节目标检测数据集,面向医学影像检测方向的学习者与算法开发者,可用于目标检测模型训练、课程实验与科研验证。数据来自真实场景,图像质量高、场景丰富,经labelimg精细标注,同时提供voc、coco和yolo三种格式标签,分别存放于不同文件夹,可直接接入YOLO系列模型训练。压缩包共约2000个文件,以1986个xml标注文件为主,另含html教程、txt列表与py脚本,整体约77.56MB。资源附赠环境搭建、训练教程及数据集划分脚本,可按需生成训练集、验证集与测试集,并输出ImageSets下的划分列表。已有403人学习下载,适合希望快速跑通肺结节检测流程、掌握多格式标签转换与数据划分方法的读者参考使用。
1. 从一批肺结节 CT 说起:5000 张图 + 三格式标签到底解决了什么
拿到「YOLO肺结节目标检测数据集(含5000张图片)+对应voc、coco和yolo三种格式标签+划分脚本+训练教程.rar」这个标题,很多人第一反应是「又一个打包数据集」。但真正做过肺结节检测的人知道,卡住项目的从来不是模型结构,而是数据这一环:DICOM 转 PNG 的窗宽窗位怎么定、结节框该由谁标、标完怎么变成 YOLO 能吃的 txt、训练集验证集怎么切才不泄漏。这批 5000 张图配 VOC、COCO、YOLO 三套标签,本质是把「从原始影像到可训练张量」这条链路提前铺好了,你拿到手就能直接进train.py。它适合两类人:一类是想快速验证 YOLO 在医学小目标上到底行不行的算法工程师,另一类是手里有临床数据但被格式转换和划分脚本拖住的学生和初级研发。下面我按自己复现这类数据集的顺序,把选型、转换、划分、训练和踩坑讲透。
2. 三套标签格式怎么选:VOC、COCO、YOLO 的取舍与转换逻辑
2.1 为什么同一批数据要同时给三种格式
VOC 的 XML 是「一个图一个文件」,字段是xmin/ymin/xmax/ymax的绝对像素坐标,可读性最好,适合人工核对标注质量;COCO 的 JSON 是「整个数据集一个文件」,带images/annotations/categories三段结构,适合做统计、可视化和接入 detectron2 这类框架;YOLO 的 txt 是「一个图一个文件」,每行class cx cy w h全是归一化到 0~1 的相对值,是 ultralytics 系训练脚本直接读的格式。三种格式描述的是同一批框,只是坐标系和存储粒度不同。常见做法是:用 VOC 做人工抽检,用 COCO 做数据分布分析,用 YOLO 直接训练。这样一套数据能覆盖从标注审核到训练的全流程,不用你反复转来转去。
2.2 坐标转换的核心公式与一个可复用的转换脚本
转换的坑几乎全在坐标系上。VOC 是左上角 + 右下角绝对坐标,YOLO 是中心点 + 宽高归一化。公式如下:
# voc_to_yolo.py # 输入:VOC xml 目录、图片目录、类别列表 # 输出:YOLO txt 目录(与图片同名) import os import xml.etree.ElementTree as ET from PIL import Image CLASSES = ["nodule"] # 肺结节通常单类;多类按实际顺序改 def voc_to_yolo(xml_dir, img_dir, out_dir): os.makedirs(out_dir, exist_ok=True) 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) w, h = Image.open(img_path).size lines = [] for obj in root.findall("object"): cls_name = obj.find("name").text if cls_name not in CLASSES: continue cls_id = CLASSES.index(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) # 归一化中心点与宽高 cx = (xmin + xmax) / 2.0 / w cy = (ymin + ymax) / 2.0 / h bw = (xmax - xmin) / w bh = (ymax - ymin) / h # 裁剪到 [0,1],防止越界框导致训练报错 cx, cy = min(max(cx, 0), 1), min(max(cy, 0), 1) bw, bh = min(max(bw, 0), 1), min(max(bh, 0), 1) lines.append(f"{cls_id} {cx:.6f} {cy:.6f} {bw:.6f} {bh:.6f}") with open(os.path.join(out_dir, xml_file.replace(".xml", ".txt")), "w") as f: f.write("\n".join(lines)) if __name__ == "__main__": voc_to_yolo("Annotations", "JPEGImages", "labels")逻辑说明:先读图片真实宽高,再对每个 object 算中心点和宽高并归一化。参数上,CLASSES必须和训练时的data.yaml里names顺序完全一致,否则类别会错位;:.6f保留六位是 ultralytics 的惯例,位数少了对小框精度有影响。注意最后那两行裁剪,医学影像里标注工具偶尔会写出超出图像边界的框,不裁的话 YOLO 训练时归一化坐标大于 1 会直接抛异常。
2.3 COCO 与 YOLO 互转时最容易错的两处
COCO 转 YOLO 时,bbox字段是[x, y, w, h]绝对坐标,不是[xmin, ymin, xmax, ymax],很多人直接套 VOC 公式就翻车。正确做法是cx = (x + w/2) / img_w。另一处是 COCO 的category_id往往从 1 开始,而 YOLO 的 class id 从 0 开始,转换时要减 1,否则训练时会出现「类别 0 没有样本」的玄学现象。这两处我在不同项目里各踩过一次,血泪经验就是转换完一定用脚本统计一遍每类框数量,和原始标注对得上再进训练。
3. 划分脚本怎么写:避免数据泄漏的三种切分策略
3.1 随机切分的陷阱:同一病人的切片不能跨集
肺结节数据有个特殊性:一个病人的 CT 会切成很多张切片,相邻切片长得几乎一样。如果按图片随机切分,同一病人的切片会同时出现在训练集和验证集,验证指标虚高,实际部署时性能断崖式下跌。这就是典型的数据泄漏。正确做法是按病人 ID 分组切分,保证一个病人的所有切片只进一个集合。常见做法是先从文件名或目录里解析出病人 ID,再对 ID 列表做切分,最后把对应图片分配到各集合。
3.2 一个按病人分组、可复现的划分脚本
# split_dataset.py # 按病人 ID 分组切分,避免同一病人跨集 import os import random import shutil from collections import defaultdict SEED = 42 RATIO = (0.8, 0.1, 0.1) # train / val / test def get_patient_id(filename): # 按你的命名规则改,例如 "P0012_slice_034.png" -> "P0012" return filename.split("_")[0] def split(img_dir, label_dir, out_root): random.seed(SEED) patients = defaultdict(list) for f in os.listdir(img_dir): if f.lower().endswith((".png", ".jpg")): patients[get_patient_id(f)].append(f) pids = list(patients.keys()) random.shuffle(pids) n = len(pids) n_train = int(n * RATIO[0]) n_val = int(n * RATIO[1]) splits = { "train": pids[:n_train], "val": pids[n_train:n_train + n_val], "test": pids[n_train + n_val:], } for split_name, pid_list in splits.items(): img_out = os.path.join(out_root, "images", split_name) lbl_out = os.path.join(out_root, "labels", split_name) os.makedirs(img_out, exist_ok=True) os.makedirs(lbl_out, exist_ok=True) for pid in pid_list: for img_file in patients[pid]: shutil.copy(os.path.join(img_dir, img_file), img_out) lbl_file = img_file.rsplit(".", 1)[0] + ".txt" src_lbl = os.path.join(label_dir, lbl_file) if os.path.exists(src_lbl): shutil.copy(src_lbl, lbl_out) if __name__ == "__main__": split("images", "labels", "dataset")逻辑说明:get_patient_id是整套脚本的关键,命名规则不同就改这一处。SEED=42保证每次切分结果一致,方便复现实验。RATIO按 8:1:1 切,样本少时可以调成 7:1.5:1.5。注意脚本只复制有对应标签的图片,如果某张图没标签会被静默跳过,跑完最好统计一下三个集合的图片数之和是否等于原图数,对不上就说明有图缺标签。
3.3 划分完必须做的两项校验
第一项是数量校验:train + val + test的图片数应等于原始图片数,标签数也应一致。第二项是泄漏校验:写个脚本把所有图片的 patient id 提取出来,检查三个集合的 id 集合两两无交集。这两步花不了五分钟,但能省掉后面调参时「为什么验证集 mAP 高得离谱」的困惑。我一般会把校验结果打印成表格存下来,作为实验记录的一部分。
4. 用这批数据跑通 YOLO 训练:环境、配置与关键参数
4.1 环境配置与 data.yaml 的写法
环境用 ultralytics 官方包最省事,pip install ultralytics即可,它会把 torch 等依赖一起装好。数据目录按上一章切分后的结构放,然后写data.yaml:
# data.yaml path: ./dataset train: images/train val: images/val test: images/test nc: 1 names: ["nodule"]path是数据集根目录,train/val/test是相对路径。nc是类别数,肺结节单类就写 1。names的顺序必须和转换脚本里的CLASSES完全一致,这是最容易出错的地方。如果后面要加「钙化」「磨玻璃」等子类,改这里和转换脚本两处即可。
4.2 训练命令与肺结节场景下的参数调整
# 单卡训练,肺结节小目标场景 yolo detect train \ data=data.yaml \ model=yolov8s.pt \ epochs=150 \ imgsz=640 \ batch=16 \ lr0=0.01 \ patience=30 \ mosaic=0.5 \ degrees=10 \ fliplr=0.5 \ project=runs/nodule \ name=exp1参数说明:model选yolov8s是精度和速度的折中,显存紧张换yolov8n,追求精度换yolov8m。imgsz=640是默认值,肺结节在 CT 里往往只占几十像素,可以试imgsz=1024,但显存和耗时都会涨。mosaic=0.5比默认的 1.0 低,因为医学影像拼接过多会引入不真实的解剖结构,反而干扰学习。degrees=10和fliplr=0.5是轻量几何增强,肺结节对翻转和轻微旋转不敏感,可以放心用。patience=30是早停,验证指标 30 轮不涨就停,省时间。
4.3 训练中该盯哪些指标
重点看三个:metrics/mAP50、metrics/mAP50-95和每类的precision/recall。肺结节检测里 recall 通常比 precision 更重要,漏检一个结节的代价远大于多报一个。如果 recall 偏低,先查标注框是不是太小被过滤了,YOLO 默认会忽略宽或高小于 2 像素的框。如果 mAP 一直上不去,用验证集跑一次预测,把预测框和真值框画在同一张图上,肉眼看看是漏检还是定位不准,比盯着数字猜有效得多。
5. 避坑与排查:肺结节 YOLO 训练里最常见的五个翻车点
5.1 现象:训练 loss 正常下降,但 mAP 始终接近 0
原因:data.yaml里的names顺序和标签文件里的 class id 对不上,或者标签根本没被读到。解决:先确认labels/train下的 txt 和images/train下的图片同名,再随便打开一个 txt 看 class id 是不是 0。如果 txt 是空的,说明转换时类别名没匹配上CLASSES,回去检查 VOC 里的name字段。
5.2 现象:训练几轮后 loss 突然变成 nan
原因:学习率过大或某个批次的框坐标异常。医学数据里偶尔有宽高为 0 的退化框,归一化后参与损失计算会炸。解决:在转换脚本里加一行过滤,if bw <= 0 or bh <= 0: continue;同时把lr0从 0.01 降到 0.001 试一次。如果还炸,把batch调小,排除个别坏样本。
5.3 现象:验证集 mAP 很高,但拿新数据预测全是误检
原因:数据泄漏,同一病人的切片跨了训练集和验证集。解决:按第 3 章的脚本重新按病人 ID 切分,切完做一次 id 交集校验。这个坑最隐蔽,因为指标好看,人容易放松警惕,等部署才发现问题。
5.4 现象:小节点(小于 10 像素)几乎全漏检
原因:YOLO 的下采样倍率导致小目标特征在深层特征图上消失。解决:把imgsz提到 1024,或者改用带 P2 检测层的模型配置。另一个办法是把大图裁成小块再训练,让结节在单块里的相对尺寸变大。这三种我都试过,提 imgsz 最省事,裁块效果最好但工程量大。
5.5 现象:训练速度异常慢,GPU 利用率只有 30%
原因:数据加载成了瓶颈,通常是图片太大或磁盘 IO 慢。解决:先把图片统一缩放到训练尺寸再存一份,别让 dataloader 每次现场 resize;workers调到 CPU 核数的 0.75 左右;如果数据在机械盘上,拷到 SSD 再跑。这三步做完,GPU 利用率一般能上到 80% 以上。
6. 进阶技巧:把 5000 张图的价值榨干
数据量固定的时候,提升效果靠的是「让每张图被更充分地学」。我常用的两个手段,一个是难例挖掘,一个是交叉验证。难例挖掘的做法是:第一轮训练完,用模型在训练集上跑预测,把漏检和误检的图挑出来,人工复核标注后加入下一轮训练。肺结节里最难的是贴着血管或胸膜的结节,这类样本往往只占 5%,但贡献了大部分错误,把它们找出来单独加权,mAP 提升比调参明显。交叉验证则是把病人按 5 折切,每折轮流做验证集,最后把 5 个模型的预测做集成。5000 张图按病人切可能只有几百个病人,单次切分的验证集方差很大,5 折能给出更稳的指标估计,也能顺带产出 5 个模型做集成,召回率通常能再涨两三个点。
下面这个脚本用来统计每折的类别分布,确保切分后各折正负样本比例接近:
# cv_stats.py # 统计 K 折切分后每折的框数量,检查分布是否均衡 import os def count_boxes(label_dir): total = 0 for f in os.listdir(label_dir): if f.endswith(".txt"): with open(os.path.join(label_dir, f)) as fp: total += len([l for l in fp if l.strip()]) return total for fold in range(5): n = count_boxes(f"dataset/fold{fold}/labels/val") print(f"fold{fold} val boxes: {n}")逻辑说明:遍历每折验证集的标签目录,统计非空行数即框总数。如果某折框数明显偏离均值,说明该折病人构成特殊,要么重新切,要么在报告指标时注明。参数上没什么可调的,纯粹是个体检脚本。
最后说个习惯:我每次拿到新数据集,第一件事不是跑训练,而是花半小时把标签可视化一遍,随机抽 20 张图把框画上去看。这半小时能提前发现坐标系错位、类别错标、图片和标签不对应这三类问题,比训练跑完再回头查省太多时间。肺结节数据尤其如此,标注质量参差不齐,肉眼过一遍心里才有底。希望帮到你。
本文还有配套的精品资源,点击获取