☰
鹿数据集VOC+YOLO双格式实战:504张图664个框,从标注校验到YOLO训练全流程
2026/10/5 14:29:05 网站建设 项目流程

简介:本资源为鹿类目标检测数据集,面向从事计算机视觉、深度学习目标检测训练与验证的学生、算法工程师及科研人员,可解决鹿只识别场景下样本获取与标注成本高的问题。压缩包共1514个文件,包含504张jpg原始图像、504个VOC格式xml标注文件、504个yolo格式txt标注文件及少量说明文本,整体约177.8MB,xml与txt分别适配Pascal VOC和YOLO两种主流训练框架,无需额外转换即可直接投入训练。标注采用labelImg完成,仅设Deer一个类别,共标注664个矩形框,图像命名规范、类别统一,适合作为单类别检测的基线数据或迁移学习素材。目前已有126人学习下载,读者可据此快速搭建鹿只检测实验,验证模型在真实场景下的识别效果,并对比VOC与YOLO两种标注格式的读取与训练流程。

1. 鹿数据集 VOC+YOLO 双格式:504 张图、664 个框,这份资源到底能不能直接开训

手上有一批野生动物红外相机照片,想跑一个鹿类目标检测,最卡脖子的往往不是模型结构,而是标注。自己拉框、写 XML、再转 YOLO 的 txt,一套流程走下来,几百张图能耗掉两三天。这份「鹿数据集 VOC 格式 + yolo 格式 504 张 1 类别」就是冲着这个痛点来的:504 张 jpg 图片,配套 504 个 Pascal VOC 的 xml 标注文件和 504 个 YOLO 的 txt 标注文件,单类别Deer,总框数 664,用 labelImg 画的矩形框。它解决的是「从零标注」到「直接进训练脚本」之间那段最枯燥的体力活,适合做野生动物检测、边缘端小模型验证、或者单纯想拿一份干净单类数据集练手 YOLO 全流程的人。下面我按「这份资源是什么 → 怎么接进训练 → 哪里会翻车」的顺序拆一遍,能抄的步骤我都落成代码。

2. 先看清目录结构和标注口径:VOC 与 YOLO 到底差在哪

拿到压缩包别急着解压进训练目录,先花五分钟把结构摸清楚。这份资源的摘要写得很明确:只有 jpg 图片、VOC 的 xml、YOLO 的 txt,不包含分割路径的 txt 文件(也就是没有 train/val 划分清单)。这意味着划分训练集验证集这一步得你自己做,别指望开箱就有train.txt。

2.1 两种格式的坐标口径差异

VOC 的 xml 里,<bndbox>存的是绝对像素坐标xmin, ymin, xmax, ymax,原点在左上角。YOLO 的 txt 每行是class_id x_center y_center width height,全部归一化到 0~1,且是相对整张图宽高。这两套口径如果混用,最常见的事故就是框整体偏移或者缩成一团。判断一份数据集是否「双格式自洽」,可以抽几张图把 xml 和 txt 都读出来,反算回像素坐标对比。

import xml.etree.ElementTree as ET from PIL import Image def voc_to_pixel(xml_path): tree = ET.parse(xml_path) root = tree.getroot() size = root.find('size') w, h = int(size.find('width').text), int(size.find('height').text) boxes = [] for obj in root.iter('object'): name = obj.find('name').text bb = obj.find('bndbox') xmin = float(bb.find('xmin').text) ymin = float(bb.find('ymin').text) xmax = float(bb.find('xmax').text) ymax = float(bb.find('ymax').text) boxes.append((name, xmin, ymin, xmax, ymax)) return w, h, boxes def yolo_to_pixel(txt_path, w, h): boxes = [] with open(txt_path) as f: for line in f: cid, xc, yc, bw, bh = line.split() xc, yc, bw, bh = map(float, (xc, yc, bw, bh)) # 反归一化回像素 xmin = (xc - bw / 2) * w ymin = (yc - bh / 2) * h xmax = (xc + bw / 2) * w ymax = (yc + bh / 2) * h boxes.append((int(cid), xmin, ymin, xmax, ymax)) return boxes w, h, voc_boxes = voc_to_pixel('firc_Deer_11.xml') yolo_boxes = yolo_to_pixel('firc_Deer_11.txt', w, h) print('VOC:', voc_boxes) print('YOLO:', yolo_boxes)

