news 2026/10/20 16:10:11

目标检测标注格式转换实战:XML、COCO JSON与YOLO TXT互转方案

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
目标检测标注格式转换实战:XML、COCO JSON与YOLO TXT互转方案

做目标检测的,应该没有哪个没为标注文件头疼过。手头数据是从某个标注平台导出的,可能清一色是XML;拉到开源数据集一看,又是COCO的JSON结构;回头训练YOLO系列模型,框架指明要TXT格式的归一化坐标。三套格式来回倒腾,我用过网上不少现成脚本,也踩过不少坑,后来干脆自己整理了一套可靠的转换方案,今天把这个过程完整写出来,希望对正在被格式问题折磨的朋友有帮助。

这套方案解决的是目标检测数据预处理中最绕不开的环节:在Pascal VOC的XML标注、COCO的JSON标注、YOLO的TXT标注之间实现无损、可控、可复现的相互转换。内容适合刚入门目标检测、被数据格式折磨的新手,也适合需要批量处理大规模数据的开发者参考。

1. 三种标注格式的底层逻辑

1.1 它们分别来自哪里

先说清楚三套格式各自的“出身”,这决定了它们的字段含义和数据组织方式。

XML格式主要对应Pascal VOC数据集的标准,标注工具像LabelImg导出默认就是这套结构。它的特点是层次清晰、人类可读性好,每个<object>节点代表一个目标框,包含类别名称和左上角、右下角两对绝对像素坐标。

JSON格式对应MS COCO数据集的标注标准,现代标注工具如Labelme、X-AnyLabeling、CVAT导出时经常用这套。COCO的核心特点是所有信息都放进了一个大的JSON对象,里面分images、annotations、categories三个数组,通过ID进行关联。坐标在annotation的bbox字段里,注意它的格式是[x, y, width, height],也就是左上角加宽高,而不是两对角点。

TXT格式是YOLO系列框架约定俗成的标注形式,每张图片对应一个同名TXT文件,每行代表一个目标,格式是“类别ID 中心点x 中心点y 宽度 高度”,全部归一化到0到1之间。归一化坐标消除了分辨率差异,训练时无论图片缩放到什么尺寸,标注都能正确映射。

1.2 核心差异用一个例子说透

假设一张宽100像素、高200像素的图片上,有一只猫所在区域从坐标(20, 30)延伸到(60, 100)。

VOC的XML会写成这样:

<object> <name>cat</name> <bndbox> <xmin>20</xmin> <ymin>30</ymin> <xmax>60</xmax> <ymax>100</ymax> </bndbox> </object>

COCO的JSON里对应的是:

{"id": 1, "image_id": 1, "category_id": 1, "bbox": [20, 30, 40, 70]}

这里40是宽度(60减20),70是高度(100减30)。

YOLO的TXT里是:

0 0.4 0.325 0.4 0.35

计算方式是中心点x等于(20加60)除以2再除以100,得到0.4;中心点y等于(30加100)除以2再除以200,得到0.325;宽度是40除以100得到0.4;高度是70除以200得到0.35。

这三组数字描述的是同一个标注框,但表达方式完全不同。转换的本质,就是在“对角点坐标”和“中心点加宽高”之间换算,同时决定要不要除以图片宽高做归一化。

1.3 实际场景中什么时候需要转换

最常见的场景有三个。第一,开源数据集是COCO格式,但你用的是YOLO框架,需要把JSON转成TXT。第二,标注团队用LabelImg产出XML,但你的训练管线只吃COCO格式,需要把XML转成JSON。第三,数据增强或模型集成时需要统一到一种格式,避免维护多套标注体系。

我自己遇到最头疼的情况是接手一个历史项目,标注文件三种格式混着放,训练前光统一格式就花了大半天。从那之后我决定自己维护一套转换脚本,把所有格式都清洗规范,这件事后来帮我省下了大量重复劳动。

