资讯动态

目标检测数据集格式转换全指南:VOC、COCO、YOLO 互转原理与 Python 实现

发布时间:2026/9/17 12:42:49 来源:尧图企业网站定制
简介这是一份面向目标检测算法工程师、数据标注人员及计算机视觉初学者的数据集制作与格式转换工具包围绕VOC、COCO、YOLO三类主流数据集的构建、解析与互转需求提供了可直接运行的Python脚本覆盖XML/JSON解析、边界框坐标换算、类别映射等关键环节。压缩包共139个文件以jpg图像、txt标注、xml注释、py脚本和json文件为主并附带说明文档整体仅3.79MB轻量实用。资源将VOC的XML标注、COCO的JSON结构与YOLO的txt格式统一梳理同时提供图像与标注示例使用时可按需抽取脚本完成批量转换也可对照示例快速理解格式差异与制作流程。已有2754人学习下载适合正在制作私有数据集、或需要在不同检测框架间迁移数据的开发者能显著减少手工标注和格式对齐的时间成本提升模型训练前的数据准备效率。1. 为什么数据集格式决定了训练效率先描述一个常见场景你从开源项目里下载了带标注的图片集打算跑一次 YOLO 训练但对方给的标注是 VOC 的 XML或者你手里只有 COCO 的 json却要训练一个只吃 YOLO 格式的检测器。网上找的转换脚本往往只适配特定类名或目录结构改了半天结果框全偏到图片外。目标检测数据格式看起来五花八门内核其实只有三个变量标注文件用什么容器装、目标框坐标用什么方式表达、类别用什么编号索引。把这三个变量搞清楚了VOC、COCO、YOLO 之间的转换就是套公式。本文会讲透三种格式的结构差异给出可复现的制作流程再给出一套能直接改用的 Python 互转脚本。2. VOC、COCO、YOLO三种主流格式的目录与标注结构转换脚本出错十次有九次是对原始格式的理解有偏差。这一章先把三种格式的“物理形态”和“数据字段”掰开看。2.1 VOCXML 与目录约定Pascal VOC 格式的特点是“一图一 XML”目录分工明确JPEGImages放原始图片Annotations放对应的标注 XMLImageSets/Main放训练集与验证集的划分文件通常是train.txt、val.txt。每张图片对应一个同名的 XML核心结构如下annotation folderJPEGImages/folder filename000001.jpg/filename source databaseUnknown/database /source size width1920/width height1080/height /size object nameataxia/name difficult0/difficult bndbox xmin10/xmin ymin20/ymin xmax100/xmax ymax120/ymax /bndbox /object /annotationsize给出的是原始图片的宽高这在转换时是必须用到的信息。bndbox里面的四个坐标是绝对像素值且要求xmin xmaxymin ymax。difficult是 VOC 特有的标志位表示难例样本很多训练框架会直接忽略该目标转换时也可以按业务需求决定是否保留。注意解析 XML 时不要只读filename和bndboxsize的宽高必须与图片真实分辨率一致。否则 COCO 或 YOLO 的归一化坐标会整体偏移。2.2 COCO一个 JSON 描述整个数据集COCO 格式把整份数据集的图片信息和标注信息集中到一个 JSON 文件里内存加载后可随机访问任意图片这也是很多现代检测框架如 Detectron2、MMDetection默认采用它的原因。{ images: [{ id: 1, file_name: 000001.jpg, width: 1920, height: 1080 }], annotations: [{ id: 1, image_id: 1, category_id: 2, bbox: [10, 20, 90, 100], area: 9000, iscrowd: 0 }], categories: [{ id: 0, name: other }, { id: 1, name: ataxia }] }COCO 的bbox是[x, y, width, height]左上角加宽高而不是 VOC 的左上右下。category_id在 COCO 官方规范里从 1 开始但很多第三方数据集会从 0 开始转换前必须检查。area字段在官方评测中有意义如果iscrowd为 0 area通常等于width * height若iscrowd为 1表示该目标是一组重叠物体area可能不等于框面积这时需要单独处理。2.3 YOLO每个图片一个 TXTYOLO 系列从 Darknet 到 Ultralytics普遍使用的格式是“一图一 txt”图片images/000001.jpg对应labels/000001.txt每行描述一个目标。2 0.005208333333333333 0.018518518518518517 0.046875 0.09259259259259259 0 0.375 0.5 0.25 0.3第一个数是类别索引从 0 开始。后面四个数分别是目标的中心点 x、y 以及宽、高均除以图片原始宽高做了归一化所以理论上都在 0~1 之间。YOLO 格式本身不保存图片尺寸训练时由数据加载器读取图片来获取宽高因此目录结构里必须同时有图片路径和对应标签路径。三种格式的核心差异归纳如下维度VOCCOCOYOLO标注文件组织XML 按图片拆分单 JSON 文件TXT 按图片拆分框坐标左上右下[xmin, ymin, xmax, ymax]左上宽高[x, y, width, height]中心宽高归一化[x_center, y_center, width, height]类别表示字符串 nameinteger category_idinteger class_id从 0 开始是否含图片尺寸在 XML 的 size 节点在 images 数组中不包含常见适用场景自定义小数据集、传统检测大规模数据、评估评测YOLO 训练、嵌入式部署这些差异来自设计目标VOC 为离线竞赛而设计面向人眼检查COCO 面向海量数据的内存检索YOLO 面向训练时快速读取单张图片。理解了这一点遇到“为何 COCO 的 bbox 是 x,y,w,h”这类问题时就不会再去死记硬背。3. 制作目标检测数据集从原图到带格式标注格式原理清楚了下一步就是实际制作数据集。这一步如果做不好后面转换脚本写得再漂亮训练出来的模型也不可靠。3.1 图片整理格式统一、命名规范、损坏检查无论最终输出什么格式原图整理是地基。常见做法是把所有图片统一转成.jpg文件名改成 6 位数字000001.jpg并提前剔除损坏图片。find source_images/ -type f \( -name *.png -o -name *.JPEG \) \ -exec mogrify -format jpg {} \;说明find指定扩展名是为了避免重复处理 jpg-exec将匹配到的文件逐个交给 ImageMagick 的mogrify -format jpg处理。如果源文件很多建议配合xargs -P并行执行否则速度会很慢。转换后建议用 Python 做一次完整性校验from PIL import Image import os img_dir source_images for name in os.listdir(img_dir): path os.path.join(img_dir, name) try: im Image.open(path) im.verify() except Exception: print(f损坏文件: {path})im.verify()只校验图片文件头和编码结构不完整解码所以数千张图片也能在几十秒内扫完。这一步能避免后续训练时中途 crash。3.2 用 LabelImg 标注并保存为 VOC XMLLabelImg 是最常见的本地标注工具默认导出格式就是 VOC XML。启动后设置“保存目录”指向项目的Annotations打开JPEGImages目录开始标注。标注时有几个细节影响后续转换类别名统一用英文小写单词之间用下划线不要用空格。XML 中的空格会被解析成文本节点后面写脚本时容易出问题。框要贴合目标但并不要刻意切到每一个像素。比如“人”这个类别通常要框住头、躯干和四肢而不是只框躯干。快捷键习惯w画框a/d切换上一张和下一张CtrlS保存。建议每标完一张立即按CtrlS防止程序崩溃丢失标注。标注完成后用下面的脚本扫描所有 XML检查size与图片实际尺寸是否一致import xml.etree.ElementTree as ET import glob from PIL import Image for xml_path in glob.glob(Annotations/*.xml): root ET.parse(xml_path).getroot() filename root.find(filename).text img_path fJPEGImages/{filename} with Image.open(img_path) as im: real_w, real_h im.size size root.find(size) xml_w int(size.find(width).text) xml_h int(size.find(height).text) if (real_w, real_h) ! (xml_w, xml_h): print(f不一致: {xml_path} 图片({real_w},{real_h}) XML({xml_w},{xml_h}))这段代码还能顺带验证图片文件是否存在是数据集制作阶段的高频自查手段。3.3 用 CVAT 在线标注并导出 COCO 与 YOLO如果是团队协作或标注量较大我一般会改用 CVAT。CVAT 是开源 Web 标注工具用 Docker 部署在服务器上多人可以同时标注同一个任务。它支持在任务中定义标签并在完成标注后直接导出“COCO JSON”或“YOLO zip”。操作流程通常是创建任务 → 上传图片 → 定义标签类例如ataxia→ 在图片上画框 → 任务列表中点击 “Export” 选择目标格式。导出的 COCO JSON 中categories的 id 起始值可能随版本变化导出的 YOLO zip 里会附带一个obj.names文件记录类别顺序。这两个文件都要妥善保存后续转换时它就是你唯一的类别依据。提示CVAT 导出的 YOLO 格式有时会默认从类别 1 开始而 Darknet 和 Ultralytics 要求从 0 开始。拿到压缩包后先打开一个 txt 看一眼再决定转换脚本里是否要减 1。3.4 构建统一的训练目录无论是手工标注的 VOC XML还是 CVAT 导出的 COCO JSON我最终都会把它们整理成一个统一结构dataset/ images/train/000001.jpg images/val/000002.jpg labels/train/000001.txt labels/val/000002.txt classes.yaml这个结构直接可以被 YOLO 系加载器使用。如果原始格式是 VOC就先转成 YOLO如果是 COCO JSON也先转成 YOLO。把来自不同渠道的数据都归一化到这个目录后续换框架、做数据增强都会省很多事。classes.yaml内容很简单0: person 1: bicycle 2: car脚本从这份文件读取类别映射关系而不是在 XML 或 JSON 里临时提取。这样即使某张图没有出现某个类别也不会因为类别集合不完整导致转换后索引错乱。4. 用 Python 写一套 VOC、COCO、YOLO 互转脚本转换脚本是这个标题的核心。我会把常见转换方向VOC→COCO、COCO→YOLO、YOLO→VOC拆成三个独立函数方便在不同数据集上改参复用。4.1 坐标映射与字段映射转换本质上就是字段重命名和坐标重计算。先记住下面的映射表写代码时照着填就行数据项VOCCOCOYOLO图片信息size节点images数组外部图片路径目标框bndboxbbox每行后四项框坐标xmin,ymin,xmax,ymaxx,y,width,heightx_center,y_center,width,height类别name字符串category_id整数class_id整数对应公式VOC → COCOx xmin; y ymin; w xmax - xmin; h ymax - yminCOCO → YOLOx_center (x w/2) / image_width; y_center (y h/2) / image_heightYOLO → VOCxmin (x_center - w_norm/2) * image_width其余类推4.2 VOC 转 COCO 的 Python 实现import xml.etree.ElementTree as ET import json def voc_to_coco(xml_files, category_dict): images [] annotations [] ann_id 1 for img_id, xml_path in enumerate(xml_files, start1): root ET.parse(xml_path).getroot() size root.find(size) width int(size.find(width).text) height int(size.find(height).text) filename root.find(filename).text images.append({ id: img_id, file_name: filename, width: width, height: height }) for obj in root.iter(object): if int(obj.find(difficult).text) 1: continue # 难例不参与训练默认丢弃 cls obj.find(name).text bnd obj.find(bndbox) xmin int(bnd.find(xmin).text) ymin int(bnd.find(ymin).text) xmax int(bnd.find(xmax).text) ymax int(bnd.find(ymax).text) annotations.append({ id: ann_id, image_id: img_id, category_id: category_dict[cls], bbox: [xmin, ymin, xmax - xmin, ymax - ymin], area: (xmax - xmin) * (ymax - ymin), iscrowd: 0 }) ann_id 1 categories [{id: v, name: k} for k, v in category_dict.items()] return {images: images, annotations: annotations, categories: categories}这个函数的重点是category_dict。它由外部传入例如{ataxia: 1, person: 2}。enumerate(..., start1)是为了让img_id从 1 开始difficult被跳过的处理是大多数目标检测任务的默认选择。如果业务上需要难例把continue删掉并加一个ignore: 1字段即可。4.3 COCO 转 YOLO注意归一化和目录重建import json import os def coco_to_yolo(coco_path, output_dir): with open(coco_path) as f: coco json.load(f) os.makedirs(f{output_dir}/labels, exist_okTrue) img_info {img[id]: img for img in coco[images]} anns_by_image {} for ann in coco[annotations]: anns_by_image.setdefault(ann[image_id], []).append(ann) for img_id, anns in anns_by_image.items(): info img_info[img_id] img_name os.path.splitext(info[file_name])[0] label_path f{output_dir}/labels/{img_name}.txt with open(label_path, w) as out: for ann in anns: x, y, w, h ann[bbox] img_w, img_h info[width], info[height] x_center (x w / 2) / img_w y_center (y h / 2) / img_h nw w / img_w nh h / img_h # 防止贴边目标因浮点误差越界 x_center min(max(x_center, 0.0), 1.0) y_center min(max(y_center, 0.0), 1.0) nw min(max(nw, 0.0), 1.0) nh min(max(nh, 0.0), 1.0) out.write(f{ann[category_id]} {x_center:.6f} {y_center:.6f} {nw:.6f} {nh:.6f}\n)这里有个隐藏问题COCO 的category_id如果从 1 开始直接写入 YOLO 的 txt 就会导致类别从 1 开始但 YOLO 要求从 0 开始。所以在调用这个函数前需要先处理类别索引。常见做法是读取coco[categories]后把ann[category_id]重新编号成 0 到 N-1才能写入。4.4 YOLO 转 VOC反向转换需要补上图片尺寸反向转换最容易出错的点是YOLO 的 txt 里没有图片尺寸必须从磁盘读取图片实际分辨率。from PIL import Image import xml.etree.ElementTree as ET def yolo_to_voc(txt_path, images_dir, class_names): img_file os.path.join(images_dir, os.path.splitext(os.path.basename(txt_path))[0] .jpg) with Image.open(img_file) as im: width, height im.size root ET.Element(annotation) size_elem ET.SubElement(root, size) ET.SubElement(size_elem, width).text str(width) ET.SubElement(size_elem, height).text str(height) with open(txt_path) as f: for line in f: parts line.strip().split() if not parts: continue cls_id int(parts[0]) x_center float(parts[1]) * width y_center float(parts[2]) * height w_pix float(parts[3]) * width h_pix float(parts[4]) * height obj ET.SubElement(root, object) ET.SubElement(obj, name).text class_names[cls_id] bnd ET.SubElement(obj, bndbox) for tag, val in [ (xmin, x_center - w_pix / 2), (ymin, y_center - h_pix / 2), (xmax, x_center w_pix / 2), (ymax, y_center h_pix / 2)]: ET.SubElement(bnd, tag).text str(int(round(val))) return root注意x_center float(parts[1]) * width这里的parts[1]是归一化中心点换算回像素需要乘以图片宽。round()之后转int可以去掉多余小数但会导致框边缘有一两像素偏差对于 VOC 的 XML 来说是可以接受的。4.5 批量转换与类别映射文件实际项目中我建议把类别映射单独声明避免每次转换时重复扫描 XML。这里给出一个完整的调用示例假设要把整个Annotations目录转成 COCO JSONimport xml.etree.ElementTree as ET import glob xml_files sorted(glob.glob(Annotations/*.xml)) # 自动提取类别并保证每次运行顺序一致 all_categories set() for path in xml_files: root ET.parse(path).getroot() for obj in root.iter(object): all_categories.add(obj.find(name).text) class_names sorted(all_categories) # 这里从 0 开始适合后续再转 YOLO category_dict {name: i for i, name in enumerate(class_names)} coco_data voc_to_coco(xml_files, category_dict) with open(instances.json, w) as f: json.dump(coco_data, f, indent2, ensure_asciiFalse)indent2可以让人能直接阅读 JSONensure_asciiFalse防止中文类别名被转成\u编码。如果后续要训练的是 COCO 风格框架则把category_dict的start改成1并同步修改voc_to_coco调用参数。5. 验证转换结果可视化检查与 3 个容易踩的坑转换完成了必须验证。图形验证是最直接的手段能第一时间发现坐标偏移、类别串号等问题。5.1 用 OpenCV 快速可视化以 YOLO 格式为例把归一化坐标换算成像素画框import cv2 def draw_yolo_boxes(image_path, label_path, class_names): img cv2.imread(image_path) h, w img.shape[:2] with open(label_path) as f: for line in f: parts line.strip().split() if not parts: continue cls_id int(parts[0]) x_center, y_center, bw, bh map(float, parts[1:]) x1 int((x_center - bw / 2) * w) y1 int((y_center - bh / 2) * h) x2 int((x_center bw / 2) * w) y2 int((y_center bh / 2) * h) cv2.rectangle(img, (x1, y1), (x2, y2), (0, 255, 0), 2) cv2.putText(img, class_names[cls_id], (x1, max(0, y1 - 5)), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (255, 0, 0), 2) cv2.imshow(check, img) cv2.waitKey(0) cv2.destroyAllWindows()注意cv2.rectangle的坐标如果是负数或超出图片边界OpenCV 会自动裁剪但训练框架不一定容忍。所以可视化时看到红色文字和绿色框错位基本可以判断是坐标换算出了问题。5.2 三个容易踩的坑第一个坑COCO 的category_id起始值不统一。官方 COCO 从 1 开始但 CVAT 导出的可能从 0 开始自己写的 JSON 则完全取决于代码。转换前先打印coco[categories]确认 id 的区间再决定是否归一化到 0~N-1。第二个坑YOLO 归一化坐标越界或为 0。目标贴边时x_center - w/2可能小于 0导致在 YOLO 里变成负值。转换函数里最好加上numpy.clip或将极端值所在的行打印出来人工排查而不是盲目标记成 0。第三个坑图片路径与文件名不匹配。VOC 的filename可能带子目录COCO 的file_name可能来自不同的根路径而 YOLO 训练只认相对路径。转换后统一输出成纯文件名并用os.path.basename清洗一遍能省去很多环境迁移时的麻烦。5.3 一个小技巧转换前备份 classes.yaml我通常在项目根目录放一份classes.yaml记录类别并同步标注版本。每次转换时脚本优先读取这份文件而不是从 XML 或 JSON 中临时提取。这样当某一张图的类别缺失、某次标注漏标时转换脚本不会因为类别集合漂移而静默错位。把这份文件纳入 Git 管理回滚数据集版本时只需回滚它即可。下次再遇到“转换后框全乱了”的报错第一步就应该检查类别 id 是否还能和classes.yaml对上。本文还有配套的精品资源点击获取

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

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

免费获取报价