☰
摔倒检测数据集实战:从解压到YOLOv8训练全流程
2026/9/26 1:39:05 网站建设 项目流程

简介:面向计算机视觉与安防领域的摔倒检测模型训练需求,这份数据集以YOLO框架为主要应用方向,包含fall、falling、normal三个类别,分别对应实际摔倒、即将倒地及正常活动,可为智能家居、养老监护等场景提供算法开发基础。压缩包内共931个文件,其中310张jpg图像搭配310个xml标注(VOC格式)与311个txt标注(YOLO格式),两种标签互相配合,方便在不同检测框架间切换使用,整体大小约12.85MB,适合快速下载与验证。数据集特别区分了fall与falling状态,有助于模型理解摔倒的动态过程,从而降低误报和漏报,提升实时监测的可靠性。目前已有1469人学习使用,适合刚接触YOLO目标检测的开发者直接用于训练测试,也适合研究人员在此基础上进行数据增强与模型调优,是构建摔倒检测原型系统的实用训练资源。

1. 摔倒检测数据集:一个压缩包里装着整个基线方案

摔倒检测在工程落地时的尴尬在于:算法原理都懂,但你手里没数据,模型就永远只活在论文里。fall-dataset 这类 rar 压缩包,就是把“能用来训练摔倒检测模型”的图像帧和标注文件打包好分发的东西,解压后你能拿到人体框、姿态状态标签,甚至关键点,直接喂给目标检测网络训练。它的价值不是“一个数据集”,而是给你省掉了从现场采集、人工标注到格式清洗那两三周最脏最累的活,让你直接从训练开始。

这里有个反直觉的结论:摔倒检测最难的不是把“躺着的人”检出来,而是把“躺着休息”和“坐着滑倒”这类静态帧上几乎无法区分的状态分清楚。fall-dataset 的意义,恰恰在于它提供了一批带明确“摔倒时刻”的正样本帧和大量日常姿态负样本,让模型学的不是“有没有人”,而是“人的状态是不是异常”。适合做智慧养老、居家看护、工地防跌倒告警方案评估的工程师;纯粹做动作识别算法研究的人,这个包反而不够学术化。

2. 先拆包看懂 fall-dataset:目录结构与标注格式的“读法”

2.1 解压前先确认它是哪种组织方式

拿到 fall-dataset.rar,第一步不是急着解压,而是先看压缩包内部的顶层目录结构。因为“摔倒检测数据集”这个说法并不指向唯一的标准格式,常见做法是以下几种形态:video-style,一段段视频文件配骨骼点或标注文件;frame-style,每帧一张 jpg,标签按文件名一一对应;annotation-json,几份大 json 按视频段组织所有实例信息。rar 包只是一个外壳,真正决定后面脚本怎么写的,是内部是逐帧标注还是按视频段标注。

rar 单文件分发的好处是做 sha256 完整性校验很方便,我一般会先测试压缩包是否完整,再谈后续,避免解压到一半才发现分卷缺失或文件损坏。下面两条命令分别做完整性和内容预检:

# 测试 rar 完整性,不解压 unrar t fall-dataset.rar # 查看顶层目录结构,统计各级路径的文件数量 unrar lb fall-dataset.rar | awk -F/ '{print $1}' | sort | uniq -c

第一条命令只做测试,不会产生解压文件,跑完看到All OK再继续。第二条命令把压缩包内每个文件的第一级路径做计数——如果看到的是frames/ 2400、labels/ 2400,说明是 frame-style;如果看到videos/ 40、annotations/ 40,大概率是 video-style。这个判断决定你后续的数据加载路径是直接读帧还是先进 PyAV 或 OpenCV 抽帧。

如果包里带pos和neg两类子目录,那更好办。pos是摔倒正样本帧,neg是日常姿态负样本帧。很多团队拿到手直接全量丢进 YOLOv8 训练,结果在真实场景里把“躺地休息”识别成摔倒,原因就是没有维护好负样本的比例和内容分布。

2.2 json 标注里到底记了哪些字段

从业者视角看,这种数据集的标注信息至少包含三个层级:image 字段,记录图片路径或 video_id 加 frame_id 的组合;instance 字段,记录每个人体框 bbox 和类别 person;action 字段,记录当前帧的状态标签,如 standing、walking、sitting、falling。有些数据集还会附带 17 点或 25 点的 COCO 骨架关键点,有些则把 falling 作为独立检测类别,其他帧统一标 person。