2. 转换方案的整体设计与技术选型

2.1 为什么不能只写两两转换的脚本

XML转JSON、JSON转TXT、TXT转XML,看着是六个脚本,实际维护起来就是灾难。这些脚本之间大量重复代码不说,更难受的是逻辑散落在各个文件中,改一个坐标计算逻辑要动六个地方,容易改出不一致。

我采用的思路是“统一中间表示”。设计一个Python字典结构作为中转站,内部统一用绝对坐标的[x1, y1, x2, y2]即左上角右下角表示,类别统一用字符串名称。每种输入格式只负责把标注解析成中间表示,每种输出格式只负责把中间表示写为目标格式。

这样做的好处是在架构上降低了耦合度。新增一种格式只要写两个解析和导出函数,老格式完全不受影响。

2.2 图片尺寸从哪里获取

YOLO的TXT格式和COCO的JSON都有归一化坐标需求,而JSON里的images字段本身就包含了width和height,读取时可以直接拿到。但XML格式里不存图片尺寸,转成YOLO时必须另想办法。

最稳妥的方式是读取图片文件本身拿到真实的宽高,通过PIL或者OpenCV读取图片的尺寸信息。这样能确保尺寸值一定是对的,但代价是速度比较慢,处理万张图片集时要等一会儿。如果图片集是固定分辨率,我可以先在XML里存一份尺寸信息作为缓存,但我不推荐盲目信任XML里的字段,因为很多标注工具并不把这个字段写完整。

图像尺寸获取要放在转换脚本最核心的位置,因为后续所有的归一化计算都要以此为基准,一旦这里错了,整批标注框都会偏移。

2.3 类别管理策略

YOLO的TXT格式只存类别ID不存类别名,所以类别ID到类别名称的映射关系必须单独维护。我习惯在项目里维护一份classes.txt,每个类名占一行,行号就是类别ID。也可以是YAML或JSON结构,核心是保持“类别名到ID”的映射稳定。

转换前先扫描数据集里出现的所有类别名称,自动建一个类别列表,处理多数据集合并时尤其管用。但注意,如果是多个来源的数据合并,一定要人工检查类别列表的顺序一致性,不能依赖自动生成的顺序,否则A数据集的“猫”和B数据集的“猫”可能ID对不上。

2.4 目录结构设计建议

我的实践是用images、labels、annotations三个目录分别存放图片、YOLO的TXT、XML或JSON标注。转换脚本通过命令行参数指定输入目录、输出目录、输入格式、输出格式,灵活切换。

dataset/ ├── images/ # 原始图片 ├── annotations/ # 存放XML或JSON └── labels/ # 存放YOLO TXT

这种目录结构的好处是格式之间转换时,图片目录不需要动,只需要在annotations和labels两个目录间同步生成新的标注文件。

3. 核心代码实现

3.1 公共中间表示定义

在设计中间表示时,我定义了一个统一的标注对象结构,每张图片对应一份字典:

image_annotation = { "filename": "000001.jpg", "width": 100, "height": 200, "objects": [ {"name": "cat", "bbox": [20, 30, 60, 100]}, {"name": "dog", "bbox": [70, 20, 95, 80]} ] }

所有具体的解析函数都把XML或JSON格式转成这个结构,所有导出函数都从这个结构出发生成目标文件。这个数据模型是整个工具链的地基,写的时候要考虑清楚字段名,后面不要轻易改动。

3.2 XML转成统一结构

解析XML用Python标准库xml.etree.ElementTree即可,不需要额外安装依赖。核心逻辑是遍历每一个<object>节点,把<bndbox>下的四个坐标值取出来。需要注意,XML标签名和大小写要完全对上,如果解析不到任何对象,多半是标签结构不同。

