
简介一套完整的VOC格式目标检测数据集面向深度学习目标检测方向的入门与进阶开发者。数据集包含20个常见类别按VOC标准xml标注组织训练集13700张图片及对应标签、测试集3425张图片及对应标签同时附有检测类别json字典支持类别映射与训练配置。整个压缩包共2000个文件以1999个xml标注文件和1个可视化py脚本为主包大小约178.97MB图片与标注一一对应目录按train/test拆分结构清晰可直接接入YOLO、SSD等常见检测框架无需额外转换。附带的show.py脚本无需修改即可运行随机读取一张图片即可绘制边界框并保存到当前目录适合快速检查标注质量与数据分布。目前已有153人学习下载适用于需要标准VOC数据快速开展实验、课程设计或算法对比的开发者。1. 认识VOC20分类、xml标注为什么到现在深度学习训练还在用它做目标检测的人手头几乎都存过一份VOC数据集。这个标题里的“VOC目标检测数据集(20分类)”说的就是PASCAL VOC挑战赛留下的那套标准化数据资产配套的“VOC标注格式的xml文件”则是这套资产里最需要吃透的部分。它没有花哨的格式每个目标用一段xml记录类别和矩形框坐标却能让深度学习模型在统一口径下完成训练、验证和误差对比。如果你正在跑目标检测入门、想验证一个改过的检测头有没有效果、或者要接入别人给的标注数据VOC格式几乎就是绕不开的中转站。很多人问xml文件怎么打开和编辑其实就是用普通文本编辑器打开看结构、改坐标、换类别名本质是一份带标签的文本。这篇我按自己落地的顺序讲先拿到数据集、再解析xml、再对付20个类别映射、然后把坑一个个填平。2. 拿到VOC数据集目录结构、文件命名与本地核对2.1 VOCdevkit里到底装了什么常见做法是去PASCAL VOC官网按年份下载压缩包解压后会得到一个VOCdevkit目录。你在训练脚本里配置数据路径时指的就是这个目录。它的内部结构多年没变过但很多人一进去就迷失在几十个文件夹里其实需要关心的只有下面这几个。目录或文件内容训练时有什么用JPEGImages所有原始图片统一为jpg模型的输入图像Annotations与图片同名的xml标注文件提供每个目标的类别和坐标ImageSets/Maintrain.txt、val.txt、trainval.txt等决定哪些图片进训练集、哪些进验证集SegmentationClass语义分割的彩色标签图做分割才用检测可忽略SegmentationObject实例分割的标签图做分割才用检测可忽略打开ImageSets/Main里的txt每一行是一个图片文件名不带.jpg后缀、不带路径。比如trainval.txt里写的是000001这种纯文件名。这个设计坑过不少人脚本读txt时要注意加上路径和扩展名反之从JPEGImages遍历时又要去掉扩展名才能去txt里查。VOC最常见的是2007和2012两个版本两个版本类别完全一致都是20类。数据量上2007的trainval大约5011张图2012的trainval大约11540张图。很多人把两者合并起来用2007trainval2007test做训练、2012trainval做验证或者反过来。合并后类别数不变但图片数量大了不少深度学习模型的收敛稳定性和评测说服力都会更强。2.2 下载后先做三件事查数量、查配对、查大小拿到压缩包直接解压就开始训练是我见过翻车最多的操作。我一般会在训练前先跑一遍基础核对用bash几行就能完成。echo 图片数量: ls JPEGImages/ | wc -l echo 标注数量: ls Annotations/ | wc -l echo 无标注的图片: for img in JPEGImages/*.jpg; do name$(basename $img .jpg) [ ! -f Annotations/$name.xml ] echo $name done这里的逻辑是第一统计JPEGImages和Annotations下的文件总数两者理论上应该相等第二逐张检查图片是否有对应xml。如果无标注图片数量不为0说明数据集有残缺直接喂给模型会在读取标签时抛FileNotFoundError。注意basename $img .jpg会把文件名里的扩展名剥掉这样正好能拼出Annotations里应存在的xml路径。跑这个脚本不需要GPU几十秒就能出结果。除了数量配对还要看图片尺寸。有的数据集在压缩传输过程中图片被二次压缩但xml里记录的width和height还是原始值这会导致目标框整体偏移。简单做法是用Python批量读图片尺寸再和xml里的size节点比对不一致就重点排查。后文画框校验会再提这件事。2.3 ImageSets/Main里那些txt到底怎么用VOC官方把数据集划分写进txt而不是按目录分文件夹这让不少人栽跟头。你从网上下到的VOC数据集JPEGImages里是全部图片Annotations里是全部标注真正决定哪个文件用于训练、哪个用于验证的只有ImageSets/Main下的txt。echo train.txt 行数训练图片数: wc -l ImageSets/Main/train.txt echo val.txt 行数验证图片数: wc -l ImageSets/Main/val.txt echo trainval.txt 前5行: head -n 5 ImageSets/Main/trainval.txttrain.txt和val.txt的并集就是trainval.txt。很多框架比如各种YOLO系列的voc脚本会把train.txt和val.txt直接透传给数据加载器txt里每一行对应一张图。行数如果和你预期不符先检查是否有Windows换行符混入VOC官方文件是Unix换行如果有人转存过行尾会多出\r导致路径拼接后带了一个隐藏字符、文件读不到但报错又很隐晦。如果别人给的数据集没有txt也可以自己按比例生成。常见做法是先把所有图片文件名洗牌再按8:2或9:1切分写入两个txt。注意洗牌时要固定随机种子否则每次跑脚本分出来的训练验证集合都不一样实验结果就没法复现。我自己习惯把随机种子固定成固定数字保证数据划分稳定。3. 解析VOC标注格式的xml从根节点到bndbox3.1 一份标准xml长什么样VOC标注格式的xml文件是整个数据集的核心它把“图片里有什么目标、目标在哪”用纯文本结构化地表达出来。第一次接触的人建议直接用文本编辑器打开一个xml看不用装任何工具。下面这份是一个简化但字段完整的模板实际文件基本就是这个骨架。annotation folderVOC2007/folder filename000001.jpg/filename source databaseThe VOC2007 Database/database annotationPASCAL VOC2007/annotation imageflickr/image /source size width500/width height375/height depth3/depth /size segmented0/segmented object nameperson/name poseFrontal/pose truncated0/truncated occluded0/occluded difficult0/difficult bndbox xmin100/xmin ymin50/ymin xmax400/xmax ymax350/ymax /bndbox /object /annotation最关键的是annotation下的几个节点filename对应JPEGImages里的图片名size记录图宽高和通道数是坐标归一化时的依据每个object代表一个目标name是类别名bndbox里的xmin、ymin、xmax、ymax是目标矩形框的绝对像素坐标原点在图片左上角x轴向右y轴向下。注意最好是整数。truncated表示目标是否被图片边界截断occluded表示是否被遮挡difficult表示这个目标是否因太小、太模糊等原因难以辨认。初学者容易把xml当配置文件觉得只要格式长得像就行。实际上坐标字段的语义非常重要不是中心点坐标是边框四边的绝对位置。有人转换到COCO或YOLO格式时把xmin、ymin当中心点用模型loss低但预测框整体乱飘就是这个原因。3.2 用ElementTree批量解析xmlPython标准库里的xml.etree.ElementTree就能完成解析不需要引入额外依赖。深度学习项目里动辄上万个xmlElementTree完全扛得住没必要为此上lxml。下面这段代码读入一个xml输出图片信息和所有目标框。import xml.etree.ElementTree as ET def parse_voc_xml(xml_path): tree ET.parse(xml_path) root tree.getroot() filename root.findtext(filename) width int(root.findtext(size/width)) height int(root.findtext(size/height)) objects [] for obj in root.findall(object): name obj.findtext(name) difficult int(obj.findtext(difficult)) bndbox obj.find(bndbox) xmin int(bndbox.findtext(xmin)) ymin int(bndbox.findtext(ymin)) xmax int(bndbox.findtext(xmax)) ymax int(bndbox.findtext(ymax)) objects.append({ name: name, difficult: difficult, bbox: [xmin, ymin, xmax, ymax] }) return { filename: filename, width: width, height: height, objects: objects } if __name__ __main__: result parse_voc_xml(Annotations/000001.xml) print(result[filename], result[width], result[height]) for obj in result[objects]: print(obj[name], obj[bbox])代码本身不复杂但有两个容易写错的点一是findtext(size/width)这种带斜杠的路径写法可以跨层级取值省去先找size节点再找width节点的两行代码二是bndbox里的数值必须转成int有人拿到字符串直接喂给训练管线numpy计算时数据类型全乱掉。输出里我把difficult字段单独保留训练前要根据它在数据加载阶段做过滤后文避坑清单会再讲。用ET.parse直接读文件遇到非UTF-8编码的xml会在这一行抛异常。别人分享的数据集里偶尔混着手工编辑过的文件报错后你只能逐个查。更稳的写法是先读bytes再交给ET.fromstring但那样代码量会多一些多数项目里没必要。3.3 统计整个数据集有图片、有标注、有对象单看一个xml看不出数据集质量要一次跑完所有标注才心里有数。下面这段是我每次拿到新数据集必跑的统计脚本输出每个类别的目标数、出现该目标的图片数以及总目标数。import glob from collections import Counter xml_files glob.glob(Annotations/*.xml) class_counter Counter() image_with_class Counter() total_objects 0 for xml_path in xml_files: result parse_voc_xml(xml_path) seen set() for obj in result[objects]: total_objects 1 class_counter[obj[name]] 1 seen.add(obj[name]) for cls in seen: image_with_class[cls] 1 print(xml文件数:, len(xml_files)) print(目标总数:, total_objects) print(类目, 目标数, 含该类的图片数) for cls, count in class_counter.most_common(): print(cls, count, image_with_class[cls])这段的输出能直接暴露三类问题类别名拼写不统一比如person和Person同时存在某些类目标数少得可怜label分布极度倾斜。统计完我通常还会检查一下xml里的filename和实际文件名是否完全对应因为有的xml在复制过程中被改过名文件名对不上会导致加载图片成功但标签属于另一张图训练loss曲线看着正常验证指标却一塌糊涂属于最难查的隐性错误之一。4. 20个类别不是摆设类别映射、样本分布与迁移训练4.1 20个类别的标准顺序与名字VOC的20个类别名称是固定的但很多人不知道类别在代码里是有顺序的。下面按PASCAL VOC官方惯例列出这个顺序也是许多目标检测库默认的索引顺序。索引类别名索引类别名0aeroplane10pottedplant1bicycle11sheep2bird12sofa3boat13train4bottle14tvmonitor5bus15person6car16diningtable7cat17dog8chair18horse9cow19motorbike这个顺序不是随便排的它来自VOC挑战赛官方对类别字母排序后的结果。如果你把索引0当成person训练也不是不能跑但最后输出层的20个位置语义全错验证时画框会出现“人标成飞机”的诡异现象。4.2 为什么类别顺序会害死你的训练xml文件里的name是字符串“person”“car”而神经网络输出层是一个20维向量每一维对应一个类别。从字符串到索引的转换必须有一张固定的映射表。很多人拿到别人的数据集转换脚本里写死class_list [person, car, ...]和训练脚本里的定义顺序不一致结果就是模型一直在学错标签。我习惯把所有类别顺序集中定义在一个Python文件里训练和转换共用同一份。VOC_CLASSES ( aeroplane, bicycle, bird, boat, bottle, bus, car, cat, chair, cow, diningtable, dog, horse, motorbike, person, pottedplant, sheep, sofa, train, tvmonitor ) CLASS_TO_INDEX {cls: i for i, cls in enumerate(VOC_CLASSES)} def voc_name_to_index(name): if name not in CLASS_TO_INDEX: raise ValueError(f未知类别名: {name}) return CLASS_TO_INDEX[name]这段代码看起来平凡实际能挡掉很多问题。VOC_CLASSES的元组顺序决定索引编号CLASS_TO_INDEX用dict做反向查找转换时直接调用voc_name_to_index。如果xml里有脏数据比如类别名带了空格或大小写不一样函数会直接抛错而不是静默地给个错误索引。训练前把这段丢进数据加载模块能省下大量排错时间。迁移学习时也要注意这个顺序。从官方或第三方模型库下载在VOC上预训练好的权重其输出头是按上面顺序排列的。如果你改了类别顺序迁移过来的权重在输出层没有意义即使你把最后的检测头重新随机初始化前面的特征提取层仍然是通用的可以保留。这也是为什么我在任何实验里都固定用VOC官方顺序而不是按自己喜好重排。4.3 样本不均衡VOC里哪几类多、哪几类少VOC这个数据集本身并不是类别均衡的。跑一下3.3节的统计脚本就能看到person、car、chair这些类别的目标数明显偏高而cat、sheep、horse这类相对少。深度学习模型在小类别上AP往往会低几个点这属于数据集的天然属性不是你模型调参的问题。遇到类别不均衡我一般会看实际的训练目标。如果是自己练着玩或者做算法验证直接按原样训练就行如果是要做垂直场景的模型比如燃气管道图像或者电力红外检测VOC数据只适合做预训练真正的业务数据才是主菜。迁移时把VOC预训练权重加载进来冻结前几层只微调后面的层收敛速度会明显加快。5. VOC数据处理的避坑清单5条实操记录5.1 xml读出来中文乱码现象用ElementTree解析别人给的xml报ParseError或者print出来中文全是乱码训练脚本直接中断。原因VOC官方xml是UTF-8编码但有人用Windows记事本编辑过标签保存成了GBK或其他本地编码。Python的ET.parse默认按UTF-8解码遇到非法字节就抛异常。解决写一个编码自适应函数先读原始bytes尝试用UTF-8解码失败再用GBK解码。def parse_voc_xml_safe(xml_path): with open(xml_path, rb) as f: content f.read() try: text content.decode(utf-8) except UnicodeDecodeError: text content.decode(gbk) root ET.fromstring(text) # 后续解析逻辑与3.2节相同这里的关键是先解码成字符串再交给ET.fromstring而不是ET.parse因为ET.parse不接受按指定编码读取。遇到GBK文件时xml头里声明的encoding可能是UTF-8ET解析时会困惑人为指定解码能绕开这个矛盾。批量处理前先跑一遍这个函数做试错能提前暴露所有问题文件。5.2 图片和标注对不上现象训练日志里频繁出现FileNotFoundError或者loss在某个epoch后突然掉到接近0。检查后发现JPEGImages里有的图片叫IMG_001.JPG而Annotations里对应xml叫img_001.xml大小写不一致导致加载失败。原因VOCdevkit按Linux文件系统规范制作文件名区分大小写数据在Windows上转手时文件系统不区分大小写同名不同大小写的文件直接相互覆盖。解决下载后立刻做一次配对检查这一步在2.2节的bash脚本里已经做过。如果发现配对不上先用find命令找出所有大写扩展名或大写文件名的文件统一改成小写再重新核对。另一种常见情况是JPEGImages里混入PNG格式但后缀为jpg的图片看起来配对成功实际读取时解压出错。用file命令扫一遍能看出真实格式。5.3 坐标全对但画框整体偏移现象把标注画回图片上发现框的位置和物体错开有的偏左上有的偏右下且偏移量随目标位置变化。原因xml里size节点的width/height与图片真实尺寸不一致。很多工具把图片缩放后标注坐标还是原图坐标或者反过来坐标被缩放而图片是原图。VOC里坐标是绝对像素值必须和xml里记录的尺寸配套使用。解决批量核对xml里size节点的数值与图片真实尺寸。用OpenCV读取图片逐张与xml比对不一致就重新生成正确的size节点或按比例修正坐标。这个操作偶尔会遇到不属于常态但一旦碰上就是全局性错误训练再久也学不出好模型。5.4 mAP一直在0附近但loss正常现象训练时loss曲线平滑下降验证时mAP始终在个位数徘徊甚至等于0。换网络、调学习率都没用。原因VOC评价机制会忽略difficult1的目标。如果训练时把所有目标一股脑塞给模型而验证时又把difficult目标统计进去或者反过来评估口径就乱了。更常见的是转换脚本把difficult目标的坐标也写进label文件模型花大量能力去拟合那些本身模糊、被截断、难以辨别的目标。解决数据加载阶段过滤掉difficult1的目标。上面3.2节的解析函数里我已经保留了这个字段训练脚本里加一行过滤即可。同时确认评估时用的是VOC官方评价逻辑或者至少保证训练和评估对difficult的处理一致。5.5 文件传输后数量诡异地减少现象从网盘或U盘拷贝过来的数据集解压后JPEGImages里图片数量与官方对不上而且没有报错感觉就像数据凭空消失。原因常见于Windows下解压Linux压缩包。数据集中存在仅大小写不同的文件名比如a.jpg和A.jpgWindows解压时后一个文件直接覆盖前一个数量莫名其妙少掉一批。还有一种情况是网盘下载时文件被安全软件拦截个别xml或jpg静默缺失。解决在Linux环境下用unzip重新解压解压后跑2.2节的配对检查同时用zip -T测试压缩包完整性。数据集的原始压缩包建议保留一份不要反复修改后再分发减少转手过程中引入的污染。6. 把VOC转成YOLO格式并用画框校验标注质量6.1 转换脚本要处理的三件事YOLO系列训练时一般不直接读VOC的xml而是用每张图一个txt的标注文件每行格式为“类别索引 x_center y_center width height”其中坐标都是相对于图片宽高的归一化值。把VOC转成这个格式主要做三件事类别名映射成索引、绝对坐标转归一化坐标、按ImageSets里的划分输出到对应目录。import os def voc_to_yolo(xml_path, out_label_path, img_width, img_height): result parse_voc_xml(xml_path) lines [] for obj in result[objects]: if obj[difficult] 1: continue cls_id voc_name_to_index(obj[name]) xmin, ymin, xmax, ymax obj[bbox] x_center (xmin xmax) / 2.0 / img_width y_center (ymin ymax) / 2.0 / img_height w (xmax - xmin) / img_width h (ymax - ymin) / img_height lines.append(f{cls_id} {x_center:.6f} {y_center:.6f} {w:.6f} {h:.6f}) with open(out_label_path, w) as f: f.write(\n.join(lines))关键参数img_width和img_height必须来自xml里的size节点而不是脚本里写死的值否则不同尺寸图片会集体错位。归一化时所有数值都用浮点保留6位小数足够坐标值在0到1之间理论上不会超过这个范围但目标贴着边界时计算出的w或h可能略大于1一般不影响训练。类别索引必须按训练脚本里定义的顺序这里直接复用4.2节的voc_name_to_index保证两边的映射始终一致。转换完成后每个jpg对应一个同名txt。验证这一步是否成功不需要马上开训练先去数一下txt数量和jpg数量是否一致再用下面的画框脚本抽查。6.2 用画框校验当后悔药格式转换后最怕的就是坐标算错或类别错位。我习惯随机抽几十张图把标注框画回去看一眼这是最快、最直观的“后悔药”式自查。一张图就几行代码的事。import cv2 def draw_boxes(image_path, label_path, class_names): img cv2.imread(image_path) h, w img.shape[:2] with open(label_path) as f: lines f.readlines() for line in lines: cls_id, x_center, y_center, bw, bh map(float, line.split()) xmin int((x_center - bw / 2) * w) ymin int((y_center - bh / 2) * h) xmax int((x_center bw / 2) * w) ymax int((y_center bh / 2) * h) cv2.rectangle(img, (xmin, ymin), (xmax, ymax), (0, 255, 0), 2) cv2.putText(img, class_names[int(cls_id)], (xmin, max(ymin - 5, 0)), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 255, 0), 2) return img sample draw_boxes(JPEGImages/000001.jpg, labels/000001.txt, VOC_CLASSES) cv2.imwrite(check_000001.jpg, sample)画框时要格外注意坐标系cv2的图片数组是height×width而xml坐标是width×height转换时先用img.shape[:2]取出h和w再计算xmin、ymin这些像素位置。如果顺序写反框的宽高比会整体变形这类错误光看数字发现不了画出来一眼就能看出来。抽查样本要覆盖不同类别、不同大小的目标不要只挑几张看着顺眼的图。6.3 我留下的检查习惯每次处理完一份VOC数据或者把VOC转成其他格式我都会跑三遍统计第一遍数文件数量和匹配关系第二遍看类别分布第三遍随机抽图画框。这个习惯救过我很多次有一次别人给的数据集跑下来train_loss正常、val_loss正常画框才发现类别索引全错了root cause就是对方自定义的类别顺序和VOC官方顺序不一致。从那以后我拿到任何数据集第一件事不是训练而是先摸清它的类别映射表。如果你现在正在着手自己的目标检测项目可以把VOC数据集当作一块标准试验田先在这里把数据管线跑通、把模型验证到位再迁移到自己的垂直场景。原始xml标注格式虽然老但生态最丰富、踩坑资料最多把它吃透了再去用任何新标注格式都不会慌张。希望这些经验能帮你少走一段弯路。本文还有配套的精品资源点击获取