简介:这份虎目标检测数据集面向计算机视觉初学者与需要快速验证检测模型的研究者,解决虎类目标样本获取与标注成本高的问题。数据以VOC与YOLO双格式提供,jpg图片与对应的xml、txt标注文件一一对应,可直接接入主流检测框架训练与评估。压缩包共约2000个文件,包含959个txt标注、958个xml标注及958张jpg图片,整体约361.65MB,解压后按图片、xml、txt三个文件夹分类存放,无需密码即可查看标注情况。标注由labelImg完成,类别统一为tiger,遵循边界框选准确、目标尽量不遗漏、完成后交叉检查一致性等原则,标注质量较为可靠。目前已有76人学习下载,适合作为虎类检测的入门练手数据或小规模实验的补充样本,帮助读者省去从零采集与标注的环节,把精力集中在模型选型、训练调参与效果对比上。
1. 虎数据集 VOC 与 YOLO 双格式:958 张标注到底能训出什么
手上拿到一份 958 张左右的虎类目标标注数据,同时给了 VOC 和 YOLO 两套格式,很多人第一反应是「数据量太小,训不动」。但真正决定成败的不是张数,而是这 958 张里有多少独立目标、标注框的紧致程度、以及两套格式之间是否严格对齐。我见过用 900 多张猛兽数据把 mAP50 做到 0.85 以上的,也见过 3000 张因为框飘、类别串、坐标越界而彻底翻车的。这份数据集的价值在于它同时提供了 VOC 的 XML 和 YOLO 的 TXT,意味着你可以在同一批图上做格式互转验证、做标注质量审计、做训练前的分布统计,而不是拿到一堆黑匣子直接喂进去。适合谁:想跑通 YOLOv8 自定义训练全流程的工程师、需要做野生动物检测原型的团队、以及想拿一份真实标注数据练手格式转换和增强策略的人。下面从格式差异讲到训练落地,再到我踩过的坑,一步步拆。
2. VOC 与 YOLO 标注格式的差异与互转验证
2.1 两种格式的坐标体系到底差在哪
VOC 格式每张图对应一个 XML 文件,里面用<bndbox>记录xmin, ymin, xmax, ymax,这是绝对像素坐标,原点在左上角。YOLO 格式每张图对应一个 TXT 文件,每行是class_id x_center y_center width height,全部归一化到 0 到 1 之间,原点同样在左上角。差异看起来只是坐标变换,但实际转换时有三个容易出错的点:第一,VOC 的坐标是整数,YOLO 需要浮点,取整方式不同会导致框偏移一两个像素;第二,VOC 的类别名写在<name>里,YOLO 需要映射成从 0 开始的整数索引,映射表一旦顺序错了,虎会被标成猫;第三,有些 VOC 文件里会出现xmin > xmax或坐标为负的情况,直接转 YOLO 会得到越界值,训练时损失直接炸掉。
我一般会先写一个校验脚本,把 VOC 里所有框的坐标读出来,检查xmin < xmax、ymin < ymax、坐标是否在图像宽高范围内。这一步不做,后面转出来的 YOLO 标签就是定时炸弹。
import os import xml.etree.ElementTree as ET from PIL import Image def validate_voc(xml_dir, img_dir): issues = [] for xml_file in os.listdir(xml_dir): if not xml_file.endswith('.xml'): continue tree = ET.parse(os.path.join(xml_dir, xml_file)) root = tree.getroot() img_name = root.find('filename').text img_path = os.path.join(img_dir, img_name) if not os.path.exists(img_path): issues.append(f"{xml_file}: 图像缺失 {img_name}") continue w, h = Image.open(img_path).size for obj in root.findall('object'): name = obj.find('name').text bbox = obj.find('bndbox') xmin = int(bbox.find('xmin').text) ymin = int(bbox.find('ymin').text) xmax = int(bbox.find('xmax').text) ymax = int(bbox.find('ymax').text) if xmin >= xmax or ymin >= ymax: issues.append(f"{xml_file}: 框无效 {name} ({xmin},{ymin},{xmax},{ymax})") if xmin < 0 or ymin < 0 or xmax > w or ymax > h: issues.append(f"{xml_file}: 框越界 {name} 图像尺寸 {w}x{h}") return issues issues = validate_voc('annotations/', 'images/') for i in issues: print(i) print(f"共发现 {len(issues)} 个问题")这段代码的逻辑是遍历所有 XML,读取图像真实宽高,然后逐框检查坐标合法性和边界。参数说明:xml_dir是 VOC 标注目录,img_dir是原图目录,两者文件名必须能对应上。如果输出问题数为 0,说明 VOC 源数据是干净的,可以放心转 YOLO。如果有越界框,常见做法是裁剪到图像边界内,而不是直接丢弃,因为虎的标注框越界往往是因为标注员手抖多拉了几个像素。
2.2 从 VOC 转 YOLO 的完整脚本与类别映射
转换的核心是归一化公式:x_center = (xmin + xmax) / 2 / img_w,y_center = (ymin + ymax) / 2 / img_h,width = (xmax - xmin) / img_w,height = (ymax - ymin) / img_h。类别映射我习惯单独维护一个classes.txt,每行一个类别名,行号就是 class_id。这样训练时 YOLO 的data.yaml直接引用这个文件,避免手写names列表时顺序错乱。
import os import xml.etree.ElementTree as ET from PIL import Image classes = ['tiger'] # 按实际类别修改,行号即 class_id class_to_id = {name: i for i, name in enumerate(classes)} def voc_to_yolo(xml_dir, img_dir, out_dir): os.makedirs(out_dir, exist_ok=True) for xml_file in os.listdir(xml_dir): if not xml_file.endswith('.xml'): continue tree = ET.parse(os.path.join(xml_dir, xml_file)) root = tree.getroot() img_name = root.find('filename').text img_path = os.path.join(img_dir, img_name) w, h = Image.open(img_path).size lines = [] for obj in root.findall('object'): name = obj.find('name').text if name not in class_to_id: continue bbox = obj.find('bndbox') xmin = max(0, int(bbox.find('xmin').text)) ymin = max(0, int(bbox.find('ymin').text)) xmax = min(w, int(bbox.find('xmax').text)) ymax = min(h, int(bbox.find('ymax').text)) xc = (xmin + xmax) / 2.0 / w yc = (ymin + ymax) / 2.0 / h bw = (xmax - xmin) / w bh = (ymax - ymin) / h lines.append(f"{class_to_id[name]} {xc:.6f} {yc:.6f} {bw:.6f} {bh:.6f}") txt_name = os.path.splitext(xml_file)[0] + '.txt' with open(os.path.join(out_dir, txt_name), 'w') as f: f.write('\n'.join(lines)) voc_to_yolo('annotations/', 'images/', 'labels/')逻辑说明:先建立类别到 id 的映射,然后逐 XML 读取,对每个框做边界裁剪后再归一化,保留 6 位小数足够 YOLO 训练精度。参数说明:classes列表必须和实际标注中的<name>完全一致,大小写敏感;out_dir是 YOLO 标签输出目录,文件名与图像同名但扩展名为.txt。转换完成后,建议随机抽 5 张图用可视化脚本画框对比,确认 VOC 和 YOLO 的框完全重合。这一步是后悔药,不做的话训练时 loss 不降你都不知道是标签错了还是模型错了。
2.3 反向验证:YOLO 转回 VOC 做交叉检查
有时候你拿到的是别人转好的 YOLO 标签,想确认它和 VOC 是否一致,可以反向转回去做像素级对比。反向公式:xmin = (xc - bw/2) * w,ymin = (yc - bh/2) * h,xmax = (xc + bw/2) * w,ymax = (yc + bh/2) * h。转回后和原始 VOC 的框计算 IoU,如果 IoU 低于 0.99,说明转换过程中有精度损失或标注本身不一致。我一般会设一个阈值 0.95,低于这个值的样本单独列出来人工复核。958 张里如果有超过 20 张 IoU 异常,那这批数据在标注阶段就有问题,不是转换脚本能救的。
3. 用这 958 张跑通 YOLOv8 训练的最小闭环
3.1 目录结构与 data.yaml 的四个必填字段
YOLOv8 对目录结构有约定,但也不是死的。我习惯这样组织:
tiger_dataset/ images/ train/ # 约 766 张 val/ # 约 192 张 labels/ train/ val/ data.yamldata.yaml里四个字段必须写对:path是数据集根目录,train和val是相对路径,names是类别字典。常见错误是path写了绝对路径但换机器后失效,或者names写成列表但顺序和classes.txt不一致。我一般直接写:
path: ./tiger_dataset train: images/train val: images/val names: 0: tiger注意names的键是整数,不是字符串。如果类别只有虎一种,nc可以省略,YOLOv8 会自动从names长度推断。划分比例上,958 张按 8:2 分,训练集 766 张,验证集 192 张。如果虎的姿态差异大,建议用分层抽样,保证训练集和验证集里各种姿态都有。我见过随机划分后验证集全是正面虎、训练集全是侧面虎,mAP 虚高但实际部署就翻车。
3.2 训练命令与关键参数怎么设
YOLOv8 的训练入口很简洁,但参数设不对,958 张也能训出垃圾。我常用的命令:
yolo detect train \ data=tiger_dataset/data.yaml \ model=yolov8n.pt \ epochs=150 \ imgsz=640 \ batch=16 \ lr0=0.01 \ lrf=0.01 \ patience=30 \ augment=True \ mosaic=1.0 \ mixup=0.1 \ device=0参数说明:model=yolov8n.pt是预训练权重,小模型在 958 张上比大模型更不容易过拟合;epochs=150配合patience=30,如果 30 轮验证指标不升就早停;lr0=0.01是初始学习率,小数据集可以降到 0.005 更稳;mosaic=1.0开启马赛克增强,对虎这种单目标场景能显著提升小目标召回;mixup=0.1轻微混合,再高会模糊虎的纹理特征。batch=16在 8G 显存上跑 640 分辨率刚好,如果显存不够降到 8,但学习率也要相应减半。
训练过程中重点看三个指标:box_loss是否稳定下降、mAP50是否在 50 轮后超过 0.6、cls_loss是否震荡。如果box_loss降到 0.5 以下但mAP50不动,大概率是标注框太松,虎的边界框里混了太多背景。这时候回去看 VOC 的框是不是把整个虎身加周围草地都框进去了,是的话需要重新紧致标注。
3.3 训练完怎么验证不是「假学好」
958 张数据量小,最容易出现的是过拟合:训练集 mAP 0.95,验证集 mAP 0.4。验证方法不能只看 YOLO 输出的results.csv,要自己做三件事。第一,用yolo detect val跑一遍验证集,看混淆矩阵,如果虎被大量判为背景,说明召回不够;第二,拿 10 张训练集里没出现过的虎图做推理,看框是否紧致、置信度是否合理;第三,用yolo detect predict输出带框图像,肉眼检查有没有漏检和误检。我一般会额外算一个指标:验证集上所有预测框和真实框的平均 IoU,如果低于 0.7,说明框回归有问题,不是分类问题。
from ultralytics import YOLO import cv2 model = YOLO('runs/detect/train/weights/best.pt') results = model('test_tiger.jpg', conf=0.25, iou=0.45) for r in results: im = r.plot() cv2.imwrite('pred.jpg', im) print(f"检测到 {len(r.boxes)} 个目标") for box in r.boxes: print(f"类别 {r.names[int(box.cls)]} 置信度 {float(box.conf):.3f} 坐标 {box.xyxy.tolist()}")这段代码加载训练好的权重,对单张图推理,conf=0.25是置信度阈值,iou=0.45是 NMS 的 IoU 阈值。如果虎重叠严重,iou可以调到 0.5 到 0.6,避免把两只虎合并成一个框。输出里重点看置信度分布,如果大部分框置信度在 0.3 到 0.5 之间,说明模型没学扎实,需要更多数据或更强增强。
4. 小样本虎数据集的避坑与排查记录
4.1 标注框不紧致导致 mAP 虚低
现象:训练 loss 正常下降,但验证集 mAP50 卡在 0.5 上不去,预测框明显比虎大一圈。原因:VOC 标注时框了太多背景,YOLO 学到的边界是草地和树的边缘,不是虎的轮廓。解决:写脚本统计所有框的面积占图像面积的比例,如果平均超过 0.6,说明框太松。我一般会挑出面积比最大的 20 张,人工重新紧致标注,再重新训练。958 张里如果有 50 张以上框松,建议全部返工,否则模型永远学不准。
4.2 类别映射顺序错导致虎被标成背景
现象:训练时cls_loss一直很高,推理时虎的置信度极低,但背景被大量误检。原因:VOC 的<name>里可能有tiger和Tiger两种写法,或者classes.txt里写了多个类别但实际只有虎,导致 class_id 错位。解决:先跑一遍统计脚本,列出所有出现的<name>值,确认只有一种写法。如果有多种,统一转成小写再映射。classes.txt只保留实际存在的类别,不要留空行。
4.3 训练集验证集划分随机种子没固定
现象:每次重新划分数据后训练,mAP 波动超过 0.1,无法复现。原因:用了random.shuffle但没设种子,或者用 YOLO 自动划分时没指定seed。解决:手动划分并固定seed=42,把划分后的文件名列表存成train.txt和val.txt,训练时用data.yaml引用这两个文件而不是目录。这样每次训练的数据完全一致,指标可复现。
4.4 图像尺寸不一致导致归一化坐标错乱
现象:YOLO 标签里有些框的x_center接近 1.0,但 VOC 里框明明在图像中间。原因:VOC 的<size>里记录的宽高和实际图像宽高不一致,转换脚本用了<size>而不是Image.open().size。解决:永远以PIL读取的实际图像尺寸为准,不要信 XML 里的<size>。我踩过这个坑,一批图被工具压缩过但 XML 没更新,转出来的标签全飘了。
4.5 验证集里混入训练集图片
现象:验证集 mAP 异常高,但实际推理新图效果差。原因:划分时按文件名排序后直接切分,但数据集中有重复图片或同一视频抽帧的相似图,导致训练集和验证集高度相似。解决:用感知哈希或简单的像素差去重,把相似度高于 0.95 的图片只保留一张。958 张里如果来自视频抽帧,这一步必做,否则验证指标就是自欺欺人。
5. 把 958 张用到极致:增强策略与推理调参技巧
数据量小的时候,增强不是可选项,是必选项。但增强用过头,虎的纹理被破坏,模型反而学不到东西。我一般分两阶段:前 100 轮用强增强,mosaic=1.0、mixup=0.2、hsv_h=0.015、hsv_s=0.7、hsv_v=0.4、flipud=0.5、fliplr=0.5;后 50 轮关掉 mosaic 和 mixup,只保留颜色抖动和翻转,让模型在接近真实分布的数据上微调。这样做的原因是强增强能防止小样本过拟合,但最后阶段需要让模型看到「干净」的虎,否则推理时对正常光照的虎反而犹豫。
推理阶段有三个参数值得反复调:conf、iou、imgsz。conf从 0.25 开始,如果漏检多降到 0.15,如果误检多升到 0.4。iou在虎群密集场景调到 0.5 到 0.6,单虎场景 0.45 足够。imgsz如果测试图分辨率远高于 640,可以升到 1280 再推理,小目标召回会明显提升,但速度下降约 3 倍。我一般会做一个简单的网格搜索,在验证集上跑conf从 0.1 到 0.5、iou从 0.3 到 0.7 的组合,选 mAP50 最高的那组。
还有一个技巧是测试时增强(TTA),YOLOv8 支持augment=True推理,对同一张图做翻转和缩放后合并预测。958 张训练出来的模型用 TTA 通常能再涨 1 到 2 个点 mAP,代价是推理时间翻倍。如果部署环境算力够,这个开关值得开。
最后说一个我自己的习惯:每次训练完,把best.pt和last.pt都在验证集上跑一遍,如果last.pt比best.pt好,说明早停的patience设小了,模型还在下降。这时候把patience调到 50 重新训,往往能多榨出几个点。958 张数据量不大,一次训练也就几十分钟,多试几组参数比盲目加数据划算。希望帮到你。
本文还有配套的精品资源,点击获取