import xml.etree.ElementTree as ET def parse_xml(xml_path): tree = ET.parse(xml_path) root = tree.getroot() filename = root.findtext("filename") size_node = root.find("size") width = int(size_node.findtext("width")) height = int(size_node.findtext("height")) objects = [] for obj in root.findall("object"): name = obj.findtext("name") bndbox = obj.find("bndbox") xmin = float(bndbox.findtext("xmin")) ymin = float(bndbox.findtext("ymin")) xmax = float(bndbox.findtext("xmax")) ymax = float(bndbox.findtext("ymax")) # 统一的中间表示使用 xyxy 格式 objects.append({ "name": name, "bbox": [xmin, ymin, xmax, ymax] }) return { "filename": filename, "width": width, "height": height, "objects": objects }

解析时有个细节:坐标值在XML里可能是整数,也可能是浮点数,读取后用float()而不是int()转换,防止某些标注工具导出带小数的坐标时被截断。

3.3 JSON转为统一结构

COCO格式的JSON解析稍微麻烦一点,因为它的数据结构是扁平的,images和annotations是两个独立数组,要靠id字段关联。所以解析时要先构建image_id到图片信息的映射,再把annotations按image_id聚合。

import json def parse_coco_json(json_path): with open(json_path, "r", encoding="utf-8") as f: coco_data = json.load(f) # 类别ID到名称映射 cat_id_to_name = {} for cat in coco_data["categories"]: cat_id_to_name[cat["id"]] = cat["name"] # 图片ID到信息映射 image_id_to_info = {} for img in coco_data["images"]: image_id_to_info[img["id"]] = { "filename": img["file_name"], "width": img["width"], "height": img["height"] } # 聚合标注对象 anns_by_image = {} for ann in coco_data["annotations"]: image_id = ann["image_id"] if image_id not in anns_by_image: anns_by_image[image_id] = [] x, y, w, h = ann["bbox"] name = cat_id_to_name.get(ann["category_id"], "unknown") anns_by_image[image_id].append({ "name": name, "bbox": [x, y, x + w, y + h] }) result = [] for image_id, info in image_id_to_info.items(): result.append({ "filename": info["filename"], "width": info["width"], "height": info["height"], "objects": anns_by_image.get(image_id, []) }) return result

这段代码会返回一个列表,因为COCO的JSON可以包含多张图片的信息,而单个XML文件只描述一张图。整体结构上JSON的信息密度更高,一个文件承载整个数据集的标注。

3.4 从统一结构导出YOLO格式

导出YOLO的TXT时,关键步骤是坐标换算。中心点坐标是取上下左右两端的均值,宽高是两端坐标的差值,最终都要除以图片宽高做归一化。图片宽高来自中间表示里保存的尺寸信息,不依赖外部读取图片文件,所以速度较快。

def export_yolo_txt(image_ann, class_list, output_path): width = image_ann["width"] height = image_ann["height"] lines = [] for obj in image_ann["objects"]: class_name = obj["name"] if class_name not in class_list: class_list.append(class_name) class_id = class_list.index(class_name) xmin, ymin, xmax, ymax = obj["bbox"] # 计算中心点与宽高,并归一化 center_x = ((xmin + xmax) / 2) / width center_y = ((ymin + ymax) / 2) / height box_width = (xmax - xmin) / width box_height = (ymax - ymin) / height lines.append(f"{class_id} {center_x:.6f} {center_y:.6f} {box_width:.6f} {box_height:.6f}") with open(output_path, "w", encoding="utf-8") as f: f.write("\n".join(lines))

归一化坐标保留几位小数是个经验问题。我保存6位,在宽高上万像素的图片上精度已经足够。保留位数越多文件越大,但实际训练时浮点数加载后差异微乎其微。

这里有个容易踩的坑:class_list在脚本运行期间要保持稳定。如果在转换过程中动态往列表里追加类别,前面图片已经写好的TXT类别ID就不会变,但新增类的ID会接在后面,最终导出结果和class_list的最终顺序是一致的,只要确保导出完成后再保存class_list为classes.txt就可以。

