新闻详情

新闻详情

首页 / 资讯中心 / 详情

VOC转YOLO格式全攻略:XML到txt的转换与数据集划分实践

发布时间:2026/9/28 2:37:03来源:尧图网络
VOC转YOLO格式全攻略:XML到txt的转换与数据集划分实践
简介面向目标检测入门者及需要自制数据集的算法工程师这份资源提供将VOC格式数据集转换为YOLO格式并按比例分割训练集与测试集的完整Python方案覆盖从标签坐标解析到数据划分的全流程可无缝对接YOLO系列模型训练。压缩包共2个文件均为Python脚本分别负责VOC标签到YOLO格式的转换与训练/测试集自动划分体积约2KB结构精炼代码采用参数化设计路径、类别与划分比例均可一键修改注释清晰并附带运行结果验证。已有560人学习下载。对于环境配置或运行不熟的用户作者提供私信支持。脚本经过实际测试运行结果一目了然既能用于计算机、电子信息工程、数学等专业大学生的课程设计、期末大作业和毕业设计也可作为目标检测项目中的标准预处理工具帮助快速摆脱手工整理数据的繁琐工作。1. 把VOC转YOLO这件事拆开看一次转换背后藏着三个必须同时解决的需求很多人拿到一份Pascal VOC标注好的数据集第一反应是“把XML后缀改成txt不就行了”结果丢给YOLO训练脚本之后不是解析报错就是loss直接变成nan。我过去在目标检测项目里反复做“数据准备”这门苦差事最后沉淀出的处理套路很明确VOC转YOLO不是改后缀而是把像素坐标系下的绝对坐标换成归一化比例坐标把object名称换成类别索引再用固定随机种子把图片和标签成对切成训练集与测试集中间留一份验证集。这篇笔记把整套流程写成可直接复用的源代码和配套文档说明覆盖正在把老数据集喂给YOLO训练脚本的算法工程师、学生和正在自建数据中台的团队。2. VOC与YOLO的标注差异坐标系、归一化与为什么不能只改后缀2.1 先读明白VOC的XMLannotation根节点下藏着四个关键字段第一步不是写转换函数而是先把VOC的XML字段摆到桌面上看清楚。用LabelImg或老工具箱标出来的Pascal VOC一张图对应一个同名的XML文件通常在Annotations目录图片本身在JPEGImages目录可选的划分名单在ImageSets/Main目录。转换脚本真正消费的字段只有五个filename、size.width、size.height、object.name以及object.bndbox里的四个坐标。annotation folderJPEGImages/folder filename000001.jpg/filename size width1920/width height1080/height depth3/depth /size object nameperson/name bndbox xmin100/xmin ymin150/ymin xmax300/xmax ymax400/ymax /bndbox /object /annotationXML路径含义是否参与转换filename图片文件名决定输出txt文件名要处理大小写和路径前缀size/width图片宽像素归一化除法的分母size/height图片高像素归一化除法的分母object/name类别字符串查类别映射表得到class_idobject/bndbox四个绝对像素坐标全部换算成比例值这里有个容易忽略的点depth字段是通道数不是缩放依据很多人在脚本里顺手把它当height读最后所有框的y坐标全错。另外VOC里还有difficult、truncated、occluded这类辅助标签YOLO没有对应概念转换时我一般默认保留所有目标但如果训练场景对难例敏感可以把difficult1的对象过滤掉或单独统计别让它们在无差别训练时拖累模型。2.2 YOLO txt的要求一个类别索引加四个0到1之间的比例YOLO的标签格式长得极简labels目录下放与图片同名的txt每行五个数依次是class_id cx cy w h。cx、cy是目标中心点相对于图片宽高的比例w、h是目标宽高相对于图片宽高的比例。四舍五入写六位小数就够了。把VOC的绝对坐标换算过去的公式长这样# 单个box的换算演示先理解公式再谈批量脚本 xmin, ymin, xmax, ymax 100, 150, 300, 400 img_w, img_h 1920, 1080 cx (xmin xmax) / 2.0 / img_w # 0.1041 cy (ymin ymax) / 2.0 / img_h # 0.2546 w (xmax - xmin) / img_w # 0.1041 h (ymax - ymin) / img_h # 0.2315这段代码有三个要点。第一所有分子在Python3里都是浮点数除法不会出现整数截断。第二分母必须是图片实际尺寸读取size字段后先用int()包一层再除。第三如果算出来的cx、cy落在0到1之外说明标注本身越界或者图片被裁剪过但XML没同步这是脏数据信号不能靠裁切掩盖。为什么YOLO坚持用比例因为训练时输入图像会被统一缩放绝对像素坐标在resize之后全乱套而归一化比例天然适配任意输入尺寸。你后面把数据喂给yolov8训练自己的数据集时模型内部还会做letterbox补边也只有比例值能在这种动态尺寸下活下来。反过来看直接把.xml后缀改成.txt文件内容第一行还是annotationYOLO的加载器按空格切分后根本拿不到数字报错是必然的就算解析侥幸通过object里的字符串类别也没有任何地方帮你翻译成索引。这两点决定了“改后缀”这条路从原理上就走不通。3. 写XML到YOLO转换脚本解析、类别映射与输出目录设计3.1 从Annotations到labelsxml_to_yolo.py的完整实现转换脚本用一个Python文件就能承载依赖只有标准库。我一般把脚本命名为xml_to_yolo.py放在项目根目录的scripts/下输入VOC的Annotations目录和一个类别清单输出一个平铺的labels目录。#!/usr/bin/env python3 # xml_to_yolo.py import os import argparse import xml.etree.ElementTree as ET def load_classes(classes_file): 读取每行一个类名的文本文件返回 name - index 的映射 with open(classes_file, r, encodingutf-8) as f: names [line.strip() for line in f if line.strip()] return {name: idx for idx, name in enumerate(names)} def convert_one_xml(xml_path, classes_map, labels_dir, ignore_unknown): tree ET.parse(xml_path) root tree.getroot() filename root.findtext(filename) if filename is None: print(f[skip] {xml_path}: missing filename) return None filename filename.strip() size root.find(size) if size is None: print(f[skip] {xml_path}: missing size) return None img_w int(size.findtext(width)) img_h int(size.findtext(height)) if img_w 0 or img_h 0: print(f[skip] {xml_path}: bad size {img_w}x{img_h}) return None stem os.path.splitext(os.path.basename(filename))[0] out_lines [] for obj in root.findall(object): name obj.findtext(name) if name not in classes_map: if ignore_unknown: print(f[warn] unknown class {name} in {xml_path}, skipped) continue raise ValueError(funknown class {name} in {xml_path}) box obj.find(bndbox) xmin float(box.findtext(xmin)) ymin float(box.findtext(ymin)) xmax float(box.findtext(xmax)) ymax float(box.findtext(ymax)) # 合法性校验框宽高必须为正出现异常就停宁停勿错 if xmax xmin or ymax ymin: raise ValueError(fbad box {xmin},{ymin},{xmax},{ymax} in {xml_path}) cx (xmin xmax) / 2.0 / img_w cy (ymin ymax) / 2.0 / img_h w (xmax - xmin) / img_w h (ymax - ymin) / img_h out_lines.append(f{classes_map[name]} {cx:.6f} {cy:.6f} {w:.6f} {h:.6f}) os.makedirs(labels_dir, exist_okTrue) out_path os.path.join(labels_dir, stem .txt) with open(out_path, w, encodingutf-8) as f: f.write(\n.join(out_lines)) return out_path def main(): parser argparse.ArgumentParser(descriptionConvert VOC XML to YOLO txt labels) parser.add_argument(--annot_dir, requiredTrue, helpVOC Annotations directory) parser.add_argument(--labels_dir, defaultlabels, helpoutput directory for YOLO txt files) parser.add_argument(--classes_file, requiredTrue, helpplain text file, one class name per line) parser.add_argument(--ignore_unknown, actionstore_true, helpskip objects whose class is not in the map) args parser.parse_args() classes_map load_classes(args.classes_file) print(classes_map) xml_files [f for f in os.listdir(args.annot_dir) if f.lower().endswith(.xml)] for fname in sorted(xml_files): xml_path os.path.join(args.annot_dir, fname) convert_one_xml(xml_path, classes_map, args.labels_dir, args.ignore_unknown) print(fdone: {len(xml_files)} xml files processed - {args.labels_dir}) if __name__ __main__: main()这段代码围绕“宁可停下来也不要产出脏数据”这个原则写。load_classes负责把类别字符串翻译成索引返回的字典在后续每个XML里被反复查询convert_one_xml解析单个文件遇到缺失size、非正宽高、反向了bbox时直接报错或跳过等排查完再重跑。参数含义对照参数作用备注--annot_dirVOC的Annotations目录目录里只放后缀为.xml的标注文件--labels_dir输出YOLO标签的目录建议叫labels与images目录平级--classes_file类别清单每行一个类名行号即class_id--ignore_unknown遇到未映射类别时跳过默认False遇到未知类直接抛异常运行命令python xml_to_yolo.py \ --annot_dir data/VOC/Annotations \ --labels_dir data/yolo_labels \ --classes_file voc_classes.txt跑完先数一下生成的txt数量如果数量明显小于XML数量多半是filename字段缺失、size字段缺失或未知类别被跳过控制台的[skip]日志会告诉你去查哪几个文件。注意filename在部分数据集里写成JPEGImages/000001.jpg甚至..\\image\\a.JPG脚本统一取basename再砍后缀就是为了应对这种不规范的路径写法。3.2 类别映射的稳定写法dict、classes.txt与顺序冻结类别映射有两种常见写法。第一种是在脚本里硬编码一个Python字典例如{person: 0, car: 1}适合一次性转换、类别永远不变的小项目。第二种是把类别清单放到独立的classes.txt里脚本运行时加载。我强烈推荐第二种因为当数据集从几十张涨到几万张、当多人协作训练时类别清单本身就是一份需要版本化的配置。classes.txt长这样person car bicycleload_classes会严格按照文件里的行顺序生成映射关系。这里最关键的纪律是顺序冻结第一次转换用的classes.txt后续所有训练、测试、再转换都必须沿用同一个文件不能因为新增类别就插在中间某一行。class_id一旦重排之前训练的模型权重和后来的数据标签就对不上了。我见过最惨的一次翻车是同事为了加一个“bus”类别把新类名插在了第0位结果整个训练集所有box全部错位模型跑了两天报废。类别映射还要处理一个边界VOC的一个object通常只有一个name但偶尔能看到一个目标被同时赋予两个标签的情况。YOLO的txt不支持一行多类别我的做法是保留主要类别把次要类别拆成另一个独立框或者直接用置信度更高的人工标注结果决定归属而不是在转换脚本里搞“标签融合”。4. 分割训练集与测试集比例、随机种子、图片与标签必须同进退4.1 图片和标签成对切分split_dataset.py的完整实现转换出labels之后下一步是把数据集切出训练集与测试集。新手最常见的手法是分别shuffle图片列表和标签列表再按数量截断这几乎必然导致jpg和txt对不上号。正确做法是把图片路径和标签路径绑成一个pair整个pair参与shuffle和分配。#!/usr/bin/env python3 # split_dataset.py import os import random import shutil import argparse def collect_pairs(images_dir, labels_dir, img_ext.jpg): pairs [] for fname in sorted(os.listdir(images_dir)): if not fname.lower().endswith(img_ext): continue stem os.path.splitext(fname)[0] img_path os.path.join(images_dir, fname) label_path os.path.join(labels_dir, stem .txt) if not os.path.exists(label_path): print(f[warn] missing label for {fname}) continue pairs.append((img_path, label_path)) return pairs def split_list(items, ratios): n len(items) n_train int(n * ratios[0]) n_val int(n * ratios[1]) n_test n - n_train - n_val return items[:n_train], items[n_train:n_train n_val], items[n_train n_val:] def main(): parser argparse.ArgumentParser(descriptionSplit imagelabel pairs into train/val/test) parser.add_argument(--images_dir, requiredTrue) parser.add_argument(--labels_dir, requiredTrue) parser.add_argument(--out_dir, defaultdataset_split) parser.add_argument(--train_ratio, typefloat, default0.7) parser.add_argument(--val_ratio, typefloat, default0.2) parser.add_argument(--seed, typeint, default42) args parser.parse_args() pairs collect_pairs(args.images_dir, args.labels_dir) print(fcollected {len(pairs)} valid image-label pairs) random.seed(args.seed) random.shuffle(pairs) test_ratio 1.0 - args.train_ratio - args.val_ratio if test_ratio 0: raise ValueError(train_ratio val_ratio must be 1.0) train_set, val_set, test_set split_list( pairs, (args.train_ratio, args.val_ratio, test_ratio) ) for split_name, split_items in [ (train, train_set), (val, val_set), (test, test_set), ]: img_out os.path.join(args.out_dir, images, split_name) lab_out os.path.join(args.out_dir, labels, split_name) os.makedirs(img_out, exist_okTrue) os.makedirs(lab_out, exist_okTrue) for img_path, label_path in split_items: shutil.copy2(img_path, os.path.join(img_out, os.path.basename(img_path))) shutil.copy2(label_path, os.path.join(lab_out, os.path.basename(label_path))) print(f{split_name}: {len(split_items)} pairs copied) if __name__ __main__: main()这段脚本的逻辑分三步。第一步collect_pairs遍历images_dir为每张图片找同名txt找不到就打印警告并跳过这张图这个行为意味着“缺标签的图不进任何集合”避免训练时读到空文件。第二步固定random.seed(args.seed)再shuffle保证两次执行结果完全一致。第三步按比例切片后在同一个循环里把图片和标签一起复制到目标子目录。参数说明参数默认值说明--train_ratio0.7训练集比例--val_ratio0.2验证集比例YOLOv8训练强依赖--test_ratio自动计算不需要单独填脚本用1减去前两者--seed42随机种子固定它才能复现切割结果运行命令python split_dataset.py \ --images_dir data/VOC/JPEGImages \ --labels_dir data/yolo_labels \ --train_ratio 0.7 \ --val_ratio 0.2 \ --seed 42seed42是个约定俗成的选择本质上是任何固定整数都可以玄学成分大过科学成分。真正重要的是换一张图、换一个比例、换一次数据集都要同步记录seed否则一年后复查实验时根本说不清当初那份训练集从哪来。4.2 三种目录组织方式平铺list、物理复制与软链接切分后的目录组织方式有三种各有适用场景。第一种是平铺式所有图片放在同一个images/目录所有标签放在labels/目录再生成train.txt、val.txt、test.txt三个清单文件每个文件里写图片路径。第二种是物理复制式复制成images/train、images/val、images/test和对应的labels/*子目录磁盘占用翻倍但结构直观YOLOv5、v8的data.yaml可以直接指目录。第三种是软链接式不复制原图只创建符号链接指向源文件省磁盘但要注意目标盘是否支持符号链接。方式目录结构适用场景注意点平铺listimages/ labels/ train.txt超大数据集标签加载走内存清单文件里的路径要和代码匹配物理复制images/train/ labels/train/中小数据集、云盘迁移占两份磁盘软链接images/train/xxx.jpg - 源服务器磁盘紧张跨文件系统时可能失效切分完成后训练配置里最要命的地方在data.yaml的names顺序这个列表的顺序必须和classes.txt完全一致因为它就是class_id的索引表。物理复制式目录的配置写法如下# data.yaml train: dataset_split/images/train val: dataset_split/images/val test: dataset_split/images/test nc: 3 names: [person, car, bicycle]如果你的VOC数据集自带ImageSets/Main/train.txt、val.txt、test.txt优先沿用官方划分不要重新随机切分——这些名单是数据集作者精心设计过的往往已经考虑了场景均衡和类别均衡。我一般只在官方划分不符合任务需求时才自己重切并且会明确记录下为什么放弃官方分割。5. 避坑VOC转YOLO最容易翻车的5个现场与排查步骤5.1 转换阶段的三个高频错误现象一转换出的txt里坐标全是0或者超过1YOLO训练loss在几百轮后突然nan。原因XML里的bndbox存在脏数据比如某个xmin是负值、xmax比xmin还小或者字段内容根本是空字符串float()把非数字内容转成0或直接抛异常。解决在转换脚本里做显式合法性校验xmax xmin或ymax ymin时立即抛错不要静默修正。我经历过一次教训当时为了图省事把非法框直接skip结果模型在测试集上漏检率异常查了三天才发现是脏框被跳过导致的。现象二所有目标都被标成了同一个类别。原因classes.txt的顺序和训练时data.yaml里names的顺序不一致。比如转换时把person放在第0位训练时又把bicycle放在第0位class_id整体错位。解决把classes.txt纳入版本管理每次训练前用diff和data.yaml逐行比对转换脚本启动时把映射表打印到控制台肉眼扫一眼前5行再继续。现象三框画出来整体位移或会漂到图片外。原因XML里的size字段和JPEG的实际像素尺寸不一致。常见于LabelImg在某台设备上记录的宽高和最终保存的图片不一致或者图片后期被裁剪但标注没有同步更新。解决转换前用OpenCV或PIL读取每张实际图片的宽高与XML的size比对不一致时以图片实际尺寸为准并打印warning。这一步能拦截掉大概两成的不规范VOC数据集。5.2 分割阶段的两个逻辑错误现象四跑了上面的split脚本但还是会出现某张图的jpg在train、txt在val。原因有人为了绕过脚本手工分目录分别对图片列表和标签列表做了shuffle和截断。解决回到split_dataset.py的“成对收集、成对shuffle、成对复制”逻辑。如果你已经手工分了一半最快的止损办法是重新把原目录合拢再跑一次脚本。现象五测试集精度虚高部署到现场掉点严重。原因按单张图片随机切分没有考虑数据的序列结构。自动驾驶、监控视频这类数据集里相邻帧高度相似同一段视频的帧被同时分进train和test等于测试集参与过训练指标自然好看。解决如果数据有video_id、sequence_id这类分组字段先按组打乱再整组分配没有分组字段时至少按文件名前缀做一次聚类把前缀相同的帧归到同一侧。这个坑在没有显式报错全得靠mAP曲线和现场表现来暴露属于最阴险的翻车现场。6. 验证转换结果的两种方法以及把文档写到位6.1 用OpenCV把YOLO框还原到图上做一次肉眼验收转完不验证等于白转。最常见的验证方法是把YOLO的比例坐标还原成像素坐标在图上画框拿原始XML画框做对比。写一个精简的verify_yolo.pyimport cv2 import os def draw_yolo_box(img_path, label_path, class_names): img cv2.imread(img_path) h, w img.shape[:2] with open(label_path, r) as f: for line in f: cid, cx, cy, bw, bh map(float, line.split()) x1 int((cx - bw / 2) * w) y1 int((cy - bh / 2) * h) x2 int((cx bw / 2) * w) y2 int((cy bh / 2) * h) cv2.rectangle(img, (x1, y1), (x2, y2), (0, 255, 0), 2) cv2.putText(img, class_names[int(cid)], (x1, y1 - 5), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 255, 0), 1) out verify_ os.path.basename(img_path) cv2.imwrite(out, img) print(saved, out)这个脚本不要加坐标clip目的就是暴露越界问题。从train、val、test三个集合里各随机抽5到10张逐眼对比XML原框和还原框两者应该肉眼看不出差别。框完全对不上先查归一化分母是不是用了depth框整图偏移查中心点公式里是不是把加号写成了减号。6.2 把转换过程写进文档四个必写块文档说明不是写给同事看的是写给两个月后的自己看的。我组织README时只保留四块内容第一命令块把xml_to_yolo.py和split_dataset.py的完整命令原样贴进去参数值写死成实际用过的第二类别文件注明classes.txt的版本和来源并写一句“顺序不可改动”第三分割记录写明切割时的seed、三个比例、每个集合的样本数第四已知边界例如空标签已过滤、难例标签被保留、序列泄漏已按组切分。这样一段文档加上三个脚本整个数据准备链路就能在任何一台新机器上复现。我自己养成的习惯是每次转换完先抽图对拍再花十分钟把seed和classes.txt写进README。看着多花了十分钟实际省下的是训练中途怀疑数据准备错误的整整两三天。希望帮到你。本文还有配套的精品资源点击获取
网站建设高端定制企业官网
RELATED

