熊猫数据集双格式解析:VOC与YOLO坐标转换及训练校验全流程
2026/9/23 18:08:35 网站建设 项目流程

简介:这是一份以熊猫为单一类别的目标检测数据集,面向计算机视觉初学者与算法工程师,可直接用于训练或验证Pascal VOC、YOLO两类常见检测模型。全部图像均已通过labelImg完成矩形框标注,类别标注为Panda,共114个真实框,标注信息与图片一一对应。资源包共332个文件,其中jpg图片110张、VOC格式xml标注110个、YOLO格式txt标注112个,整体大小36.34MB,压缩后便于下载和快速解压使用。目前已有189人学习下载。数据集不附带权重或训练结果,但标注准确、格式完整,适合用来练习数据清洗、格式转换、模型迭代等完整流程;同时也可作为小样本迁移学习的测试集,帮助对比不同网络结构在单一类别上的表现。若需扩展类别或增加样本,可参照其标注格式自行补充。

1. 熊猫数据集双格式解析:110 张图把 VOC 和 YOLO 的流程彻底走通

这份熊猫数据集,数量上非常克制:110 张 JPEG 原图,每张图都同时配好了 Pascal VOC 格式的 XML 标注和 YOLO 格式的 TXT 标注,类别只有一个 Panda,所有标注框加起来正好 114 个。它不像 COCO 那种大而全的基准,更像一个特意为「跑通流程」准备的干净样本集。适合两类人:一类是刚接触目标检测、想用 YOLOv5 或 YOLOv8 训练自己数据集的新手;另一类是想把 VOC 到 YOLO 的坐标换算彻底吃透、以后不用再被标注格式坑的开发者。我建议你别急着把数据丢进训练脚本,先花半小时把这个数据集的文件结构和坐标关系理顺,后面能少踩一半的坑。

2. 文件结构与格式对应:110 张 JPG、110 个 XML、110 个 TXT 是怎么对齐的

2.1 解压后的文件清单与数量核对

拿到 zip 包之后,第一件事不是双击看图片,而是把文件数量统计一遍。这个数据集打包文件名里带了中文和加号,在 Windows 图形界面解压一般没问题,但在 Linux 服务器上我习惯用命令行操作,避免某些解压工具因为文件名编码问题丢文件。

unzip "动物数据集65熊猫数据集VOC格式+yolo格式110张1类别.zip" -d panda_dataset cd panda_dataset find . -name "*.jpg" | wc -l find . -name "*.xml" | wc -l find . -name "*.txt" | wc -l

unzip-d参数指定解压目标目录,后面的find分别统计三类文件的个数。这份数据集的简介里写得很清楚:jpg 文件 110 个、xml 文件 110 个、txt 文件 110 个,所以三个命令的输出都应该等于 110。如果你发现某个数量对不上,先别急着训练,大概率是解压中断或者下载不完整。

文件命名从项目文件列表里能看出来,是firc_Panda_40.jpgfirc_Panda_26.jpg这种「前缀_类别_编号」的结构。正常用 labelImg 标注并导出时,XML 和 TXT 会与 JPG 保持同名,只差后缀。但我不建议靠肉眼去逐对核对,直接用一个 Python 脚本检查更稳妥。

import glob, os imgs = glob.glob('**/*.jpg', recursive=True) xmls = glob.glob('**/*.xml', recursive=True) txts = glob.glob('**/*.txt', recursive=True) img_names = {os.path.basename(p).replace('.jpg', '') for p in imgs} xml_names = {os.path.basename(p).replace('.xml', '') for p in xmls} txt_names = {os.path.basename(p).replace('.txt', '') for p in txts} print('只有图片没有标注:', img_names - xml_names - txt_names) print('只有XML没有图片:', xml_names - img_names - txt_names) print('只有TXT没有图片:', txt_names - img_names - img_names)

这段脚本先把三类文件的完整路径都找出来,再去掉扩展名得到主文件名,最后用集合的差集找出“对不上”的那些。输出为空说明配对正常。这里要特别注意一个细节:数据集的说明里写了“不包含分割路径的 txt 文件”,也就是说这 110 个 txt 都应该是真正的标签文件;如果你统计 txt 数量时发现多了一个,很可能工作目录里还有个classes.txt,这类文件是 labelImg 自动生成的类别清单,不是标注,不用管它。