3.5 从统一结构导出XML和JSON

导出XML其实是对中间表示的逆变换,把中心点加宽高还原成左上角右下角坐标。编写时用ElementTree构造XML树,注意节点的嵌套顺序要符合VOC的格式规范。

导出COCO格式时,需要为每张图片生成ID、为每个标注框生成ID,并且统计类别列表生成categories字段。这些ID建议用确定性规则生成,比如图片ID是文件名按顺序递增,这样才能保证多次转换结果一致,Git追踪时不会产生大量无意义的diff改动。

3.6 命令行主流程整合

把所有功能集成到一个脚本入口,通过参数指定转换方向。我用的是标准库的argparse,不需要额外引入click或fire,降低环境依赖。

import argparse import os def main(): parser = argparse.ArgumentParser(description="目标检测标注格式转换工具") parser.add_argument("--input_dir", type=str, required=True) parser.add_argument("--input_format", type=str, choices=["xml", "json", "txt"], required=True) parser.add_argument("--output_dir", type=str, required=True) parser.add_argument("--output_format", type=str, choices=["xml", "json", "txt"], required=True) parser.add_argument("--classes_file", type=str, default="") args = parser.parse_args()

这里建议主流程设计得简单直接,不要嵌套太多抽象层级。转换脚本的价值在于稳定可靠,架构上花哨没有意义。

4. 实际转换过程中的常见问题

4.1 坐标精度和边界问题

YOLO格式归一化到0到1后,理论上坐标值不应超出这个范围。但我遇到过很多次因为标注框部分超出图片边界导致的异常值,比如某人体的头部框打到了图片边缘外,xmax比width还大,归一化后变成1.02。

这类数据直接拿去训练,模型会在训练时报错或者损失异常。我的建议是在转换脚本中加入一个过滤或修正步骤,把超出边界的坐标裁剪回合法区间。裁剪的逻辑很简单:xmin小于0则置0,xmax大于width则置width。

但要注意,有些情况下目标框超出边界是标注错误,直接裁剪会掩盖错误。我倾向于在裁剪的同时把异常情况打印出来,输出到一个日志文件,转换结束后人工检查有没有大量边界问题。

4.2 类别映射不一致

多数据集合并时,类别列表的冲突是最大的坑。两个数据集都有“汽车”,但A数据集的类别ID是1,B数据集的类别ID是2,直接拼接会导致模型学到错位的语义。

我采取的方案是转换阶段就统一类别映射表。维护一个全局的类别字典,比如{"cat": 0, "dog": 1, "car": 2},所有输入格式在解析时就把类别名称映射到这个统一字典上,而不是沿用各自数据集的原始ID。输出TXT时再根据这个统一字典生成ID。

类别名称里的大小写、空格、下划线也是常见问题。比如一个数据集标注为“Cat”,另一个是“cat”,合并时会被当作两个类。脚本里做一个简单的字符串规范化处理,全部转小写并去空格,能减少很多麻烦。

4.3 图片文件名对应不上

YOLO的TXT文件和图片同名,只是后缀不同。但有些数据集里图片叫000001.jpg,标注文件叫000001.xml,看起来对应没问题,实际上标注内容的filename字段写的可能是img_0001.jpg,乱套了。

我的处理方式是转换时以实际扫描到的文件为准,不依赖标注内部记录的filename字段。遍历图片目录生成文件名集合,然后对每个图片文件查找对应的标注文件。标注内部的filename字段只做参考,不用于文件匹配。

4.4 批量处理性能优化

处理几千张图片时,如果每张图片都重新解析一次类别列表,性能会很差。我的优化策略是:先扫描所有标注文件,建立类别名称到ID的映射表,然后把映射表传入转换函数使用,避免每张图都动态扩展列表。

另一个性能瓶颈是用PIL读取图片尺寸。如果数据结构里已经包含宽高,如XML的<size>节点或COCO的images字段,我直接读取,不重复读图片文件。只有当缺失尺寸信息时才回退到读取图片,这样能大幅减少IO开销。

