肺结节检测数据集实战:VOC/COCO/YOLO格式转换与YOLO训练全流程 简介本资源为YOLO肺结节目标检测数据集面向医学影像检测方向的算法学习者与课程实践者解决肺结节检测任务中数据获取难、标注格式不统一的问题。数据来自真实场景图像质量高、场景丰富经labelimg精细标注同时提供voc(xml)、coco(json)与yolo(txt)三种格式标签可直接接入YOLO系列模型训练。压缩包共2000个文件以1986个xml标注文件为主另含少量html教程、txt说明与py脚本整体约77.56MB目录按格式分文件夹存放便于按需取用。资源还附赠数据集划分脚本可自行切分训练集、验证集与测试集并配套Windows与Linux环境搭建、训练教程帮助读者快速跑通从环境配置到模型训练的完整流程。目前已有403人学习下载适合需要快速验证肺结节检测方案、补充标注数据或练习格式转换的读者。1. 肺结节检测数据集到手后5000 张图、三种标签格式先跑通哪一步拿到一个标注好的肺结节目标检测数据集最怕的不是模型不收敛而是标签格式对不上、划分方式不一致、训练脚本和目录结构互相打架。这个数据集包含 5000 张肺部 CT 切片图像同时提供 VOC、COCO、YOLO 三种格式标签外加划分脚本和训练教程本质上解决的是从“原始医学影像”到“YOLO 可训练输入”之间的工程断层。适合两类人一是刚接触医学图像检测、想用现成数据跑通 YOLO 全流程的工程师二是手里有私有 CT 数据、想参考一套标准标注与划分方案再迁移到自己场景的从业者。肺结节本身目标小、对比度低、正负样本极不均衡这决定了后面调参和评估不能照搬自然图像那套。2. 三种标签格式到底怎么选VOC、COCO、YOLO 的落地差异2.1 先看清三种格式各自描述了什么VOC 格式的核心是每张图对应一个 XML 文件里面用object记录类别名和xmin/ymin/xmax/ymax绝对像素坐标。它的优点是可读性强标注工具支持广缺点是文件数量翻倍5000 张图就是 5000 个 XML磁盘小文件多批量解析慢。COCO 格式把所有标注压进一个 JSON字段分images、annotations、categories三块bbox是[x, y, width, height]绝对坐标。它适合做多任务统一管理但 JSON 一旦损坏整份标注都读不出来。YOLO 格式每张图一个.txt每行class_id x_center y_center width height全部是相对图像宽高的归一化值范围 0 到 1。YOLO 训练器直接吃这种格式读取最快但人眼几乎没法直接核对。格式单图标注文件坐标类型类别表示典型用途VOCXML绝对像素字符串标注交换、可视化COCO单个 JSON绝对像素数字 id多任务、评测YOLOTXT归一化相对值数字 id直接训练选型上我的习惯是训练用 YOLO 格式归档和跨工具交换保留 VOC做 benchmark 或需要统一评测时转 COCO。这个数据集三种都给省掉了自己写转换器的麻烦但要注意三种格式的类别顺序必须一致否则训练出来的类别名会整体错位。2.2 用脚本把 VOC 转成 YOLO 并核对坐标虽然数据集已经带 YOLO 标签但实际项目中经常遇到只有 VOC 的情况所以转换脚本必须自己能写、能改。下面这段代码读取 VOC XML输出 YOLO txt并顺手做一次越界检查。import os import xml.etree.ElementTree as ET # 类别顺序必须与训练时的 data.yaml 完全一致 CLASSES [nodule] IMG_W, IMG_H 512, 512 # 按实际图像尺寸修改 def voc_to_yolo(xml_path, out_txt_path): tree ET.parse(xml_path) root tree.getroot() lines [] for obj in root.findall(object): cls_name obj.find(name).text.strip() if cls_name not in CLASSES: continue cls_id CLASSES.index(cls_name) bnd obj.find(bndbox) xmin float(bnd.find(xmin).text) ymin float(bnd.find(ymin).text) xmax float(bnd.find(xmax).text) ymax float(bnd.find(ymax).text) # 越界裁剪避免归一化后出现负值或大于1 xmin max(0, min(xmin, IMG_W)) xmax max(0, min(xmax, IMG_W)) ymin max(0, min(ymin, IMG_H)) ymax max(0, min(ymax, IMG_H)) if xmax xmin or ymax ymin: continue # 丢弃无效框 x_c (xmin xmax) / 2.0 / IMG_W y_c (ymin ymax) / 2.0 / IMG_H w (xmax - xmin) / IMG_W h (ymax - ymin) / IMG_H lines.append(f{cls_id} {x_c:.6f} {y_c:.6f} {w:.6f} {h:.6f}) with open(out_txt_path, w) as f: f.write(\n.join(lines))逻辑说明先按类别名映射到数字 id再把绝对坐标裁剪到图像范围内最后做归一化。参数上IMG_W和IMG_H必须和真实图像一致如果数据集里图像尺寸不统一要先统一 resize 再转换否则归一化基准就错了。CLASSES的顺序是血泪经验一旦和data.yaml里的names顺序不同模型会把结节学成背景。2.3 划分脚本怎么用才不泄漏验证集数据集自带划分脚本但很多人直接随机切分就开跑结果同一患者的多次扫描被分到训练和验证两边验证指标虚高。正确做法是按患者 id 分组后再划分。常见做法是读取文件名或额外元信息里的患者编号用GroupShuffleSplit或自己写分组逻辑。import os import random from collections import defaultdict def split_by_patient(img_dir, out_dir, train_ratio0.8, seed42): random.seed(seed) # 假设文件名格式为 patientID_sliceID.png groups defaultdict(list) for name in os.listdir(img_dir): if not name.lower().endswith((.png, .jpg)): continue patient_id name.split(_)[0] groups[patient_id].append(name) patients list(groups.keys()) random.shuffle(patients) n_train int(len(patients) * train_ratio) train_patients set(patients[:n_train]) for split in [train, val]: os.makedirs(os.path.join(out_dir, split), exist_okTrue) for pid, files in groups.items(): split train if pid in train_patients else val for f in files: src os.path.join(img_dir, f) dst os.path.join(out_dir, split, f) os.replace(src, dst) # 移动而非复制避免重复占空间参数说明train_ratio控制训练患者比例seed固定后划分可复现。os.replace会移动文件如果原目录还要保留改成shutil.copy2。分组划分是医学图像检测里最容易被忽略的一步不做的话验证集准确率能虚高十几个点。3. 用 YOLO 训练肺结节目录结构、data.yaml 与关键超参3.1 目录结构不对训练脚本第一行就报错YOLO 系列训练器对目录有固定预期图像和标签分开放且文件名一一对应。推荐结构如下。dataset/ images/ train/ val/ labels/ train/ val/ data.yamlimages/train/xxx.png对应labels/train/xxx.txt文件名主干必须相同。如果标签是 VOC 或 COCO需要先转成 YOLO txt 再按这个结构摆放。常见翻车点是图像和标签放在同一目录或者标签后缀写成.xml却指望训练器自动识别结果训练时提示找不到标签。3.2 data.yaml 里三个字段决定训练是否正常起步path: /data/nodule_dataset train: images/train val: images/val nc: 1 names: [nodule]path是数据集根目录train和val是相对路径。nc是类别数肺结节检测通常为 1。names的顺序必须和转换脚本里的CLASSES完全一致。如果后面要加“钙化”“磨玻璃”等子类改nc和names后必须重新生成标签不能只改 yaml。3.3 肺结节小目标训练的超参怎么设肺结节在 512×512 切片里可能只有十几个像素默认输入尺寸 640 会进一步缩小目标。我的习惯是把imgsz设成 640 或 768同时把mosaic关小或关掉因为 mosaic 拼接会让本就小的结节更小。学习率用lr00.001起步batch根据显存来V100 上 16 到 32 比较稳。patience设 30 到 50医学数据收敛慢早停太激进会欠拟合。yolo detect train \ datadataset/data.yaml \ modelyolov8n.pt \ imgsz768 \ epochs200 \ batch16 \ lr00.001 \ mosaic0.0 \ patience50 \ projectruns/nodule \ nameexp1参数说明model可以用官方预训练权重小目标场景下 nano 或 small 版本先跑通再换大模型。mosaic0.0关闭拼接增强imgsz768保留更多结节细节。如果显存不够优先降batch不要降imgsz否则小目标直接消失。4. 训练肺结节检测最容易踩的坑从 BN 崩溃到混淆矩阵对不上4.1 训练中 BN 崩溃loss 突然变 NaN现象训练到几十轮后 loss 突然变成 NaN终端报 BN 相关错误。原因通常是 batch 太小医学图像又经过强增强单个 batch 内统计量不稳定。解决把batch提到 16 以上或者改用group norm类结构同时检查学习率是否过大lr0从 0.001 降到 0.0005 再试。4.2 混淆矩阵总和和验证集数量对不上现象训练结束看混淆矩阵所有格子加起来不等于验证集标注框总数。原因一般是验证集里有类别 id 超出nc范围或者空标签文件被当成有效样本。解决写脚本统计所有labels/val里的最大 class id确认小于nc再检查是否有图像没有对应标签文件YOLO 会把无标签图当背景但背景图过多会拉偏矩阵。4.3 验证集 mAP 很高实际推理却漏检现象验证集 mAP 0.85拿新数据推理几乎检不出结节。原因通常是划分时同一患者数据泄漏或者验证集和训练集来自同一批扫描参数。解决严格按患者分组划分并留一批完全独立的外部数据做最终测试。医学图像里“同分布”三个字比自然图像脆弱得多。4.4 标签归一化后坐标出现负值现象转换后的 YOLO txt 里出现负数坐标训练不报错但框位置全偏。原因VOC 原始框有部分超出图像边界转换时没做裁剪。解决在转换脚本里加max(0, min(x, W))裁剪并丢弃宽高小于 1 像素的无效框。4.5 图像尺寸不统一导致归一化基准错误现象部分图像是 512×512部分是 1024×1024转换时统一按 512 归一化大图上的框全部缩小一半。原因转换脚本写死了IMG_W/IMG_H。解决用PIL或cv2读取每张图真实尺寸逐图归一化或者先统一 resize 再转换。5. 把 5000 张肺结节数据用透从单折训练到交叉验证与阈值调优数据集只有 5000 张单次划分的验证结果波动会很大。我一般会做 5 折分组交叉验证每折按患者分组训练 5 个模型后取平均 mAP这样得到的结论才敢往论文或报告里写。具体做法是把患者列表随机分成 5 份每次取 1 份做验证、其余做训练重复 5 次。代码上只需在划分脚本外层加一层循环把out_dir按折数命名即可。import os, random from collections import defaultdict def kfold_split(img_dir, base_out, k5, seed42): random.seed(seed) groups defaultdict(list) for name in os.listdir(img_dir): if name.lower().endswith((.png, .jpg)): groups[name.split(_)[0]].append(name) patients list(groups.keys()) random.shuffle(patients) folds [patients[i::k] for i in range(k)] # 轮流分配保证每折患者不重叠 for i in range(k): val_patients set(folds[i]) for split in [train, val]: os.makedirs(os.path.join(base_out, ffold{i}, split), exist_okTrue) for pid, files in groups.items(): split val if pid in val_patients else train for f in files: src os.path.join(img_dir, f) dst os.path.join(base_out, ffold{i}, split, f) os.replace(src, dst)参数说明k5是常用折数数据量再小可以设 10。folds [patients[i::k] for i in range(k)]这种切片方式能保证每折患者数量接近且不重叠。跑完 5 折后把每折的mAP50和mAP50-95记录下来求均值和标准差标准差大于 0.05 说明数据分布不稳需要检查标注质量。另一个容易被忽略的点是推理阈值。YOLO 默认conf0.25但肺结节检测里低置信度框往往对应真实小结节把阈值降到 0.1 能提高召回代价是假阳性变多。我的习惯是先在验证集上画precision-recall曲线选 F1 最大对应的阈值再拿到测试集上确认。如果临床场景更怕漏检就把阈值再往下压 0.05宁可多报几个让医生复核。最后说一个我自己的习惯每次拿到新数据集先花半小时把三种格式各抽 10 张图可视化一遍确认框的位置、类别、图像方向都对再开始训练。这半小时能省掉后面几小时的排查。肺结节检测不是把 YOLO 跑起来就完事数据划分和阈值选择才是决定落地效果的那 20%。希望帮到你。本文还有配套的精品资源点击获取