这两者的差异直接决定训练目标。如果 falling 是类别,模型学到的是“姿态像摔倒就输出 falling”;如果只有状态属性,那你需要在检测头之外额外做动作分类头。先探明数据结构再写训练管线,能省掉后面重构的大麻烦。下面这段 python 把常见 annotation json 的结构探明并拉平成一张表。

import json with open('annotation.json', 'r', encoding='utf-8') as f: data = json.load(f) # 常见做法是数据挂在 'annotations' 或 'videos' 下,先探明结构 def explore(obj, depth=0, max_depth=3): if depth > max_depth: return if isinstance(obj, dict): for k, v in list(obj.items())[:5]: print(' ' * depth + f'{k}: {type(v).__name__}') if isinstance(v, (dict, list)): explore(v, depth + 1, max_depth) else: print(' ' * (depth + 1) + f'值示例: {v}') explore(data) # 试解析成表格:image_id, bbox, label rows = [] for ann in data.get('annotations', []): # 字段名因数据集而异,但基本逃不开这几个 img_id = ann.get('image_id') or ann.get('frame_id') bbox = ann.get('bbox') or ann.get('box') label = ann.get('action') or ann.get('state') or ann.get('category_id') rows.append({'image_id': img_id, 'bbox': bbox, 'label': label}) print('解析到', len(rows), '条实例记录') # 统计标签分布,判断正负样本比例 from collections import Counter print(Counter(r['label'] for r in rows))

这段代码的意义不是追求通用解析,而是先把 json 里字段名和对齐方式摸一遍。explore函数打印前几层的键名和类型,让你知道最外层是 videos 还是 annotations。标签分布 Counter 则回答了一个关键问题:如果 falling 只有几百条而 normal 有上万条,后面训练就必须调整类别权重或过采样策略,否则模型只会学会输出 normal。

bbox 坐标系是另一个必须确认的细节。COCO 给[x, y, w, h]绝对像素值,VOC 给[x1, y1, x2, y2],而很多从 Labelme 导出的 json 给的是归一化坐标。这三种写法在转换时最容易出边界错误。建议在解析脚本里打印一两行的原始值,人工比对原图宽高,先确认坐标是像素还是比例,再写转换逻辑。

3. 在本地把 fall-dataset 跑通:解压、清洗与转成 YOLO 可用格式

3.1 解压命令与目录落位

要用 rar 包训练模型,先决定输出目录很关键。我通常建一个datasets/fall-dataset/根目录,下面严格区分raw/和processed/,raw 永远保留原始解压结果,不修改不删减,processed 才是后续清洗后供训练使用的镜像。这样做的好处是后面想换划分策略、重新生成标签时不用重新解压。

mkdir -p ~/datasets/fall-dataset/{raw,processed/images,processed/labels} unrar x fall-dataset.rar -d ~/datasets/fall-dataset/raw/ find ~/datasets/fall-dataset/raw -type f | head -20

unrar x保留压缩包内的目录结构,而不是像unrar e那样把文件全部拍到同一层目录。用e的话,如果包内有两个目录下存在同名文件,后解压的会覆盖先解压的,导致数据丢失,这是个很隐蔽的坑。find的 head 20 行用来快速查看文件种类和来源覆盖情况,例如是否同时存在 mp4 和 jpg、是否有命名重复的帧。

还有一个我自己长踩的坑:如果压缩包内部按 pos/neg 分好了目录,不要解压后直接建 train/val 目录去复制文件。先保留 raw 的原始组织方式,等到划分阶段再按需复制或写列表文件。因为一旦复制到最后发现切分策略不行,你得重新解压整个包,白白浪费时间和磁盘空间。

3.2 把标注转成 YOLO 格式:坐标系的四个边界坑

YOLO 系模型训练最常用的输入格式是图片加同名 txt,每行写class_id cx cy w h,其中 cx、cy、w、h 是相对图片宽高的归一化小数。把 fall-dataset 的 json 转成这个格式,整个过程看似简单,但坐标系的坑一个接一个。结合我做过的几个跌倒数据集的转换经验,最值得警惕的是四个边界问题。

第一个坑是 bbox 格式不统一,[x1, y1, x2, y2]和[x, y, w, h]要分开处理。第二个坑是坐标到底归没归一化,如果 json 里已经是 0 到 1 的小数,再除以宽高会得到错误结果。第三个坑是宽高可能为 0,标注软件导出的错误帧会留下这种脏数据,不过滤的话训练时损失函数直接算出 nan。第四个坑是摔倒帧上可能同时出现多个人,每个目标都要输出一行,不能只保留第一个。