5. 转换脚本的质量保证措施

5.1 转换后必须回读校验

我每次批量转换后都会做一个回读校验,把输出的文件再解析成中间表示,和原始输入做一个数值对拍。比对坐标是否在允许的误差范围内,类别名称是否一致,图片数量是否对齐。

这个环节看起来费时间,但能发现很多隐蔽问题。比如某个XML漏了<size>节点,导致宽度高度读成0,归一化产生除零错误,输出TXT全是inf或NaN。这种错误如果不校验,直接进入训练,损失函数直接崩溃。

校验脚本本身写起来很简单,先读取原标注,再读取新标注,比较两类对象数量和各数值的差值。允许误差设为1e-4即可,因为坐标换算是浮点操作,存在极微小的精度损失是正常的。

5.2 抽样可视化标注框

数值对拍能检查坐标是否正确,但看不清“语义上”是否正确。我建议从转换后的数据中随机抽几张图片,画上标注框看一眼。画框用OpenCV的rectangle函数,一行代码就能标记出坐标位置。

根据我的经验,这种可视化检查往往能发现数值校验发现不了的问题。比如某个标注框的类别名称没错、坐标数值也对,但画出来发现框偏移到了完全无关的位置,那大概率是不同图片的标注文件和图片文件配对错乱导致的。

5.3 增量转换与断点处理

数据量大的时候,一次脚本跑不完很常见。我在设计脚本时加入了简单的断点机制:转换前先查看输出目录里是否已经存在目标文件,如果存在就跳过。这样脚本中断后重新运行,不需要重头开始。

当然这个机制有个前提,就是转换过程必须是确定的:即输入相同,输出相同。我的代码里所有ID生成都基于固定规则,不使用随机数,所以幂等性满足,断点续传不会产生不一致。

6. 完整脚本结构说明与扩展思路

6.1 文件组织建议

整个工具我建议分成两个文件:核心逻辑模块和命令行入口。核心逻辑模块包含各种格式的解析与导出函数,命令行入口只负责参数处理和流程编排。这样测试时可以直接import函数做单元测试,不用反复跑命令行。

工程的代码量不大,大概400行左右,但结构和注释要写好,因为这类脚本往往要长期维护。我在函数注释里说明了坐标的约定格式,在文件顶部写清了整体流程,也会定期来更新,防止过几个月自己都忘了某个函数是干嘛的。

6.2 如果面对的是新格式

现在标注格式远不止这三类,像Labelme的JSON、五十音图标注、DOTA的TXT格式,都是常见的变种。但只要理解了这套“统一中间表示”的思路,换一种新格式无非是写一个解析函数加一个导出函数。

我实际做过一个项目的DOTA格式转YOLO,DOTA用四个顶点坐标表示任意四边形目标框,而我用的检测器只需要水平矩形框。我的做法是把四个顶点的坐标取最小外接矩形,转换成水平框后再走统一的导出流程。虽然损失了一点精度,但对水平框检测器来说已经足够。

6.3 从转换延伸到数据管线

标注格式转换只是数据预处理的起点。我用这套工具跑通转换流程后,后面陆续加了数据划分、数据增强、类别统计等功能。整个数据管线的第一步就是从原始标注到统一格式,之后所有环节都围绕统一格式展开,各模块间不需要再关心格式兼容问题。

这套架构帮我节省的时间是实实在在的。之前每次拿到新数据集都要写一个一次性脚本,用完丢弃,下次遇到类似问题又从头写。现在只需要把新数据集的解析函数补充到转换工具里,整个流程顺下来就能用,维护成本大大降低。

7. 写在最后的实操经验

我的实际体会是,标注格式转换这件事看起来简单,但因为涉及批量数据、潜在异常,实际执行时坑特别多。我踩过最深的坑是一次转换时没做校验,直接拿结果去训练,训练集和验证集的标注框错位,模型训了几天才发现数据有问题,白白浪费了时间。