这段代码的逻辑是:VOC 侧直接读绝对坐标,YOLO 侧把归一化值乘回宽高。跑完对比两组数,如果 xmin/ymin/xmax/ymax 对得上(允许 1~2 像素取整误差),说明这份资源的双格式是自洽的,可以放心二选一使用。参数上注意class_id从 0 开始,单类别就是 0,对应Deer。

2.2 类别映射与文件名对应关系

单类别的好处是不用纠结类别顺序,但坏处是很多人会忽略「xml 里的 name 和 txt 里的 class_id 必须能对上」。这份资源类别名是Deer,txt 里应该全是 0。建议写个脚本全量扫一遍,确认没有空标注文件、没有越界框、没有 name 拼写不一致。

import os, glob img_dir = 'images' xml_dir = 'annotations_xml' txt_dir = 'labels_yolo' imgs = {os.path.splitext(os.path.basename(p))[0] for p in glob.glob(f'{img_dir}/*.jpg')} xmls = {os.path.splitext(os.path.basename(p))[0] for p in glob.glob(f'{xml_dir}/*.xml')} txts = {os.path.splitext(os.path.basename(p))[0] for p in glob.glob(f'{txt_dir}/*.txt')} print('图片数:', len(imgs), 'xml数:', len(xmls), 'txt数:', len(txts)) print('缺xml:', imgs - xmls) print('缺txt:', imgs - txts) print('多余xml:', xmls - imgs)

逻辑说明:用文件名主干做集合运算,快速定位「有图没标注」或「有标注没图」的脏数据。参数上把三个目录换成你解压后的实际路径即可。这一步是血泪经验——很多数据集看着数量对,实际文件名大小写、后缀不一致,训练时直接报找不到标签。

2.3 664 个框意味着什么

504 张图 664 个框,平均每张 1.3 个框,说明大量图片是单只鹿或者空背景。这对训练有两个影响:一是正样本偏稀疏,如果直接按图随机划分,验证集里可能出现整批无目标图,mAP 波动大;二是要警惕「背景图」被误当成负样本处理。常见做法是划分时按框数分层抽样,保证 train/val 的正样本比例接近。下面给一个可复现的划分脚本。

import random, os, glob random.seed(42) names = sorted(os.path.splitext(os.path.basename(p))[0] for p in glob.glob('labels_yolo/*.txt')) random.shuffle(names) split = int(len(names) * 0.8) train, val = names[:split], names[split:] os.makedirs('splits', exist_ok=True) for tag, subset in [('train', train), ('val', val)]: with open(f'splits/{tag}.txt', 'w') as f: for n in subset: f.write(f'images/{n}.jpg\n') print('train:', len(train), 'val:', len(val))

逻辑说明:固定随机种子保证可复现,8:2 划分,输出 YOLO 训练常用的图片路径清单。参数seed换成别的值会得到不同划分,做对比实验时建议固定。注意这里写的是图片绝对/相对路径,YOLO 训练时标签路径默认由图片路径替换images为labels推导,所以目录命名要规范。

3. 接进 YOLO 训练:从 data.yaml 到第一次跑通

格式确认没问题后,真正落地就是把它喂给 YOLO。不管你是 YOLOv5、v8 还是更新的版本,数据组织逻辑基本一致:一个data.yaml描述路径和类别,图片和标签分目录放。这一章把配置、命令、参数含义讲透,新手照着能跑,熟手能直接改。

3.1 目录组织与 data.yaml 写法

推荐的结构是图片和标签平级分目录,标签目录名用labels,图片用images,这样 YOLO 能自动推导。把解压出来的 jpg 放进images,txt 放进labels,xml 单独存一份备用(VOC 训练或转其他格式时还要用)。

# data.yaml path: /data/deer_dataset train: splits/train.txt val: splits/val.txt nc: 1 names: 0: Deer

逻辑说明:path是数据集根目录,train/val指向刚才生成的清单文件,nc是类别数,names是索引到类名的映射。参数上最容易错的是names的写法——有的版本要求列表['Deer'],有的要求字典{0: Deer},以你用的 YOLO 版本文档为准。单类别时nc: 1,索引只能是 0,和 txt 里的 class_id 对齐。