import json from pathlib import Path JSON_PATH = 'raw/annotation.json' IMG_DIR = Path('processed/images') LBL_DIR = Path('processed/labels') CLASSES = ['normal', 'falling'] # 按数据集的 label 定义 with open(JSON_PATH, 'r', encoding='utf-8') as f: data = json.load(f) # data 如果按视频组织,就先拉平成逐帧列表 frames = [] for video in data.get('videos', []): for fr in video.get('frames', []): for inst in fr.get('instances', []): frames.append({ 'img_path': fr['image_path'], 'bbox': inst['bbox'], 'label': inst['action'], 'w': fr.get('width'), 'h': fr.get('height'), }) for row in frames: src = IMG_DIR.parent / row['img_path'] if not src.exists(): # 有些路径写成绝对路径,需要回退到文件名匹配 src = IMG_DIR / Path(row['img_path']).name if not src.exists(): continue # 宽高兜底:json 里没有宽高就用 PIL 读取 w, h = row['w'], row['h'] if w is None: from PIL import Image im = Image.open(src) w, h = im.size # 转 yolo 格式:默认 bbox 是 x1y1x2y2 x1, y1, x2, y2 = row['bbox'] bw, bh = x2 - x1, y2 - y1 if bw <= 0 or bh <= 0: continue # 过滤无效框 cx, cy = (x1 + x2) / 2, (y1 + y2) / 2 cx /= w cy /= h bw /= w bh /= h cls_id = CLASSES.index(row['label']) if row['label'] in CLASSES else -1 if cls_id < 0: continue txt_path = LBL_DIR / (src.stem + '.txt') with open(txt_path, 'a', encoding='utf-8') as out: out.write(f'{cls_id} {cx:.6f} {cy:.6f} {bw:.6f} {bh:.6f}\n')

逻辑说明:这一版默认输入 bbox 是 COCO 的[x1, y1, x2, y2],如果你的 json 是[x, y, w, h],把后两个相加再进同样公式即可。归一化后所有坐标都在 0 到 1 之间,训练时模型会自动映射回原图尺寸。文件以追加模式写入,同一张图有多个目标时会依次追加多行,不会互相覆盖。

参数说明:CLASSES的顺序就是训练时的类别 id 映射,YOLOv8 的 data.yaml 里 names 顺序必须跟这个保持一致,顺序乱了模型输出和可视化对应不上。如果数据集里 falling 是作为独立检测类别给出的,那么 label 字段直接就是 class 名,不需要再映射到状态。

3.3 训练集与验证集划分:一个必须遵守的原则

划分数据时最忌讳随机打散帧。同一个视频的相邻帧极度相似,随机分配到 train 和 val 会让验证集 mAP 虚高,因为大量“照抄”内容进了训练集,验证时模型等于看到过答案。正确做法是按视频维度切分,保证同一个视频的所有帧要么全在训练集,要么全在验证集。

import random from pathlib import Path from collections import defaultdict LBL_DIR = Path('processed/labels') frame_to_video = {} for p in LBL_DIR.glob('*.txt'): # 具体拆分规则按文件名命名来,常见的是 video_id_frameid video_id = '_'.join(p.stem.split('_')[:-1]) frame_to_video.setdefault(video_id, []).append(p.stem) video_ids = list(frame_to_video.keys()) random.seed(42) random.shuffle(video_ids) n_val = max(1, int(len(video_ids) * 0.15)) val_videos = set(video_ids[:n_val]) train_files, val_files = [], [] for vid, stems in frame_to_video.items(): for stem in stems: if vid in val_videos: val_files.append(stem) else: train_files.append(stem) for split, files in [('train', train_files), ('val', val_files)]: with open(f'{split}.txt', 'w') as f: for stem in files: f.write(f'processed/images/{stem}.jpg\n')

这里的核心逻辑是frame_to_video先把所有帧按视频 id 分组,然后对视频 id 列表做随机打乱,取前 15% 作为验证视频集合。文件命名规则必须和实际包一致,如果你的帧名是frame_00123.jpg这种没有视频前缀的格式,那么拆视频 id 就得改成从目录名获取。

