简介:面向狗狗行为检测任务的目标检测数据集,包含1551张清晰图片,覆盖吠叫、进食、俯卧、侧卧、坐立、睡眠、站立等8种常见行为,适合目标检测初学者练习标注格式转换,也适合开发者直接用于训练YOLO系列或Faster R-CNN等模型。数据同时提供VOC(XML)与YOLO(TXT)两种标注格式,JPEGImages、Annotations、labels三个目录一一对应,共1613个矩形框,标注规范且各类别数量分布均衡。压缩包总计2000个文件,主要包含1551个XML标注、449个TXT文件,整体大小57.77MB,目录结构清晰,便于按需调用与二次处理。该数据集已有180人学习,可直接作为行为识别、边缘计算等项目的训练或验证数据,省去自行采集、清洗与标注的时间成本。
1. 先看这包数据值不值得接:1551张8类双格式意味着什么
拿到手的是一个叫“狗狗行为检测数据集1551张8种YOLO+VOC格式.zip”的压缩包。做目标检测的人第一眼会先算一笔账:1551张图,8个行为类别,每类平均不到200张,放在YOLOv8的训练体系里属于典型的小规模数据集。它不适合直接训练一个生产级模型,但非常适合两件事:一是摸清VOC和YOLO两种标注格式之间的转换链路,二是把YOLOv8训练自己的数据集的完整流程跑通。这个包的价值不在“量”,在“双格式”——同一条数据同时给了XML和TXT两套标注,等于把格式转换、数据校验、训练推理这一条链路都给你备好了原料。下面我从数据结构、格式转换、训练避坑到视频验证,按实际动手的顺序拆开讲。
2. 把数据集当产品看:8类标签、两种格式与训练前的事实核查
2.1 8个行为类别怎么定:从XML里的name到yaml里的names
狗狗行为检测和普通的目标检测有个本质区别:普通检测只要框住人、车、猫这种“静态类别”,而行为是动态的,同一个姿态在不同上下文里可能对应不同标签。这类数据集常见的8类划分是:坐、站、趴、走、跑、跳、吠叫、摇尾巴。下载解压之后,第一步不是急着写训练命令,而是先找到类别定义文件,一般是一个classes.txt或者data.yaml。
# 查看解压后的目录结构,先别急着跑训练 unzip 数据集-狗狗行为检测数据集1551张8种YOLO+VOC格式.zip cd 数据集-狗狗行为检测数据集1551张8种YOLO+VOC格式 find . -maxdepth 2 -type f | head -30这段命令的作用是先把压缩包解开,然后列出两层目录内的文件,判断数据集内部的组织方式。常见结构是images/、labels/(存放YOLO格式txt)、Annotations/(存放VOC格式xml)三个平级目录。如果你看到classes.txt,直接打开它,里面的行顺序就是YOLO训练时的类别索引顺序。
# 打印类别定义文件,第二行之后一般就是类别名 cat classes.txtclasses.txt每一行的行号(从0开始)就是类别ID。比如第1行是sit,那所有sit行为在YOLO标注txt里类别ID就是0。这个顺序在后面写data.yaml时必须完全一致,否则模型训练出来的置信度全部对不上号。我一般会用wc -l classes.txt确认类别数是否正好8类,因为标题写“8种”,但实际文件可能多一行空行或少一类,这类低级错误最容易让后续的转换脚本静默出错。
2.2 YOLO txt和VOC xml:两种坐标体系的一次对齐
这个数据集的卖点是同时提供YOLO和VOC两套标注。理解它们的差异,比直接跑训练更重要。
| 对比项 | YOLO txt格式 | VOC xml格式 |
|---|---|---|
| 存储结构 | 每个txt文件对应一张图片,文件名与图片同名 | 每个xml文件对应一张图片,文件名与图片同名 |
| 标注内容 | 每行一个目标,格式为class_id cx cy w h | 整个文档描述图片信息,内部多个<object>节点 |
| 坐标体系 | 归一化相对坐标,cx/cy是中心点,w/h是宽高,全部在0~1之间 | 像素绝对坐标,<bndbox>里存xmin ymin xmax ymax |
| 可读性 | 机器友好,人眼难读 | 人能看懂,方便修改和核对 |
VOC的xml文件用像素坐标标注,取值范围跟图片实际宽高绑定;YOLO的txt为了跟图片尺寸解耦,把坐标归一化到0~1。两者转换逻辑一句话就能说清楚:用xmin和xmax算出中心点和宽度,然后除以图片宽度;用ymin和ymax算出中心点和高度,然后除以图片高度。但这里面有一个坑:xml里的<size>节点可能和图片实际尺寸不一致,尤其是手机拍摄的照片包含EXIF旋转信息时,OpenCV读出来的宽高跟标注工具看到的宽高会反掉。所以转换时要以xml里的<size>为准,而不是自己去用cv2.imread读图。
2.3 1551张图在YOLOv8训练里的规模位置
1551张图对行为检测来说是个尴尬的规模。按8:1:1划分,训练集约1240张,验证集约155张,测试集约155张。这个量级下,YOLOv8的nano和small模型都能在单张消费级显卡上完成训练,但很容易过拟合——模型会把背景、光线、狗的品种这些无关特征一起记住。我的建议是不要指望一把训练出高mAP,而是把这个数据集当成“数据流水线练习场”:先把转换脚本跑对、把校验脚本跑通、把训练命令跑起来,这比盲目堆epoch更有收获。
处理小数据集有一个关键技巧:在data.yaml里不要只配训练集路径,还要给足数据增强的空间。YOLOv8默认开启mosaic增强,对行为检测这种小数据集恰好能极大摊薄过拟合风险。后面第5章我会给出一组适合小数据集的训练参数。
3. 从VOC到YOLO的转换落地:脚本、划分与三项校验
3.1 VOC的XML转YOLO的TXT:坐标归一化脚本
既然数据集给了两套格式,训练时优先用YOLO txt,因为YOLOv8的原生接口直接吃txt。但如果未来你想换标签体系、合并数据集,还是得掌握从VOC向YOLO转换的能力。下面这个脚本可以处理整个Annotations/目录。
import os import xml.etree.ElementTree as ET voc_dir = "Annotations" # VOC xml 所在目录 yolo_dir = "labels" # 输出 YOLO txt 所在目录 class_list = ["sit", "stand", "lie", "walk", "run", "jump", "bark", "tail_wag"] os.makedirs(yolo_dir, exist_ok=True) def convert_voc_to_yolo(xml_path, yolo_path): tree = ET.parse(xml_path) root = tree.getroot() img_w = int(root.find("size/width").text) img_h = int(root.find("size/height").text) with open(yolo_path, "w", encoding="utf-8") as f: for obj in root.findall("object"): name = obj.find("name").text if name not in class_list: continue class_id = class_list.index(name) 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) # 像素坐标 -> 归一化坐标 w = xmax - xmin h = ymax - ymin cx = (xmin + xmax) / 2.0 / img_w cy = (ymin + ymax) / 2.0 / img_h nw = w / img_w nh = h / img_h f.write(f"{class_id} {cx:.6f} {cy:.6f} {nw:.6f} {nh:.6f}\n") for xml_name in os.listdir(voc_dir): if not xml_name.endswith(".xml"): continue xml_path = os.path.join(voc_dir, xml_name) yolo_name = xml_name.replace(".xml", ".txt") yolo_path = os.path.join(yolo_dir, yolo_name) convert_voc_to_yolo(xml_path, yolo_path) print("转换完成")逻辑分四步:解析xml取出图片宽高 → 遍历每个<object>→ 把像素坐标转成中心点和宽高的归一化值 → 按class_id cx cy w h写入txt。这里有两个参数要特别注意。
第一,class_list的顺序必须和classes.txt完全一致。如果xml里出现了class_list之外的类别名,这个目标会被静默丢弃,最后你训练时会发现某类图片明明有xml标注,但txt是空的。我遇到过一次xml里类别名多了末尾空格,导致全部被丢弃,排查了半天。建议在这个脚本里加一行打印if name not in class_list: print(xml_name, name),把所有被跳过的类别名列出来。
第二,如果出现xmin > xmax或ymin > ymax,说明标注工具或人工标注时把坐标写反了,直接用max减min会让宽度变成负数,YOLO训练时这个框会直接报错。可以用w = abs(xmax - xmin)兜底,但更稳妥的做法是把这类文件单独列出来人工检查。
3.2 把1551张图切成train/val并生成文件清单
YOLOv8不支持直接读目录下所有图片然后自动划分,需要在data.yaml里指定训练集和验证集的图片路径。我习惯先按文件清单划分,再把图片和标签复制到对应的images/train、labels/train这类目录中,这样之后换工具(比如转到YOLOv5)不用重新组织目录。
import os import random import shutil img_dir = "images" label_dir = "labels" train_img_dir = "images/train" val_img_dir = "images/val" train_label_dir = "labels/train" val_label_dir = "labels/val" os.makedirs(train_img_dir, exist_ok=True) os.makedirs(val_img_dir, exist_ok=True) os.makedirs(train_label_dir, exist_ok=True) os.makedirs(val_label_dir, exist_ok=True) all_imgs = [f for f in os.listdir(img_dir) if f.endswith(".jpg")] random.seed(42) # 固定随机种子,保证每次划分结果一致 random.shuffle(all_imgs) val_ratio = 0.1 val_count = int(len(all_imgs) * val_ratio) val_imgs = all_imgs[:val_count] train_imgs = all_imgs[val_count:] for img_name in train_imgs: shutil.copy2(os.path.join(img_dir, img_name), os.path.join(train_img_dir, img_name)) label_name = img_name.replace(".jpg", ".txt") if os.path.exists(os.path.join(label_dir, label_name)): shutil.copy2(os.path.join(label_dir, label_name), os.path.join(train_label_dir, label_name)) for img_name in val_imgs: shutil.copy2(os.path.join(img_dir, img_name), os.path.join(val_img_dir, img_name)) label_name = img_name.replace(".jpg", ".txt") if os.path.exists(os.path.join(label_dir, label_name)): shutil.copy2(os.path.join(label_dir, label_name), os.path.join(val_label_dir, label_name)) print(f"训练集 {len(train_imgs)} 张,验证集 {len(val_imgs)} 张")这个脚本的关键是random.seed(42)。没有固定种子,每次运行划分结果都不一样,你今天训练的验证集和明天复现实验时的验证集不同,指标没有可比性。val_ratio = 0.1是给1551张图用的比例,10%验证集约155张,虽然偏少,但小数据集只能这样。如果你的后续实验中总图片数超过5000张,验证集比例可以提高到15%或20%。
另外注意脚本里做了一步保护:只复制图片,对应的标签文件用os.path.exists做了检查。这能暴露数据集里“有图无标”或“有标无图”的情况。如果某个图片在images/下存在但labels/下没有同名txt,后面训练时YOLO会跳过这张图并在日志里打一条 Warning,但如果你只在复制时看见图片文件,往往不会回头检查标签是否跟上。
3.3 训练前的“图片-标签-类别”三项校验脚本
数据转换和划分完成之后,不能立刻开训练。我吃过一次亏:训练了10个epoch,Loss一直在降,但mAP恒为0。最后发现是转换脚本里类别映射表写错,把标签类别全部对到了另一个位置上。正确的做法是在训练前跑一遍下面的校验脚本,把三类问题一次查完。
import os label_dir = "labels/train" img_dir = "images/train" class_num = 8 img_names = set(os.listdir(img_dir)) label_names = set(os.listdir(label_dir)) # 第一项:标签和图片数量、名称对齐 missing_label = [f for f in img_names if f.replace(".jpg", ".txt") not in label_names] missing_img = [f for f in label_names if f.replace(".txt", ".jpg") not in img_names] print(f"有图无标签:{len(missing_label)} 个,有标签无图:{len(missing_img)} 个") # 第二项:标签内容逐行解析,检查类别越界和坐标越界 bad_files = [] for label_name in label_names: label_path = os.path.join(label_dir, label_name) with open(label_path, "r") as f: for line in f: parts = line.strip().split() if len(parts) != 5: bad_files.append((label_name, "字段数不是5")) continue cls = int(parts[0]) cx, cy, w, h = map(float, parts[1:]) if cls < 0 or cls >= class_num: bad_files.append((label_name, f"类别ID {cls} 超出范围")) if not (0 <= cx <= 1 and 0 <= cy <= 1 and 0 <= w <= 1 and 0 <= h <= 1): bad_files.append((label_name, "坐标越界")) print(f"异常标签文件:{len(bad_files)} 个") for name, reason in bad_files[:10]: print(name, reason)三个校验分别对应最容易翻车的三个点。第一项检查标签文件与图片文件是否一一对应,这能发现xml里filename写错或图片后缀不统一的问题。第二项逐行解析标签内容,检查类别ID是否在0~7之间、坐标是否在0~1之间,这能发现转换脚本里img_w取到0或坐标归一化时除以了错误尺寸的问题。第三项检查字段数是否为5,YOLO格式每行必须刚好5个数,多了或少了都会让YOLO在加载数据时直接报错。
跑这个脚本时,如果bad_files不为空,不要急着改脚本重跑,先打开一个异常文件看看模式和规律。我见过大量“坐标越界”其实是同一张图片的标注错误,局部问题不需要全部重转,单独修这个文件就行。
4. 狗狗行为检测常见翻车点与排查方法:4条血泪经验
4.1 训练Loss正常但mAP一直为0:类别索引错位
现象:训练loss从2.5一路掉到1.2,看起来一切正常,但每个epoch的验证mAP都是0,或者某些类别mAP是0、有些类别是高得离谱的0.99。
原因:类别索引错位,且错位方式很隐蔽。比如原始数据集的classes.txt顺序是sit, bark, stand,但你在转换脚本里把class_list写成了sit, stand, bark。所有标签文件里的类别ID没变,但类别名对不上,YOLO训练时按新的names数组去解释ID,导致所有狗的行为都被贴到了错误的标签上。分类数目一致时模型完全不会报错,loss正常下降,只是学的东西和你想要的东西完全错开。
解决:用第3章的校验脚本,检查标签txt里类别ID是否落在合法范围内,然后人工抽10个txt打开对照classes.txt逐行确认。不要只抽查一张,因为错位往往是全局性的,抽查第一张就能发现。
4.2 标签框整体往左上偏:图片旋转信息在捣乱
现象:训练出的模型在单张图片上推理,检测框位置偏差明显,左上角偏移尤其严重;验证集mAP偏低但又不是0。
原因:手机拍的JPG照片存在EXIF方向信息,比如竖屏拍摄的照片实际存储时可能是旋转90度的。标注工具在打开图片时会自动按EXIF方向显示并标注,所以xml里的xmin/ymin/xmax/ymax是“旋转后显示的样子”的坐标;但OpenCV的imread会忽略EXIF方向,直接按存储的原始像素读出,图片宽高和显示宽高是反的。你在自己做转换时如果用OpenCV重新读图取宽高,那归一化坐标全部错位。
解决:转换脚本里不要用OpenCV读图获取尺寸,直接用xml里的<size>节点的宽高。训练前还要统一图片方向,用PIL打开后ImageOps.exif_transpose把图片落盘成正常方向,这样推理时的输入和标注时看到的画面一致。
4.3 小体型狗和远景狗漏检严重:小目标与样本占比
现象:验证集里坐、站、走这些大姿态框得很准,但跑、跳、吠叫这些动态行为的mAP极低,或者图中狗比较小、距离远时完全没框出来。
原因:这有两层。第一层是数据集本身的样本不均:静态行为(坐、趴)容易拍到,动态行为(跳、跑)本来就难捕捉,1551张分到8类,动态类可能只有百来张。第二层是小目标问题:跳和跑往往在画面中占比小,而YOLO默认输入尺寸640x640下,小目标的特征经过多次下采样后剩不了几个像素。
解决:训练参数上把imgsz从640提到960,小目标召回率会明显提升;数据增强上不要关掉mosaic,它能把小目标拼到大图里让模型见得多一点。换更大的模型从nano换成small也有帮助。如果动态类别还是太少,先只保留数量最多的4~5类做一次消融实验,观察是哪一类拖低了整体mAP,再决定要不要补样本。
4.4 验证指标不错但实际视频里误判:静态图缺时序
现象:验证集mAP0.5到了0.85,但把模型接到视频流里逐帧推理,行为标签在“坐”和“趴”之间疯狂跳动,一帧坐一帧趴。
原因:数据集本身是静态图片,行为检测模型只能看到单帧的姿态。有些行为在单帧上看就是模棱两可的:狗从坐到趴过渡的中间帧,正面视角下人和机器都很难判断。“坐”和“趴”的标注者在切分边界的时候也未必一致,训练时模型学到的是两类的交叠分布。
解决:不要指望单帧模型直接输出稳定的行为结论。先让模型输出每一帧的类别和置信度,再对连续N帧做一次投票或取置信度加权平均,把跳变得结果平滑掉。具体实现我放在第5章。
5. 让YOLO输出更接近“行为判断”:时序平滑与最终验证
5.1 用滑窗投票把帧级检测变成行为结论
行为检测在视频场景下的正确姿势是“检测 + 时序平滑”。把YOLO当成一个帧级感知器,连续多帧的感知结果用滑窗投票汇总,这样偶尔一帧误判不会让最终结果乱跳。
from collections import deque, Counter frame_queue = deque(maxlen=15) # 最近15帧的类别结果 def smooth_predict(curr_class, conf): # 只有置信度足够高的帧才进入投票队列 if conf >= 0.5: frame_queue.append(curr_class) if len(frame_queue) < 5: return None # 前5帧不输出结论,等积累样本 counter = Counter(frame_queue) return counter.most_common(1)[0][0]maxlen=15是把判定窗口拉到约半秒(30fps视频)。窗口太短平滑效果差,窗口太长行为切换时会有明显延迟。conf >= 0.5这个阈值在小数据集上要放宽到0.4,因为行为检测模型在这种数据集上整体置信度偏低,卡0.5会让队列经常被清空。如果你发现平滑后的结果仍然频繁切换,把maxlen调到30,但代价是反应变慢。
5.2 训练完的最后一页考卷:真实视频上的帧级验证
模型训练完,别只盯着验证集mAP看。mAP是静态图片上的框级别指标,而你要的是行为结论,两个维度。我一般会准备一段20秒左右的狗狗视频,逐帧推理后把结果按时间戳打点,然后自己人工数一下“坐”“走”“跑”的切换时刻,对一下模型的切换时刻误差在多少帧。这个误差如果超过30帧,说明模型不是被遮挡干扰,就是行为边界本身模糊。
还有一个值得试的参数:训练时开启TTA。YOLOv8的命令行里加一行--tta,推理时对输入做翻转、缩放增强取平均结果,在小数据集上通常能把mAP提升1到2个点。代价是推理速度变慢,离线分析视频时用TTA很划算,实时场景不要开。
最后说一个我自己的习惯:拿到任何新数据集,先跑校验脚本再分配时间。早期我拿到这类小数据集,总急着把训练命令敲下去,结果花了三四个小时在Loss震荡里打转,最后发现是标签类别索引整个错位了。从那以后,每份数据集先花十分钟跑一遍“图片-标签-类别”对齐检查,再用一张图做单样本过拟合训练,确认loss能降到接近0,然后才上完整训练。这套流程走了两年,翻车率降了很多,希望帮到你。
本文还有配套的精品资源,点击获取