3.2 训练命令与关键参数

以常见的 YOLO 训练入口为例,一条命令能跑起来,但参数决定成败。

yolo detect train \ data=data.yaml \ model=yolov8n.pt \ epochs=100 \ imgsz=640 \ batch=16 \ workers=4 \ project=runs/deer \ name=exp1 \ seed=42

逻辑说明:model选 nano 版本是因为 504 张图属于小数据量,大模型容易过拟合,先用小模型验证流程通不通。imgsz=640是通用起点,如果你的原图分辨率远大于 640,小目标会被缩没,需要调大或做切图。batch=16在显存不够时降到 8 或 4。workers是数据加载线程,Windows 下建议设 0 避免多进程报错。seed固定保证划分和增强可复现。第一次跑建议epochs先设 10,确认 loss 正常下降、验证能出结果,再拉长。

3.3 训练前用脚本做一次标注可视化

在正式训练前,把标注画到图上肉眼过一遍,能提前发现框错位、漏标、类别错。这一步比看 loss 曲线更直接。

import cv2, glob, os for txt in glob.glob('labels_yolo/*.txt')[:20]: # 先抽查20张 name = os.path.splitext(os.path.basename(txt))[0] img = cv2.imread(f'images/{name}.jpg') h, w = img.shape[:2] with open(txt) as f: for line in f: cid, 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(f'vis_{name}.jpg', img)

逻辑说明:把归一化坐标还原成像素画框,抽查 20 张足够看出整体质量。参数上[:20]控制抽查数量,正式检查可以全量跑但会慢。如果发现框普遍偏小或偏移,多半是归一化时用错了宽高(比如用了缩放后的尺寸),回到 2.1 的对比脚本排查。

提示:可视化输出目录别和训练数据混在一起,否则 YOLO 扫描图片时可能把带框的图也当训练样本。

4. 避坑与排查:这份数据集最容易翻车的五个地方

数据集本身不复杂,但「简单」往往让人跳过检查,结果在训练中途才暴露问题。下面五条是我在实际项目里反复遇到的,按现象、原因、解决写清楚。

4.1 训练报「找不到标签」

现象:启动训练后立刻报No labels found或Label class x is not in names。原因通常是图片和标签目录没对齐,YOLO 默认把图片路径里的images替换成labels去找 txt,如果你把 txt 放在别的目录名,它就找不到。解决:严格用images/和labels/命名,或者用清单文件显式指定标签路径。另一个原因是 txt 里 class_id 不是 0,而names只定义了 0,直接越界。

4.2 验证集 mAP 忽高忽低

现象:同一份数据,换个随机种子 mAP 差十几个点。原因:504 张图里正样本稀疏,随机划分可能让验证集集中了难样本或空背景。解决:按框数分层抽样,或者用固定种子多跑几次取平均。更稳的做法是做 5 折交叉验证,虽然费时间但结论可靠。

4.3 框整体偏移或缩成一点

现象:可视化时框位置明显不对,或者所有框挤在左上角。原因:VOC 转 YOLO 时归一化用错了基准尺寸,比如用了统一 resize 后的 640,而实际图片是原始分辨率。解决:归一化必须用每张图自己的原始宽高,回到 2.1 的对比脚本逐张核对。这类问题在双格式数据集里尤其要警惕,因为两套文件可能不是同一次导出生成的。

4.4 小目标被 imgsz 缩没

现象:训练 loss 正常降,但推理时小只鹿检测不到。原因:原图分辨率高,鹿在画面里占比小,统一缩到 640 后目标只剩几个像素。解决:调大imgsz到 960 或 1280,或者用切图推理(SAHI 之类)。代价是显存和速度,边缘端部署要权衡。

4.5 过拟合:训练集 mAP 很高,验证集上不去

现象:训练几十轮后训练指标接近 1.0,验证集停滞。原因:504 张图对检测模型偏少,加上单类别,模型容易记住背景。解决:开强增强(mosaic、mixup、随机缩放裁剪),加早停,或者用预训练权重冻结主干先训头部。别一上来就上大模型,nano 或 small 足够。

5. 进阶:把 VOC 和 YOLO 双格式用出额外价值