划分文件里写入的是相对路径列表。这个 train.txt 和 val.txt 后面直接喂给 YOLOv8 的 data.yaml 使用,好处是后续增删帧不用改配置文件,重新生成列表即可。

4. 用摔倒检测数据集训出一版能告警的模型:训练配置与验证指标

4.1 组一个最小的 data.yaml:训练路径与类别定义

数据就绪后,训练配置是影响收敛速度的第一变量。用 YOLOv8 训练自己的数据集,data.yaml 是入口:

path: /home/user/datasets/fall-dataset train: train.txt val: val.txt names: 0: normal 1: falling

这里 train 和 val 指向的是文本文件路径而非目录,因为列表文件可以精确控制参与训练的文件范围。如果你后续要排除某些低质量帧,只需要从列表文件里删掉对应行,不用动目录结构。

批大小根据显存设置,盲用默认值常常翻车。一个大致参考:1080p 图像下 YOLOv8n 大约每个 batch 占 4 到 6 GB 显存,YOLOv8s 翻倍。第一次跑通完整流程建议用yolov8n、imgsz 640、batch 16,消费级显卡就能带起来。确认流程没问题后再加大模型尺寸。

4.2 训练命令与参数:把一句话命令拆成关键旋钮

一般用下面的命令启动训练:

yolo detect train \ model=yolov8n.pt \ data=data.yaml \ epochs=80 \ imgsz=640 \ batch=16 \ workers=4 \ patience=15 \ project=runs/fall_det \ name=baseline_v1

逐个说要调什么:model=yolov8n.pt用 COCO 预训练权重做迁移学习,fall-dataset 只有几千张的话从头训会欠拟合,预训练带来的基础特征非常关键。epochs=80对这个样本量级别的任务是一个合理预算,检测网络通常在 40 轮左右 mAP 就到平台期,设 80 是为了给早停留空间。patience=15表示验证集 mAP 连续 15 轮不升就早停,能有效防止过拟合,摔倒检测的过拟合症状不是 train loss 掉,而是 val 上 falling 类 recall 突然下降。

workers=4是数据加载进程数,容易被忽略但很重要,如果机器上同时跑多个实验,workers 过高会导致 CPU 争抢,实际吞吐反而下降。imgsz=640是基准分辨率,falling 框通常是大尺度人形,640 够用;如果摄像头画面里人只占很小一块,必须改到 960 或 1280,代价是显存翻倍、推理速度下降。训练结束后结果在runs/fall_det/baseline_v1/,重点看混淆矩阵和 PR 曲线,不要只盯 loss 曲线。

4.3 类别不均衡处理:先动权重再动数据

fall-dataset 里 normal 样本往往远多于 falling,这是所有跌倒数据集的通病。第一次看到 val 上 falling 类 PR 曲线上不去时,不要急着加 mosaic 和 mixup 增强,三步走的顺序很重要。

第一步查计数,确认正负样本比例。如果 ratio 超过 10 比 1,先改损失权重,把 falling 类的 loss 乘一个系数 2 到 4,让模型在优化时更重视少数类。第二步做正样本过采样,把 falling 帧在训练列表里重复出现两三轮,这比复制文件更灵活,因为列表引用不占额外磁盘空间。第三步才是数据增强,对 falling 帧做上下翻转和轻度随机裁剪、亮度抖动,监控摄像头机位可能在天花板或墙角,同一个摔倒方向不能代表全部视角。

调完权重重新训一版,判别指标不要只看总的 mAP@0.5。对摔倒告警系统而言,漏报的代价远高于误报,falling 类的单类 recall 优先于 precision。单独看混淆矩阵中 falling 那一行,确认有多少正常帧被误判为摔倒,有多少摔倒帧漏掉,再决定是否调整置信度阈值。

5. 摔倒检测数据集使用避坑:五条高频翻车记录

5.1 标注框过大:把行人也圈进摔倒框

现象:验证集上 normal 类被误判为 falling,且框明显偏大。

原因:标注框本身框住了跌倒者的全身,而模型的真实判别依据其实应该是躯干姿态,不是人形外包围盒。训练中模型把“横向长条形框”当成了关键特征,远处一个横躺的箱子也会被输出为 falling。

解决:对 falling 类标注框做一次收缩,把上下左右各裁掉 0.05 到 0.1 的空白区域,让模型去学躯干角度而非外部背景。注意裁剪必须同步更新 bbox 坐标再写回 txt,否则训练时模型看到的区域和标注不对齐,反而引入更多噪声。