2.2 同一张图的两种标注视图:XML 与 TXT 的字段对照

解压完成后,随便挑一张图,比如firc_Panda_3.jpg,把同名的 XML 和 TXT 分别打开看一眼。这是理解双格式最直接的办法。

cat firc_Panda_3.xml cat firc_Panda_3.txt

XML 是典型的 Pascal VOC 结构,里面会有<filename><size><object>这样几组关键信息;而 TXT 在 YOLO 格式下每一行只有五个数字,用空格分隔,第一列是类别 id,后四列分别是归一化后的目标中心点 x、中心点 y、宽度 w、高度 h。两者的关系可以简单理解成:同一份标注,用两种坐标系写了两遍。

XML 字段TXT 中的对应说明
<filename>firc_Panda_3.jpg</filename>文件名对应图片名
<size><width>分母图片像素宽度
<size><height>分母图片像素高度
<object><name>Panda</name>第一列 0类别名到类别 id 的映射
<bndbox>里的 xmin、ymin、xmax、ymax后四列像素坐标换算成归一化坐标

为什么要同时看两个文件?因为只看 XML,你很难直观理解 YOLO 为什么要把坐标归一化;只看 TXT,你又不知道 0.5 这样的数字到底对应图上哪个位置。两边对照着看几张图,换算逻辑基本就清楚了。

2.3 为什么只有 110 张图,也值得把双格式保留

见过不少数据集只给 VOC 格式或只给 YOLO 格式。只给 VOC 的,你想训练 YOLO 必须先写转换脚本,而转换脚本本身又是最容易出错的环节;只给 YOLO 的,你想用 labelImg 复查标注、或者想用可视化工具看框,又得从归一化坐标反推像素坐标,很麻烦。这份数据集两边都给,最大的价值是你可以随时做交叉验证:XML 和 TXT 相互对拍,任何一边出了问题都能马上发现。

对新手来说,这 110 张图就是一个天然的对照教材。XML 里是百分百的像素坐标,TXT 里是归一化坐标,每一对文件都演示了一次坐标换算。把其中十张图手动算一遍,你对 YOLO 标签格式的记忆会比背十遍文档都牢。

3. 坐标换算与格式拆解:VOC 的 bndbox 怎么变成 YOLO 标签里的一行五个数字

3.1 XML 里真正有用的字段:filename、size、object

VOC 格式的 XML 里并不是每个字段都有用。对这个数据集来说,<folder><path><segmented>基本可以忽略,真正决定标注内容的是三组信息:<filename>告诉你这张图叫什么;<size>告诉你图片原始宽高;<object>可能有一个或多个,每个 object 内部的<name>是类别名,<bndbox>是矩形框。下面是读取并打印这些字段的脚本。

import xml.etree.ElementTree as ET tree = ET.parse('firc_Panda_3.xml') root = tree.getroot() print('filename:', root.find('filename').text) size = root.find('size') print('image size:', size.find('width').text, 'x', size.find('height').text) for obj in root.findall('object'): name = obj.find('name').text bbox = obj.find('bndbox') xmin = float(bbox.find('xmin').text) ymin = float(bbox.find('ymin').text) xmax = float(bbox.find('xmax').text) ymax = float(bbox.find('ymax').text) print(name, xmin, ymin, xmax, ymax)

这个脚本用ElementTree解析 XML,find按字段名定位节点。需要特别提醒的是,<bndbox>里的 xmin、ymin、xmax、ymax 都是像素绝对值,取值范围是 0 到图片宽高之间,而不是 0 到 1。很多人第一次写转换脚本时在这里栽跟头,以为 YOLO 的归一化是直接在 XML 数值上除以 255 之类,完全不是一回事。

另外,这个数据集的简介里写了“总框数 114”,而图片是 110 张,说明至少有 4 张图里有两个或以上的熊猫框。一个 XML 里出现多个<object>是正常的,遍历的时候用findall('object')而不是find('object'),否则只会取到第一个框,标签数量就会对不上。

3.2 从像素坐标到归一化坐标:公式与代码实现

把 VOC 转成 YOLO 格式,核心公式就四个,全部依赖<size>里的图片真实宽高:

x_center = (xmin + xmax) / 2 / image_width
y_center = (ymin + ymax) / 2 / image_height
box_width = (xmax - xmin) / image_width
box_height = (ymax - ymin) / image_height

