资讯动态

CSV转VOC2007 XML标注格式:目标检测数据预处理完整指南

发布时间:2026/10/19 7:47:49 来源:尧图企业网站定制
做目标检测的人十有八九都经历过这种尴尬标注平台导出一份CSV里面清清楚楚写着图片名、类别、坐标框看起来规整得很结果一喂给检测训练脚本就报错——人家要吃的是VOC2007格式的XML标注文件。我去年整理某跨平台检测项目的数据时光这一道转换就折腾了小半天后来把脚本理顺了才意识到这种“格式倒腾”才是检测项目里最耗时间的隐形工作。这篇内容就把CSV转VOC2007 XML的整个思路、代码实现、踩坑点一次讲明白。适合正在准备检测数据集的人也适合做数据增强时需要统一标注格式的朋友。1. 为什么要做这个转换两种格式背后是两套工作流1.1 CSV还停留在“表格思维”XML才是检测框架的通用语言CSV文件的本质是给人看、给表格软件看的一行一条记录用逗号分隔方便筛选和排序。但检测框架不一样它需要的是一个结构化的、能表达层级关系的标注格式——一张图里可能有多个人、多个车每个目标都有坐标、类别、是否被截断、是否难识别等信息。XML这种树形结构天然适合表达这种层级。很多检测工具和数据增强库读标注时直接写死了XML路径。比如离线增强脚本往往要求输入目录是Annotations放XML、JPEGImages放图这种VOC风格的结构。你手头只有CSV就得先把数据“翻译”成它们认识的格式然后才能跑增强流程。1.2 为什么不用现成转换工具非要自己写脚本网上确实能找到一些现成的CSV转VOC XML工具但这类工具一般只能处理很标准的表格比如正好是“图片名、类别、xmin、ymin、xmax、ymax”这六列。一旦你的CSV里混着width、height列或者坐标过来源是归一化格式或者是中心点格式现成工具基本就废了。自己写脚本最大的好处是可控你可以自定义类别映射表可以处理不同坐标体系可以批量检查异常甚至能把转换过程嵌入到整体数据增强pipeline里。这些是死工具给不了的。2. 写脚本前必须先搞懂的格式细节2.1 VOC2007 XML标注到底长什么样VOC2007格式的标注文件名字通常与图片名一致比如000001.jpg对应的标注文件是000001.xml。内容大致长这样annotation folderVOC2007/folder filename000001.jpg/filename path/data/VOC2007/JPEGImages/000001.jpg/path source databaseUnknown/database /source size width960/width height540/height depth3/depth /size segmented0/segmented object nameperson/name poseUnspecified/pose truncated0/truncated difficult0/difficult bndbox xmin120/xmin ymin180/ymin xmax480/xmax ymax520/ymax /bndbox /object /annotation重点字段里folder表示当前数据集分目录名很多检测框架不强校验但建议写对filename必须和实际图片名完全一致path是图片完整路径有的框架会读它最好写成绝对路径或者相对路径统一格式。size里是图片宽、高、通道数这个信息极其重要因为很多框架在resize或数据增强时需要按比例还原坐标。object是每个目标的完整描述name是类别字符串bndbox就是标注框的边界坐标。有些转换脚本会习惯把truncated和difficult都写成0如果标注平台里确实没有这些信息这样写没问题。但如果你在CSV里有专门的标记列就应该逐行对应放进去而不是一刀切。2.2 CSV的常见布局一种是有尺寸一种是没有尺寸我见过最多的CSV标注长这样filename,width,height,class,xmin,ymin,xmax,ymax 00001.jpg,960,540,person,120,180,480,520 00001.jpg,960,540,car,42,100,301,280 00002.jpg,1280,720,person,640,220,900,690同一个图片名出现多行每行代表一个目标框。这种格式好在每行都有完整的尺寸信息生成XML时可以直接把宽高填进去省去读图片文件的功夫。但也有很多标注工具导出的CSV只有四个相对坐标甚至只有文件名和类别没有图片尺寸。这时候生成XML时就得分两步第一步先从CSV里收集图片名反正每张图的尺寸是全局一致的第二步用图像库读一下真正图片的宽高再回填到size里。还有一类CSV里存的是归一化坐标也就是x_center, y_center, width, height都被除以了图片宽高数值都在0到1之间。转换的时候必须先乘以实际宽高还原成绝对坐标再填入bndbox。这一步最容易出错我后面会专门讲。2.3 坐标体系与类别表转换的底层逻辑写转换脚本前先问自己三个问题。第一我的CSV里坐标是xmin,ymin,xmax,ymax还是中心点加宽高如果是中心点格式转xmin要做一次减法运算xmin cx - w / 2ymin cy - h / 2xmax cx w / 2ymax cy h / 2。第二坐标是绝对像素还是归一化数值如果是归一化的先读图片宽高再乘回去。第三类别是直接写字符串还是有类别Id有的CSV里类别存的是数字比如0代表person1代表car而XML里需要存字符串那就必须有一张映射表。类别映射表建议单独存成一个配置比如在一个叫classes.txt的文件里逐行写person car dog然后脚本启动时读进来数字Id就是行号类别字符串就是内容。这样以后增删类别不需要改动转换脚本的代码只需要改这个文本文件就行。3. 实操实现从CSV到XML的完整脚本3.1 环境准备这一步不需要太重的依赖。Python自带的csv模块和xml.etree.ElementTree模块就能完成主要工作。如果你可能需要读取图片尺寸建议装上PIL或者OpenCV的Python绑定二选一即可。我用的是PIL因为轻量且够用。pip install pillow如果你的CSV头几行有中文或特殊编码建议明确指定CSV文件的读取编码一般标注平台导出的CSV常用UTF-8但偶尔会有用GBK的。读取时统一用utf-8如果报错异常再尝试gbk。这一点在后面的代码里会有体现。3.2 核心代码按图片名分组逐张生成XML转换的整体思路很直接用CSV的filename列作为分组键把同一个图片的所有标注行聚合到一起然后为每个图片生成一个XML文件。我用Python写一版完整的示例方便直接抄作业。import csv import os import xml.etree.ElementTree as ET from PIL import Image from collections import defaultdict def csv_to_voc(csv_path, img_root, xml_output_dir, classes_pathNone): os.makedirs(xml_output_dir, exist_okTrue) # 读取CSV同时兼容UTF-8和GBK try: f open(csv_path, r, encodingutf-8) except UnicodeDecodeError: f open(csv_path, r, encodinggbk) reader csv.DictReader(f) # 可选读取类别映射表 class_id_map {} if classes_path: with open(classes_path, r, encodingutf-8) as cf: for idx, line in enumerate(cf.read().strip().splitlines()): class_id_map[line.strip()] idx # 按图片名分组 groups defaultdict(list) for row in reader: groups[row[filename]].append(row) f.close() # 逐张图片生成XML for filename, rows in groups.items(): xml_name os.path.splitext(filename)[0] .xml xml_path os.path.join(xml_output_dir, xml_name) # 获取图片尺寸优先取CSV里的width/height否则读图 try: width int(rows[0][width]) height int(rows[0][height]) except (KeyError, ValueError): img Image.open(os.path.join(img_root, filename)) width, height img.size # 根节点 annotation ET.Element(annotation) folder ET.SubElement(annotation, folder) folder.text VOC2007 filename_elem ET.SubElement(annotation, filename) filename_elem.text filename path ET.SubElement(annotation, path) path.text os.path.join(img_root, filename) source ET.SubElement(annotation, source) database ET.SubElement(source, database) database.text Unknown size ET.SubElement(annotation, size) width_elem ET.SubElement(size, width) width_elem.text str(width) height_elem ET.SubElement(size, height) height_elem.text str(height) depth_elem ET.SubElement(size, depth) depth_elem.text str(3) segmented ET.SubElement(annotation, segmented) segmented.text 0 # 遍历该图片的所有目标行 for row in rows: obj ET.SubElement(annotation, object) name ET.SubElement(obj, name) name.text row[class] if class in row else str(row[id]) pose ET.SubElement(obj, pose) pose.text Unspecified truncated ET.SubElement(obj, truncated) truncated.text row.get(truncated, 0) difficult ET.SubElement(obj, difficult) difficult.text row.get(difficult, 0) bndbox ET.SubElement(obj, bndbox) xmin ET.SubElement(bndbox, xmin) xmin.text str(int(float(row[xmin]))) ymin ET.SubElement(bndbox, ymin) ymin.text str(int(float(row[ymin]))) xmax ET.SubElement(bndbox, xmax) xmax.text str(int(float(row[xmax]))) ymax ET.SubElement(bndbox, ymax) ymax.text str(int(float(row[ymax]))) tree ET.ElementTree(annotation) tree.write(xml_path, encodingutf-8, xml_declarationTrue) print(f已生成: {xml_path})这段代码有几处细节值得说明。第一读取CSV时先试着用UTF-8失败就改用GBK很多标注平台在Windows上导出的CSV老编码问题很常见这步能省去不少报错。第二图片尺寸优先取CSV里已有的width和height列万一没有这两列就用PIL打开图片读取这种方式虽然多花点时间但能在不修改表格的情况下生成正确的XML。第三坐标值统一用int(float(...))处理因为CSV里坐标字段经常被写成120.0这类浮点形式直接取整后填入XML比较干净也避免后续检测框架读坐标时出现类型问题。3.3 扩展支持中心点坐标和归一化坐标实际工作中你的CSV很可能不是标准的xmin,ymin,xmax,ymax布局而是cx,cy,w,h这种中心点格式或者是归一化格式。这时候不能死板套用上面的脚本需要先把坐标换算成边界框格式。假设CSV列名是cx,cy,w,h对应代码如下def cxcywh_to_xyxy(row, width, height, normalizedFalse): if normalized: cx float(row[cx]) * width cy float(row[cy]) * height w float(row[w]) * width h float(row[h]) * height else: cx float(row[cx]) cy float(row[cy]) w float(row[w]) h float(row[h]) xmin cx - w / 2 ymin cy - h / 2 xmax cx w / 2 ymax cy h / 2 return int(xmin), int(ymin), int(xmax), int(ymax)注意数值精度的问题。你要是直接对归一化坐标乘以整数宽高结果可能带有小数位最后一取整框的大小可能会有几个像素的波动。对于检测任务来说几个像素通常在可以接受的范围内但如果你的任务对标注精度要求极高建议在乘法后先四舍五入再取整比直接截断更稳。我用的是int(round(...))而不是直接int(...)。还有一种情况是CSV里存的是多边形顶点坐标而不是矩形框。如果是那种格式你需要先遍历所有顶点求横坐标最小值和最大值、纵坐标最小值和最大值缩成一个外接矩形再填入bndbox。这个过程不复杂核心就是xmin min([p[0] for p in points])这种写法但要注意顶点顺序和重复点的问题。3.4 目录组织按VOC2007标准摆放文件转换出XML还只是第一步更常见的问题是生成的XML和图片目录不被训练代码识别。VOC2007风格的数据集目录一般长这样dataset/ ├── Annotations/ │ ├── 000001.xml │ ├── 000002.xml ├── JPEGImages/ │ ├── 000001.jpg │ ├── 000002.jpg ├── ImageSets/ │ └── Main/ │ ├── train.txt │ ├── val.txtAnnotations放XMLJPEGImages放图片ImageSets/Main放划分好的图片名列表每行一个不带后缀的纯文件名。有的框架不强制要求ImageSets但很多基于VOC逻辑的检测代码会用这个目录读取训练和验证集。所以在转换脚本里我建议顺手把图片名写入ImageSets/Main下的txt文件里。如果CSV已经包含了某个字段标记训练集还是验证集那就按字段分别写入两个txt如果没有就统一写一个train.txt后续你自己再按比例抽样本划分。这里有个容易疏忽的点XML里的filename标签是带后缀的完整图片名而ImageSets/Main里的txt通常是不带后缀的裸文件名。检测框架在读取数据集列表时最终会用裸文件名拼出图片路径和标注路径如果你在txt里写了带后缀的名字而代码拼接时又加了一次后缀就变成000001.jpg.jpg那文件就找不到了。这个细节我曾真实踩过排查了半天才发现是txt里多了个后缀。3.5 转换结果可视化验证格式转换完不能直接开训首先要验证生成的XML和原图是否对应。最直接的方法是写个简单的可视化脚本把XML里的坐标框画到原图上肉眼核对。import cv2 import xml.etree.ElementTree as ET def draw_xml_on_image(xml_path, img_path): img cv2.imread(img_path) tree ET.parse(xml_path) root tree.getroot() for obj in root.findall(object): name obj.find(name).text bndbox obj.find(bndbox) xmin int(bndbox.find(xmin).text) ymin int(bndbox.find(ymin).text) xmax int(bndbox.find(xmax).text) ymax int(bndbox.find(ymax).text) cv2.rectangle(img, (xmin, ymin), (xmax, ymax), (0, 255, 0), 2) cv2.putText(img, name, (xmin, ymin - 5), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 255, 0), 1) cv2.imshow(result, img) cv2.waitKey(0) draw_xml_on_image(000001.xml, 000001.jpg)这段代码适合抽检。推荐每次转换完随机抽十张图跑一遍重点看三件事标注框是否完全落在图内框的大小和CSV里的原始坐标是否一致类别名称是否显示正常。如果发现框偏移出图片边界多半是坐标换算或者图像尺寸读错的问题这时候回头检查CSV里是否有归一化坐标没有还原。3.6 数据增强后的格式一致性维护标题里提到了“数据增强”这个转换在增强流程里的角色容易被忽略。做离线数据增强的时候比如随机裁剪、水平翻转、调整亮度对比度图片被改写了标注坐标也得跟着变。很多增强工具输出的标注就是VOC XML所以如果你手里只有CSV第一步就是先转成XML再喂给增强工具否则增强工具根本不认得你的标注格式。另外增强后的XML还要注意不能“丢了”原图的尺寸信息。比如随机裁剪后新的标注框坐标要基于裁剪后的图片尺寸来重新表示。增强工具一般会自动更新size节点但如果你是自己写增强脚本就要记住同步更新XML里的宽高和中点坐标不然训练时很可能出现越界或坐标错位的诡异问题。4. 常见问题与排查技巧实录4.1 生成的XML在Windows和Linux上都打不开或者中文乱码XML看起来能打开但在某些解析器里中文变成乱码多半是因为写入XML时没有指定编码或者源CSV编码格式没统一。解决办法就是我前面代码里用的tree.write(xml_path, encodingutf-8, xml_declarationTrue)同时确保CSV读取时真的解对了码。如果你发现CSV里中文本来就是GBK但你的代码按UTF-8读那生成XML里的类别名自然是一团糟而且这个问题通常不会立刻触发报错只有到了训练时才暴露。4.2 XML文件里出现了空节点用ElementTree构造XML时如果CSV某一行缺了某个字段你直接把该字段写成空字符串生成的XML里就会出现xmin/xmin。很多检测框架解析整数时会直接崩溃或者解析得到0。这个问题特别隐蔽因为你的转换脚本不会报错只有训练中段才炸。我习惯在写入前做一个校验函数把必填字段抽出来一旦发现空值就打印警告并跳过这条记录。虽然会牺牲一点效率但转换本身就是离线任务可靠性比速度重要得多。4.3 坐标越界框跑到图片外面去了坐标越界的根源主要有两类。一类是CSV标注本身就带了一点“出格”比如人工标框时手滑把坐标标记成负值或者框的右下角超过了图片边界。另一类是坐标换算错误常见于归一化坐标忘记乘回宽高。排查时可以写个简单的统计脚本遍历所有生成的XML判断xmin 0、ymin 0、xmax width、ymax height把不符合的全列出来。这个步骤强烈建议做因为越界框轻则导致减采样时出现无意义的训练样本重则让模型学到错误的边界回归目标。如果确实发现少量越界可以有两种处理思路一种是直接把坐标钳制到图内也就是xmin max(0, xmin)但这样做会改变框的真实位置通常还会连带改变框的宽高不建议大量使用另一种是把这张图的标注整体剔除返回给标注平台重新修一遍。对于严重越界的样本直接删除更安全。4.4 类别映射错误XML里的name和训练脚本的类别顺序对不上这是最常见、也最坑的问题。VOC的name标签是字符串而训练时需要将其转成数字类别Id。如果转换脚本里的类别表顺序和训练代码里的classes.txt顺序不一致那么同一个“person”会被解释成不同的Id模型基本就废了。解决思路是让转换脚本和训练脚本共用同一个classes.txt而不是各自维护。我在前面代码里专门留了classes_path参数就是从外部文件读取类别映射这样两边都引用同一份配置就永远不会对不上。如果你之前已经转了一堆XML突然发现类别顺序错了最省事的办法是重新转一遍不要试图批量改XML里的字符串映射关系容易漏改。4.5 图片文件名带空格或特殊字符文件名里带空格、括号、中文在Windows上是常规操作但转成XML后很多检测框架在处理路径拼接时会把空格当成分隔符或者因为编码问题找不到文件。稳妥的做法是转换前先统一改文件名建议全部改成纯数字加下划线的形式比如000001_1.jpg避免用中文和空格。如果只是偶尔几个文件名有特殊字符可以先写个改名脚本批量处理再转XML。如果你偏要保留中文文件名至少确保框架和增强工具都支持UTF-8但除非项目必须否则我不推荐在这个地方较劲。4.6 转换脚本在超大标注集上跑得很慢如果你有几万张图每张图有几十个目标逐行生成XML的速度也不算慢但读图片尺寸那一步会很拖时间因为PIL打开一张图要消耗不少IO。如果CSV里没有图片尺寸我的建议是先用一个脚本批量扫描JPEGImages目录把所有图片的宽高缓存到一个image_sizes.csv里然后转换时优先查表查不到再打开图片。这个方法能把转换时间从几十分钟缩短到几分钟。5. 几个值得记住的实操细节转换脚本看起来简单但实际用起来我总结下来有几条经验一定要记住。第一永远保留原始CSV的备份不要在转换脚本里直接覆写或修改原文件。格式转换本质上是派生数据CSV才是源数据。万一你转换后发现XML字段漏了、坐标错了源CSV还在重新转一次就行不用回标注平台重新导出。第二转换脚本的参数尽量留成外部配置比如CSV表头不一定总是class也可能是label或category坐标列也不一定叫xmin可能是x_left。写成可配置的表头映射比你写死更省心。代码可以这样写column_map { filename: filename, class: label, xmin: x_left, ymin: y_top, xmax: x_right, ymax: y_bottom, }然后在读取CSV时统一把列名替换成标准列名这样不管你拿到的CSV来自哪个标注平台只要改一份映射配置就行。第三转换完成后不要急着删除中间文件尤其是生成的可视化验证结果。哪怕你已经肉眼看过十几张图都没问题也建议把坐标统计信息存一份比如每个类别的框数量、平均框面积这样在训练前期怀疑标注质量时可以快速比对而不是重新生成数据。第四一定要把转换脚本和数据集版本关联起来。我的做法是在数据集的根目录放一个convert_info.txt写明用的转换脚本版本、源CSV的MD5值、转换时间。听起来有点仪式感但当你需要复现一个旧实验时这个文件能帮你快速判断当初用的数据是不是现在这套。最后再说一个后期扩展思路这篇文章只讲了CSV转VOC2007 XML但反过来也有不少人需要把VOC XML转成别的格式比如转成YOLO的txt格式或者转成标注平台的JSON格式。其实你只要理解清楚格式间的映射关系也就是把XML的树形结构再拍平回表格就能写出反向脚本。建议把这篇里的转换脚本和可视化验证脚本放在同一个工具包里后续扩展会顺手很多。

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

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

免费获取报价 →
↑