资讯详情

肺结节VOC数据集转YOLO格式与YOLOv8训练全流程实践

📅 2026/9/12 23:00:36 | 华诺云谱 👁 阅读
肺结节VOC数据集转YOLO格式与YOLOv8训练全流程实践
简介YOLO肺结节检测数据集以VOC标准格式整理面向目标检测学习者、医学影像入门研究者以及需要完成相关课题的本专科学生。资源包共约2000个文件主要包含jpg图像与对应的xml标注文件xml记录了肺结节目标框的坐标与类别信息压缩包整体大小约129.35MB。目前已有1257人学习下载适合计算机、电子信息工程、数学等专业的学生用于课程设计、期末大作业和毕业设计。数据已按VOC标准组织目录图像与标注文件一一对应使用者无需手动标注或转换格式配置好YOLO环境后即可划分训练集与验证集开展训练。数据集内肺结节样本尺度多样便于测试YOLOv5、YOLOv8等不同版本模型的检测效果同时也可配合数据增强、mAP评估等操作使用能够帮助快速完成实验对比与论文结果分析是目标检测入门和医学影像识别实战的高质量标注数据集。1. 拿到7048张肺结节已标注图像先用YOLO跑通再谈优化一份「7048张图像对应已标注文件」的肺结节VOC数据集到手时最常见的想法是图像有了、标注有了是不是直接扔给YOLO就能训练其实不是。VOC格式的XML标注记录的是框的左上角和右下角像素坐标而YOLO训练读取的txt标签需要的是归一化后的中心点坐标和宽高。两者之间隔着一层坐标转换这个环节的出错率往往决定后续训练是省心还是返工。这份数据集的价值在于它的体量7048张图像对肺结节检测来说不算大但足够验证训练流程、判断标注质量也能训练出一个可用的基线模型。标注好的数据可以直接使用意味着核心工作不是画框而是把VOC格式翻成YOLO能消费的格式再考虑训练参数怎么设、损失函数怎么判读、需不需要GPU、显存多大够用这些落地的具体问题。这篇文章我从VOC标注结构、XML解析、坐标转换脚本写起落到YOLOv8的训练命令和参数含义最后用mAP曲线和可视化回检来验证这批标注是否真的合格。目标是让这份数据一次通过验收而不是训练到一半才发现标注文件里有坏数据。2. 肺结节VOC数据集目录结构与XML标注解析解压rar之后第一件事不是打开文件夹逐张看图而是先把目录结构扫一遍。VOC格式有约定俗成的布局JPEGImages和Annotations里的文件按同名一一对应文件名就是两者的关联键。2.1 JPEGImages与Annotations的同名对应关系一份标准的Pascal VOC目录长这样lung_nodule_voc/ ├── JPEGImages/ # 7048张原图jpg或png │ ├── LN_00001.jpg │ ├── LN_00002.jpg │ └── ... ├── Annotations/ # 与图像同名的xml标注 │ ├── LN_00001.xml │ ├── LN_00002.xml │ └── ... └── ImageSets/ └── Main/ ├── train.txt ├── val.txt └── test.txtImageSets/Main下的txt文件记录的是不带扩展名的文件名列表用来划分训练、验证和测试集。有些数据集会省略这个目录这时需要自己按比例切分。注意一个常见的坑JPEGImages里是jpg但XML的filename字段可能写的是jpeg或png解析时不要拿filename字段直接拼路径而应该以实际存在的文件为准。XML的内部结构相当规整每个标注文件包含图像尺寸、文件名和若干个object节点。object节点里是类别名和bndbox边界框字段定义如下字段XPath路径含义典型取值filename/annotation/filename图像文件名LN_00001.jpgwidth/annotation/size/width图像宽度像素512height/annotation/size/height图像高度像素512depth/annotation/size/depth图像通道数3name/annotation/object/name目标类别nodulexmin/annotation/object/bndbox/xmin框左边界像素坐标213ymin/annotation/object/bndbox/ymin框上边界像素坐标118xmax/annotation/object/bndbox/xmax框右边界像素坐标302ymax/annotation/object/bndbox/ymax框下边界像素坐标197肺结节数据集的标注框通常是单个类别nodule但也存在同一张图多个结节的情况也就是一个XML里挂多个object。转换脚本如果写成了只读第一个object会直接丢掉一部分正样本这是标注数据使用中最高频的错误。2.2 用脚本读取XML先看清标注内容再动手批量检查XML内容时没必要用标注工具打开7000个文件一个几十行的Python脚本就够了。下面这个函数读取单个XML并返回结构化字典import xml.etree.ElementTree as ET def parse_voc_xml(xml_path): tree ET.parse(xml_path) root tree.getroot() image_info { filename: root.find(filename).text, width: int(root.find(size/width).text), height: int(root.find(size/height).text), depth: int(root.find(size/depth).text) if root.find(size/depth) is not None else 3, objects: [] } for obj in root.findall(object): bndbox obj.find(bndbox) box { name: obj.find(name).text, xmin: int(float(bndbox.find(xmin).text)), ymin: int(float(bndbox.find(ymin).text)), xmax: int(float(bndbox.find(xmax).text)), ymax: int(float(bndbox.find(ymax).text)) } image_info[objects].append(box) return image_info这段代码的关键有三处root.find(size/width)用XPath路径直接取嵌套节点省去逐层find的冗余写法bndbox下的坐标先用float()再转int()兼容标注工具写成小数坐标的情况root.findall(object)用findall而不是find保证多个结节时全部读到。解析之后建议先打印三条信息XML里的filename和实际文件是否存在、每张图的object数量分布、所有框的宽度和高度分布。最后这条尤其重要肺结节的框通常很小如果发现大量框的边长超过200像素先怀疑是标注把整片肺实质圈了进去这种数据直接训练会带偏模型。2.3 标注质量体检越界框、坐标反序与类别统计VOC标注文件看起来规整但手工标注或自动化标注导出的数据里越界框和坐标反序并不少见。越界指的是xmax大于图像宽度或ymax大于图像高度坐标反序指的是xmin大于xmax。前者常出现在增强后的图像重新导出时后者常出现在标注工具版本切换时。批量体检可以用一段检查脚本import glob issues [] for xml_file in glob.glob(Annotations/*.xml): info parse_voc_xml(xml_file) w, h info[width], info[height] for obj in info[objects]: if obj[xmin] obj[xmax] or obj[ymin] obj[ymax]: issues.append(f坐标反序: {xml_file} - {obj}) if obj[xmax] w or obj[ymax] h or obj[xmin] 0 or obj[ymin] 0: issues.append(f越界: {xml_file} - {obj}) print(f发现问题 {len(issues)} 条) for line in issues[:20]: print(line)越界和反序的框在VOC格式下不会报错但转成YOLO格式后会出现宽高为负或大于1的值训练时loss直接跳成NaN。体检这一步省不掉它把问题暴露在训练之前而不是让它在第20个epoch才显现。再统计一下类别分布单类数据集看框总数和每张图的框数均值如果所有图都是单个框说明数据来源比较单一训练时要注意过拟合风险。做完这些检查XML这边就算验收通过了可以进入坐标转换环节。3. VOC坐标转YOLO格式归一化txt标签的完整脚本VOC和YOLO的标注差异不只是文件格式本质是坐标系定义不同。VOC记录的是像素坐标系下的绝对坐标YOLO记录的是归一化坐标系下的相对坐标。转换不是简单地把数字抄一遍而是要理解两种坐标系的边界条件。3.1 像素坐标到归一化坐标的换算逻辑VOC的bndbox给出四个值xmin、ymin、xmax、ymax分别是框的左上角和右下角的像素坐标。YOLO的txt标签每行是五个值class_id、x_center、y_center、width、height其中中心点坐标和宽高都以图像宽高做归一化取值区间是0到1。换算公式如下x_center (xmin xmax) / 2 / image_width y_center (ymin ymax) / 2 / image_height box_width (xmax - xmin) / image_width box_height (ymax - ymin) / image_height注意除数是图像宽还是高x系列坐标除以widthy系列坐标除以height。写反了的结果是标签整体错位模型训练时loss能正常下降但推理出来的框位置全偏这类错误最隐蔽因为训练过程看起来一切正常。另一个细节是归一化后要不要截断到[0,1]区间。如果前面体检没做过这里可以用clip把越界值拉回来但要做记录。截断本身是兜底手段不代表标注没问题截断掉的比例超过千分之一就该回到原始XML查原因。3.2 xml_to_yolo.py一份可直接落地的转换脚本下面这个脚本读取Annotations目录下的所有XML输出与图像同名的txt到指定目录import os import glob import xml.etree.ElementTree as ET CLASSES [nodule] # 类别列表索引就是class_id def voc_to_yolo(xml_path, output_dir): tree ET.parse(xml_path) root tree.getroot() img_w int(root.find(size/width).text) img_h int(root.find(size/height).text) base_name os.path.splitext(os.path.basename(xml_path))[0] txt_path os.path.join(output_dir, base_name .txt) lines [] for obj in root.findall(object): class_name obj.find(name).text # 类别不在列表里直接跳过并打印警告 if class_name not in CLASSES: print(f未知类别 {class_name} 出现在 {xml_path}) continue class_id CLASSES.index(class_name) xmin float(obj.find(bndbox/xmin).text) ymin float(obj.find(bndbox/ymin).text) xmax float(obj.find(bndbox/xmax).text) ymax float(obj.find(bndbox/ymax).text) x_center (xmin xmax) / 2.0 / img_w y_center (ymin ymax) / 2.0 / img_h box_w (xmax - xmin) / img_w box_h (ymax - ymin) / img_h # 兜底截断避免越界值进入训练 x_center max(0.0, min(1.0, x_center)) y_center max(0.0, min(1.0, y_center)) box_w max(0.0, min(1.0, box_w)) box_h max(0.0, min(1.0, box_h)) # YOLO格式保留6位小数足够表达亚像素精度 lines.append(f{class_id} {x_center:.6f} {y_center:.6f} {box_w:.6f} {box_h:.6f}) with open(txt_path, w) as f: f.write(\n.join(lines) \n) os.makedirs(labels_all, exist_okTrue) for xml_file in glob.glob(Annotations/*.xml): voc_to_yolo(xml_file, labels_all)脚本里两个细节值得说明坐标从XML读出时统一走float()因为VOC坐标可能是整数也可能是小数直接int会损失精度输出格式化保留6位小数对512x512的CT图像来说归一化坐标的6位小数对应约0.0008个像素的误差完全够用。CLASSES列表的顺序就是训练时class_id的顺序多类别时必须保证和后续data.yaml里的names一致。3.3 训练集与验证集划分及data.yaml生成转换完标签后需要把7048张图按比例切分成训练集和验证集。医学影像数据不建议按文件名哈希切分同一患者的多个切片可能分布在相邻编号随机切分更容易保证分布一致性。按85比15切分验证集大约1057张对评估模型已经足够import os import random import shutil random.seed(42) base_names [ os.path.splitext(f)[0] for f in os.listdir(Annotations) if f.endswith(.xml) ] random.shuffle(base_names) val_count int(len(base_names) * 0.15) def organize(image_src_dir, label_src_dir, target_dir, names): img_dir os.path.join(target_dir, images) lbl_dir os.path.join(target_dir, labels) os.makedirs(img_dir, exist_okTrue) os.makedirs(lbl_dir, exist_okTrue) for name in names: shutil.copy(os.path.join(image_src_dir, name .jpg), img_dir) shutil.copy(os.path.join(label_src_dir, name .txt), lbl_dir) organize(JPEGImages, labels_all, lung_yolo/train, base_names[val_count:]) organize(JPEGImages, labels_all, lung_yolo/val, base_names[:val_count])切分时random.shuffle之前必须固定random.seed(42)否则每次运行划分结果不同实验之间没法对比。组织好的目录下data.yaml按YOLO的约定写path: /data/lung_yolo train: images/train val: images/val nc: 1 names: 0: nodule训练脚本和推理脚本都通过这个yaml定位数据。path用绝对路径最省事用相对路径时要注意yolo命令的工作目录必须和yaml中约定一致否则报错找不到图片。3.4 转换后的抽样验证这步偷懒后面全白干转换和切分完成后抽10到20张图把txt画回图上看看框和实际病灶是否对齐。这一步很多人跳过但它是验证坐标转换正确性的唯一直接手段import cv2 import numpy as np def draw_yolo_boxes(image_path, txt_path, class_names): img cv2.imread(image_path) h, w img.shape[:2] with open(txt_path) as f: for line in f.readlines(): parts line.strip().split() if not parts: continue cls_id int(parts[0]) xc, yc, bw, bh map(float, parts[1:]) x1 int((xc - bw / 2) * w) y1 int((yc - bh / 2) * h) x2 int((xc bw / 2) * w) y2 int((yc bh / 2) * h) cv2.rectangle(img, (x1, y1), (x2, y2), (0, 255, 0), 2) cv2.putText(img, class_names[cls_id], (x1, y1 - 8), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 255, 0), 2) return img img draw_yolo_boxes(lung_yolo/val/images/LN_00001.jpg, lung_yolo/val/labels/LN_00001.txt, [nodule]) cv2.imwrite(check_LN_00001.jpg, img)把框还原成像素坐标时的乘除法要小心txt里是归一化值乘回w和h时x系乘w、y系乘h。抽样结果里如果出现框整体偏移半个图、框明显比病灶大一圈这类现象先回去查转换脚本的除数和中心点公式不要急着调训练参数。4. 用YOLOv8训练肺结节数据参数设置与损失函数判读VOC转YOLO格式这一关过了后面就是标准训练流程。YOLOv8是目前用起来最顺的版本它的数据格式要求和YOLOv5、YOLO11完全一致也就是说转换出来的txt标签可以直接跨版本复用。训练前先把数据检查清单过一遍然后按显存选模型和batch。4.1 训练前的数据检查清单启动训练前用15分钟做三件事确认images和labels目录下文件一一对应数量都是7048确认每个txt文件非空没有漏掉标签导致正样本丢失确认data.yaml的路径能直接访问。第一条可以用一行命令验证diff (ls lung_yolo/train/images | sed s/\.[^.]*$// | sort) \ (ls lung_yolo/train/labels | sed s/\.[^.]*$// | sort) echo OK这行命令把images和labels两个目录下的文件名去掉扩展名后排序对比没有差异就输出OK。批量转换时如果出现过文件写入失败这里能直接暴露。空标签文件的问题更隐蔽YOLO训练会把空txt当背景处理少数几个空文件影响不大但如果比例超过5%要回看是不是XML解析时漏读了object节点。4.2 关键训练参数设置与显存估算肺结节的CT图像通常是512x512或1024x1024输入YOLO时默认缩放到640x640。对这份7048张的数据集模型选YOLOv8n或YOLOv8s就足够从中型模型开始只会让训练时间变长收益有限。常用参数参考如下参数建议值说明imgsz640小目标场景不要降到320会丢失结节细节batch1624G显存可上32显存小用梯度累积补偿epochs100配合早停不必死等到收敛patience20验证集连续20轮无提升就停optimizerAdamW小数据集上比SGD收敛更平稳lr00.001用预训练权重微调时这个量级合适device0需不需要GPU看batch和imgsz组合显存预估按经验公式算YOLOv8n在640分辨率下batch为16时约需6G显存batch为32时约10GYOLOv8s对应翻一倍。如果只有8G显存用YOLOv8n加batch 16配合fraction0.5参数限制数据加载比例也能跑起来。没有GPU纯CPU训练不是不能跑但7048张图100个epoch的耗时在数十小时量级不建议。4.3 训练命令与损失函数判读数据目录和data.yaml就绪后训练命令如下yolo detect train \ datadata.yaml \ modelyolov8n.pt \ epochs100 \ imgsz640 \ batch16 \ patience20 \ optimizerAdamW \ lr00.001 \ projectlung_nodule_run \ nameexp1 \ seed42modelyolov8n.pt会下载COCO预训练权重作为起点迁移学习让肺结节模型在小数据集上也能较快收敛。project和name指定输出目录训练日志、权重文件和训练曲线都落在lung_nodule_run/exp1下。训练过程中终端每轮会打印一组指标核心是三个损失和两个mAP指标含义正常趋势box_loss边界框回归损失YOLOv8用CIoU持续下降不反弹cls_loss分类损失BCE变体单类场景下降很快dfl_loss分布焦点损失影响框的定位精度缓慢下降mAP50IoU阈值0.5下的平均精度平缓上升后走平mAP50-95IoU从0.5到0.95的平均值比mAP50低是正常的有个常见误区看到box_loss降不下去就反复调学习率。实际上肺结节这类小目标场景box_loss的绝对值比通用检测数据高是正常的因为小框的IoU对像素偏移更敏感几个像素的偏差就会让CIoU损失明显变化。判断训练是否正常的标准是验证集mAP50在上升、三个loss没有发散性反弹而不是box_loss的绝对数值降到某个阈值。4.4 小目标占比高时的参数调整肺结节在512x512的原图中往往只占几十到一百像素属于典型的小目标检测场景。训练时有两个针对性调整一是imgsz尽量保持在640以上图像缩放得太小结节可能缩到几个像素特征基本丢失二是在数据加载阶段开启Mosaic增强时小目标被切碎的概率较高如果发现验证集mAP50很高但mAP50-95偏低可以把mosaic关闭或调低验证期的增强强度。还有一个容易被忽视的参数是close_mosaic它控制训练最后几轮关闭Mosaic增强。YOLOv8默认在最后10轮自动关闭让模型在接近真实分布的数据上微调。如果你自己把mosaic0全局关闭小数据集上反而容易欠拟合肺结节这种小目标建议保留默认。5. 标注可用性验证mAP曲线、置信度阈值与可视化回检训练跑完后验证这批7048张的标注是否真的可用不能只看训练集上的loss。三个步骤能给出可靠结论验证集上的mAP指标、推理时conf与iou的配合、预测框回绘原图。5.1 用mAP50和PR曲线确认标注质量yolo detect val \ modellung_nodule_run/exp1/weights/best.pt \ datadata.yaml \ splitval命令输出的mAP50是首要参考。肺结节单类检测任务里mAP50达到0.85以上说明标注质量和训练效果都合格如果只有0.6出头先不要急着改网络结构回看2.3的体检结果常见原因是标注框本身偏大或偏移。mAP50-95比mAP50低0.2到0.3在这个场景下正常因为小目标对IoU阈值变化特别敏感。5.2 推理时conf与iou的配合调参验证用的conf和iou不是越高越好和场景强相关from ultralytics import YOLO model YOLO(lung_nodule_run/exp1/weights/best.pt) results model.predict( sourcelung_yolo/val/images, conf0.25, iou0.45, saveTrue, save_txtTrue, line_width2, imgsz640 )肺结节筛查场景里漏检的代价高于误检conf建议放在0.2到0.3之间而不是默认的0.25以上让模型多输出候选框供后续判断。iou参数主要控制NMS时重叠框的合并结节本身不会高度重叠0.45够用调高到0.7反而可能把相邻的两个小结节合并成一个框。如果推理结果里出现大量碎片框把conf往上抬到0.4左右就能过滤掉。5.3 用预测框回绘确认样本级表现最后把预测结果画回原图挑验证集里mAP边界上的样本看预测正确的、漏检的、误检的各看10张。漏检的样本如果集中在框特别小或对比度低的结节那说明标注本身没问题是数据分布和小目标检测的固有限制后续从数据增强或更高分辨率输入入手。误检的样本如果集中在血管断面或胸膜增厚的位置那就属于医学语义层面的挑战单靠通用目标检测框架很难完全解决但至少说明标注数据本身是干净可用的。到这里一份VOC格式的肺结节数据集就算真正跑通了从解析、转换到训练验证的完整链路后续无论是换YOLO系列其他版本还是调整网络结构这套数据和脚本都能直接复用。本文还有配套的精品资源点击获取
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。