相关资讯

更多精彩内容,欢迎继续阅读

较早相关资讯

最新相关资讯

成人和小孩检测数据集实战:从1738张YOLOv9标注到70.9%识别率提升 2026/9/28 5:44:48

成人和小孩检测数据集实战:从1738张YOLOv9标注到70.9%识别率提升

简介:这份成人与小孩图像数据集面向计算机视觉入门与目标检测实践者,尤其适合正在做YOLO系列模型训练、需要二分类人物属性识别素材的开发者与学习者。数据集共1738张原始图片,全部按yolov9格式完成标注,可直接接入YOLOv9训练流程…

阅读更多 →
SpringBoot+Vue招聘系统全栈实战:从权限设计到部署上线 2026/9/28 5:44:47

SpringBoot+Vue招聘系统全栈实战:从权限设计到部署上线

1. 毕设选题为什么要做招聘系统:一个既稳又耐打的全栈练手项目每年到毕设季,我都能收到一堆私信,问的大多是同一个问题:市面上那么多开源项目,电商、博客、商城、后台管理系统到处都是,为什么我建议做招聘平…

阅读更多 →
Python外卖配送数据分析与可视化系统:从数据清洗到ECharts大屏全流程 2026/9/28 5:44:46

Python外卖配送数据分析与可视化系统:从数据清洗到ECharts大屏全流程