从那之后我定下一条规矩:任何格式转换都必须走完“数值对拍加抽样可视化”这套验证流程,才能进入训练环节。流程虽然多花了十分钟,但换来的是整个训练周期的稳定安心。

最后分享一个小技巧:转换脚本中所有路径相关的参数,都不要在代码里写死,全部通过命令行参数传入。这能让同一个工具在多个数据集上复用,不用每次复制粘贴后手动改路径。还有,建议给转换脚本加上日志输出功能,记录转换数量、异常数量、耗时等信息,出问题时排查起来方便得多。

这些工具代码我维护了很久,中间多次调整过细节,但整体架构一直没变。如果你也要处理多格式标注数据,建议直接按这个思路搭一套自己的工具链,前期稍微花点时间,后面就能一劳永逸。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/20 16:07:50

兰伯特光照模型从原理到Shader落地:漫反射、法线校正与避坑指南

简介&#xff1a;一份面向航天动力学研究者和轨道设计人员的MATLAB工具包&#xff0c;聚焦于求解兰伯特&#xff08;Lambert&#xff09;转移轨道问题&#xff0c;适用于星际任务轨迹规划、地月转移或行星际变轨策略分析。压缩包内共15个m文件&#xff0c;体积仅5KB&#xff0c…

作者头像 李华
网站建设 2026/10/20 16:05:14

diagram-design:从装饰图表到可执行系统契约的工程实践

1. 项目概述&#xff1a;这不是画图&#xff0c;是构建可执行的系统逻辑骨架“diagram-design”这个词组乍看像美术课作业&#xff0c;但在我过去十年带过的几十个跨领域项目里&#xff0c;它从来不是PPT里的装饰性插图&#xff0c;而是工程师、产品经理、硬件调试员和教学设计…

作者头像 李华
网站建设 2026/10/20 16:04:45

C#遗留系统现代化:从存量盘点到迁移服务化的实战路线

我非常确定&#xff0c;这不是一个"该不该重写"的问题&#xff0c;而是一个"怎么把存量代码从旧容器里救出来"的问题。MWGA是我在一个遗留系统现代化专项里用的内部代号&#xff0c;当时我手里那套C#业务系统&#xff0c;累计代码量已经到了几十万行&#…

作者头像 李华
网站建设 2026/10/20 16:03:15

不靠AI写脚本:蹲点访谈如何打造有真实感的企业宣传片

我们拍宣传片有句老话&#xff1a;脚本不是写出来的&#xff0c;是“问”出来的。最近我和独立导演“山间清风”聊了一次&#xff0c;他正在为三家不同行业的企业制作宣传片&#xff0c;全程没有用AI生成脚本&#xff0c;而是花了一周多时间蹲在客户现场&#xff0c;每天跟老板…

作者头像 李华
网站建设 2026/10/20 16:02:55

LeetCode 736题解析:Lisp表达式递归下降求值与作用域管理

当初在刷题平台上刷到第736题的时候&#xff0c;我差点把它当成一道"只要递归就能过"的简单题。题目要求实现public int evaluate(String expression)&#xff0c;输入是一段 Lisp 风格表达式&#xff0c;输出是一个整数。等你真动手写&#xff0c;才会发现它把"…

作者头像 李华
网站建设 2026/10/20 16:01:26

AI芯片算子库跨平台适配:硬件抽象层HAL设计与实践

做AI加速卡软件栈的人&#xff0c;基本都绕不开ops-math这类数学算子库。它看着不起眼&#xff0c;却在每一次模型推理、每一次张量运算里被反复调用&#xff1b;而让这套算子在不同芯片之间跨平台适配&#xff0c;背后真正起作用的&#xff0c;不是某个神奇编译器&#xff0c;…

作者头像 李华