5.2 坐着滑倒与蹲下站起分不清

现象:val 上 falling 的 recall 卡在 74% 左右上不去,错检集中在坐、蹲、起立的过渡帧。

原因:单帧静态图像包含的信息不足。数据集标注本身按帧独立打标,帧与帧之间不构成序列,模型就没有机会学到“上一帧站立、下一帧倒地、再下一帧还躺着”的时序语义。

解决:在数据组织层面,把相邻 3 帧拼接成一个小序列样本,或者在训练完单帧模型后,在后处理阶段加连续帧投票逻辑。后者实现成本更低,改成“连续 N 帧中至少 M 帧判为 falling 才告警”就能压掉大部分瞬时误报,详见下一章。

5.3 标注帧号与解压图片对不上

现象:生成 train.txt 后大量图片缺失,训练直接报文件不存在。

原因:原始标注的 frame_id 对应原始视频帧号,但解压出来的 jpg 序列是每隔几帧才导出的。直接按整段视频的 frame_id 映射图片名,自然大量落空。

解决:先用脚本列出 images 目录下所有实际文件名,统计序号步长和范围,再按步长对齐标注帧。不要盲信 json 里的 image_path 字段,有些包的路径是数据生产时的服务器路径,到本地必然失效。按文件名重匹配是通用解法,代码在第三章已经覆盖。

5.4 负样本太多拖垮整体精度

现象:加了两万张负样本帧后,val 的 mAP 反而下降 6 个点。

原因:负样本里大量无人体、重复背景的帧稀释了模型对姿态的学习。检测网络的损失在所有类别上求和,重复背景不贡献有效的定位梯度,可以把这种帧看作纯噪声。

解决:对负样本做一次初筛,保留那些包含“完整人形”但状态不是 falling 的帧。这些才是真正有价值的难例。rar 包里的 normal 目录不是每一张都值得进训练集,筛完通常能回到更好的精度水平。

5.5 上次训练的权重混进了下一次实验

现象:第二次训练的 loss 曲线正常,但 val 输出和第一次几乎完全一样。

原因:project和name没换,YOLO 自动续接上次训练目录里的权重,或把上次的 best.pt 用于验证。

解决:每次训练换name加版本后缀,或显式指定model=yolov8n.pt重置初始权重。训练前看一眼项目目录,把上一次输出清的干干净净再启动,排除黑匣子干扰。

6. 把模型推进一步的技巧:连续帧投票与夜间场景验证法

单帧模型输出天然带抖动。摔倒这个事件最可靠的判据不是“这一帧倒了”,而是“连续数帧里目标没有恢复直立”。低成本改造方案是后处理投票:维护一个长度 5 的滑动窗口,窗口内 falling 置信度超过阈值的帧达到 3 帧才触发告警,这样能把弯腰捡东西、座椅起身这类瞬时误报压掉一大半。

from collections import deque def fall_alert(score_iter, threshold=0.5, window=5, hit=3): ring = deque(maxlen=window) for frame_id, score in enumerate(score_iter): ring.append(1 if score > threshold else 0) if sum(ring) >= hit: yield frame_id, sum(ring) / len(ring) ring.clear()

参数说明:window=5意味着只看最近 5 帧,hit=3意味着至少 3 帧判定为 falling。窗口大了告警延迟高,窗口小了抑制不了抖动。部署时还要加一条防重复触发间隔,比如触发一次后 60 秒内不再输出第二次。这个系统的端到端延迟取决于推理帧率,如果摄像头只有 5 fps,5 帧窗口相当于 1 秒出一次结论,对看护场景完全够用。

夜间场景验证是另一个容易漏的坑。普通监控摄像头在红外夜视模式下,输出图像和 fall-dataset 里的白天照片差异很大,直接套模型 recall 会掉。我的习惯是额外准备一小批夜间帧,哪怕只有几十张,作为验证集扩展单独观察 falling 类单类 recall,不要等到现场部署才发现光照域差异导致翻车。

最后说一个我自己的教训:预处理脚本、标注转换脚本和划分脚本一定要固化进仓库并与数据一起提交。fall-dataset 这类 rar 包数据第一次跑通后,后面换数据源、换标注格式、补样本时都要重跑转换流程,没有固化脚本就等于每次手工重新踩坑。做一个项目建立一套可复现的数据管线,比任何单次的调参都更值钱。希望帮到你。

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

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

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

立即咨询