注意:image_widthimage_height是整张原始图片的尺寸,不是标注框的尺寸,也不是你打算 resized 到 640x640 之后的尺寸。YOLO 标签文件里存的永远是相对原始图片的归一化坐标,训练时数据加载管线会自己处理缩放。

import xml.etree.ElementTree as ET def xml_to_yolo(xml_path, image_width, image_height, class_map): tree = ET.parse(xml_path) root = tree.getroot() lines = [] for obj in root.findall('object'): name = obj.find('name').text.strip() if name not in class_map: continue class_id = class_map[name] bbox = obj.find('bndbox') xmin = float(bbox.find('xmin').text) ymin = float(bbox.find('ymin').text) xmax = float(bbox.find('xmax').text) ymax = float(bbox.find('ymax').text) x_center = (xmin + xmax) / 2.0 / image_width y_center = (ymin + ymax) / 2.0 / image_height w_norm = (xmax - xmin) / image_width h_norm = (ymax - ymin) / image_height lines.append(f"{class_id} {x_center:.6f} {y_center:.6f} {w_norm:.6f} {h_norm:.6f}") return lines class_map = {"Panda": 0}

这个函数的参数里,class_map是类别名到 id 的映射,目前只有Panda: 0。输出采用 6 位小数,对 110 张图这个量级完全够用,不会因为精度丢失导致框偏移。我在实际项目中通常会把image_widthimage_height直接写成从 XML 的<size>里读取,而不是作为外部参数传入,这样可以少暴露一个出错的口子。

3.3 类别映射与扩类准备:为什么只有 0 也要维护好 classes 列表

单类别是最容易麻痹大意的场景。这个数据集所有 txt 的第一列都是 0,因为你只有 Panda 一个类。但即使如此,我强烈建议你从一开始就把类别映射文件维护好。YOLOv5 和 YOLOv8 的 data yaml 里会写names列表,训练时类别 id 就是列表下标。如果写成names: ['Panda'],那么 id 0 对应 Panda,没问题;但有一天你想加一个 Bear,并且把它放在列表第一位,就变成 id 0 对应 Bear,原有标注全部错位。

常见的翻车操作是:在 labelImg 里给某几张新图标了类,但保存后只更新了 XML,没有重新生成 TXT,导致同一个框在两种格式下类别不一致。这类错位训练时不会报错,只会表现为验证集指标莫名其妙变差。所以我养成的习惯是,不管数据集有没有自带 txt,都要用 3.2 的脚本基于 XML 重新生成一遍 txt,再和原始 txt 做 diff。这样改任何标签,都不会留下两头不同步的隐患。

4. 训练前校验三板斧:核对 114 个框、排除越界标注、划好 train/val

4.1 用 XML 统计每个类别的框数,先和 114 对一对

拿到数据集不做任何校验直接开训,等于把一个黑匣子交给训练脚本。我一般会先写一个十几行的统计脚本,把每个类别的框数算出来。

import glob import xml.etree.ElementTree as ET xmls = glob.glob('**/*.xml', recursive=True) counter = {} for xml_path in xmls: tree = ET.parse(xml_path) root = tree.getroot() for obj in root.findall('object'): name = obj.find('name').text.strip() counter[name] = counter.get(name, 0) + 1 print(counter)

预期输出是{'Panda': 114}。如果输出不是这个数,就需要排查:少框了可能是 XML 解析时漏了某些<object>,多框了可能是 labelImg 打标过程产生了重复框。这一步的价值在于把“标注文件本身的质量”和“训练效果”解耦。如果框数都不对,后面训练出来的 mAP 再好看也没有意义。

4.2 越界检测:坐标超出图片范围或宽高为负数

YOLO 训练过程中出现 loss 为 nan,或者验证集完全不检框,很多时候不是模型问题,而是标注数据里有越界 box。最常见的情况是标注框的 xmax 大于图片宽度,或宽高等于 0。归一化之后,这些值会变成大于 1 或者是负数,模型在计算损失时就会算出异常值。

