资讯动态

VOC转YOLO格式转换实战:从XML到txt的完整脚本与避坑指南

发布时间:2026/10/8 19:05:17 来源:尧图企业网站定制
简介一套面向目标检测数据预处理的Python源码可将VOC格式的XML标注数据集批量转换为YOLO格式的TXT标注并自动完成训练集与测试集的随机划分。适合计算机、电子信息工程、数学等专业的学生用于课程设计、期末大作业与毕业设计也适合刚接触YOLO的开发者快速扫清数据准备障碍。压缩包内含2个文件均为.py脚本voc_to_yolo.py负责解析XML并生成YOLO格式标签cutdata.py负责按指定比例划分数据集整体包大小仅2KB结构清晰、便于二次修改。代码采用参数化编程路径、类别、划分比例均可灵活调整注释详细且已通过运行测试功能正常。目前已有560人学习下载可直接用于实验或项目起步省去手动转换与划分的麻烦。1. 别把时间耗在数据转换上这份资源解决VOC转YOLO最麻烦的一步做目标检测实验的同学十有八九都在数据预处理阶段卡过壳。下载下来的公开数据集是VOC格式标注文件全是一堆XML而YOLO训练要的是每个图片对应一个txt文件里面写归一化坐标和类别ID。格式不匹配训练脚本直接报错连第一步都迈不出去。这份资源的核心就是把这个断点补上包含voc_to_yolo.py批量转换脚本和cutdata.py分割脚本带详细注释和运行结果适合正在做课程设计、期末大作业或者毕业设计的计算机、电子信息工程、数学相关专业的同学。以下是我拆解这份源码的完整过程包括参数怎么调、运行顺序、常见报错和排查方法。2. VOC与YOLO格式差异为什么不能直接拿去训练2.1 VOC数据集的目录结构与XML标注字段VOC格式的数据集通常按VOCdevkit的目录规范组织这是从PASCAL VOC时代就固定下来的习惯。解压之后能看到Annotations、JPEGImages、ImageSets这三个核心目录分别存放XML标注文件、原始图片和训练测试划分的列表文件。ImageSets目录下面还有Main子目录里面放着train.txt、val.txt这类纯文本文件每一行是图片文件名不带扩展名用来告诉训练脚本哪些图片属于哪个集合。XML标注文件是理解转换逻辑的起点。打开一个典型的VOC标注文件根节点是annotation下面挂着一串关键信息。folder字段记录图片所在文件夹名称filename字段是图片文件名size节点里是图片的宽度、高度和通道数。真正有用的是object节点每个object代表图中的一个目标实例里面有一个name子节点表示类别名还有一个bndbox子节点记录目标的边界框用xmin、ymin、xmax、ymax四个值表示左上角和右下角的像素坐标。这份资源里的voc_to_yolo.py脚本做的就是读取这些XML字段提取出文件名、图片尺寸、类别名和边界框坐标然后计算出YOLO需要的归一化参数。在开始转换之前建议先用一个简单的Python脚本统计一下数据集的类别分布确认所有需要训练的类别都在XML里出现过避免转换过程中发现类别缺失再返工。2.2 YOLO的txt标注格式每行一个目标YOLO系列从YOLOv5到YOLOv8再到最新的yolo26标注文件的格式要求始终是统一的每张图片对应一个同名txt文件文件里的每一行代表一个目标实例包含五个数值依次是类别ID、归一化后的目标中心点x坐标、中心点y坐标、目标宽度和目标高度五个数值之间用空格分隔。与VOC的XML格式相比YOLO格式有几个关键差异决定了不能直接把XML硬塞给训练脚本。第一是坐标系不同VOC用绝对像素坐标描述边界框的左上角和右下角YOLO用的是相对坐标描述中心点和宽高。第二是归一化要求YOLO要求所有坐标值都在0到1之间这样可以适配不同分辨率的输入图片训练时无论图片缩放到什么尺寸标注都不会失真。第三是类别表示方式VOC用字符串类别名YOLO用整数IDID的顺序由类别列表文件决定。需要特别注意的是VOC原始标注是水平矩形框只适合水平目标检测。如果你做的是旋转目标检测比如mmrotate框架训练DOTA数据集那种带角度信息的任务本资源的转换逻辑完全不适用还得引入角度参数这一点在选型之前要搞清楚。2.3 从绝对坐标到归一化坐标的数学关系VOC转换到YOLO格式的数学关系并不复杂核心是两套坐标系的换算公式。假设一张图片的宽度为img_w高度为img_h目标边界框的绝对坐标为xmin、ymin、xmax、ymax那么YOLO格式需要的四个归一化参数计算公式如下中心点x坐标等于xmin加xmax之和的一半再除以img_w中心点y坐标等于ymin加ymax之和的一半再除以img_h目标宽度等于xmax减xmin的差除以img_w目标高度等于ymax减ymin的差除以img_h。这个换算逻辑看起来简单但实际写代码时很容易踩坑。最典型的问题是直接用整数做除法导致精度丢失Python的整数除法会向下取整坐标一旦取整边界框的位置和大小都会产生偏差。正确做法是把分子分母都转成float类型再运算。另一个问题是XML里读出来的坐标值可能是字符串需要做类型转换才能参与计算。最好的习惯是写一个独立的换算函数先取数值并转float再套公式最后按六位小数格式化输出这样生成的文件可读性和精度都有保障。数据项VOC格式YOLO格式存储方式XML文件每个文件一个图片txt文件每个图片对应一个同名文件坐标表示xmin, ymin, xmax, ymax 绝对像素值中心点x, y, 宽度, 高度 归一化相对值类别表示字符串类别名整数类别ID多目标表示多个object节点多行每行一个目标坐标范围0到图片宽高0到1理解了这层换算关系再看资源里的转换脚本就不会觉得是黑匣子了。它本质上就是一个坐标映射器把VOC的四个角点坐标换算成YOLO的中心点加宽高同时把字符串类别名映射成整数ID。3. 转换脚本voc_to_yolo.py批量转换的完整流程3.1 脚本的运行方式与输入输出这份资源里的voc_to_yolo.py脚本设计思路是参数化编程把输入输出目录、类别文件路径都做成可配置的参数这样换一个数据集不用改代码逻辑只改参数就行。脚本的运行方式很直接在命令行执行python voc_to_yolo.py就可以但运行前需要根据实际目录结构调整脚本顶部的配置项。脚本主要接收三个输入参数XML标注文件所在的目录路径、转换后txt文件要输出的目录路径、类别列表文件路径。输出则是与图片同名的txt标注文件同时脚本会在运行过程中打印转换进度和处理结果。我在拆解这份资源时发现脚本内部还内置了一个图像目录参数用于生成总文件列表时拼接图片路径这个参数在生成训练集测试集列表时是必需的。脚本的入口部分通常是这样的结构# -*- coding: utf-8 -*- # voc_to_yolo.py 核心转换逻辑 import xml.etree.ElementTree as ET import os def voc_to_yolo(xml_dir, out_txt_dir, classes_file, image_dir): # 读取类别列表注意保持顺序一致性 with open(classes_file, r, encodingutf-8) as f: class_names [line.strip() for line in f if line.strip()] # 创建输出目录 os.makedirs(out_txt_dir, exist_okTrue) # 遍历所有XML文件 xml_files [f for f in os.listdir(xml_dir) if f.endswith(.xml)] total len(xml_files) for idx, xml_file in enumerate(xml_files): # 这里调用核心转换函数处理单个XML convert_one(xml_file, xml_dir, out_txt_dir, class_names) if (idx 1) % 50 0: print(f已处理 {idx 1}/{total} 个文件)这段逻辑说明了几件事。第一类别列表读取后按行存储每行一个类别名顺序就是YOLO训练时的类别ID顺序必须和后续训练配置保持一致。第二输出目录用os.makedirs自动创建exist_okTrue参数避免目录已存在时抛异常。第三遍历XML文件列表进行批处理每处理50个文件打印一次进度方便观察转换是否卡住。3.2 核心转换函数的代码实现单个XML文件的转换是脚本的灵魂部分。需要从XML树中提取filename、size和object节点然后对每个object执行坐标换算并生成一行YOLO格式的标注文本。def convert_one(xml_file, xml_dir, out_txt_dir, class_names): xml_path os.path.join(xml_dir, xml_file) tree ET.parse(xml_path) root tree.getroot() # 提取图片文件名和尺寸 image_name root.find(filename).text img_w int(root.find(size/width).text) img_h int(root.find(size/height).text) yolo_lines [] # 遍历所有目标实例 for obj in root.iter(object): cls_name obj.find(name).text # 避免类别不在列表中导致IndexError if cls_name not in class_names: print(f警告: 类别 {cls_name} 不在类别文件中已跳过) continue cls_id class_names.index(cls_name) # 提取边界框坐标并转为float 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 ((xmin xmax) / 2.0) / img_w y_center ((ymin ymax) / 2.0) / img_h box_width (xmax - xmin) / img_w box_height (ymax - ymin) / img_h yolo_lines.append(f{cls_id} {x_center:.6f} {y_center:.6f} {box_width:.6f} {box_height:.6f}) # 写入同名txt文件 txt_name os.path.splitext(image_name)[0] .txt txt_path os.path.join(out_txt_dir, txt_name) with open(txt_path, w, encodingutf-8) as f: f.write(\n.join(yolo_lines))代码里的几个细节值得展开说。root.iter(object)是遍历XML中所有object节点比root.findall(object)更稳健因为对嵌套结构不敏感。类别不在列表中时用continue跳过并打印警告这避免了IndexError导致整个脚本中断这个处理在第5章会详细展开。归一化计算里所有数值都转成了float用2.0做除数避免整数除法问题。最后写入txt文件时用图片的文件名拼接txt后缀这是YOLO格式的核心约定训练脚本靠这个同名机制找标注文件。3.3 类别列表文件是全局配置的关键在这个转换流程里类别列表文件是最容易被忽视但影响最大的配置文件。它决定了类别名到整数ID的映射关系而ID一旦分配错误后续训练出来的模型就会张冠李戴识别结果全部错位。维护类别列表文件有几点经验值得记住。第一类别名的顺序一旦定下来就不要随便调整除非整套数据重新转换一遍。在写voc_to_yolo.py的时候会读取这个文件生成映射表训练的时候data.yaml或者自定义配置的类别列表必须跟这个文件一致。这里有个实用的做法值得借鉴把类别文件命名为classes.txt放在数据集根目录下转换脚本、训练配置都从这个文件读取避免多个地方手动维护类别顺序导致不一致。第二类别名要和XML里的name字段严格匹配包括大小写和空格。VOC数据集里常见的情况是同一类目标有的标注为person有的标注为Person大小写不一致会导致脚本跳过部分目标。在转换前用一行Python脚本统计所有XML里出现的类别名再和类别文件做对比是最快的验证方式。第三对于越界坐标问题很多VOC标注框在标注时手一抖就超出了图片边界可能出现xmin小于0或者ymax大于图片高度的情况。归一化后这些值会变成负数或者大于1的数YOLO训练脚本遇到这种标注会报错。常见做法是在脚本里加一个坐标裁剪函数把超出[0,1]范围的值强行修正到边界这是参数化编程里很实用的一种容错策略。4. 数据集分割脚本cutdata.py训练集与测试集划分的正确姿势4.1 分割逻辑与随机种子有了转换后的txt标注文件下一步就是按比例把数据集分割成训练集和测试集。这份资源里的cutdata.py脚本把这一步做成了独立模块好处是复用性高以后跑任何目标检测任务都能用同一个分割逻辑。分割的核心是一个随机数种子加洗牌操作。数据集里的图片通常按拍摄时间或者采集批次排列相同环境、相近时间拍摄的图片非常相似不做打乱直接切分的话训练集和测试集之间可能存在大量相似图片导致测试集精度虚高。直接设置随机种子可以保证每次运行分割结果完全一致实验可复现。# -*- coding: utf-8 -*- # cutdata.py 训练集与测试集分割脚本 import os import random def split_dataset(image_dir, train_ratio0.8, val_ratio0.2, seed42): 将图片列表按比例分割为训练集和测试集 参数: image_dir: 图片所在目录路径 train_ratio: 训练集占比默认0.8 val_ratio: 测试集占比默认0.2 seed: 随机种子保证可复现 # 收集所有图片文件名 all_images [] for img_name in os.listdir(image_dir): if img_name.lower().endswith((.jpg, .jpeg, .png)): all_images.append(img_name) # 设置随机种子并打乱顺序 random.seed(seed) random.shuffle(all_images) # 按比例切分 total len(all_images) train_end int(total * train_ratio) train_images all_images[:train_end] val_images all_images[train_end:] print(f数据集总量: {total}) print(f训练集数量: {len(train_images)}, 占比 {len(train_images)/total:.1%}) print(f测试集数量: {len(val_images)}, 占比 {len(val_images)/total:.1%}) # 返回划分结果 return train_images, val_images这段代码展示的是分割核心逻辑。train_ratio参数控制训练集比例默认0.8在目标检测任务里这个比例是比较常见的选择数据量特别大时可以调到0.9数据量小时要保留足够多的测试样本才能评估出稳定精度。val_ratio参数看起来似乎没被直接用但在某些实现版本里会用它做二次验证集划分比如从测试集里再分出一部分做final test。这里用random.shuffle实现洗牌确保打乱后切分出来的两个集合分布相对均匀。4.2 生成YOLO可读的train.txt与test.txt分割完成后最关键的一步是生成YOLO训练框架能直接读取的文件列表。YOLO的训练脚本通常需要两个txt文件一个记录训练集图片路径一个记录测试集图片路径每行一个绝对路径或者相对于数据集根目录的相对路径。这里有一个非常容易翻车的细节路径分隔符。在Windows系统上os.path.join生成的是反斜杠路径而YOLO训练脚本在Linux环境下运行时反斜杠会被当作转义字符导致图片路径解析失败。常见的做法是在写入txt时统一把反斜杠替换成正斜杠兼容Windows和Linux双平台。def write_image_lists(train_images, val_images, image_dir, out_txt_path): 将训练集和测试集的图片路径写入txt文件 with open(out_txt_path, w, encodingutf-8) as f: for img in val_images: # 统一使用正斜杠兼容Linux训练环境 img_path os.path.join(image_dir, img).replace(\\, /) f.write(img_path \n) print(f已写入 {len(val_images)} 条记录到 {out_txt_path})实际资源里的cutdata.py会把生成train.txt和生成test.txt分开处理分别写入两个文件并在末尾打印统计信息。我在使用过程中习惯再加一行校验代码计算两个文件的行数总和是否等于总图片数防止因为某些图片名重复或者编码问题导致漏写。4.3 分割后目录结构与资源打包分割完成后的标准目录结构是训练脚本直接能识别的形态。图片目录里是全部图片labels目录里是转换后的txt标注文件根部是train.txt和test.txt两个列表文件。这个结构在后续跑YOLO训练时不用再做任何调整直接指定数据配置文件就能开始训练。这份资源附带了一个文档说明里面提到一个实用的组织思路训练前把VOC原始XML目录也保留一份作为原始数据备份一旦转换后的标注有问题随时可以回看原始XML确认标注的原始信息。这个习惯救过我很多次。有一次转换脚本跑完训练时发现有个类别的loss曲线异常回查XML才发现那个类别的某些标注宽高比特别极端手工修正后重新转换问题就消失了。需要注意的是资源里同时包含了运行结果截图这是判断脚本能否正常工作的最直观依据。如果你下载后运行报错先检查Python版本和依赖库是否完整脚本依赖的库只有标准库不需要额外安装任何第三方包理论上Python 3.6以上版本都能直接运行。5. 转换避坑指南五条高频踩坑记录与排查方法5.1 XML里filename字段与图片文件名对不上现象转换脚本运行到一半报FileNotFoundError提示找不到某张图片。原因VOC数据集在采集时存在一批图片改名的情况但XML里的filename字段没有同步更新导致脚本根据filename拼接路径时找不到原始图片。还有一种常见情况是filename里带有多余的空格或者大小写与真实文件名不完全一致。解决转换前先做一次文件名对账。遍历整个XML目录提取所有filename字段的值与图片目录里的实际文件名求差集把不一致的项打印出来逐个处理。我在实际操作中写了一个几十行的校验脚本跑一遍就能列出所有问题比转换过程中报错再回头查高效得多。资源里的voc_to_yolo.py内部对这种情况只做了警告处理建议在正式转换前手动执行一遍这种校验。5.2 类别顺序与训练配置不一致导致识别错位现象训练过程不报错loss正常下降但用训练好的权重做推理时所有目标的类别标签都是错的比如检测到汽车却标注成行人。原因转换时用的类别列表文件顺序是cat、dog、person训练时data.yaml里的类别顺序改成了person、dog、cat同一个类别ID在不同阶段映射到了不同类别名模型学到的特征和标签配对全部错位。解决强制规定类别列表文件为全局唯一配置源。转换脚本、训练配置、推理脚本都读取同一个classes.txt文件任何地方都不允许手工指定类别顺序。从那以后我再也没有遇到过因为类别错位导致的翻车问题这个习惯已经固化成我处理所有目标检测数据集时的第一步。5.3 边界框坐标越界导致归一化结果异常现象转换后生成的txt标注文件里出现了负数坐标或者大于1的坐标值训练时部分样本报错或者loss曲线剧烈震荡。原因数据标注人员在标注时框选范围超出了图片边界或者标注工具对边界框做了扩展操作导致xmin、ymin小于0xmax、ymax超过图片宽度和高度。这类越界坐标在VOC格式下不影响显示但归一化后会产生超出[0,1]区间的异常值。解决在转换函数里增加坐标裁剪逻辑把归一化后的数值强行限制在0到1之间。具体做法是添加一个clip操作对x_center、y_center、box_width、box_height分别做上下界约束。同时建议在转换脚本中统计并打印越界框的数量如果比例超过一定阈值说明原始标注质量有问题需要回到标注阶段修正。5.4 Windows路径分隔符导致训练环境读不到图片现象在Windows上转换和分割都正常把数据集拷到Linux服务器上训练时训练脚本报错说找不到图片但路径看起来是对的。原因Windows系统用反斜杠\作为路径分隔符Linux系统只认正斜杠/。train.txt里写入的图片路径保留了反斜杠Linux在读取时把\当作转义字符处理实际得到的路径是错误的。解决在cutdata.py中写入图片路径时统一调用.replace(\, /)方法把所有反斜杠替换成正斜杠。这个处理在Windows和Linux环境下都能正常工作不依赖运行平台。更彻底的做法是在设计阶段就把数据集路径写成相对路径训练时由脚本动态拼接彻底规避路径分隔符问题。5.5 训练集与测试集划分不均导致模型评估失真现象模型在测试集上mAP值很高但实际部署后效果很差。原因训练集和测试集从同一批数据里按顺序切分相同场景下连续拍摄的相似图片被分到了两个集合中模型在训练时已经见过几乎相同的内容测试集精度虚高。解决用cutdata.py的随机种子洗牌功能重新划分数据集。同时写一个去重脚本计算所有图片的感知哈希值把相似度超过阈值的图片组标记出来确保它们不会被分到两个不同的集合中。这个方法对那些视频抽帧得到的数据集尤其重要相邻帧之间的差异非常小不做去重处理会让评估数据完全失真。6. 转换后的验证检查把标注画回图片上确认转换正确性转换脚本跑完不代表万事大吉我习惯在进训练流程前做一次标注可视化验证。做法是写一个简单的检查脚本用OpenCV读回原始图片把转换后的YOLO标注从归一化坐标换算回像素坐标在图片上画出边界框和类别标签然后和原版VOC标注画出的框做视觉对比。# -*- coding: utf-8 -*- # verify_yolo_bbox.py 验证YOLO标注是否正确 import cv2 import os def verify_yolo_label(img_path, txt_path, classes_file, save_path): # 读取图片并获取尺寸 img cv2.imread(img_path) if img is None: print(f错误: 无法读取图片 {img_path}) return img_h, img_w img.shape[:2] # 读取类别列表 with open(classes_file, r, encodingutf-8) as f: class_names [line.strip() for line in f if line.strip()] # 读取YOLO标注并画框 with open(txt_path, r, encodingutf-8) as f: for line in f: parts line.strip().split() if len(parts) ! 5: continue # 跳过非法行 cls_id int(parts[0]) x_center float(parts[1]) y_center float(parts[2]) box_width float(parts[3]) box_height float(parts[4]) # 归一化坐标还原为像素坐标 x1 int((x_center - box_width / 2) * img_w) y1 int((y_center - box_height / 2) * img_h) x2 int((x_center box_width / 2) * img_w) y2 int((y_center box_height / 2) * img_h) # 画框和类别标签 cv2.rectangle(img, (x1, y1), (x2, y2), (0, 255, 0), 2) label f{class_names[cls_id]} cv2.putText(img, label, (x1, max(0, y1 - 5)), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 255, 0), 2) # 保存验证结果图片 cv2.imwrite(save_path, img) print(f验证图片已保存至: {save_path})这个脚本的验证逻辑很直接逐行解析txt标注按公式换算坐标画框和标签。跑完随机抽三张图片检查如果框的位置和类别都对得上说明转换脚本没毛病。如果发现框偏移或者类别标错优先检查那张图对应的XML原始标注排查问题源头。这套验证流程的成本很低但收益很大训练前花十分钟能省下后面调模型的好几天。从那以后我每次转换完数据集都会强制走一遍这个可视化验证随机抽三张图确认标注质量再进训练环节这个习惯帮我避掉了大量因为数据问题导致的训练事故也让我在检查数据时更有底气。希望帮到你这份资源和文档说明能让你少走很多弯路。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