又到毕业设计选题的时候了,后台天天有人问“外卖配送分析”这类题目怎么做。外卖配送确实是Python毕设里的常青树——业务场景好理解、数据维度丰富、可视化展示又直观,最关键的是整套流程踩中数据分析的所有核心环节:采集、清洗、建模、统计…

阅读更多 →
从IP地址入手排查线上故障:一次完整链路追踪复盘 2026/9/28 5:44:46

从IP地址入手排查线上故障:一次完整链路追踪复盘

凌晨两点四十,手机连着震了十几次,我爬起来看了一眼告警群:线上服务成功率掉到65%,响应时间已经逼近十秒,持续了快二十分钟。我第一反应不是打开日志追堆栈,而是先问了句自己——现在进来的流量&#xff0c…

阅读更多 →
3步搞定wordpress时间归档,一文搞懂防黑优化全攻略 2026/9/28 5:44:45

3步搞定wordpress时间归档,一文搞懂防黑优化全攻略

3步搞定wordpress时间归档,一文搞懂防黑优化全攻略 网站被黑挂马不知道怎么办?别慌,这种“中招”后的慌乱感,我见过太多新手站长。往往页面突然多出一堆乱七八糟的广告链接,或者被360、百度直接标记为“含有非法信息”,流量瞬间归零。这时…

阅读更多 →
从VS Code、Vim到Emacs:为什么这个近四十年编辑器仍值得学 2026/9/28 5:44:26

从VS Code、Vim到Emacs:为什么这个近四十年编辑器仍值得学

"还在学Emacs?那不是一个上世纪的东西吗?"这是我在技术交流群里反复听到的话,也是每次有人看到我的工作环境时最常给出的反应。作为一个用了十年Emacs、中间反复横跳过Vim和VS Code、最后还是回到Emacs常驻的深度用户,今…

阅读更多 →

今日资讯

本周资讯

本月资讯

看完文章仍有疑问?

联系尧图顾问,获取一对一建站咨询

立即免费咨询 📞 400-888-8888
📞 ✉