from PIL import Image import glob import xml.etree.ElementTree as ET def validate_xml(xml_path): tree = ET.parse(xml_path) root = tree.getroot() filename = root.find('filename').text img = Image.open(filename) img_w, img_h = img.size for obj in root.findall('object'): bbox = obj.find('bndbox') xmin = float(bbox.find('xmin').text) ymin = float(bbox.find('ymin').text) xmax = float(bbox.find('xmax').text) ymax = float(bbox.find('ymax').text) if xmin < 0 or ymin < 0 or xmax > img_w or ymax > img_h: print(f'坐标越界: {xml_path} -> {xmin}, {ymin}, {xmax}, {ymax}') if xmax <= xmin or ymax <= ymin: print(f'退化框: {xml_path} -> 宽或高小于等于 0') for xml_path in glob.glob('**/*.xml', recursive=True): validate_xml(xml_path)

这个脚本里,我用 PIL 打开对应图片拿到真实宽高,再逐个框检查。xmax 大于图片宽度或者 xmin 小于 0 都属于越界;xmax 小于等于 xmin 属于退化框。退化框在校验后应该直接删掉,而不是保留在数据里。特别注意,PIL 的Image.open需要 XML 同级目录下有对应的 jpg 文件,如果脚本报找不到图片,说明文件名配对本身就出了问题,这也是一个间接检查。

4.3 划分训练集与验证集,并搭出 YOLO 需要的目录结构

110 张图不算多,但完全不划分验证集也不行。我一般按 9:1 划分,也就是 99 张训练、11 张验证。这个比例对百张量级的数据集比较合理,验证集太小的话指标波动会非常剧烈,每个 epoch 之间的 mAP 可能相差 0.2。

import glob import os import random import shutil imgs = glob.glob('images/*.jpg') random.seed(42) random.shuffle(imgs) split = int(len(imgs) * 0.9) train_imgs = imgs[:split] val_imgs = imgs[split:] for img in train_imgs: base = os.path.basename(img).replace('.jpg', '') shutil.copy(img, f'dataset/images/train/{base}.jpg') shutil.copy(f'labels/{base}.txt', f'dataset/labels/train/{base}.txt') for img in val_imgs: base = os.path.basename(img).replace('.jpg', '') shutil.copy(img, f'dataset/images/val/{base}.jpg') shutil.copy(f'labels/{base}.txt', f'dataset/labels/val/{base}.txt') print('train:', len(train_imgs), 'val:', len(val_imgs))

这段脚本对图片和标签用的是shutil.copy而不是move,原目录的数据被完整保留。这样做的原因是,划分比例和随机种子是可以反复调整的,如果把原始数据移动了,之后想重新划分就要重新解压。训练时使用的 data yaml 通常长这样:

train: dataset/images/train val: dataset/images/val nc: 1 names: ['Panda']

这里trainval指向图片目录,YOLO 训练时会自动在同级的labels目录下寻找同名 txt。如果你自己把 txt 放在了别的路径,需要在 yaml 或数据集类里显式指定,否则会一直提示找不到标签。

5. 常见问题与避坑:小样本训练最容易翻车的五个场景

5.1 文件缺失、配对错位与数量对不上

现象:用find统计后,jpg 只有 108 个,xml 却有 110 个。或者 jpg、xml、txt 数量都是 110,但打开某张图发现框标注到了另一只熊猫身上。

原因:前一种情况多半是 zip 下载不完整或解压过程跳过了个别文件;后一种情况通常是自己写的配对脚本有问题,比如按列表索引一一对应,而列表排序时把firc_Panda_10.jpg排到了firc_Panda_9.jpg前面,导致后缀替换后错位。

解决:下载后先用 2.1 的脚本统计三类文件数量,再按文件名而不是列表顺序建立映射。文件名相同的才是一对,任何靠“第几个文件”配对的做法都应该抛弃。

5.2 坐标归一化除以了 resize 之后的尺寸

现象:训练时 loss 前几个 epoch 下降正常,但验证集的 mAP 一直是 0,像是模型完全没学到东西。

原因:你写 VOC 转 YOLO 脚本时,把图片先 resize 到 640x640 再读取宽高,用 640 做了归一化分母。但数据加载器实际读取的是原始图片,导致标签和图像内容错位。

解决:归一化永远基于 XML<size>里的原始宽高。YOLO 训练时对图片的缩放发生在数据加载阶段,标签会同步变换,不需要人工把标签改成 640 输入尺寸的坐标。写转换函数时把<size>直接冷冻在 XML 解析结果里,不要允许外部传入自定义宽高。

5.3 labelImg 打标遗留的退化框与小目标框

