简介:面向目标检测入门与进阶开发者,这份老虎数据集按Pascal VOC与YOLO两种常见格式整理,标注类别为Tiger,适合用于单类动物检测模型训练、标注格式转换练习或迁移学习实验。压缩包共2000个文件,以1999个XML标注文件为主体,另有1个TXT说明文件,整体大小约69.83MB,下载与解压都比较轻量。XML标注文件遵循VOC标准,可被多数检测框架直接读取;说明txt则帮助快速了解包内结构与格式约定。目前已有144人学习下载,适合作为教学演示或小型项目的基础数据。资源由labelImg工具标注,Tiger类别累计标注框数2487个,用户拿到后可直接进行数据集划分、格式校验或模型训练前的预处理;相比从零标注,能明显节省时间和人力成本。
1. 老虎目标检测数据集:2055张标注图,单类检测模型能用它练到什么程度
做目标检测最烦的不是训练那几步,而是数据从哪来。这个老虎数据集就是冲着"省事"来的:2055张真实场景图片,每张都带目标框标注,同时打包了VOC和YOLO两种格式,解压出来就能直接喂给训练脚本。对于只检测"老虎"这一类的单类别任务,这个量级配合预训练权重,足够练出一个日常场景里能用的模型。
反直觉的地方在于:2055张听起来不多,但单类别检测的收敛速度远快于多类别。从COCO预训练权重起步,模型真正要学的只是"老虎"这个类别的特征。数据够不够用,不只看得张数,还看场景多样性、标注质量和train/val划分是否干净。标注里如果有大量"半截老虎"、框得离谱或者文件名对不上,2080张也是白搭。
这篇文章写给两种人:第一次碰YOLO、想用现成标注数据跑通全流程的新手;以及手里有自定义检测需求、想评估单类数据能压榨出多少性能的工程师。下文从格式拆解开始,一路走到训练、参数调优和踩坑记录,最后落到部署验证。
2. VOC与YOLO两种标注格式:目录结构、坐标换算与转换脚本
拿到一个目标检测数据集,第一件事不是解压就开训,而是搞清标注格式。这份老虎数据集同时给了VOC和YOLO两种格式,本质上是同一批框的两种表达方式:VOC用xml存绝对像素坐标,YOLO用txt存归一化中心点。转换关系不复杂,但坐标越界、文件名错位、类别编号对不上这几个坑,几乎每个数据集里都藏着几个。
2.1 VOC格式的xml标注结构:绝对像素坐标怎么读
VOC是Pascal VOC比赛定下的格式,特点是"一张图配一个xml",框的坐标是绝对像素值。常见打包目录是这样:
VOC/ ├── JPEGImages/ │ ├── tiger_0001.jpg │ └── tiger_0002.jpg ├── Annotations/ │ ├── tiger_0001.xml │ └── tiger_0002.xml └── ImageSets/ └── Main/ ├── train.txt └── val.txtJPEGImages放原始图片,Annotations放同名xml,ImageSets/Main里的txt记录train和val的文件名。很多下载的数据集打包时只保留前两个目录,ImageSets可能是空的,这种时候划分要自己做,别指望里面自带。一个标准的VOC xml长这样:
<annotation> <folder>VOC</folder> <filename>tiger_0001.jpg</filename> <size> <width>1920</width> <height>1080</height> <depth>3</depth> </size> <object> <name>tiger</name> <truncated>0</truncated> <difficult>0</difficult> <bndbox> <xmin>120</xmin> <ymin>80</ymin> <xmax>840</xmax> <ymax>760</ymax> </bndbox> </object> </annotation>注意几个字段的实际含义:xmin、ymin、xmax、ymax是目标框左上角和右下角在原图坐标系下的像素坐标,单位是像素,不是比例。truncated=1表示目标被图像边缘截断,训练时有人会直接过滤掉这类样本;difficult=1表示目标难以辨认,竞赛里不计入评估,但普通训练脚本默认不筛。同一张图如果有多个老虎,xml里就会有多个object节点,转换时一个都不能漏。
像LabelImg、labelme这类目标检测常用标注工具,导出VOC基本是默认动作,所以网上能下载的目标检测数据集里VOC格式最常见。它的优点是人可读,打开xml就能看明白框在哪;缺点是每个框多一个文件,文件碎片多,训练框架读起来慢。这也是现代检测框架默认吃txt的根源。
2.2 YOLO格式的txt标注:归一化中心点怎么换算
YOLO格式里,每张图对应一个同名txt,一行一个目标,五个数字用空格分隔:
0 0.2500 0.3889 0.3750 0.6296 0 0.7800 0.2500 0.1800 0.2200五个数字依次是:类别编号、目标中心点x、目标中心点y、目标框宽、目标框高。后四个值全部除以图片宽度或高度做了归一化,数值范围在0到1之间。第一列类别编号从0开始,如果这个老虎数据集只有一类,所有txt的第一列应该全是0;如果zip里还混了其他动物的标注,就得找到打包时附带的classes.txt或names.txt,按里面的顺序对编号。
换算关系用上面的xml验证一下:图片宽1920、高1080,框xmin=120、xmax=840,那么框宽是840-120=720,中心点x是(120+840)/2=480。归一化后cx=480/1920=0.25,w=720/1920=0.375,正好对上txt里第一行的前两个数值。反过来从txt还原像素坐标,就是用cx、cy、w、h分别乘以图片宽高,再算左上角和右下角。
归一化坐标的好处有三条:不受图片分辨率影响,训练时从640改成1280都不用动标注;数值范围固定,模型回归目标稳定;类别和坐标分离,多类训练只改第一列。代价是人不直观,打开txt看不出框在图上什么位置,所以抽查标注时必须写脚本把框画回图片,不能肉眼读小数。
2.3 VOC转YOLO的Python脚本:越界处理与批量转换
下面这个脚本是我处理目标检测数据集时一直在用的:读xml、取size和bndbox、算归一化坐标、写出txt。针对下载数据集的常见脏数据做了三道防护。
import os import xml.etree.ElementTree as ET classes = ['tiger'] # 类别表,顺序就是类别编号 def voc_to_yolo(xml_path, output_dir): tree = ET.parse(xml_path) root = tree.getroot() size = root.find('size') img_w = float(size.find('width').text) img_h = float(size.find('height').text) # 优先用xml文件名取主干,不要信xml里的filename字段 xml_name = os.path.basename(xml_path) stem = os.path.splitext(xml_name)[0] lines = [] for obj in root.iter('object'): name = obj.find('name').text.strip() if name not in classes: continue # 不在类别表里的对象直接跳过 cls_id = classes.index(name) bnd = obj.find('bndbox') xmin = float(bnd.find('xmin').text) ymin = float(bnd.find('ymin').text) xmax = float(bnd.find('xmax').text) ymax = float(bnd.find('ymax').text) # 坐标越界保护:把越界坐标夹到图片范围内 xmin = max(0.0, min(xmin, img_w - 1)) xmax = max(0.0, min(xmax, img_w - 1)) ymin = max(0.0, min(ymin, img_h - 1)) ymax = max(0.0, min(ymax, img_h - 1)) if xmax <= xmin or ymax <= ymin: continue # 宽度或高度为负,无效框直接丢弃 cx = (xmin + xmax) / 2.0 / img_w cy = (ymin + ymax) / 2.0 / img_h w = (xmax - xmin) / img_w h = (ymax - ymin) / img_h lines.append(f"{cls_id} {cx:.6f} {cy:.6f} {w:.6f} {h:.6f}") with open(os.path.join(output_dir, stem + '.txt'), 'w', encoding='utf-8') as f: f.write('\n'.join(lines))三个细节值得说:第一,越界保护必须写。标注软件里偶尔会出现xmin写成负数、xmax超过图片宽这种手滑值,不夹一下,YOLO训练时letterbox缩放会引入非法坐标。第二,xml里的filename字段不可信。标注工具重命名图片时经常忘记同步这个字段,我直接用xml文件名取主干,绕开这个坑。第三,类别编号用classes列表的index,不写死0。如果这个zip里只有老虎,classes=['tiger']没问题;如果打开txt发现第一列有1,说明打包时混了多类,必须重建类别表。
批量转换用一条bash循环:
mkdir -p yolo_labels for xml in VOC/Annotations/*.xml; do python voc2yolo.py "$xml" yolo_labels done转完后再做一步核对:每张图片必须有同名txt,txt为空表示这个框被过滤掉了,这种图要么删掉,要么手工补框。随机抽十张图,用OpenCV把框画回去肉眼看一遍,这一步能暴露几乎所有标注问题。
import cv2 def draw_yolo_boxes(img_path, txt_path, out_path): img = cv2.imread(img_path) h, w = img.shape[:2] with open(txt_path) as f: for line in f: cid, cx, cy, bw, bh = map(float, line.split()) x1 = int((cx - bw / 2) * w) y1 = int((cy - bh / 2) * h) x2 = int((cx + bw / 2) * w) y2 = int((cy + bh / 2) * h) cv2.rectangle(img, (x1, y1), (x2, y2), (0, 0, 255), 2) cv2.imwrite(out_path, img)如果你拿到的zip里YOLO的txt和VOC的xml能互相换算,说明打包的人做了双格式同步;如果两边对不上,以你实际画框验证过的那份为准。我一般默认用YOLO的txt直接进训练,VOC留作人工抽查,毕竟训练框架不关心你标注格式有多少种,它只认txt。
3. 训练前的数据准备:2055张图划分、tiger.yaml与YOLOv8最小训练
格式确认没问题之后,进入训练前最后三件事:划分数据集、写data.yaml、跑通最小训练。这三件事看着简单,却决定了后面调参时是省心还是闹心。划分不干净,模型指标虚高;yaml路径写错,训练直接报错;batch没算好显存,来回试错浪费时间。
3.1 train/val/test划分脚本:随机种子与同源连拍泄漏
2055张图不可能全拿去训练,必须留出验证集和测试集。我的默认比例是8:1:1,单类数据量小,验证集给10%约两百多张,足够统计出稳定的mAP。划分时固定随机种子,保证每次重跑结果可复现。
import os import random import shutil random.seed(42) # 固定种子,复现结果的关键 src_img = 'images' train_dir = 'images/train' val_dir = 'images/val' test_dir = 'images/test' os.makedirs(train_dir, exist_ok=True) os.makedirs(val_dir, exist_ok=True) os.makedirs(test_dir, exist_ok=True) imgs = [f for f in os.listdir(src_img) if f.lower().endswith(('.jpg', '.jpeg', '.png'))] imgs.sort() random.shuffle(imgs) n = len(imgs) train_imgs = imgs[:int(n * 0.8)] val_imgs = imgs[int(n * 0.8):int(n * 0.9)] test_imgs = imgs[int(n * 0.9):] def move_files(files, dst): for f in files: shutil.move(os.path.join(src_img, f), os.path.join(dst, f)) label = os.path.splitext(f)[0] + '.txt' label_src = os.path.join('labels', label) if os.path.exists(label_src): label_dst = dst.replace('images', 'labels', 1) os.makedirs(label_dst, exist_ok=True) shutil.move(label_src, os.path.join(label_dst, label)) move_files(train_imgs, train_dir) move_files(val_imgs, val_dir) move_files(test_imgs, test_dir)这段脚本有两个容易忽略的点。第一:图片和标注txt必须一起移动,只移图片不移txt,训练时会出现大量图片没有标注,日志里labels数量明显小于images数量。第二:dst.replace('images', 'labels', 1)假设图片和标注在同一个上级目录下、目录名分别是images和labels,如果实际解压结构不是这样,改成显式传参更稳。
随机划分最大的隐患是同源连拍泄漏。如果这2055张里包含从同一段视频抽出来的连续帧——比如tiger_0120到tiger_0160来自同一段拍摄,画面几乎一样——随机划分会把高度相似的帧同时分进训练集和验证集,验证指标虚高,模型一到新场景就露馅。处理办法是先看文件名,如果名字带连续数字前缀,按前缀分组,把整个组划进同一个集合。这个zip里的文件具体怎么命名我无法替你确认,解压后先扫一遍文件名再决定要不要分帧组。看到连续递增的编号,直接联想到连拍,这是做数据集的职业本能。
3.2 data.yaml的路径写法:相对路径、names编号与检查命令
YOLOv8用yaml描述数据集,单类检测的yaml是最短的一类:
# datasets/tiger/tiger.yaml path: ./datasets/tiger # 相对于当前工作目录 train: images/train val: images/val test: images/test names: 0: tigerpath是数据集根目录,train、val、test是相对path的路径。这里最容易翻车的是绝对路径和相对路径混用:写成绝对路径,换机器必须改;不写path,YOLO会把train理解成当前目录下直接叫images/train的路径。我的习惯:在项目根目录运行训练命令,yaml里全用相对路径,换机器只需要把datasets整个目录拷过去。
names定义一个类别,编号0对应标注txt里第一列的0。如果txt第一列出现了1,训练时会报num_class不匹配或者验证时显示unknown类。可以不写nc,YOLO根据names自动推断;写了就必须和names数量一致,单类就是nc=1,多写个nc=2会直接让矩阵混乱。检查txt里到底有哪些类别编号,用下面这段:
cut -d' ' -f1 labels/train/*.txt 2>/dev/null | sort | uniq -c如果输出只有一行"2055 0",说明所有标注都是第0类,yaml可以放心写单类。出现其他数字就得回到类别表重新映射了。
3.3 最小训练命令:显存不够先减batch还是imgsz
数据准备好后,用COCO预训练权重启动训练。yolov8n最省资源,yolov8s精度更好。2055张单类数据,我一般从s起步,100个epoch足够看到结论。
yolo detect train \ data=datasets/tiger/tiger.yaml \ model=yolov8s.pt \ epochs=100 \ imgsz=640 \ batch=16 \ device=0模型文件yolov8s.pt不需要提前手动下载,ultralytics检测到本地没有会自动拉取。如果公司内网挡住外网下载,需要提前把预训练权重放到当前目录,或者在代码里指定权重路径,这是yolo预训练模型下载最常见的拦路虎。
显存不足时,优先减batch而不是减imgsz。batch=16在8G显存上通常能跑yolov8s;还爆显存就降到batch=8,或者换yolov8n.pt。imgsz不建议低于512,低于这个值,远处的小目标被直接缩放没了,对野生动物这类大尺度检测伤害明显。epoch也要理性看待:单类数据100轮够用,如果看到val mAP 50轮就平台期,提前用best.pt收工,没必要死等。
4. 单类检测训练参数:老虎目标在复杂背景下的增强与调参
数据集结构和训练链路都通了,剩下的是纯调参。这个阶段有个常见误区:以为YOLO默认参数就是最优解。对于老虎这种野外目标,丛林背景复杂度高、目标尺度变化大、光照乱,有五个参数值得单独拎出来调。
4.1 数据增强参数:mosaic、hsv和水平翻转怎么取舍
YOLOv8的训练增强默认值写在配置文件里,核心几项是mosaic=1.0、hsv_h=0.015、hsv_s=0.7、hsv_v=0.4、fliplr=0.5。对老虎检测,这些默认值方向对,但有几个点值得改。
第一,fliplr水平翻转保留。老虎不存在"必须朝左站"这种方向敏感性,水平翻转等于白送一倍训练样本,对只有2055张的数据集非常划算。第二,mosaic=1.0保留,它把四张图拼一起训练,强制模型学习小目标上下文,对遮挡场景帮助明显。但mosaic有个副作用:最后阶段模型尺寸感知会偏移,所以YOLOv8默认在最后10个epoch自动关闭mosaic,这个close_mosaic=10不用改。第三,hsv_v亮度增强保持默认0.4就可以,不要为了"让模型适应逆光"调到0.8以上,颜色失真会让模型丢掉老虎条纹这个关键特征。
如果你训练后发现树荫里的老虎漏检很多,优先调大translate和scale,而不是动mosaic。translate控制目标在画面内平移范围,scale控制随机缩放幅度,这两个参数直接决定模型应对目标位置偏移和远近变化的能力。
4.2 前景背景不平衡与loss权重:别让cls_loss带偏回归
单类检测没有多类别不平衡问题,但有两个更隐蔽的点要处理。
第一个是前景背景不平衡。平均每张图只有一个目标,其他区域全是负样本,模型很容易收敛到"啥都不框也能把loss压到很低"的状态。解决办法不是改loss权重,而是保证训练充分——单类数据要跑到验证集mAP平台期,不要看到训练loss降得慢就提前停。
第二个是类别任务退化问题。只有一个类别时,分类任务本质上退化成"是不是老虎"的二分类,模型容量应该大部分花在bndbox回归上。这就是为什么我建议用yolov8s而不是yolov8n,以及不要为了加速收敛去调大cls_loss的权重。cls_loss调大会诱发大量假阳框,框了一片树影说它像老虎。box_loss、cls_loss、dfl_loss三者的默认权重在YOLOv8里已经是平衡好的,单类场景下最该做的反而是什么都不动。
4.3 训练过程盯什么:loss曲线、mAP50与PR曲线的判断标准
训练开始后,终端里滚动的实时日志不用细看,真正要盯的是runs/detect/train目录下的results.csv,训练结束后还有一堆png图表。重点看三件事。
第一,loss曲线。box_loss和cls_loss在前30轮应当单调下降,如果震荡剧烈甚至往上走,大概率是学习率偏大。对单类小数据集,我习惯把lr0从默认0.01改成0.005,收敛慢一点但稳。第二,mAP50。单类干净数据练到0.9以上是及格线,0.95以上是好结果。如果只有0.7,先怀疑数据问题而不是参数问题——回去画框看标注,八成有框错位。第三,PR曲线。曲线下面积大、向右上方鼓起说明precision和recall平衡;如果曲线贴着顶边往下掉,说明recall虚高、precision不足,部署时需要把conf阈值往上提。
除此之外,每个epoch结束会输出混淆矩阵,单类场景下就是一个2x2矩阵,看TP的位置就知道模型把老虎当背景的比例高不高。这些图和csv都是黑匣子里最直接的证据,不要只盯着"epoch 50/100"那行进度条。
5. 避坑记录:老虎数据集从解压到训练的五个常见问题
数据集类的zip包,坑几乎都集中在数据本身,而不是模型。下面五条是我处理各种下载数据集时反复碰到的翻车现场,每一条都写成"现象、原因、解决"三段,方便你直接对号入座。
5.1 解压后路径带中文或空格,训练脚本读不到文件
现象:yolo命令报AssertionError: train: No labels in images/train,或者No labels found,检查目录文件明明都在。
原因:zip里目录名带中文或空格,Windows上解压出来没问题,换到Linux或Mac上路径解析不一致;有些训练框直接读不到带空格的路径。这是zip解压后最常见的连锁反应。
解决:解压后立刻把目录名改成纯英文加下划线,放到项目根目录的datasets下。yaml里全部用相对路径。如果已经训练到一半发现这个问题,不需要重新下载,直接把目录改名再改yaml里的path即可。
5.2 图片名和标注文件名对不上,一批图被无声跳过
现象:训练日志里出现N images found, M labels found,M明显小于N,有些图片被跳过但没报错。
原因:转格式时用了xml里的filename字段,而该字段和真实图片名不一致。下载的数据集里,xml是标注工具自动生成的,图片可能被后期重命名过,两边就对不上了。
解决:以图片文件名为基准,找出所有没有对应txt的图片,看差异模式:
import os imgs = set(os.path.splitext(f)[0] for f in os.listdir('images')) txts = set(os.path.splitext(f)[0] for f in os.listdir('labels')) print('缺标注:', sorted(imgs - txts)[:20])如果缺的是后缀或前缀不同,比如图片叫tiger_001.jpg而txt叫0001.txt,写个批量重命名脚本统一;如果完全对不上,回到xml重新转换。
5.3 标注框坐标越界或为负,训练时loss变成nan
现象:训练一开始loss就是nan,或者训练正常但验证框全部错位,预测框跑到画面外。
原因:xml或txt里的坐标本身出错了,比如xmin大于图片宽度、中心点出现负数。YOLO的letterbox虽然会对越界框做钳制,但标注错得太离谱会让回归目标直接错乱,造成loss发散。
解决:转换前先跑一遍越界检查,把所有超出图片范围的框打出来,决定丢弃还是钳制。2055张图里有那么几个坏框非常正常,不要为这几个框手工重新标注整个数据集。我写的转换脚本里那三行越界保护,就是专门干这个的。
5.4 txt类别编号和names对不上,模型把tiger学成第二类
现象:训练能跑,mAP也不难看,但预测输出的标签显示unknown或者类别名为空,和自己yaml里对不上。
原因:txt第一列写的是1,而yaml里的names从0开始编号,两者错位了一个。遇到数据标注工具默认从1开始计数的情况,这个坑就会存在。
解决:解压后先看txt第一列的唯一值分布:
cut -d' ' -f1 labels/*.txt | sort | uniq -c如果出现1,要么把所有txt第一列减1,要么在names里把编号改成匹配的值。二选一,关键是让txt和names对齐,别在训练脚本里自动修正,那会让复现变得更加玄学。
5.5 划分数据集时拆散同一段连拍,验证指标虚高
现象:训练时val的mAP50高达0.97,自己拿着模型跑测试视频却频繁漏检,落差很大。
原因:随机划分时,同一段连拍的相似帧被同时分进了train和val,模型在训练时就见过"答案"了。这个不是造假的虚高,是划分方案带来的数据泄漏,属于静态数据里最隐蔽的坑。
解决:按文件名前缀分组划分,把连续编号的帧归到同一个集合。如果改完划分后mAP跳水,说明之前那个成绩本来就不真实。这个修正越早做越好,因为调参阶段的超参数选择都是对着val指标做的,val指标脏,后面所有判断都跟着脏。
除此以外,还要检查一种情况:如果解压出来的zip不是正版打包,而是被人二次压缩过,里面可能混入了损坏的jpg或空txt。训练前跑一遍全量检查,把无法解码的图片单独归到broken目录,不要直接删,万一后面还要核对原始数据呢。
6. 从数据集到可部署检测器:test推理验证与ONNX导出
训练完先不要信results.csv里的数字,用没进过训练也没进过验证的test目录做一次端到端推理,这是模型上线前最便宜的一次体检。
6.1 用测试集跑一次完整推理验证
yolo predict \ model=runs/detect/train/weights/best.pt \ source=datasets/tiger/images/test \ conf=0.25 \ save_txt=True跑完后看两个东西。第一个是置信度分布:被漏掉的老虎大多是半藏在草丛里或者拍得极小,这类图单独拉出来分析,如果集中在某个特定场景,说明训练数据的场景多样性不够,回数据层面想办法。第二个是重复框:大量0.9以上的高置信框压在同一只老虎身上,说明NMS阈值需要调;如果低置信框一大堆,conf从0.25往上提到0.4,单类场景下保precision比保recall更实际。
6.2 导出ONNX:模型离开训练环境前的最后一步
验证通过后,把best.pt导出成ONNX,供边缘盒子或服务端推理使用:
yolo export \ model=runs/detect/train/weights/best.pt \ format=onnx \ imgsz=640导出时注意opset默认用12,如果你的推理框架版本旧,遇到底层算子不支持,可以指定opset=11或者开dynamic动态输入。这块是最容易因为版本搭配摆烂的地方,排错时去查onnx和onnxruntime的版本对应关系,别回头重新训练模型。
我的习惯是:划分脚本、随机种子、tiger.yaml和转换脚本全部提交进代码库,任何人拿到这个老虎数据集zip都能复现我的结果。数据集的坑永远先于模型出现,把数据链路钉死,训练才有后悔药可吃。希望帮到你。
本文还有配套的精品资源,点击获取