VOC与YOLO双标注虎数据集:格式转换到目标检测训练实战

发布时间:2026/10/5 14:47:48
VOC与YOLO双标注虎数据集:格式转换到目标检测训练实战
简介虎目标检测数据集提供约958张真实虎图片的VOC与YOLO双格式标注面向计算机视觉初学者、算法工程师及相关科研人员可直接用于目标检测模型的训练、验证与评估也可作为labelImg标注工具操作的练习素材。压缩包内共2000个文件主要包括959个txt标注文件、958个xml标注文件及jpg图像整体大小361.65MB按jpg图片、xml标注、txt标注三个文件夹分类存放解压后即可查看对应标注结果。标注类别统一为tiger由labelImg工具完成遵循边界准确、目标全标、一致性检查等规范标签质量较有保障。数据集已形成可直接投入训练的结构便于快速接入YOLO、Faster R-CNN等常见检测框架也支持自行划分训练集与验证集。压缩包无需解压密码图像与标注文件一一对应可借助LabelImg、CVAT等工具直观复核。目前已有76人学习下载适合需要现成数据集开展迁移学习、模型调优或标注规范教学的用户。1. 虎数据集到手别急着训练VOC和YOLO双标注的958张图先补齐格式认知做老虎目标检测的人都知道找数据比调模型还头大。这套虎数据集VOC和YOLO格式的目标标注一共958张左右把两种最常见的标注格式一次给齐正好解决从标注到训练的衔接问题。拿到它说明你至少在起步阶段不用再为数据格式发愁。它适合两类人一类是刚进YOLO目标检测的大门想用一份干净数据把训练流程完整跑通另一类在做动物园、保护区或红外相机项目需要一个能快速验证模型效果的起步集。先说透千张级别的数据不足以支撑生产级精度但它非常适合入门复现、迁移学习和踩坑练习。下面从VOC和YOLO两种标注格式的底层差异讲起手把手走过转换、训练、排错最后教你用双格式一致性检查给这套数据上一道保险。2. 两种标注格式的底层逻辑VOC的XML与YOLO的txt到底差在哪2.1 打开一张XMLVOC标注的骨架VOCPascal VOC格式最容易被记住的就是那张XML。它把一张图的元数据、目标列表和每个框的绝对像素坐标全部塞进同一个文件。你在这套虎数据集的Annotations目录里会看到一堆与图片同名的.xml文件打开一个长这样annotation foldertiger/folder filenametiger_001.jpg/filename path/data/tiger/tiger_001.jpg/path source databaseUnknown/database /source size width1920/width height1080/height depth3/depth /size segmented0/segmented object nametiger/name poseUnspecified/pose truncated0/truncated difficult0/difficult bndbox xmin200/xmin ymin150/ymin xmax900/xmax ymax800/ymax /bndbox /object /annotationXML里真正影响训练的字段只有五个filename要保证和图片文件名一致否则后面找图像会出错size里的width和height是原图真实尺寸不是你想缩放的尺寸object.name是类别名在这套数据里绝大多数是tigerbndbox给的是框左上角xmin/ymin和右下角xmax/ymax单位是绝对像素。还有两个容易忽略的truncated和difficult野外标表常用它们标记遮挡和难例YOLO格式没有这两个字段转换时要自己决定是保留还是丢弃。如果一张图里有两只老虎XML里就会有多个object节点每个节点对应一个框。新手最容易在这里翻车只读第一个object就跑结果漏掉一多半标注。我的习惯是用root.findall(object)全量遍历一个都别落下。用标准库解析就够不需要引入额外的解析框架import xml.etree.ElementTree as ET tree ET.parse(tiger_001.xml) root tree.getroot() for obj in root.findall(object): name obj.find(name).text box obj.find(bndbox) xmin box.find(xmin).text ymin box.find(ymin).text xmax box.find(xmax).text ymax box.find(ymax).text print(name, xmin, ymin, xmax, ymax)这段代码的每一个find都是按标签名找子节点路径写错就会返回None再访问.text直接抛异常。我在处理下载的数据时经常先跑一遍这段脚本把几百个XML全部吐出来人工抽查确认没有缺坐标、缺类别名再进下一步。2.2 YOLO的txt一行一个框的压缩表达YOLO格式把每张图的标注压成一个与图片同名的txt文件一行代表一个目标格式固定为五个数字类别id中心点x中心点y框宽框高其中后四个全部除以图片宽高做了归一化。同一张虎图的txt内容大概是0 0.2864 0.4398 0.3646 0.6019如果图片分辨率为1920乘1080那么0 0.2864 0.4398 0.3646 0.6019表示框的中心点在(0.2864 * 1920, 0.4398 * 1080) (550, 475)框宽为0.3646 * 1920 700高为0.6019 * 1080 650。反归一化时一定要乘回原图分辨率不能乘模型输入分辨率这是最容易搞乱的环节。从VOC的绝对坐标转成YOLO的归一化坐标只需要四个公式x_center (xmin xmax) / 2 / img_w y_center (ymin ymax) / 2 / img_h w (xmax - xmin) / img_w h (ymax - ymin) / img_h公式里分子是像素差分母是真实图片宽高结果必然落在0到1之间。如果算出w或h大于1基本可以断定坐标或分母写错。类别id是从0开始的整数CLASSES列表里tiger排第几位txt里就写数字几如果这套虎数据集以后要加human、vehicle等类别顺序必须保持一致否则旧标签全部错位。我通常会专门用一个classes.txt把顺序记下来并同时放进参数说明里。为了直观VOC和YOLO两种结构可以对照一下维度VOC XMLYOLO txt文件数量每张图片一个xml每张图片一个txt坐标单位绝对像素值归一化小数(0~1)类别表示object.name字符串数字id多目标多个object节点多行文本是否依赖原图尺寸XML自带size不依赖外部必须配合原图宽高才能还原这个表做出来不是为了背概念而是要在转换前心里有数VOC的XML自带size字段所以独自就能还原坐标YOLO的txt不存尺寸脱离原图就只是一堆小数。这就是为什么数据归档时两份格式都要留——VOC负责可读和归档YOLO负责训练速度。2.3 选型理由为什么训练时大家都默认喂txt在实际目标检测项目里我和同事几乎不会让训练器直接读VOC XML。读取N个XML再逐个解析比读取N个txt的代价高一个量级尤其是958张这种规模虽然不大但来来回复现实验就很明显。YOLO文本格式天然适合数据增强裁剪、翻转、缩放时只要对归一化坐标做同步运算不容易因为像素坐标和尺寸变化产生错位。Ultralytics仓库默认的训练入口就是读YOLO txt不需要额外写数据加载器。不过我不想劝你把VOC删掉。很多可视化工具、评测脚本、EDA分析只认VOC不认txt而双格式是最好的防呆设计。后面第六节要讲的一致性检查就是靠着VOC和YOLO两份数据互相纠错。所以在转换脚本里常见做法是保留原始VOC目录输出新的labels目录不覆盖任何原文件。3. 把VOC转成YOLO格式可抄的Python转换脚本与坐标边界坑3.1 先定目录结构后面少改脚本转换之前先把数据目录立起来。我推荐这种结构一步到位兼容YOLO训练器tiger_dataset/ ├── images/ │ ├── train/ │ │ ├── tiger_001.jpg │ │ └── ... │ └── val/ │ ├── tiger_101.jpg │ └── ... ├── labels/ │ ├── train/ │ │ ├── tiger_001.txt │ │ └── ... │ └── val/ │ ├── tiger_101.txt │ └── ... ├── voc_xml/ │ ├── train/ │ │ ├── tiger_001.xml │ │ └── ... │ └── val/ │ ├── tiger_101.xml │ └── ... └── tiger.yaml这样分的原因有两点。第一图片和标注分离训练器读取时路径逻辑简单也不会因为标注文件和图片放同一目录被混算。第二train和val从源头分开避免后期误把验证集掺进训练集。如果你的虎数据集下载下来是一整个images加一整个labels或者只有VOC的Annotations先手动按8比2或85比15分开再继续执行转换脚本。划分脚本会在第四章给一个相对严谨的版本这里先不展开。为什么强调先定目录再写脚本因为转换脚本里的out_dir如果不存在程序不会自动创建子目录硬跑结果就是报错或者把文件写到别处。我在代码里通常会加一行os.makedirs(out_dir, exist_okTrue)头一次写的人很容易漏掉这个细节。3.2 核心转换脚本可直接抄这是最常见做法按CLASSES列表映射类别ID从XML的size节点读取真实宽高忽略difficult1的难例框把每个object转成一行YOLO格式。脚本如下import os import xml.etree.ElementTree as ET # 类别顺序即YOLO类别ID训练和推理必须保持一致 CLASSES [tiger] def voc_to_yolo(xml_file, out_dir): tree ET.parse(xml_file) root tree.getroot() # 原图尺寸从XML自带size节点读取不要自己猜 img_w int(root.find(size/width).text) img_h int(root.find(size/height).text) lines [] for obj in root.findall(object): name obj.find(name).text.strip() if name not in CLASSES: print(f跳过未知类别: {name} {xml_file}) continue cls_id CLASSES.index(name) # difficult1的对象通常遮挡严重或标注质量差先过滤掉 difficult int(obj.find(difficult).text) if difficult: continue box obj.find(bndbox) xmin float(box.find(xmin).text) ymin float(box.find(ymin).text) xmax float(box.find(xmax).text) ymax float(box.find(ymax).text) # 中心点归一化公式 x_center ((xmin xmax) / 2.0) / img_w y_center ((ymin ymax) / 2.0) / img_h w (xmax - xmin) / img_w h (ymax - ymin) / img_h # 越界防御一旦出现大概率是坐标写错 if not (0 x_center 1 and 0 y_center 1): print(f越界警告: {xml_file}, box({xmin},{ymin},{xmax},{ymax})) continue lines.append(f{cls_id} {x_center:.6f} {y_center:.6f} {w:.6f} {h:.6f}) if lines: base os.path.splitext(os.path.basename(xml_file))[0] out_path os.path.join(out_dir, base .txt) with open(out_path, w) as f: f.write(\n.join(lines)) def batch_convert(xml_dir, out_dir): os.makedirs(out_dir, exist_okTrue) for f in os.listdir(xml_dir): if f.endswith(.xml): voc_to_yolo(os.path.join(xml_dir, f), out_dir) # 参数XML输入目录txt输出目录train和val分开跑 batch_convert(voc_xml/train, labels/train) batch_convert(voc_xml/val, labels/val)逻辑说明脚本先解析XML树再从size/width和size/height拿到原图尺寸随后遍历所有object跳过不在CLASSES里的类别名和difficult1的框对每个框用归一化公式算出中心点和宽高最后写到输出txt。如果一张图没有任何有效框不会生成txt文件这是刻意的因为YOLO训练器遇到空txt会一直报错。参数说明第一个参数xml_dir指向VOC的Annotations目录第二个参数out_dir是训练器会读的labels目录。CLASSES列表的第一个元素对应id 0追加类别时只能往后追加不能插到中间。difficult过滤逻辑写在if difficult里如果你明确想保留难例把这三行注释掉。os.makedirs(out_dir, exist_okTrue)保证目录不存在时自动创建避免报错。这个脚本还有一个边界坑xmin/xmax/ymin/ymax全部用float读取但VOC标注里某些老工具会写成整数型字符串不影响。真正的问题在于如果XML中xmin用1起始坐标而你的图像坐标是0起始x_center会偏半个像素958张里单看一张不明显整体框会系统地偏一点。我在实际项目里的做法是转换后统一做一次可视化抽查把偏差暴露出来。3.3 转换后必做验证可视化加统计转换完成不等于万事大吉。我一般会写一个快速验证脚本随机抽几张验证集图片把txt里的归一化框反算回像素坐标并画出来眼睛直接看框和老虎贴不贴合。import cv2 import os img_dir images/val label_dir labels/val CLASSES [tiger] for img_name in sorted(os.listdir(img_dir))[:3]: img_path os.path.join(img_dir, img_name) label_path os.path.join(label_dir, os.path.splitext(img_name)[0] .txt) if not os.path.exists(label_path): print(缺失标签文件:, label_path) continue img cv2.imread(img_path) h, w img.shape[:2] # 注意这里用的是真实图片宽高 with open(label_path) as f: for line in f: cls_id, xc, yc, bw, bh map(float, line.split()) x1 int((xc - bw / 2) * w) y1 int((yc - bh / 2) * h) x2 int((xc bw / 2) * w) y2 int((yc bh / 2) * h) cv2.rectangle(img, (x1, y1), (x2, y2), (0, 255, 0), 2) cv2.imwrite(check_ img_name, img) print(已输出, check_ img_name)这段代码的要点在于反归一化时用的是cv2.imread读出的真实h,w不是YOLO训练时的输入imgsz。很多人在这一步把h,w写成640,640画出来的框全部错位。os.path.splitext(img_name)[0]把tiger_001.jpg转成tiger_001再拼上.txt去找标签严格保证同名。除可视化外我还会统计一下每个txt的行数看有没有大量1行或几乎为0的文件。可以用一个简单命令find labels/train -name *.txt | xargs wc -l | sort -n | head -20如果看到很多文本文件行数为0说明转换脚本里过滤条件过严常见的是difficult字段默认为1导致大片框被删。这时要回到XML里看difficult的实际值分布再调整过滤逻辑。4. 用虎数据集跑YOLO训练划分、data.yaml和三个必看指标4.1 划分train和val避免同一只老虎出现在两边在YOLO目标检测流程里训练集和验证集划分直接影响最终指标的可信度。958张左右的数据量不算大如果随机分很可能会让同一个场景、同一只老虎的连续帧同时出现在两边造成验证集虚高。我的习惯是尽量按场景分组划分把同一场景的图放一起。假设图片文件名带场景前缀比如tiger_001里的tiger是场景组脚本如下import os import random import shutil from collections import defaultdict random.seed(42) groups defaultdict(list) for f in os.listdir(images): if f.endswith(.jpg): # 用下划线前的前缀作为场景ID按需修改 scene f.split(_)[0] groups[scene].append(f) scene_names list(groups.keys()) random.shuffle(scene_names) val_scenes set(scene_names[:int(len(scene_names) * 0.15)]) os.makedirs(images/train, exist_okTrue) os.makedirs(images/val, exist_okTrue) os.makedirs(labels/train, exist_okTrue) os.makedirs(labels/val, exist_okTrue) for f in os.listdir(images): if not f.endswith(.jpg): continue scene f.split(_)[0] dest val if scene in val_scenes else train shutil.move(os.path.join(images, f), os.path.join(images, dest, f)) label_name os.path.splitext(f)[0] .txt src_label os.path.join(labels, label_name) if os.path.exists(src_label): shutil.move(src_label, os.path.join(labels, dest, label_name))脚本参数说明random.seed(42)固定随机种子保证复现val_scenes比例取15%对958张图大约能分到140张左右验证按场景分组后同一组图片不会跨集。如果文件名没有明显前缀也可以退化为纯随机划分但要在实验记录里标明这一点免得部署效果和验证对不上。划分完成后跑一遍find labels/train -name *.txt | wc -l确信标签数量跟图片数量对得上。注意很多人划分完就忘了一件事训练集和验证集的分布应该尽量接近。虎数据集里如果全是近景大头照验证也可以用近景如果实际部署是远距离监控最好单独准备一小批远距离图作为测试集只有这样才能暴露第5章的小目标漏检问题。4.2 配置data.yaml与训练命令划分完目录接下来写Ultralytics YOLO训练器需要的data.yaml。这个文件负责告诉训练器图片和标签在哪里、类别有哪几个。最简版本如下path: ./tiger_dataset train: images/train val: images/val names: 0: tigerpath是数据集的根目录推荐用相对路径避免换机器后路径失效train和val是相对于path的目录名names的键必须从0开始并且跟转换脚本里的CLASSES顺序一模一样。如果顺序对不上模型会把tiger当成另一类推理结果一团糟。准备好之后训练命令一行就能起yolo detect train datatiger.yaml modelyolov8n.pt epochs100 imgsz640 batch16参数说明modelyolov8n.pt是轻量级起步配置显存占用小958张的数据集跑几十轮就能看到趋势如果你显卡够猛换yolov8s.pt或更大模型。imgsz640是常见的推理部署分辨率也是T4这类GPU上用TensorRT做1080p 25帧视频检测时最常用到的一档先按这个尺寸训练后面导出部署才能对应上。batch16在8G显存上跑到yolov8n通常没问题一旦OOM就把batch减半不要硬撑。epochs100是起步值实际看早停或loss曲线再决定。训练过程里YOLO会打印两类信息loss曲线和验证指标。很多人只看总loss下降就安心这是不够的。YOLO的损失函数由分类、置信度和边界框回归等多项组成每一项变化趋势不同loss下降只说明模型在拟合训练集不代表模型在验证集上真的泛化。更可靠的判断标准是看每个epoch末尾打出的mAP50和mAP50-95。4.3 训完看什么先看mAP50-95再看混淆矩阵训练结束后runs/detect/train目录下会留下权重文件和结果曲线。我一般先打开results.png重点看验证集上的mAP50-95曲线是否还在上升如果最后20个epoch几乎走平说明已经收敛。然后看confusion_matrix.png里面会标出背景误检和类别混淆对虎数据集来说最怕的是把岩石或树叶当老虎混淆矩阵能直接看到这类错误有多少。如果mAP达到预期接着导出部署常用的ONNX格式yolo export modelruns/detect/train/weights/best.pt formatonnx imgsz640导出参数里imgsz必须和训练时一致否则后续用TensorRT优化边界框坐标时容易出错。formatonnx是目前最常见的中间格式可以在不同部署端平滑迁移。训练过程中的几个指标要盯紧。mAP50是IoU阈值为0.5时的平均精度容易虚高958张近景老虎做到0.9并不稀奇mAP50-95把多个阈值平均更严格也更接近真实部署感受。如果mAP50-95远低于mAP50说明模型框的位置不够准常见原因是标注本身框边界参差不齐。我一般会在训练前随机抽一部分标注做可视化训练后再次可视化预测结果两者并排看比盯着数字有用得多。5. 虎数据集训练翻车排查五个常见问题现象、原因到解决按照前四章的正常路径走958张虎数据集跑训练一般能顺利出结果但总有人卡在某个阶段。我把自己和身边同事遇到的五个高频问题整理在这儿不绕弯子。5.1 现象一loss曲线前期下降后期震荡不收敛现象前30个epoch的loss正常下降之后开始上下跳验证集mAP不升反降。原因训练集里混进了一批空标签或只有极少数框的图片。VOC转YOLO时过滤difficult后有些图片原本就只有一个框转换脚本发现框越界又把它跳过了最终生成一个空txt或者完全没有txt。YOLO在训练时遇到空标签梯度方向混乱后期优化就会震荡。解决训练前先洗标签。用以下命令找出空文件并删掉find labels/train -name *.txt -empty -delete然后到images里把对应的图片也移出训练集不然模型总会在每个epoch重新翻到这张没有标签的图OOM不会但loss莫名其妙震荡。删除前先统计一下如果空文件占总数超过5%说明转换脚本过滤太狠该回到第3章检查difficult过滤逻辑而不是盲目删除。处理完再看一轮训练loss曲线前40个epoch应该保持单边下降不再出现突然跳高的尖峰。5.2 现象二预测框比老虎大一圈现象训练完预测框把老虎周围背景都包进去了误差几乎达到框宽的一半。原因这是坐标公式写错的最典型表现。x_center公式正确但宽度算成了(xmax xmin) / img_w相当于把两个端点加起来当作宽度或者img_w忘除以导致归一化值全部偏大。还有可能是可视化代码里反归一化时用错了h,w把模型输入尺寸当成真实尺寸这时框会整体错位但比例正确。解决回到转换脚本把公式严格写成w (xmax - xmin) / img_wh (ymax - ymin) / img_h。然后跑第3.3节的验证脚本输出一张画框图。如果框只偏位不放大优先检查反归一化时的h,w来源如果框呈比例放大基本确定是宽高公式问题。注意同一个txt被不同工具读取时坐标解释也可能不同建议用一个统一的IO函数不要每处都各自拆行。验证脚本跑通后把老标签重新生成一遍再用同一批图片做推理对比确认框边缘和老虎前腿、背线贴合。5.3 现象三训练直接报错No labels in train现象命令一执行立刻出现类似AssertionError: No labels in train或image not found。原因最常见的是路径配置问题data.yaml里的train写的是绝对路径而path又重复拼了一层或者目录结构里根本没有labels/train。另一个很隐蔽图片后缀是.jpeg但txt命名用了.jpeg.txt训练器按.jpg去找tiger_001.txt翻不到就报无标签。解决先把data.yaml的路径写成相对path的简单形式不要用/data/tiger/images/train这种绝对路径。再运行一个校验脚本遍历images/train和labels/train找出同名不一致的文件。最简单的排查命令for f in images/train/*.jpg; do base$(basename $f .jpg); [ -f labels/train/$base.txt ] || echo missing $base; done这个命令会列出所有缺标签的图片看到输出后对应处理。如果只缺几张把XML重新转换就行如果缺一大批检查划分脚本是不是把labels目录漏了。跑完这个命令再启动训练报错就会消失。5.4 现象四mAP50很高换成小目标视频一个都检不出现象验证集mAP50到了0.9部署到1080p监控画面里老虎远在几十米外只占几个像素检测框时有时无甚至全漏。原因虎数据集大多是近景、大尺寸目标框占图片比例高模型只学会了在大尺度特征图上找虎。YOLO对8像素以下的小目标几乎没有特征可看验证集里没有这类样本指标自然虚高。这是数据分布与部署场景不匹配的问题不是模型训练失败别在超参里找玄学先去看数据分布。解决先用脚本统计标注框面积占图片面积的比例确认小目标占比。如果小目标很少就要么补充远距离数据要么调整训练分辨率。常见做法是把imgsz从640提到1280让远距离老虎在输入里占更多像素同时用Ultralytics的多尺度训练参数让模型适应不同尺度。部署时再用原来的640推理精度会比直接训练时好。如果目标只出现在特定区域还可以做切片推理把大图切成若干小patch再检测。处理后单独跑一段远距离视频测试用召回率而不是mAP来评判。5.5 现象五显存OOM或训练速度突然变慢现象训练跑到一半控制台直接CUDA out of memory或者每个epoch时间越来越长GPU利用率只有一半。原因显存不足是直接原因batch16配合imgsz640在8G卡上跑较大模型就会爆。速度变慢则更可能是数据加载卡住workers设置太高导致CPU狂转或者缓存被频繁换入换出GPU不得不在原地等数据。解决先减半batch把batch16改成batch8再不行把imgsz降到480。训练命令里加workers4 cacheFalse关闭不必要的缓存和过高的并行加载。每次开始训练前先在终端执行nvidia-smi确认没有其他进程占显存。养成这个习惯后OOM基本能避免大半。如果一张卡实在塞不下就换更轻量的模型配置或者把训练集的缓存关掉减少内存压力。6. 双格式一致性检查一个脚本把VOC和YOLO框对齐6.1 并行读两份标注计算框重叠率VOC和YOLO两份标注能否完全对应单靠肉眼抽几幅不可靠。一个更稳的技巧是把同一张图的VOC框和YOLO框反算回同一坐标系按IoU逐框配对再输出平均重叠率。脚本思路如下import xml.etree.ElementTree as ET def read_voc(xml_path): root ET.parse(xml_path).getroot() w int(root.find(size/width).text) h int(root.find(size/height).text) boxes [] for obj in root.findall(object): box obj.find(bndbox) boxes.append([ float(box.find(xmin).text), float(box.find(ymin).text), float(box.find(xmax).text), float(box.find(ymax).text) ]) return w, h, boxes def read_yolo(txt_path, img_w, img_h): boxes [] with open(txt_path) as f: for line in f: _, xc, yc, bw, bh map(float, line.split()) x1 (xc - bw / 2) * img_w y1 (yc - bh / 2) * img_h x2 (xc bw / 2) * img_w y2 (yc bh / 2) * img_h boxes.append([x1, y1, x2, y2]) return boxes然后在同名的xml和txt上用read_voc拿到的宽高作为统一基准把YOLO的归一化坐标反算成像素坐标再和VOC的绝对坐标逐框匹配。配对成功后算IoU平均IoU低于0.9就报警说明两份标注至少有一份错位。这个检查的优势在于不依赖真值因为两份格式描述的是同一批物体如果它们互相不一致要么转换脚本出了bug要么原始VOC本身就有坐标跳跃。我现在的习惯是每拿到一套双格式数据集第一件事就是跑这个对齐检查再跑第3.3节的可视化。曾经有一次数据集在VOC里看起来很整齐但YOLO txt里类别id全被右移了一位训练出来的模型把背景当成老虎浪费了两天时间。之后我就再也不敢跳过这个动作了。多花十分钟检查能省后面几小时的血泪排错。希望帮到你。本文还有配套的精品资源点击获取