现象:一张图里某个熊猫只占十几个像素,标注框宽度只有 3 像素。训练若干轮后 loss 变成 nan,或者该目标永远被漏检。

原因:过小或退化的框在模型下采样过程中,特征图上的响应可能小于一个像素,损失计算不稳定。labelImg 手动打标时,如果鼠标拖拽幅度太小,很容易生成宽高几乎为 0 的框。

解决:在训练前用越界检测脚本跑一遍,过滤掉宽或高小于 2 像素的框。这个阈值可以按你的输入分辨率调整,如果训练分辨率是 640,低于 2 像素的框基本没有学习意义,删掉反而更稳定。

5.4 对 110 张图的训练效果产生不切实际的预期

现象:训练 300 轮,mAP@0.5 停在 0.5 上下,怎么看都觉得是自己代码写错了。

原因:这个数据集只有 110 张图、114 个框,类别也只有一种,能覆盖的熊猫姿态、背景环境、光照条件非常有限。模型在小数据集上很容易过拟合训练集,验证时换一批照片效果马上掉下来。数据集描述里特意写了“不对模型精度作任何保证”,就是在提醒你它的定位是标注准确的流程验证集,不是刷分数据集。

解决:把它当 pipeline 验证用,跑通训练、验证、导出、推理全流程就够了。想提升实际效果,应该继续扩充数据,或者用预训练权重做迁移学习,把学习率调低,配合马赛克增强等策略,而不是责怪数据集。

5.5 zip 解压路径混乱与中文目录名带来的问题

现象:解压出来所有图片散落在多层嵌套目录里,YOLO 的 glob 匹配不到;或者在 Windows 上解压后路径带中文和空格,在 Linux 训练时路径解析失败。

原因:zip 包内可能有嵌套顶层目录,不同解压器对中文文件名的处理方式也不一致。直接写死相对路径的代码换一个环境就失效。

解决:解压后先find . -type f | head看清楚实际目录层级,再决定 glob 的匹配模式。所有 yaml 和脚本里统一用正斜杠相对路径,不要用反斜杠,避免在 Linux 服务器上翻车。如果项目目录本身带空格,训练数据路径建议放在没有空格的纯英文目录下。

6. 让这份双格式数据集变成自己的标注模板:画框验证与滚动更新

最后一个技巧,也是我每次拿到新数据集必做的一步:把 XML 里的框画回原图上,用眼睛确认标注质量。这一步能发现所有统计脚本发现不了的问题,比如框偏移了半个身体、框住了背景树枝、框的大小明显不符合熊猫体型。

import cv2 import xml.etree.ElementTree as ET img_path = 'firc_Panda_3.jpg' xml_path = 'firc_Panda_3.xml' img = cv2.imread(img_path) tree = ET.parse(xml_path) root = tree.getroot() for obj in root.findall('object'): bbox = obj.find('bndbox') xmin = int(float(bbox.find('xmin').text)) ymin = int(float(bbox.find('ymin').text)) xmax = int(float(bbox.find('xmax').text)) ymax = int(float(bbox.find('ymax').text)) cv2.rectangle(img, (xmin, ymin), (xmax, ymax), (0, 255, 0), 2) cv2.putText(img, 'Panda', (xmin, max(0, ymin - 8)), cv2.FONT_HERSHEY_SIMPLEX, 0.8, (0, 255, 0), 2) cv2.imwrite('check.jpg', img)

这个脚本把 XML 坐标转换成整数后,用 OpenCV 在图上画出绿色矩形框,并标上类别名。输出check.jpg后,人工抽查十张图,基本就能确认数据集能不能进入训练流程。你也可以顺手把 3.2 的xml_to_yolo函数集成进来,让每个被保存的 XML 都同步重新生成一次 TXT,这样双格式永远保持一致。

我自己做目标检测项目已经有几年了,最大的教训就是:格式化转换和人工校验这两步省不掉。这些数据集大多来自手动标注,不同标注工具、不同标注员的习惯都会造成细微差异,而训练脚本不会告诉你异常在哪个文件里。从那以后,我每次拿到任何来源的数据集,都强制走一遍“数量核对 → 越界检测 → 画框抽查”这套流程,然后才允许自己打开训练命令。这套流程跑下来通常不超过十分钟,但能避免你为一个错误标注浪费一整天训练时间。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询