
简介面向目标检测训练与算法验证的虎图像数据集包含958张左右真实虎场景图片由labelImg标注完成同时输出VOC与YOLO格式覆盖tiger单一类别适合目标检测入门练习、模型迁移测试及课堂演示。压缩包共2000个文件总大小约361.65MBjpg图像、VOC格式xml标签和YOLO格式txt标签分别归入三个文件夹解压即可用xml便于接Faster-RCNN、SSD等检测框架txt则可直接喂入YOLO系列训练流程。标注过程遵循边界框贴合、目标不遗漏和交叉一致性核对原则能有效降低训练噪声数据集无需密码解压和格式转换自带规整目录适合需要快速获得干净标注数据的目标检测初学者与科研人员。资源已有76人学习浏览是一份可快速上手的实用数据集。1. 虎数据集从标注格式到训练落地的完整样本做目标检测的人都知道数据集的坑往往比模型还多。我拿到这份虎的数据集时第一反应是查两样东西标注格式是不是规范、图片和标注文件数量能不能对上。这份资源958张左右每张jpg图片配一个VOC格式的xml和一份YOLO格式的txt类别只有tiger一个标注工具是labelImg。对做单类别动物检测、想快速跑通训练流程、或者刚入门想搞清楚两种标注格式区别的人来说这份数据比网上那些裁剪过的演示数据集更接近真实工程环境——它保留了你日常收集数据时会遇到的琐碎问题比如文件名不连续、图片大小范围从1KB到500KB不等。解压之后三个文件夹jpg图片、xml标注、txt标注各一摞直接就能用。2. 两种标注格式的对应关系xml 与 txt 里的边界框怎么换算2.1 先看 xml 文件VOC 格式的字段含义VOC格式的标注长这样我随机挑了一张tiger_587.jpg对应的xml来看annotation foldertiger_jpg/folder filenametiger_587.jpg/filename pathC:/Users/xxx/Desktop/tiger/tiger_jpg/tiger_587.jpg/path source databaseUnknown/database /source size width640/width height480/height depth3/depth /size segmented0/segmented object nametiger/name poseUnspecified/pose truncated0/truncated difficult0/difficult bndbox xmin132/xmin ymin87/ymin xmax589/xmax ymax431/ymax /bndbox /object /annotation注意里面几个关键字段size记录了图片宽度、高度和通道数bndbox是真实像素坐标name就是类别名。xml文件里每一个object代表一个目标框如果一张图里有两只虎就会出现两个object块。这里最重要的一个隐藏问题是path字段——很多标注工具的旧版本会保存当时的本机路径换一台电脑后这个路径就失效了后面避坑篇我会专门讲这个问题。2.2 再看 txt 文件YOLO 格式的五列含义YOLO格式的标注是纯文本每行一个目标五列数据用空格分隔。同上面那张图对应的txt长这样0 0.563281 0.499586 0.714062 0.716667五列的含义分别是类别索引、归一化后的中心点x坐标、中心点y坐标、归一化后的框宽、框高。类别索引从0开始因为数据集里只有tiger这一个类别所以所有行的第一列都是0。归一化是指把像素坐标除以图片宽高得到的值全部落在0到1之间。这套格式是YOLO系算法的标准输入不管是yolov5、yolov8还是最新版本的ultralytics仓库读标注文件时都默认按这个格式解析。2.3 两种坐标怎么换算以及 labelImg 为什么会同时生成两套从VOC像素坐标转成YOLO归一化坐标核心就一个公式组# 假设 xml 里有 bndbox, 图片宽高为 width, height x_center ((xmin xmax) / 2) / width y_center ((ymin ymax) / 2) / height box_width (xmax - xmin) / width box_height (ymax - ymin) / height反过来从YOLO转回像素坐标就是逆运算用中心点加减半宽半高就能还原出xmin、xmax、ymin、ymax。我在拿到这批数据后做过一次抽查把txt的归一化坐标还原成像素坐标再和xml里的bndbox对比两者完全对得上误差只在浮点精度层面。这说明标注数据是同一套框通过工具转出来的没有出现人工分别标注导致的两份文件不一致。labelImg这个工具有个特点它默认保存VOC格式的xml文件但在工具里切换到YOLO模式后保存的变成txt文件。很多人在标注时只选了一个格式导出导致后面换训练框架时还得自己转这份数据集直接两个都给了省掉了转换那一步。这里把两类格式的对应关系再捋一遍。VOC的size和YOLO的归一化坐标关键衔接点就是图片的实际像素尺寸。有些标注文件里xml的size和图片真实尺寸对不上一般是图片被resize过而xml没更新这是老标注数据的常见毛病。我检查这份数据时发现size和图片信息能对应说明整理的时候做过一轮校验。对新手来说不要只看标签内容先打开图片和标注框叠在一起看几眼比盯着数字高效得多。3. 验证数据质量写个脚本把 958 张标注全部跑一遍再交给训练3.1 校验第一步文件数量与命名的对应关系训练框架加载数据时通常默认图片和标注文件同名。比如tiger_587.jpg对应tiger_587.xml和tiger_587.txt一旦有图片缺了标注或者标注多了没对应图片训练时就会报错或者跳过样本。我拿到压缩包后的第一件事是写脚本统计三个文件夹的文件数量并检查同名对应关系import os from collections import Counter jpg_dir tiger_jpg # 图片文件夹 xml_dir tiger_xml # xml 标注文件夹 txt_dir tiger_txt # yolo 标注文件夹 # 统计扩展名分布和文件总数 for d in [jpg_dir, xml_dir, txt_dir]: files os.listdir(d) exts Counter(os.path.splitext(f)[1] for f in files) print(f{d}: 共 {len(files)} 个文件, 扩展名分布 {dict(exts)}) # 以图片文件名为基准检查 xml 和 txt 是否齐全 jpg_names set(os.path.splitext(f)[0] for f in os.listdir(jpg_dir)) xml_names set(os.path.splitext(f)[0] for f in os.listdir(xml_dir)) txt_names set(os.path.splitext(f)[0] for f in os.listdir(txt_dir)) print(缺少 xml 的图片:, jpg_names - xml_names) print(缺少 txt 的图片:, jpg_names - txt_names) print(没有对应图片的 xml:, xml_names - jpg_names) print(没有对应图片的 txt:, txt_names - jpg_names)这个脚本的逻辑分两层第一层统计扩展名确认xml文件里没有混入无关文件比如备份的.xml.bak第二层用集合差集看缺失对应关系。我跑完后三个文件夹都是958个文件命名完全对齐。这一步虽然简单但建议每次拿到新数据都必须做训练到一半发现数据加载出错再回头查文件对应关系就晚了。3.2 校验第二步标签内容与边界框合法性文件级校验过了还要做内容级校验。常见问题包括xml里的name字段写错、归一化坐标超出0到1范围、边界框宽高为负数、一张空图没有任何标注等。我写了一个批量解析脚本一次性把这些情况全查出来import os import xml.etree.ElementTree as ET def check_xml(xml_path): tree ET.parse(xml_path) root tree.getroot() objs root.findall(object) if len(objs) 0: return [empty_object] issues [] for obj in objs: name obj.findtext(name) if name ! tiger: issues.append(fwrong_name:{name}) bnd obj.find(bndbox) xmin float(bnd.findtext(xmin)) ymin float(bnd.findtext(ymin)) xmax float(bnd.findtext(xmax)) ymax float(bnd.findtext(ymax)) if xmax xmin or ymax ymin: issues.append(finvalid_box:{xmin},{ymin},{xmax},{ymax}) return issues def check_txt(txt_path): issues [] with open(txt_path) as f: lines f.read().strip().splitlines() if len(lines) 0: return [empty_txt] for line in lines: parts line.split() if len(parts) ! 5: issues.append(fbad_cols:{line}) continue vals [float(p) for p in parts[1:]] if any(v 0 or v 1 for v in vals): issues.append(fout_of_range:{line}) return issues for root, dirs, files in os.walk(xml_dir): for f in files: if f.endswith(.xml): issues check_xml(os.path.join(root, f)) if issues: print(f{f}: {issues})这段代码里check_xml负责查类别名和像素坐标合法性check_txt负责查列数是否五列、归一化值是否出界。跑完之后没有任何输出说明这份数据在标注阶段就已经做过自查。这里要说明一下归一化坐标超出0到1范围并不是绝对错误——目标有一部分在画面外时训练框架会裁剪掉越界部分但如果所有坐标都比1大很多那就是明显标错了。轻度越界可以接受重度越界会影响损失计算。3.3 校验结果怎么看正常与异常的输出表现脚本跑完没有输出不代表万事大吉。还要换个角度做反向抽查——随机抽二十张图把标注框画在图片上人工过一遍。具体做法是用OpenCV读图把xml里的bndbox坐标画成矩形保存到一个预览文件夹里然后快速翻看。这一步能看到两个脚本查不出来的问题框是否贴住目标边缘、是否把背景大片圈进来、是否漏标了画面里另一只虎。框选得偏大是新手标注最常见的毛病模型训练时会把大量背景当特征学进去严重影响精度。参考数据本身标注原则的第一条尽可能准确地框选目标边界抽查时要重点关注框和虎身体的贴合度。import cv2 import xml.etree.ElementTree as ET import os, random sample random.sample(os.listdir(xml_dir), 20) # 随机抽20个 xml for name in sample: tree ET.parse(os.path.join(xml_dir, name)) root tree.getroot() img cv2.imread(os.path.join(jpg_dir, root.findtext(filename))) for obj in root.findall(object): bnd obj.find(bndbox) xmin int(float(bnd.findtext(xmin))) ymin int(float(bnd.findtext(ymin))) xmax int(float(bnd.findtext(xmax))) ymax int(float(bnd.findtext(ymax))) cv2.rectangle(img, (xmin, ymin), (xmax, ymax), (0, 0, 255), 3) cv2.putText(img, tiger, (xmin, ymin - 10), cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 255, 0), 2) cv2.imwrite(fpreview_{name.replace(.xml, .jpg)}, img)抽样数量不用多二十张足够建立对数据整体质量的感觉——如果二十张里有两三张明显框歪了那这批数据的标注质量就存疑训练效果也会打折扣。画框预览这个环节很多人会跳过但它是把黑匣子变成可见图形的唯一直接手段。我一般抽两次一次在训练前一次在训练一轮后看验证集输出。4. 接入训练流程数据划分与标签配置实操4.1 数据划分train还是val别把全部958张都丢进去训练直接拿全部958张去训练是新手最容易犯的错误。没有留验证集的训练模型过拟合了都不知道。常见做法是按8:2拆分我用下面的脚本把图片路径和对应标注同步划分import os, random, shutil random.seed(42) # 固定随机种子保证可复现 jpg_dir tiger_jpg xml_dir tiger_xml txt_dir tiger_txt names sorted(os.path.splitext(f)[0] for f in os.listdir(jpg_dir)) random.shuffle(names) val_size int(len(names) * 0.2) # 按 8:2 划分 val_names, train_names names[:val_size], names[val_size:] img_out tiger_split for split in [train, val]: os.makedirs(f{img_out}/{split}/images, exist_okTrue) os.makedirs(f{img_out}/{split}/labels, exist_okTrue)我这里没有直接移动原文件因为原始压缩包还要留底不想因为一次错误划分把原始数据搞乱。实际执行时把shutil.copy换成移动也可以。划分完成后需要检查验证集里是否覆盖了不同体型的样本虎在图片中有全身、半身、特写等不同尺度随机划分偶尔会让验证集全是大头特写这会导致验证指标虚高或虚低依赖固定随机种子可以规避一部分问题。4.2 目录结构两种摆法VOC 风格和 YOLO 风格这份数据集自带VOC和YOLO两套标注训练时可以按两种方式摆目录。用yolov5或yolov8训练推荐YOLO风格目录data配置指向它tiger_yolo ├── images │ ├── train │ │ └── tiger_587.jpg │ └── val │ └── tiger_901.jpg ├── labels │ ├── train │ │ └── tiger_587.txt │ └── val │ └── tiger_901.txt └── tiger.yaml如果后续要跑mmdetection或者老版本的SSD那就要把VOC数据聚合成标准的JPEGImages、Annotations、ImageSets/Main三层结构。yaml配置文件写法如下path: ./tiger_yolo train: images/train val: images/val nc: 1 names: 0: tigernc: 1表示只有一个类别names的索引要和txt标注文件里的第一列严格对应。如果txt里类别索引从0开始names下标0对应tiger不能写成从1开始否则训练时标签整体错位类别名对不上检测框。4.3 写 yaml 与训练前的最后一分钟检查配置写完建议训练前先加载一遍数据集不要直接敲训练命令。用yolov8的Python接口可以快速验证数据是否能被正确加载from ultralytics import YOLO model YOLO(yolov8n.pt) data model.val(datatiger_yolo/tiger.yaml, batch1, workers1) print(data.box.map50) # 打印 mAP50验证模型能正常读数据这一步实际是拿一个随机初始化的模型做验证重点不是精度而是确认路径配置、标注格式、类别数这三个环节没出错。如果标注文件有问题这里就会直接抛出异常。跑通之后再正式训练可以加--epochs 100 --imgsz 640这类参数控制训练时长。训练过程中的损失曲线也不要只盯着训练集损失要同时看验证集损失验证损失不降反升说明过拟合了话说回来958张的单类别数据量训练一个几十轮的模型是够用的。5. 避坑篇标注数据训练中的五个真实翻车点5.1 现象训练时报错 label 索引超出范围原因txt里类别索引写成了从1开始而yaml里nc设为1索引0是合法值索引1直接越界。这个翻车我见过太多次了跟踪大量标注工具导出的txtYOLO模式的第一列默认从0开始但工具更新后有时会从1开始输出还不变非常隐蔽。解决训练前写个命令扫一遍txt第一列的最大值awk {print $1} tiger_txt/*.txt | sort -n | tail -1如果输出是0说明索引正常如果是1或更大就要做批量减去1的操作或者调整yaml里nc和names映射。5.2 现象模型训练能跑但验证时所有框都画歪原因txt的归一化坐标相对的不是xml里size字段的宽高而是另一张被缩放过的图片。这份数据的jpg从1KB到500KB都有说明来源图片分辨率差异很大整理时如果发生了resize却没有同步更新标注就会产生这种错位。解决拿现象最严重的图片对照标注框预览如果txt画框和xml画框一个准一个不准说明其中一套在转换时用的尺寸基准不对。按上一章的坐标还原公式重新验一遍发现问题就批量重算。5.3 现象val精度高但实际检测效果很差原因训练集和验证集划分时打开了随机种子但没有检查类别平衡和尺度分布导致验证集全是简单样本。958张虎图片中可能有一批是同一视频连续帧截出来的相似度极高随机划分会把它们同时分进训练和验证验证集成了开卷考试。解决按图片来源分组划分或者先做图片去重再划分。一张图和多张相似图在验证集里同时出现指标会虚高到看不出真实水平。5.4 现象模型把虎周围的草地也检测成目标原因标注框包进了太多背景模型把背景纹理当成了类别特征。虎的花纹和草地背景在特征提取网络看来都有高频纹理信息框选不贴合时边界区域的梯度会把背景特征也播给分类头。解决对照3.3节的预览图把明显框偏大的样本找出来重标。如果重标成本高可以先用脚本计算所有框的宽高比筛出那些极端扁或极端方的框优先检查它们这些往往是标注时手腕抖动带出来的。5.5 现象解压后脚本报错提示文件不存在原因压缩包内目录有嵌套结构直接按平铺文件路径访问就会报错。我在3.1节写的脚本默认三个文件夹在同一目录下但你解压后的文件夹名可能带了前缀或层级关系。解决解压后第一件事不写代码先看目录树find . -type f | head -50确认jpg、xml、txt的具体位置再根据实际路径修改脚本里的目录变量。这一步几秒钟的事能省掉后面一堆路径折磨。6. 进阶VOC转YOLO核心逻辑与批量画框预览技巧6.1 自己写一个VOC转YOLO的转换核心虽然这份数据集两套格式都给全了但实际工作中经常遇到只有xml的情况或者你自己用labelImg标注了一批新数据只导出了一种格式。这时候手头有一个可靠的转换函数就很实用。核心只有十几行def voc_to_yolo(xml_path, out_txt_path, class_names): tree ET.parse(xml_path) root tree.getroot() w int(root.findtext(size/width)) h int(root.findtext(size/height)) lines [] for obj in root.findall(object): name obj.findtext(name) if name not in class_names: continue cls_id class_names.index(name) bnd obj.find(bndbox) xmin float(bnd.findtext(xmin)) ymin float(bnd.findtext(ymin)) xmax float(bnd.findtext(xmax)) ymax float(bnd.findtext(ymax)) xc ((xmin xmax) / 2) / w yc ((ymin ymax) / 2) / h bw (xmax - xmin) / w bh (ymax - ymin) / h lines.append(f{cls_id} {xc:.6f} {yc:.6f} {bw:.6f} {bh:.6f}) with open(out_txt_path, w) as f: f.write(\n.join(lines))用时把class_names传成[tiger]即可。注意size字段有可能缺失或写错转换前最好先取真实图片宽高覆盖掉xml里的值这样遇到原始标注不规范的情况也能保持输出正确。6.2 批量画框预览肉眼复核最直接前面抽查时是随机选二十张如果能批量生成全部预览图就能直接排查漏标和错标。用multiprocessing并行处理958张图很快一分钟内能出全部预览。生成之后不要一张张看用文件管理器缩略图模式快速翻颜色统一画成红色漏标一眼就能发现。我习惯用过一遍之后拿着预览图和原目录对照从第1张滑到第958张哪个框没贴住虎身马上就能定位到对应的标注文件并修正。从这个数据集的实际表现看整批标注的框线都贴着目标边界与摘要里提到的标注原则是一致的。做检测数据集这份工作吃过的亏越多越明白校验流程比模型调参更能决定最终效果。现在不管是拿到公开数据集还是自己标的新数据我都强制走一遍文件名对应、标签内容合法性、随机抽样预览、划分后重新加载测试。走完这一套流程训练才算真正开始。希望这篇笔记能帮你在自己的数据上少走这些弯路。本文还有配套的精品资源点击获取