简介:该数据集面向计算机视觉目标检测研究与开发者,聚焦人员行为状态识别,涵盖跑步、打电话、跌倒、睡觉、抽烟五类典型场景,可用于安防监控、智慧园区等场景的模型训练与算法验证。压缩包共2000个文件,以1999个XML标注文件为主,另附1个TXT说明文件,整体大小约232.8MB;标注同时提供Pascal VOC与YOLO两种格式,方便接入不同训练框架。五类目标共有5790个矩形标注框,其中Running框1724个、cellphone框916个、fall框973个、sleep框1771个、smoke框406个,标注工具为labelImg,标注规则为矩形框。需要留意的是,作者说明约四分之三的数据为增强得到,且不保证训练模型精度,仅保证标注准确合理,下载前请斟酌适用性。目前已有490人学习浏览,适合需要行为检测数据集进行实验、对比标注格式或研究数据增强影响的人群。
1. 人员状态检测数据集:4943张5类别,先看清任务边界和适用场景
如果在工地的监控大屏上,系统要同时盯住“有人跑起来”“有人躺在地上不动”“有人抽烟”“有人打电话”“有人跌倒”,这就是一个典型的人员状态检测任务。这个数据集就是为这类场景准备的:4943张图片、5个类别(跑、睡、抽烟、打电话、跌倒),以7z压缩包形式发布。先提醒一句:4943张对单帧目标检测来说不算多,但对小规模验证和快速出原型完全够用,前提是你要把标注质量检查清楚。它适合想用YOLO等检测器快速跑通人员行为识别流程的人,也适合做安防、工地、养老院场景的落地实验;但不适合期望一次性拿到视频时序行为识别训练集的人,因为它是一张张静态图片,没有连续帧关系。换句话说,你要做的是“框出画面里的状态”,而不是“预测下一帧会摔倒”。
2. 拿到 .7z 先别急着训练:解压、目录结构与标注格式
2.1 Linux 与 Windows 解压 7z:一条命令和两个注意点
7z 是 7-Zip 的高压缩率格式,在标注文件特别多的场景下,压缩率通常比 zip 高不少,所以数据集经常用它打包。在 Linux 上我一般装 p7zip 后用命令行解压,比图形界面更适合批量操作和脚本化:
# 安装 p7zip(Ubuntu/Debian 系) sudo apt-get install -y p7zip-full # 解压到指定目录,-o 后面直接跟路径,不要留空格 7z x person_state_dataset.7z -o/home/user/datasets/person_state这里的x表示解压并保留原始目录结构,-o指定输出目录。最容易翻车的地方就是-o与路径之间的空格:写成7z x xxx.7z -o /path会直接报参数错误。如果压缩包设有密码,加-p密码;没有就不加。Windows 上我习惯装 7-Zip 后右键解压,但自动化流程里最好用命令行,特别是后续要把数据往服务器上搬:
"C:\Program Files\7-Zip\7z.exe" x person_state_dataset.7z -oD:\datasets\person_state两个注意点:第一,压缩包里的图片文件名尽量别带中文,否则在 Windows 默认编码下解压容易出乱码,后续用 Python 读路径时会连累标注匹配;第二,解压前先跑一次完整性测试:
7z t person_state_dataset.7z如果输出里有ERROR或CRC Failed,说明压缩包在传输过程中损坏,直接重新下载,不要在坏包上浪费时间。解压后还要看一眼文件总数,4943 张图加上标注文件往往有几个 GB,数量对不上说明中途丢文件,必须重下。
2.2 确认标注格式:VOC XML 还是 COCO JSON
解压后第一件事不是训练,而是确认标注格式。这类人员状态数据集常见的标注格式有三种:VOC 的 XML、COCO 的 JSON、YOLO 的 txt。不同格式决定了后面怎么写转换脚本。我一般先看目录结构,再随机打开一个标注文件:
# 在解压目录下看是否有 images / Annotations / labels 等目录 find . -maxdepth 2 -type d find . -name "*.xml" | head -5 find . -name "*.json" | head -5 find . -name "*.txt" | head -5如果是 VOC XML,文件里会有<object>节点,里面是<name>和<bndbox>,框的坐标是左上角xmin,ymin和右下角xmax,ymax。如果是 COCO JSON,会看到images、annotations、categories三个数组,框的坐标格式是[x, y, width, height],注意这里的x,y是左上角,不是中心点。如果是 YOLO txt,每一行是“类别序号 中心点x 中心点y 宽 高”,全部是归一化比例。
这里有一个很容易被忽略的细节:不要假设类别名和你想的一致。“跑”可能是run,也可能是running;“睡”可能是sleep或lying;“打电话”可能是call,也可能是phone_call。我拿到任何数据集的第一个动作,是写脚本把所有类别名去重打印出来:
import os import glob # 以 VOC XML 为例,打印全量类别名 import xml.etree.ElementTree as ET classes = set() for xml_path in glob.glob("person_state/Annotations/*.xml"): root = ET.parse(xml_path).getroot() for obj in root.findall("object"): classes.add(obj.find("name").text.strip()) print(classes)打印出来的集合可能比 5 个多,因为标注员把同一个动作标成了不同单词。这个动作能帮你提前发现同义词问题,避免后面类别映射时错位。
2.3 数据体检:类别分布、图片尺寸和漏标统计
4943 张图不算大,但训练之前必须做一次“体检”。我习惯用脚本统计每个类别的框数量、每张图的标注数量、图片尺寸范围,这一步能暴露很多后续训练才暴露的问题:
import os import xml.etree.ElementTree as ET from collections import Counter annot_dir = "person_state/Annotations" class_counter = Counter() box_per_image = [] img_sizes = set() for name in os.listdir(annot_dir): if not name.endswith(".xml"): continue tree = ET.parse(os.path.join(annot_dir, name)) root = tree.getroot() size = root.find("size") img_w = int(size.find("width").text) img_h = int(size.find("height").text) img_sizes.add((img_w, img_h)) objs = root.findall("object") box_per_image.append(len(objs)) for obj in objs: class_counter[obj.find("name").text.strip()] += 1 print("类别统计:", dict(class_counter)) print("每张图标注框数量分布:", Counter(box_per_image).most_common()) print("图片尺寸组合数:", len(img_sizes), "示例:", list(img_sizes)[:5])这段代码会输出三个关键信息。类别统计能看出有没有严重不均衡:如果“睡”只有 300 框而“跑”有 3000 框,后面训练就要调权重或做增强。每张图的框数量分布用来判断是否存在大量空图——如果 20% 的图片没有任何标注,训练时这些图会被当背景,可能会把原本正常的动作学成背景,拉低召回。图片尺寸组合多说明数据来源杂,可能是多个摄像头拍的,分辨率不统一,后面要统一imgsz。
最后再做一次图片损坏检查,别等训练时才被 Dataloader 中断:
import cv2 import os bad = [] for name in os.listdir("person_state/images"): img = cv2.imread(os.path.join("person_state/images", name)) if img is None: bad.append(name) print("损坏图片数量:", len(bad), bad[:10])损坏图片超过 5 张就建议找源头重下。到这里,数据集的底细已经摸清,下一步开始转换格式。
3. 把原始标注转成 YOLO 格式:转换脚本与四个边界坑
3.1 通用转换思路:VOC/COCO 都归一成 YOLO txt
YOLO 系列训练时习惯用 txt 标注,每张图片对应一个同名 txt。转换核心是“读原标注 → 提取类别和框 → 归一化 → 写入 txt”。先给一个 VOC 到 YOLO 的完整脚本,因为 VOC XML 结构最直观:
import os import xml.etree.ElementTree as ET # 类别映射表:把同义词合并到同一索引,索引从 0 开始 CLASS_MAP = { "run": 0, "running": 0, "sleep": 1, "lying": 1, "smoke": 2, "smoking": 2, "call": 3, "phone": 3, "phone_call": 3, "fall": 4, "falling": 4, } def convert_voc(xml_path, out_txt_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) lines = [] for obj in root.findall("object"): name = obj.find("name").text.strip() if name not in CLASS_MAP: continue box = obj.find("bndbox") x1 = float(box.find("xmin").text) y1 = float(box.find("ymin").text) x2 = float(box.find("xmax").text) y2 = float(box.find("ymax").text) # 转成 YOLO 格式:中心点坐标 + 宽高,再除以原图尺寸 x_center = ((x1 + x2) / 2) / img_w y_center = ((y1 + y2) / 2) / img_h w = (x2 - x1) / img_w h = (y2 - y1) / img_h lines.append(f"{CLASS_MAP[name]} {x_center:.6f} {y_center:.6f} {w:.6f} {h:.6f}") with open(out_txt_path, "w") as f: f.write("\n".join(lines)) # 批量转换 xml_dir = "person_state/Annotations" txt_dir = "person_state/labels" os.makedirs(txt_dir, exist_ok=True) for fn in os.listdir(xml_dir): if fn.endswith(".xml"): base = os.path.splitext(fn)[0] convert_voc(os.path.join(xml_dir, fn), os.path.join(txt_dir, base + ".txt")) print("转换完成")代码里最重要的部分是CLASS_MAP。我见过一份数据里run和running混用,直接导致类别数量变 6,训练时索引错位,后面全部白跑。转换脚本跑完后,看一眼生成的 txt 数量和 xml 是否一致,如果差很多,多半是脚本里漏了某些文件的后缀名判断。
如果原始标注是 COCO JSON,核心逻辑一样,只是提取框的时候直接用bbox字段:x, y, w, h本身就是相对原图的像素值,归一化时x_center = (x + w/2) / img_w,y_center = (y + h/2) / img_h,宽高直接用w/img_w和h/img_h。不需要再求右下角坐标。
3.2 归一化坐标的计算:中心点、宽高、除以原图尺寸
这里单独强调一下归一化,因为太多人在这个环节翻车。YOLO txt 里存的不是像素坐标,而是相对原图宽高的比例,取值范围 0~1。不同标注格式的换算关系:
- VOC:给的是左上角
(x1,y1)和右下角(x2,y2),中心点就是(x1+x2)/2, (y1+y2)/2。 - COCO:给的是左上角
(x,y)和宽高(w,h),中心点要自己加一半宽高。 - 归一化分母一定是原图尺寸,不是网络输入尺寸,比如 640 或 960。
我见过最离谱的错误是:有人拿 VOC 框直接除以 640 生成 YOLO txt,因为训练时imgsz=640。结果真实物体的中心点经常算出来大于 1,YOLO 会把坐标裁剪到边界甚至忽略,模型一开始 loss 就异常高,训练完框全部偏向左上角。所以转换脚本里务必用 XML 里size/width和size/height的真实值。
另外,YOLO 格式对宽高为 0 的标注会直接报错。标注中偶尔会出现xmin == xmax或ymin == ymax的退化框,转换时要做过滤:
if w <= 0 or h <= 0: continue3.3 四个边界坑:索引、坐标越界、空标注、路径错位
第一个坑:类别索引从 0 开始。很多人按直觉把“跑”设成 1,训练时类别数写 5,但标签里出现5会被当成越界或背景,AP 全乱。记住CLASS_MAP里的值必须是0,1,2,3,4。
第二个坑:坐标越界。标注工具手滑时,框的右下角可能超出图片范围,甚至左上角出现负值。转换时最好对归一化结果做一次裁剪:
x_center = min(max(x_center, 0.0), 1.0) y_center = min(max(y_center, 0.0), 1.0) w = min(max(w, 0.0), 1.0 - x_center) h = min(max(h, 0.0), 1.0 - y_center)不裁剪的话,坐标大于 1 的训练样本会让 YOLO 在损失计算时产生异常梯度。第三个坑:空标注文件。一张图没有目标时,生成的 txt 是空的,这是正常的,YOLO 会把这张图当背景。但如果大量本应有目标的图片变成空 txt,说明标注文件名没对上,或者 XML 的<object>被漏读。第四个坑:图片和标注文件名不一致。图片叫img_001.jpg,标注却叫0001.xml,训练时找不到对应 txt。转换前先做一次文件名校验:
img_dir = "person_state/images" label_dir = "person_state/labels" imgs = {os.path.splitext(f)[0] for f in os.listdir(img_dir)} labels = {os.path.splitext(f)[0] for f in os.listdir(label_dir)} print("有图无标注:", imgs - labels) print("有标注无图:", labels - imgs)这两行的差值如果超过 0,就是问题样本,要么补文件,要么在训练配置里排除。
3.4 转换后验证:可视化框和统计 txt 行数
转换完不要直接训练,先验证一遍。我习惯用 OpenCV 把标注框画回图片上,随机抽 30 张检查,同时统计每个 label 的行数分布,确认没有大面积的错位:
import cv2 import os import random def draw_yolo_box(img_path, txt_path, out_path): img = cv2.imread(img_path) h, w = img.shape[:2] with open(txt_path) as f: lines = f.read().strip().splitlines() for line in lines: parts = line.split() if len(parts) != 5: continue cls, xc, yc, bw, bh = parts xc = float(xc) * w yc = float(yc) * h bw = float(bw) * w bh = float(bh) * h x1 = int(xc - bw / 2) y1 = int(yc - bh / 2) x2 = int(xc + bw / 2) y2 = int(yc + bh / 2) cv2.rectangle(img, (x1, y1), (x2, y2), (0, 255, 0), 2) cv2.putText(img, f"cls{cls}", (x1, max(0, y1 - 5)), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 255, 0), 1) cv2.imwrite(out_path, img) os.makedirs("check", exist_ok=True) for img_name in random.sample(os.listdir("person_state/images"), 30): base = os.path.splitext(img_name)[0] txt = f"person_state/labels/{base}.txt" if os.path.exists(txt): draw_yolo_box(f"person_state/images/{img_name}", txt, f"check/{base}.jpg")画框后人工扫一遍,重点看:框有没有明显小于目标、有没有大面积超出画面、同一目标是否被标成两个类别。这一步虽然费眼,但比训练一次后发现 mAP 低再回来查快得多。
4. 用 YOLOv8 训练人员状态检测模型:配置、参数与命令
4.1 目录组织与 data.yaml 写法
数据转换好后,目录要按 YOLO 的习惯组织:训练集和验证集分开,图片放images,标注放labels。常见结构:
person_state/ ├── images/ │ ├── train/ │ └── val/ ├── labels/ │ ├── train/ │ └── val/ └── data.yaml然后把类别信息写进data.yaml:
train: person_state/images/train val: person_state/images/val nc: 5 names: ["run", "sleep", "smoke", "call", "fall"]这里最容易踩的坑是路径写法。YOLOv8 的train路径如果是相对路径,是相对当前工作目录解析的,不是相对 yaml 文件所在目录。我一般先cd到项目根目录,再在 yaml 里写相对根目录的路径;否则干脆写绝对路径。另外注意names的顺序必须和 3.1 的CLASS_MAP索引一致。顺序错了就是灾难:比如把sleep写在索引 0,模型训练时标签 0 全被当成 sleep 的框,混淆矩阵看起来莫名奇妙,但类别其实全乱了。
4.2 训练命令与关键参数:imgsz、batch、epochs、patience
配置好 yaml,就可以用 ultralytics 的 YOLOv8 训练自己的数据集了。命令不长,但参数直接影响结果:
cd /home/user/person_state_project conda activate yolov8 yolo detect train \ data=person_state/data.yaml \ model=yolov8s.pt \ imgsz=640 \ batch=16 \ epochs=100 \ patience=20 \ project=person_state \ name=run_sleep_smoke_call_fall \ seed=42几个关键参数我一般这样定:batch先看显存,16GB 显卡跑yolov8s用 16 没什么问题,batch 太大容易 OOM,太小导致 BN 统计抖动,训练不稳;epochs对 4943 张这种规模的数据,100 轮足够,再长容易过拟合,配合patience=20让它连续 20 轮验证集 mAP 不涨就早停;imgsz默认 640,但你的图片里抽烟、打电话这类小目标较多时,可以试 960,代价是显存和时间翻倍;seed固定成 42,保证可复现。
不要一上来就用yolov8x。数据量只有 4943 张,大模型只会过拟合。我遇到过不少新手拿yolov8x训练小数据集,训练集 mAP 接近 1,验证集只有 0.3。合理路线是yolov8n或yolov8s起步,先跑通流程,再在中后期用yolov8m对比一次。如果显存紧张,yolov8n也是一个能快速验证标注有没有问题的最小模型。
4.3 分层划分验证集:避免小类别在验证集缺席
随机划分的翻车点是“睡”这类样本少的类别可能全部落进训练集,验证集一条都没有,mAP 算出来虚高。所以我用分层抽样,按“图片包含的类别组合”来分:
import os from sklearn.model_selection import train_test_split img_dir = "person_state/images" label_dir = "person_state/labels" all_imgs = [f for f in os.listdir(img_dir) if f.endswith(".jpg")] stratify = [] for name in all_imgs: base = os.path.splitext(name)[0] txt = os.path.join(label_dir, base + ".txt") classes = set() if os.path.exists(txt): with open(txt) as f: for line in f.read().strip().splitlines(): parts = line.split() if parts: classes.add(parts[0]) stratify.append(",".join(sorted(classes)) if classes else "empty") train_imgs, val_imgs = train_test_split( all_imgs, test_size=0.2, stratify=stratify, random_state=42 ) # 建立 train/val 目录并复制文件 import shutil for split, imgs in [("train", train_imgs), ("val", val_imgs)]: os.makedirs(f"person_state/images/{split}", exist_ok=True) os.makedirs(f"person_state/labels/{split}", exist_ok=True) for name in imgs: base = os.path.splitext(name)[0] shutil.copy2(os.path.join(img_dir, name), f"person_state/images/{split}/{name}") src_txt = os.path.join(label_dir, base + ".txt") if os.path.exists(src_txt): shutil.copy2(src_txt, f"person_state/labels/{split}/{base}.txt")这段代码的要点是用“类别组合字符串”作为stratify的分层标签,保证每个类别在训练集和验证集里都出现。空标注图单独归为一层,避免它们随机挤掉某一个类别。如果类别组合太多导致stratify校验失败,可以把拼接字符串转成哈希整数,或者直接用StratifiedShuffleSplit。在实际项目里 5 类组合有限,直接拼接字符串通常没问题。
跑完这一步,数据准备阶段结束。接下来训练前,先把第四章的坑看完,能省不少时间。
5. 避坑与排查:从 loss 不降到 mAP 虚高的 5 个实际问题
5.1 现象:训练 loss 一直不降
很多人拿到数据集直接训练,发现 loss 前 20 轮还在高位波动,或者干脆不降。排查顺序:第一,看标注坐标有没有错位,用 3.4 的可视化脚本检查;第二,看类别索引和names对不对,我曾经把CLASS_MAP索引从 1 开始,结果 loss 一开始就高出天际;第三,看学习率。YOLOv8 默认会自动调学习率,但用预训练权重迁移时,lr0=0.01对小数据集偏大,可以显式改成lr0=0.001。解决方法是按顺序排除,不要一上来就盲目加数据或加模型容量。
5.2 现象:验证 mAP 高但实际漏检严重
这个现象常出现在跌倒和睡觉类上。数据显示 mAP@0.5 有 0.85,但拿到真实监控视频里一测,跌倒的人完全没有框。原因通常是验证集划分不合理,或者标注本身漏标过多——如果某一类只出现在训练集的特定场景,验证集碰巧没覆盖到,mAP 自然虚高。解决方法是先用 4.3 的分层划分,再按类别单独打印 AP:
yolo detect val \ model=person_state/run_sleep_smoke_call_fall/weights/best.pt \ data=person_state/data.yaml \ split=val查看生成的results.csv里每个类别的mAP50-95,哪类低就补哪类的数据或增强。如果所有类别都高但实际漏检,说明验证集和真实场景分布差异太大,必须重新采集贴近实际工况的图片加入训练,不要只相信测试集指标。
5.3 现象:抽烟、打电话这类小目标 AP 明显低
烟头和手机在画面里占比很小,默认 640 分辨率下只有十几个像素,模型很难学好。常见解法三条:第一条,用imgsz=960甚至 1280 训练,小目标的有效特征更多,代价是训练时间变长;第二条,推理时用 SAHI 切片检测,把大图切成小块分别过模型再合并;第三条,检查标注框有没有把“手拿烟”只框了手部而没框全烟头。标注目标太小而且位置不一致,再好的网络也学不会。我一般先做第三条,因为数据质量问题的概率远高于模型问题。
5.4 现象:跌倒和跑步互相误检
这两个类别的姿态有一些相似性,人跌倒瞬间腿的摆动和跑步动作接近,模型容易混淆。如果混淆矩阵显示run和fall的 AP 互相拉低,我的做法是:第一,增加训练数据中跌倒姿态的多样性,特别是不同角度、不同遮挡的样本;第二,在损失层面给fall类别更高权重,YOLOv8 的class_weights参数可以设置,也可以把cls_loss的权重调高;第三,推理端加时间序列过滤——单帧检测器本身没有时序概念,跌倒是一个过程,连续 3 帧检测到fall且之后没有持续的run框再确认,能显著降低误检。
5.5 现象:7z 解压后图片打不开或标注文件乱码
这个坑比较隐性。7z x成功后,训练时发现部分图片解码失败,或 XML 里出现乱码。原因通常是压缩包内文件名用了非 UTF-8 编码,在 Linux 下解压后变成乱码。解决是用7z x -mcp=65001强制 UTF-8,或者在 Windows 下解压后再传到 Linux。我习惯拿到压缩包先执行7z t测试完整性,再解压;解压后遍历所有图片,用 OpenCV 读取并统计失败数量:
import cv2 import os bad = [] for name in os.listdir("person_state/images"): path = os.path.join("person_state/images", name) img = cv2.imread(path) if img is None: bad.append(name) print("损坏图片数量:", len(bad), bad[:10])如果损坏图片超过 5 张,建议重新下载或换源。这些坏图在训练时会导致 Dataloader 报错中断,而且错误信息不明显,经常显示CUDA error: device-side assert triggered,误导你以为是显卡驱动问题。
6. 让模型更稳的进阶做法:增强控制、类别权重与推理融合
6.1 用训练增强参数控制过拟合
数据集小,默认增强可能太激进。比如hsv_h色相增强对“抽烟”这种依赖烟头颜色的类别有害,建议关掉;fliplr水平翻转对跑步、跌倒这类方向敏感的动作要慎用,否则模型学不清左右方向。我一般会这样调整:
yolo detect train \ data=person_state/data.yaml \ model=yolov8s.pt \ imgsz=640 \ batch=16 \ epochs=100 \ hsv_h=0.0 \ fliplr=0.0增强不是越多越好,调整后对比验证集 mAP,找到适合当前数据集的配置。
6.2 类别均衡:给样本少的类别加权重
YOLOv8 的class_weights参数可以传入一个长度等于nc的列表,按类别样本占比的倒数设置。例如样本统计发现sleep只有run的五分之一,可以设置class_weights=[0.7, 2.0, 1.0, 1.0, 2.5]。注意权重不要差太多,否则小类过拟合,大类反而掉点。更稳妥的做法是先训练一个 baseline,混淆矩阵出来后,再针对互误检严重的类别调权重。
6.3 推理端技巧:SAHI 切片检测 + ByteTrack 时序过滤
对 1080p 监控画面,直接用 640 推理时小目标丢失很多。SAHI 把原图切成 640x640 的 patch,重叠 20% 分别推理再合并,能明显提升小目标召回。但切片推理耗时高,不适合实时场景。实时场景我更喜欢把检测器和 ByteTrack 配合:先检测出人的框,再用跟踪器维护每个目标的 ID,跌倒/跑睡这类状态按“连续 N 帧的类别投票”输出,比单帧硬判断稳定得多。这个方案我在工地场景实测过,误报率能降一半左右。
6.4 验证方法:混淆矩阵和真实场景抽样
最后别只看总 mAP,用yolo detect val输出的混淆矩阵看类别间误检,再拿一段没参与训练的真实监控视频做抽帧测试,统计每类漏检和误检数量。我自己的习惯是记录每一次调参后的混淆矩阵,日志留档,而不是只看验证集曲线。这个数据集作为起点是好的,但要真正落地,还需要补充自己场景下的大量样本,特别是跌倒时的遮挡情况和光线变化。我踩过最狠的坑是在实验室数据上跑出 0.9 的 mAP,拿到现场晚上一测全瞎,从那以后我一直坚持“验证集必须包含真实部署环境样本”这条原则。希望帮到你。
本文还有配套的精品资源,点击获取