尧图网络科技YAOTU DIGITAL 获取报价
获取报价
首页 / 资讯中心 / 文章详情

肺结节VOC数据集转YOLO格式训练YOLOv8实践与避坑指南

发布时间:2026/9/24 20:18:06

资讯中心
01
ARTICLE

肺结节VOC数据集转YOLO格式训练YOLOv8实践与避坑指南

肺结节VOC数据集转YOLO格式训练YOLOv8实践与避坑指南
简介一套已标注的肺结节VOC格式数据集专为YOLO目标检测设计共包含7048张JPEG原始图像和3493个XML标注文件标注格式严格按照VOC规范组织可直接接入YOLOv3、YOLOv5、YOLOv8等主流检测网络进行训练。该数据集主要面向需要开展肺结节自动检测、医学影像智能识别等方向的开发者与学生可广泛应用于课程设计、毕业设计以及相关科研课题能够大幅节省从零采集图像和反复标注的人工成本。资源包整体约129.35MB压缩包内图像与标注文件按编号一一对应目录层次清楚方便使用者快速划分训练集、验证集和测试集。目前已有1258人学习下载是验证目标检测算法性能时值得参考的标准数据来源。原始图像与标注内容经过基本筛选训练时可直接调用让使用者将更多精力放在网络结构设计与性能调优上适合计算机视觉初学者快速上手也适合有经验的研究者用于算法对比实验。1. 肺结节目标检测最耗时间的是数据7048张VOC标注图能解决什么搞目标检测的人都有共识训练能跑通数据才是真正耗时间的环节。肺结节检测这类医学影像任务从采集、筛选到打标注往往占掉项目大半工期。这份资源是一套标注好的肺结节VOC数据集7048张图像配上对应XML标注文件解压后就能直接喂给YOLO训练流程。VOC格式几乎所有检测框架都能直接读配合YOLO任何一代都能用。标注文件里已经包含每个结节的位置坐标和类别信息不用再拿LabelImg从头标一遍省下来的时间可以全花在模型调参和结果分析上。适合两类人想快速跑通YOLO训练验证流程、完成课设或毕设的学生以及拿肺结节场景做算法改进的从业者。下面从数据组织、格式转换、训练配置到踩坑排查完整拆一遍。2. 解剖VOC标注结构XML字段、统计脚本与数据完整性校验拿到任何一份标注数据集第一件事永远是先弄明白文件怎么组织、标注格式长什么样而不是急着开训。这套肺结节数据集的标注文件是标准VOC格式的XML每个XML对应一张图像文件名相同、扩展名不同。正文里大量出现的1059.xml、1058.xml、2753.xml这类编号文件就是和图像一一对应的标注文件。先把XML字段逐项拆开再用脚本把数据集整体分布摸清楚。2.1 XML字段逐项拆解与标注坐标的含义标准VOC格式的XML结构如下我以其中一个标注文件为例annotation folderJPEGImages/folder filename1059.jpg/filename size width512/width height512/height depth3/depth /size object namenodule/name bndbox xmin214/xmin ymin198/ymin xmax256/xmax ymax227/ymax /bndbox /object /annotation这段XML表达的信息很明确图像文件名是1059.jpg尺寸512×512depth为3表示RGB三通道。每个object标签就是一个肺结节目标name是类别名bndbox里四个坐标定义了目标边界框的左上角和右下角。部分VOC标注还会带truncated、difficult这些字段分别表示目标被截断和难例样本标记肺结节数据集里通常用不到转换时可以忽略。重点理解坐标系VOC的坐标是绝对像素坐标原点在图像左上角x轴向右、y轴向下。xmin和ymin是框左上角坐标xmax和ymax是右下角坐标单位都是像素。这种坐标直观但不同CT切片尺寸不一致直接拿绝对坐标训练会引入尺度偏差所以转YOLO格式时必须归一化。还需要注意一个细节XML里可能不止一个object同一张图像出现多个肺结节很常见尤其是相邻CT层面可能同时切到多个结节。读取时要用findall而不是find否则只拿到第一个目标后面的全部丢失这会直接导致训练样本数变少、mAP虚高。2.2 统计脚本类别分布、目标数量与图像尺寸光知道格式不够得知道这份数据集整体的数量关系。写一个统计脚本把类别分布、每张图目标数、图像尺寸范围一次性打印出来import os import xml.etree.ElementTree as ET from collections import Counter xml_dir Annotations class_stats Counter() box_counts [] img_sizes [] for fname in os.listdir(xml_dir): if not fname.endswith(.xml): continue path os.path.join(xml_dir, fname) tree ET.parse(path) root tree.getroot() # 读取图像尺寸后续归一化要用 size root.find(size) w int(size.find(width).text) h int(size.find(height).text) img_sizes.append((w, h)) # 统计每个文件里的目标个数 objs root.findall(object) box_counts.append(len(objs)) # 统计类别名注意这里才是真实标签 for obj in objs: class_stats[obj.find(name).text] 1 print(图像总数:, len(img_sizes)) print(类别分布:, dict(class_stats)) print(平均每张图目标数: %.2f % (sum(box_counts) / len(box_counts))) print(尺寸范围: %dx%d ~ %dx%d % ( min(w for w, h in img_sizes), min(h for w, h in img_sizes), max(w for w, h in img_sizes), max(h for w, h in img_sizes)))逻辑很直白遍历Annotations目录下所有XML用ElementTree解析出尺寸、目标列表和类别名分别累计。最后打印四项关键信息图像总数、类别分布、每张平均目标数、尺寸范围。参数说明xml_dir改成你解压后实际的标注目录。class_stats用Counter统计好处是不用手动判断类别是否首次出现box_counts记录每张图的目标数平均值能判断数据集是稀疏标注还是密集标注。肺结节数据集通常每张图0到几个目标如果平均超过5要留意是不是把血管断面、钙化点也标进去了。类别分布是最值得看的一项。如果输出里出现多个类别名比如nodule、nodule_benign说明数据集有子类划分如果只有一个类别那后面data.yaml里nc就写1。这一步直接决定配置文件怎么写跳过了必然踩坑。2.3 数据完整性校验标注与图像的一一对应统计完分布还要做一次完整性校验。7048张图配7048个XML是理想情况但实际下载的数据集经常出现某些XML没有对应图像或者反过来。这种问题要到训练时才会暴露报错还不直观不如提前校验import os annotation_dir Annotations image_dir JPEGImages xml_names {os.path.splitext(f)[0] for f in os.listdir(annotation_dir) if f.endswith(.xml)} img_names {os.path.splitext(f)[0] for f in os.listdir(image_dir) if f.lower().endswith((.jpg, .jpeg, .png, .bmp))} only_xml xml_names - img_names only_img img_names - xml_names print(有标注无图像:, len(only_xml)) print(有图像无标注:, len(only_img)) if only_xml: print(示例:, list(only_xml)[:5]) if only_img: print(示例:, list(only_img)[:5])这段代码用集合差集找缺失匹配。os.path.splitext取文件名主干忽略扩展名差异图像后缀判断覆盖jpg、jpeg、png、bmp常见格式。输出里有标注无图像数量应该接近0如果出现成百上千个说明压缩包里图像和标注不是同一批得回到下载页确认是否漏了图像分卷。处理这类数据集我的习惯是先跑2.2和2.3两个脚本把输出存成文本后面训练出任何问题都能对照基线排查。数据集统计信息就是后悔药训练翻车时翻出来看能省掉大量猜原因的功夫。这套资源和配套的转换校验脚本都在下载包里解压后Annotations和JPEGImages两个目录先别动等校验通过再开始转换。3. VOC转YOLO格式归一化坐标换算与转换脚本YOLO系列训练不吃VOC的XML它要的是每个标注文件对应一个同名txt每行一个目标格式是类别id 中心点x 中心点y 宽度 高度五个值全部归一化到0到1之间。这一步不做训练直接报错或者类别全乱。这一章把换算原理和完整脚本都铺开。3.1 VOC坐标系与YOLO归一化坐标的换算原理两种格式的核心差别在坐标参考系。VOC用绝对像素YOLO用相对图像尺寸的归一化值。换算公式就这么几行x_center (xmin xmax) / 2 / img_w y_center (ymin ymax) / 2 / img_h width (xmax - xmin) / img_w height (ymax - ymin) / img_himg_w和img_h就是XML里size节点读出来的宽高。这里有个常见误用有人直接把xmin除以img_w当成x_center结果框全部偏到左上角训练出来的模型检测位置整体偏移。中心点坐标必须先算平均值再归一化顺序不能颠倒。归一化的意义在于尺度无关。训练时YOLO会对输入图像做resize如果标注是绝对像素resize之后框就错位了归一化之后无论输入图缩放到640还是1024标注的相对位置不变。这也是YOLO训练自己数据集时对标注格式要求严格的原因。转换时还要处理边界情况标注坐标可能略微越界比如xmax写成513而图宽512。这在LabelImg手动标注时很常见鼠标拖过头了。越界坐标会导致归一化值大于1训练时产生nan损失或者大量警告。转换脚本里必须做钳制和过滤这就是为什么不能复制网上那种七八行的精简版脚本。3.2 转换脚本从XML到TXT的完整实现完整转换脚本如下包含越界钳制、无效框过滤和类别断言import os import xml.etree.ElementTree as ET def convert_voc_to_yolo(xml_path, class_names, output_path): tree ET.parse(xml_path) root tree.getroot() size root.find(size) img_w int(size.find(width).text) img_h int(size.find(height).text) lines [] for obj in root.findall(object): name obj.find(name).text if name not in class_names: print(f[跳过] 未知类别 {name} 在 {xml_path}) continue cls_id class_names.index(name) box obj.find(bndbox) xmin float(box.find(xmin).text) ymin float(box.find(ymin).text) xmax float(box.find(xmax).text) ymax float(box.find(ymax).text) # 越界钳制把坐标限制在图像范围内 xmin max(0, min(xmin, img_w - 1)) xmax max(0, min(xmax, img_w - 1)) ymin max(0, min(ymin, img_h - 1)) ymax max(0, min(ymax, img_h - 1)) if xmax xmin or ymax ymin: print(f[警告] 无效框 {xml_path}: {name}) continue # 归一化换算公式来自 3.1 x_center (xmin xmax) / 2.0 / img_w y_center (ymin ymax) / 2.0 / img_h w (xmax - xmin) / img_w h (ymax - ymin) / img_h # 浮点误差二次钳制保证范围在 [0,1] x_center min(max(x_center, 0.0), 1.0) y_center min(max(y_center, 0.0), 1.0) w min(max(w, 0.0), 1.0) h min(max(h, 0.0), 1.0) lines.append(f{cls_id} {x_center:.6f} {y_center:.6f} {w:.6f} {h:.6f}) with open(output_path, w, encodingutf-8) as f: f.write(\n.join(lines)) if __name__ __main__: XML_DIR Annotations LABEL_DIR labels class_names [nodule] # 以 2.2 脚本统计结果为准顺序决定类别id os.makedirs(LABEL_DIR, exist_okTrue) for fname in os.listdir(XML_DIR): if not fname.endswith(.xml): continue stem fname[:-4] convert_voc_to_yolo( os.path.join(XML_DIR, fname), class_names, os.path.join(LABEL_DIR, stem .txt) ) print(转换完成)脚本逻辑分四步解析XML取尺寸和目标列表对每个目标做类别映射、坐标读取钳制越界坐标并过滤无效框按YOLO格式写入txt。class_names是整个脚本最关键的一行它的顺序就是类别id的来源必须和XML里的name完全一致。参数说明class_names建议用2.2统计脚本打印出来的类别名去重列表不要靠猜。如果数据集有多个类别比如[nodule, nodule_calcification]顺序一旦定下来后面data.yaml里的names顺序必须一模一样否则训练时类别全部错位。输出保留6位小数足够YOLO训练用的就是浮点字符串精度过高没有意义。3.3 数据集划分与YOLO目录组织转换完txt下一步是划分数据集并组织成YOLO约定目录。YOLOv8默认从data.yaml里找images和labels两个根目录同名图像和标注文件各自放在对应的train、val子目录下。脚本如下import os import random import shutil random.seed(42) IMAGE_DIR JPEGImages LABEL_DIR labels OUTPUT_DIR datasets/lung_nodule RATIO (0.8, 0.1, 0.1) # train / val / test all_stems [os.path.splitext(f)[0] for f in os.listdir(LABEL_DIR) if f.endswith(.txt)] all_stems.sort() random.shuffle(all_stems) n_train int(len(all_stems) * RATIO[0]) n_val int(len(all_stems) * RATIO[1]) splits { train: all_stems[:n_train], val: all_stems[n_train:n_train n_val], test: all_stems[n_train n_val:], } for split_name, stems in splits.items(): img_out os.path.join(OUTPUT_DIR, images, split_name) lbl_out os.path.join(OUTPUT_DIR, labels, split_name) os.makedirs(img_out, exist_okTrue) os.makedirs(lbl_out, exist_okTrue) for stem in stems: # 图像扩展名不确定逐个尝试 for ext in [.jpg, .jpeg, .png, .bmp]: src_img os.path.join(IMAGE_DIR, stem ext) if os.path.exists(src_img): shutil.copy(src_img, os.path.join(img_out, stem ext)) break shutil.copy(os.path.join(LABEL_DIR, stem .txt), os.path.join(lbl_out, stem .txt)) print(train/val/test:, len(splits[train]), len(splits[val]), len(splits[test]))random.seed(42)保证每次划分结果一致方便复现实验。RATIO用0.8、0.1、0.1的经典比例肺结节数据集如果总量够大甚至可以往0.85、0.15、0走测试集用单独的CT序列来验证更有说服力。注意划分前先shuffle否则按文件名排序后前80%可能全是同一个扫描序列的图像训练集和验证集数据分布严重重叠。最终目录结构长这样datasets/lung_nodule/ ├── data.yaml ├── images/ │ ├── train/ # 5638 张 │ ├── val/ # 704 张 │ └── test/ # 705 张 └── labels/ ├── train/ ├── val/ └── test/4. 用YOLOv8训练肺结节模型环境配置、超参数与损失曲线判读数据和格式都准备好了进入训练环节。这里以YOLOv8为例因为它的ultralytics包把训练、验证、推理封装成了命令行对课程设计和毕设足够友好换成YOLOv5或YOLOv11流程也差不多只是参数名略有差异。4.1 环境安装与运行设备确认先装ultralytics这个包它会自动带上依赖的PyTorch。直接用pip安装是最省事的方式pip install -U ultralytics如果你有NVIDIA显卡并且想用GPU训练建议先单独装CUDA版PyTorch再装ultralytics避免pip默认装上CPU版。装完先验证一下PyTorch能不能看到显卡python -c import torch; print(torch.cuda.is_available())输出True说明GPU可用False就是CPU模式。7048张图、640输入尺寸下CPU训练一个epoch可能要十几分钟GPU只要一两分钟。没有独显也不用慌肺结节数据集单类别、目标稀疏用yolov8n这种轻量模型在CPU上跑100个epoch也就是几个小时的事课程设计完全等得起。4.2 data.yaml与关键超参数进入训练前写data.yaml指向刚才划分好的目录。这是整个配置里最容易出错的文件path: datasets/lung_nodule train: images/train val: images/val test: images/test nc: 1 names: 0: nodulepath是数据集根目录train和val相对于根目录写。nc是类别数和names列表长度必须一致。names的0号索引对应txt里类别id为0的目标顺序一错全错。如果你用2.2统计出多个类别这里按转换脚本class_names的顺序列全。训练命令和关键参数如下yolo detect train \ datadatasets/lung_nodule/data.yaml \ modelyolov8n.pt \ epochs100 \ imgsz640 \ batch16 \ patience20 \ lr00.01参数取舍看这个表参数默认值肺结节场景建议说明modelyolov8n.ptyolov8n或yolov8s显存小选n追求精度选sepochs100100~200配合patience早停不用硬跑满imgsz640640或1024小结节多就上1024batch168~32按显存调整爆显存就减半patience5020~30验证集指标连续不涨就停lr00.010.001~0.01小数据集建议从0.01往下试model选择是典型的取舍yolov8n最快但小目标能力弱肺结节很多只有二三十像素建议至少用yolov8s。imgsz是影响小目标检测最直接的参数从640提到1024小结节的感受野占比翻倍mAP往往提升两到三个点代价是显存占用翻倍。batch先按16试显存不够报OOM就降到8。4.3 训练监控、损失曲线与置信度门限训练启动后在runs/detect/train/目录下会生成一个results.png包含训练损失、验证损失、精度、召回率、mAP的曲线图。YOLOv8的损失函数是三个分量的加权和box_loss是边界框回归损失默认用CIoUcls_loss是分类损失dfl_loss是分布焦点损失。判读曲线时最怕把过拟合当成正常收敛。肺结节数据集如果每张图目标少、背景占比高训练损失会降得很快但验证损失可能在30个epoch后就开始反弹。看到train/loss持续下降、val/loss掉头向上的剪刀差就是典型的过拟合信号这时候加大数据增强、提高权重衰减或者直接提前停止把best.pt拿出来用。验证和推理时还要理解置信度门限的作用yolo detect val modelruns/detect/train/weights/best.pt \ datadatasets/lung_nodule/data.yaml yolo detect predict modelruns/detect/train/weights/best.pt \ sourcedatasets/lung_nodule/images/val \ conf0.25conf默认0.25意思是置信度低于0.25的预测框直接丢弃。医学场景里漏诊比误报严重可以把conf调到0.1到0.15宁可多框几个假阳性也不要漏掉小结节。val命令输出的mAP50和mAP50-95是评估硬指标mAP50对框的定位精度要求宽松mAP50-95则要框和真值高度重合肺结节这种小目标两个指标差距很大很正常前者0.8、后者0.4都是常见水平。5. 标注数据集避坑指南五个最容易翻车的现场与排查方法标注好的数据集不等于拿来就能训这几类问题我在处理VOC和YOLO数据集时都踩过每一条都是现象→原因→解决的完整链路建议收藏对照。5.1 训练时报FileNotFoundError标注和图像对不上现象训练第一个epoch就中断报FileNotFoundError提示找不到某张jpg。检查目录文件都在但文件名对不上。原因标注文件名和图像文件名不一致最常见的是扩展名差异——XML里写的是1059.jpg实际图像文件叫1059.png。用户下载后手工改扩展名没改全就开训。解决先跑2.3的校验脚本把有标注无图像和有图像无标注两个集合列出来。扩展名不一致的用批量重命名统一只有标注没有图像的直接把对应XML移出Annotations目录避免转换出孤立txt文件。5.2 类别标签全被跳过XML里的大小写与别名现象转换脚本运行时未知类别刷屏输出的txt全是空文件或者只有少数文件有内容。训练起来mAP一直是0。原因XML里的name字段和脚本里class_names列表不匹配。肺结节数据集里常见的有三种情况大小写不一致Nodule对nodule、带空格nodule 、带层间编号nodule_001。任何一处不一致整个目标就被跳过。解决不要靠肉眼猜类别先用脚本打印全部去重值import os import xml.etree.ElementTree as ET names set() for f in os.listdir(Annotations): if not f.endswith(.xml): continue root ET.parse(os.path.join(Annotations, f)).getroot() for obj in root.findall(object): names.add(obj.find(name).text.strip()) print(names)看到真实类别后把class_names和data.yaml里的names都按这个集合来写保持完全一致。5.3 损失直接变成nan越界框与无效框的清理现象训练到几个batch后loss变成nan或者loss曲线剧烈震荡下不来。原因标注框越界比如xmax大于图像宽度、ymin为负数归一化后出现大于1或小于0的坐标反向传播时梯度爆炸另一种是labels目录里出现0字节空txt对应一张没有任何目标的图像数据加载器拿到空标注行为异常。解决转换脚本里必须做越界钳制和无效框过滤我在3.2的代码里已经处理了。训练前检查空文件find datasets/lung_nodule/labels -name *.txt -size 0 | head有输出就把对应的空txt和图像一起从数据集里清掉。肺结节数据有些层面确实没有结节如果不想丢这些负样本要按YOLO的负样本规则单独处理不要直接丢空txt进去。5.4 小目标漏检严重肺结节只有几十个像素怎么办现象mAP50看着还行mAP50-95偏低推理时大结节框得住小结节漏一片。原因肺结节尤其是早期磨玻璃结节在原始CT切片上可能只有十几到几十像素。YOLOv8默认下采样32倍640输入下一个小结节落到特征图上只剩不到一个像素特征被背景稀释漏检几乎是必然。解决最直接的手段是把imgsz提到1024显存不够就换yolov8n配合1024。其次训练数据增强里的mosaic会进一步缩小目标对医学小目标可以关掉mosaic或者把增强比例压到0.5以下。再往上走就是改网络结构了比如加P2小目标检测头这是yolo改进方向常见的入手点适合毕设想创新点的同学。5.5 转换后框位置全偏LabelImg导出与坐标归一化陷阱现象训练完跑推理检测框位置整体偏移或者框的尺寸和实际目标对不上但训练损失正常。原因有人用LabelImg画框后直接导出YOLO格式txt绕过了VOC转换。LabelImg导出YOLO格式时用的是预先加载的classes.txt顺序如果你后来改了类别列表或加载的预定义类别和data.yaml里names顺序不一致类别id就全部错位。还有一种情况是误用了LabelImg绝对坐标导出选项出来的框是像素值却当归一化值用。解决统一走一条链路——用VOC XML配3.2转换脚本生成txt不要混用两套工具。如果已经用了LabelImg导出转换后必须抽样可视化核对下一章的可视化脚本正好干这个发现框偏移就重新导出并修正类别顺序。6. 训练前最后一道工序可视化验证与小样本试跑数据转换完、配置写好了别急着全量训练。我每次都会先做两件事把标注框画回图上肉眼检查再用小样本跑十几个epoch验证收敛性。这两步加起来不超过半小时能拦下前面章节一大半的翻车问题。6.1 标注可视化脚本把XML框画回图上从训练集里随机抽20张图把XML里的框画上去存成新图逐张翻看import cv2 import xml.etree.ElementTree as ET def draw_annotation(img_path, xml_path, out_path): img cv2.imread(img_path) tree ET.parse(xml_path) root tree.getroot() for obj in root.findall(object): name obj.find(name).text box obj.find(bndbox) xmin int(float(box.find(xmin).text)) ymin int(float(box.find(ymin).text)) xmax int(float(box.find(xmax).text)) ymax int(float(box.find(ymax).text)) cv2.rectangle(img, (xmin, ymin), (xmax, ymax), (0, 255, 0), 2) cv2.putText(img, name, (xmin, max(0, ymin - 6)), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 255, 0), 2) cv2.imwrite(out_path, img) # 逐个抽查按需替换文件名 draw_annotation(JPEGImages/1059.jpg, Annotations/1059.xml, check_1059.jpg)重点看三类问题框有没有明显超出图像范围、框里是否真的包住结节目标、类别名是否和病灶类型对应。这一步直接把5.2和5.5的隐患暴露出来比看任何日志都直观。6.2 小样本试跑用十分之一数据验证收敛性可视化没问题后临时建一个mini数据集各取训练集和验证集的前100张跑15个epochyolo detect train \ datadatasets/lung_nodule_mini/data.yaml \ modelyolov8n.pt \ epochs15 \ imgsz640 \ batch8只看一件事train/loss是否单调下降、val/loss前几个epoch是否跟着下降。如果15个epoch下来损失纹丝不动大概率是标注格式或者类别配置有问题这时候停下来排查成本极低等全量训练跑了一天再发现浪费就是几十个小时。小样本试跑通过后删掉mini目录正式命令把model参数换成runs/detect/train/weights/last.pt做迁移继续跑还能省掉前几十个epoch的冷启动时间。从那以后我每次拿到标注数据无论来源多可靠都强制先走一遍校验、统计、可视化、小样本试跑这四道工序再碰全量训练。这四步帮我排查掉了至少一半训练为什么这么怪的问题也让每一份数据集在进YOLO之前都是可信的。希望帮到你。本文还有配套的精品资源点击获取
02
RELATED NEWS

相关资讯

更多网站建设与数字化升级内容

03
WHY YAOTU

想打造同款高转化官网?

懂行业、懂生意,从建站到增长一站式陪跑

◈

场景化定制

不做模板站,围绕你的业务场景量身设计,小众不撞款。

◐

营销型架构

以转化目标组织内容与路径,让官网真正带来询盘。

▲

全周期服务

设计、开发、运营、运维一体,上线只是开始。

免费获取你的建站方案

留下需求,专属顾问 24 小时内为你输出方案建议。