这份资源同时给了 xml 和 txt,很多人只用一种,另一种就浪费了。其实双格式可以互相校验,还能支撑一些单格式做不到的玩法。下面讲两个我常用的技巧,一个用于数据质量验证,一个用于格式转换兜底。

5.1 用双格式交叉校验标注一致性

既然同一张图有两份标注,就可以写脚本逐张比对,把不一致的挑出来人工复核。这比单看一种格式可靠得多,因为转换脚本的 bug 往往只在部分图上暴露。

import glob, os, xml.etree.ElementTree as ET def load_voc(xml_path): root = ET.parse(xml_path).getroot() size = root.find('size') w, h = int(size.find('width').text), int(size.find('height').text) boxes = [] for obj in root.iter('object'): bb = obj.find('bndbox') boxes.append(tuple(float(bb.find(k).text) for k in ('xmin','ymin','xmax','ymax'))) return w, h, boxes def load_yolo(txt_path, w, h): boxes = [] with open(txt_path) as f: for line in f: _, xc, yc, bw, bh = line.split() xc, yc, bw, bh = map(float, (xc, yc, bw, bh)) boxes.append(((xc-bw/2)*w, (yc-bh/2)*h, (xc+bw/2)*w, (yc+bh/2)*h)) return boxes bad = [] for xml in glob.glob('annotations_xml/*.xml'): name = os.path.splitext(os.path.basename(xml))[0] txt = f'labels_yolo/{name}.txt' if not os.path.exists(txt): bad.append((name, 'missing txt')); continue w, h, vb = load_voc(xml) yb = load_yolo(txt, w, h) if len(vb) != len(yb): bad.append((name, f'count {len(vb)} vs {len(yb)}')); continue for a, b in zip(sorted(vb), sorted(yb)): if max(abs(a[i]-b[i]) for i in range(4)) > 2: # 允许2像素误差 bad.append((name, 'coord mismatch')); break print('异常样本:', bad)

逻辑说明:逐张读两种格式,先比框数量,再比坐标(允许 2 像素取整误差)。参数> 2是容差,可以按需收紧。输出bad列表就是需要人工复核的图。这个脚本在数据交付验收时特别有用,能挡住大部分「看着没问题、训起来出事」的隐患。

5.2 格式互转的兜底脚本

有时候你拿到的只有一种格式,或者训练框架只吃某一种,互转就是刚需。VOC 转 YOLO 的核心是归一化,YOLO 转 VOC 的核心是反归一化并补全 xml 头信息。

import xml.etree.ElementTree as ET from PIL import Image def voc2yolo(xml_path, out_txt, class_map): root = ET.parse(xml_path).getroot() size = root.find('size') w, h = int(size.find('width').text), int(size.find('height').text) lines = [] for obj in root.iter('object'): name = obj.find('name').text bb = obj.find('bndbox') xmin = float(bb.find('xmin').text); ymin = float(bb.find('ymin').text) xmax = float(bb.find('xmax').text); ymax = float(bb.find('ymax').text) xc = (xmin + xmax) / 2 / w; yc = (ymin + ymax) / 2 / h bw = (xmax - xmin) / w; bh = (ymax - ymin) / h lines.append(f"{class_map[name]} {xc:.6f} {yc:.6f} {bw:.6f} {bh:.6f}") with open(out_txt, 'w') as f: f.write('\n'.join(lines)) voc2yolo('firc_Deer_11.xml', 'firc_Deer_11_converted.txt', {'Deer': 0})

逻辑说明:class_map把类别名映射到索引,单类别就是{'Deer': 0}。归一化保留 6 位小数是 YOLO 常见精度,够用且不冗长。反方向转换时记得补<folder>、<filename>、<size>这些 VOC 必需节点,否则 labelImg 打不开。

注意:转换脚本一定要在完整数据集上跑一遍,再用 5.1 的校验脚本回验,别只测一张就批量执行。

从那以后我每次拿到新数据集,不管它标称多干净,都强制走一遍「数量核对 → 双格式交叉校验 → 可视化抽查 → 小轮次试训」这四步,宁可前期花半小时,也不愿训练到一半才发现标注是错的。这份鹿数据集结构清晰、双格式齐全,按上面的流程接进 YOLO 基本能一次跑通,剩下的就是调参和增强的事了。希望帮到你。

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

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

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

立即咨询