ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

跌倒检测数据集落地指南:从zip校验到YOLO训练与防泄漏划分

跌倒检测数据集落地指南:从zip校验到YOLO训练与防泄漏划分

简介:跌倒检测数据集是面向计算机视觉、模式识别与机器学习研究者的训练与评估资源,主要服务于老年人跌倒监测、智能安防等场景,可用于开发并验证跌倒识别算法。压缩包内共含2000个文件,包含1440个XML标注文件和560张JPG图像;XML标注详细记录了跌倒起始时间、结束时间、跌倒方向等关键信息,JPG图像则覆盖不同角度、光照条件下的跌倒姿态,压缩后整体大小为65.27MB。目前已有629人学习下载。利用该数据集,研究者可完成模型训练、精度测试与算法对比,也可通过分析标注与图像内容深入理解跌倒行为的视觉特征,从而优化检测模型在真实场景中的准确度与鲁棒性。数据集目录结构清晰,适合从计算机视觉入门到进阶的研究人员使用。

1. 跌倒检测数据集为什么先查数据,再谈模型

跌倒检测数据集,听起来像是一份“标注好的压缩包,解压就能训”,但真正拿到手才发现,它更像一个待拆的黑匣子:标注框用的什么坐标系、摔倒的姿态边界怎么定、训练集和验证集是不是同一段视频抽出来的帧——这些不确认清楚,模型结构再折腾也很难涨点。

这篇笔记讲的是拿到 .zip 文件之后的完整落地路径:先做完整性校验,再盘清数据血缘,确认标签形态并统一类别映射,完成清洗和按视频源划分,最后才谈训练与验证。适合正在找跌倒检测目标检测/时序识别数据集、准备训练自己模型的工程师和研究人员。多数人在这套流程上翻车,根因不是模型太弱,而是数据治理没做透。

2. 拿到 zip 后的第一小时:先做完整性和数据血缘检查

2.1 先用 unzip -t 验证压缩包完整性,再动手解压

从数据集站点、网盘或论文附件拿到的跌倒检测压缩包,体积往往不小,传输过程中出现损坏的概率并不低。常见的翻车现场是:解压到一半报“CRC failed”,或者某几张关键图片解码出来是花屏,训练到一半才发现数据本身就是坏的。遇到这种情况回头重下,浪费的时间比先校验多得多。

我一般把 zip 当“档案”而不是“数据集”处理:先校验,再解压,最后建立目录清单。校验命令用unzip -t,它会逐个测试压缩包内所有文件的 CRC 校验值,只测试、不释放文件。如果包特别大,可以改用7z t,校验速度会明显更快。

# 1) 先校验压缩包完整性,不实际解压 unzip -t fall_detection_v1.zip | tail -20 # 2) 校验通过后再解压,保持目录结构 unzip -o fall_detection_v1.zip -d datasets/fall_detection # 3) 解压后输出目录树,确认顶层结构 find datasets/fall_detection -maxdepth 2 -type d | sort | head -30 # 4) 统计 images 和 labels 文件数量是否对得上 find datasets/fall_detection/images -type f | wc -l find datasets/fall_detection/labels -type f | wc -l

这里几个参数值得说明:unzip -t的-t是 test 模式,只检查不写入磁盘,结尾出现No errors detected才说明归档没问题;-o表示覆盖已有文件,重复解压时不卡在交互确认;-d指定解压目标目录,避免全部文件散落当前目录。第 3 步的-maxdepth 2只往下看两层目录,能看到顶层结构就够,不要直接全量打印文件清单,容易刷屏。第 4 步的wc -l数出图片和标签文件各自数量,两边差距过大,说明数据本身可能不完整,往下的步骤先放一放,回去补数据。

提示:如果解压后用ls看到一堆乱码文件名,先别急着删,这是 zip 内层编码问题,第 5 章有具体修法。

2.2 盘“数据血缘”:一个视频来源拆出的兄弟帧

做完完整性校验,很多人会直接翻 labels 目录开始看标注。我建议再做一件事:把每个样本追溯到它来自哪一段原始视频。跌倒检测数据集的来源大多是监控录像或公开视频库,发布方通常先抽帧、再标注。抽帧密度、场景数量、机位角度直接决定模型的泛化边界。很多检测器在验证集上 mAP 很高,换到真实病房、客厅就崩,主因是训练和验证里混进了同一条视频的相邻帧——模型在“背答案”,不是在“学跌倒”。

判断来源最直接的办法是看文件名。常见命名是video_12_000123.jpg这类结构,下划线分隔的前缀就是视频编号。如果压缩包里没有这种信息,就从目录名或图片 EXIF 里找;两样都没有,就得接受一个事实:这批数据泛化能力不可控,后续评估分数只能当作参考,不能当作上线依据。

# 按文件名前缀(视频源)分组,统计每个来源的样本数 # 假设文件名为 video_12_000123.jpg ,取前两段 video_12 作为来源id ls datasets/fall_detection/images | awk -F_ '{print $1"_"$2}' \ | sort | uniq -c | sort -rn | head -20

awk -F_按下划线拆分文件名,{print $1"_"$2}取出前两段组成来源 id;uniq -c统计每个来源的帧数;sort -rn按样本数倒序排列。这样一眼就能看出是不是有某个来源异常刷量——如果某个来源占了全量数据的 30% 以上,训练出来的模型大概率对这个机位和场景过拟合。命名格式不一样就调整分隔符和字段号,原则是找出“最小视频单位”。

盘完来源,我一般会顺手重建目录结构,把原始 zip 里的散乱文件归成下面这样:

datasets/fall_detection/ ├── images/ │ ├── train/ │ ├── val/ │ └── test/ ├── labels/ │ ├── train/ │ ├── val/ │ └── test/ └── source_map.txt

这一步不做格式转换,只按来源把文件物理分组。source_map.txt记录每个样本对应的视频来源,后续做划分和排查泄漏时都会用到。别嫌这一步啰嗦,真实项目里我有过因为没查来源,训练集和验证集出现同一段视频关键帧,导致模型评估虚高 20 个点的经历。数据血缘盘清楚,后面所有评判才站得住。

3. 把 zip 解码成好用的训练集:标签协议嗅探与统一

3.1 标签文件的组织方式决定你能用什么模型

解压后别急着写训练脚本,先搞清楚 labels 目录到底是什么格式。同一个词“跌倒检测数据集”,不同发布方给的东西差别很大:有的是单帧图像加目标框,适合 YOLO 这类检测模型;有的是视频片段加起止时间标签,适合时序动作识别;还有的是骨骼关键点序列。压缩包里的标签组织方式,决定了你这条技术路线能往哪走。

常见三种标签组织方式对比如下:

组织方式典型形态标注内容适配方向
YOLO txtimages/ 与 labels/ 成对每行 class x_center y_center w h,归一化 0~1单帧跌倒姿态检测
COCO JSONannotations/ 下 jsonbbox 为像素坐标,含图片尺寸与类别表通用目标检测/实例分割
时序 JSON按 clip 组织动作类别、起止帧、帧序列路径跌倒过程识别/时序模型

静态框检测和时序识别有本质差别:单帧标注只能学“倒地姿态”,学了摔倒的一瞬间,但学不到“从站立到倒地”这个过程。真实场景里“躺着”和“摔倒了”是两个概念,前者要报警,后者要看情况;只靠单帧框,大量误报几乎是必然。压缩包里如果只有静态标注,要么接受“姿态检测”这个边界,要么想办法补视频源做时序建模。先认清这一点,后面选模型才不会跑偏。

3.2 写一个标签嗅探脚本,别靠猜

面对一个陌生的 zip,统一做法是先嗅探再下结论。用 Python 打开压缩包,列出所有标签相关文件的路径和大小,先看文件分布,再抽取一两个实际内容确认格式。

import zipfile zp = "fall_detection_v1.zip" with zipfile.ZipFile(zp) as z: names = z.namelist() label_files = [n for n in names if n.endswith((".json", ".txt"))] # 打印前 20 个标签文件路径,不打印图片避免刷屏 for n in label_files[:20]: print(n, z.getinfo(n).file_size)

这里只筛选.json和.txt后缀,是因为大多数标注协议跑不出这两种形态。z.getinfo(n).file_size返回文件未压缩大小,大文件说明标注信息丰富,小文件多半是单行 txt 或空标签。接下来读一个真实标签文件内容:

with zipfile.ZipFile(zp) as z: raw = z.read("labels/000001.txt") text = raw.decode("utf-8", errors="replace") print(text)

输出如果是0 0.5113 0.4207 0.0814 0.1652这种五行数字,就是标准的 YOLO 格式每行含义:类别 id、归一化后的中心 x、中心 y、框宽、框高。注意检查数值范围,bw和bh出现大于 1 的值,多半是发布方把像素坐标直接塞了进来,这种数据集无论训练还是验证都会出怪问题。解不出来时把utf-8换成cp936,很多中文 Windows 环境下生成的标签用 GBK 编码。

3.3 统一类别映射:fallen、lying 都是“跌倒”吗

跌倒检测数据集的类别命名常常不统一。有的把“跌倒中”和“已倒地”分成两类,有的全部叫fall,还有的用fallen / lying / down混着写。同一个压缩包里,不同标注人员用词不一致太常见了。训练前先把所有标签里出现过的类别 token 统计出来,再做映射,不要想当然。

# 统计所有 YOLO txt 第一列出现的类别token cut -d' ' -f1 datasets/fall_detection/labels/*.txt | sort | uniq -c

cut -d' ' -f1按空格切分取第一列。注意如果标签是用逗号分隔的,这里要改用-d','。拿到全部类别 token 后,再决定合并还是拆分,比如:

CLASS_MAP = { "0": "fall", # 已统一 "fall": "fall", "fallen": "fall", "lying": "lying", # 保持独立,避免误并 "normal": "normal", } # 类别按序编号写入 data.yaml names = {0: "fall", 1: "lying", 2: "normal"}

这里有个容易踩的坑:lying和fall在某些姿势下视觉很像,但语义不同。如果发布方区分了这两类,训练时强行合并成一类,模型会被“躺床上的人”和“倒地老人”的矛盾标注反复拉扯,loss 降不下去。相反,如果发布方把“倒地”“摔下”都归为fall,就不要自己拆出细类,没有对应标注支撑的类别拆了只会制造噪声。统一映射这一步做得干净,后面训练才不用反复回头改。

4. 训练前的清洗与划帧:让假指标现出原形

4.1 五类需要优先处理的脏样本

跌倒检测属于长尾分布明显的数据:正常行走、坐、躺的样本往往远多于“跌倒”本身。标注质量参差时,模型很容易被少数脏样本带偏。我处理这类数据集时,会优先清理下面五类样本:

样本类型为什么危险判定方式
静坐/平躺但标成跌倒直接给模型错误标签人工抽检,按视频源抽查
框占比过小目标距离远、信息量不足脚本统计 bbox 面积比
同一场景刷帧大量近重复帧造成过拟合感知哈希去重或来源统计
遮挡过半标注框里大半是背景人工复核 + 框宽高比筛查
时序标签错位跌倒开始/结束帧切错位置按序列播放逐段看

优先处理框占比过小这一类,因为可以完全自动化。跌倒检测里目标框占比通常在 3%~30% 区间,低于 1% 的基本是远景行人或角落里的模糊目标,模型学不到有效特征,反而会当成噪声记进去。遮挡过半的样本要人工复核,直接删会损失跌倒姿态多样性,但留太多又影响收敛。

4.2 用一组小脚本完成批量过滤

清洗的第一步是跑统计脚本,把可疑样本列出来,而不是直接删文件。清洗要有后悔药,先输出清单,人工过一遍,确认误判再动手。

import cv2 from pathlib import Path img_dir = Path("datasets/fall_detection/images") label_dir = Path("datasets/fall_detection/labels") for txt_path in sorted(label_dir.glob("*.txt")): img_path = img_dir / (txt_path.stem + ".jpg") img = cv2.imread(str(img_path)) if img is None: print("图片缺失或损坏:", txt_path) continue h, w = img.shape[:2] for line in txt_path.read_text().splitlines(): parts = line.split() if len(parts) != 5: print("异常标签行:", txt_path, line) continue cls, xc, yc, bw, bh = parts xc, yc, bw, bh = map(float, (xc, yc, bw, bh)) box_area = bw * bh # 归一化框面积小于1%,信息量不足 if box_area < 0.01: print("框占比过小:", txt_path, line) # 坐标越界,多半是坐标系或格式问题 if not (0 <= xc <= 1 and 0 <= yc <= 1 and 0 < bw <= 1 and 0 < bh <= 1): print("坐标越界:", txt_path, line)

box_area = bw * bh是归一化后的面积比例,不需要乘图像宽高。阈值0.01是经验值,可以先跑一遍看输出量再调整:输出过多就降到 0.005,过少就提到 0.02。坐标越界的判断很关键,一旦出现越界,不是简单删一条标签,而是整个数据集的坐标系定义要重新确认。

4.3 按视频来源划分数据集,而不是随机切帧

划分 train/val/test 是另一个容易做错的地方。很多人在拿到数据集后直接random split,按帧随机分,这是评估失真的最大来源。跌倒检测数据大多从视频抽帧而来,相邻帧高度相似,随机切帧会让训练集和验证集出现大量近乎重复的图片,模型在验证集上学到的“记忆”远超“泛化”。

正确做法是按视频来源分组划分,同一个视频的帧只能出现在同一个集合里。

import random from pathlib import Path samples = list(Path("datasets/fall_detection/images").glob("*.jpg")) groups = {} for s in samples: video_id = s.stem.split("_")[0] # video_12_000123 -> video_12 groups.setdefault(video_id, []).append(s) keys = list(groups.keys()) random.Random(42).shuffle(keys) n_val = max(1, int(len(keys) * 0.15)) val_keys = set(keys[:n_val]) test_keys = set(keys[n_val:n_val * 2]) train_files, val_files, test_files = [], [], [] for s in samples: v = s.stem.split("_")[0] if v in val_keys: val_files.append(s) elif v in test_keys: test_files.append(s) else: train_files.append(s) print(f"train {len(train_files)} val {len(val_files)} test {len(test_files)}")

random.Random(42)固定随机种子,保证每次运行得到同一套划分,后续对比实验才公平。n_val = max(1, int(len(keys) * 0.15))按来源数留出 15% 做验证集,实际项目里 10%~15% 都够,跌倒检测类别本身稀少,验证集太大反而稀释正样本。按视频源划分后,模型验证分数通常会比随机切帧低一些,这是正常的——低下来的那个数,才是上线后真实能见到的数。

提示:如果压缩包里自带官方划分文件,也要先做泄漏检查。用来源分组脚本重新跑一遍看分布,官方划分同样可能出现同一视频关键帧串到两个集合的情况。

5. 避坑排查。跌倒检测数据集落地的高频问题

5.1 解压后中文乱码,文件名面目全非

现象:unzip解压后文件名变成锟斤拷或一堆无法识别的乱码,图片路径失效,标签和图片对不上。

原因:zip 文件内部记录文件名用的是本机编码,很多中文 Windows 环境下生成的 zip 用 GBK 编码,而新版 Linux/macOS 的 unzip 默认按 UTF-8 解码,编码对不上就产生乱码。

解决:不要手动改名,用 Python 重新解压,把内层字节手动按 GBK 解码后落盘。

import zipfile zp = "fall_detection_v1.zip" out = "datasets/fall_detection" with zipfile.ZipFile(zp) as z: for info in z.infolist(): # zipfile 默认把文件名按 cp437 解码,先还原字节,再按 gbk 解码 new_name = info.filename.encode("cp437").decode("gbk", errors="replace") z.extract(info, out)

这里info.filename.encode("cp437")是把乱码文件名还原成原始字节流,再.decode("gbk")解析出中文原名。errors="replace"是为了个别无法解析的字符不直接抛异常。跑完检查几个目录名是否恢复,再继续后面的操作。

5.2 标签文件与图片数量对不上,训练时总报缺图

现象:labels 目录里的 txt 数量和 images 目录的 jpg 数量相差几百,训练日志里频繁出现image not found或label not found。

原因:数据集发布方在整理时丢失了部分对应文件,或者抽帧后没有同步清理空标签。另外有些正常帧没有目标,发布方只保留了一个空 txt,也容易被误删。

解决:先做一次集合比对,列出缺失清单,再决定补缺失还是建空标签。

# 列出只有图片没有标签的文件 find images -type f | sed 's/\.jpg$//' | sort > /tmp/img_ids.txt find labels -type f | sed 's/\.txt$//' | sort > /tmp/lab_ids.txt comm -23 /tmp/img_ids.txt /tmp/lab_ids.txt | head -20

comm -23输出只存在于第一个文件的行,也就是有图无标签的样本。如果数量不多,可以给这些图建空 txt,表示“该帧没有跌倒目标”;数量大,说明数据发布有缺失,建议回源核实。要注意:空标签在 YOLO 训练里会影响negative samples数量占比,缺失严重时要在 loss 权重上做调整。

5.3 类别叫法混乱,同一个跌倒有三种写法

现象:标签文件里fall、fallen、fall down混着出现,训练脚本读进来发现类别数超出预期,或同一个语义被当成两个类。

原因:多人标注时没有严格执行类别表,标注工具直接写入了自然语言。中英文混杂也是常见情况。

解决:先统计全部类别 token,再建立合并映射,统一成数字 id。

from collections import Counter from pathlib import Path cnt = Counter() for txt in Path("labels").glob("*.txt"): for line in txt.read_text().splitlines(): cnt[line.split()[0]] += 1 print(cnt.most_common(20)) # 输出示例: [('0', 8521), ('fallen', 633), ('fall', 1201), ...]

看到fall和fallen都要映射成同一个 id,down这类修饰词要确认是否和跌倒同义。统一后检查映射覆盖率,出现未登记 token 就停下来补映射,别让脚本静默跳过。这一步做好,后面训练的类别数才是真实可控的。

5.4 验证集分数虚高,训练集 loss 却在降

现象:训练 loss 稳步下降,验证集 mAP 也一路走高,但把模型放到实际视频回放里检测,误报漏报都很严重,与验证结果完全不成比例。

原因:大概率是划分时按帧随机切,同一视频相邻帧串进了训练集和验证集。模型记住了这组画面的“答案”,验证分数是虚高的。另一种可能是验证集和训练集来自同一场景同一机位,即使不是同一帧,场景背景的相似性也让模型钻了空子。

解决:回到第 4 章,按视频来源重新划分。如果是公开数据集,先用video_id维度做分组,再看每个来源的场景分布。重新划分后验证分数会下降,但下降后才是真实水平。

5.5 转成 YOLO 格式后,目标框大面积偏移

现象:标签数值看起来正常,但训练出的模型在测试图上框的位置偏了半张图,或者框大小明显不对。

原因:最常见的是原数据给的是像素坐标x1 y1 x2 y2,转换时忘了除以图像宽高;其次是类别 id 从 1 开始编号,而 YOLO 要求从 0 开始,导致类别错位。

解决:转换脚本里强制做归一化,并在转换后做一次数值范围检查。

# x1y1x2y2 -> cxcywh, 并除以图像宽高 img_w, img_h = 1920, 1080 cx = (x1 + x2) / 2 / img_w cy = (y1 + y2) / 2 / img_h bw = (x2 - x1) / img_w bh = (y2 - y1) / img_h

转换完成后跑一句快速检查,所有框的数值必须全部落在 0~1 之间:

# 检查是否有坐标值大于1,大于1说明还没归一化 awk '{for(i=2;i<=5;i++) if($i>1) print FILENAME": "$0}' labels/*.txt | head

这条 awk 遍历每行第二到第五列,出现大于 1 的值就打印文件名和整行内容。输出越多,说明原标签离 YOLO 格式越远,需要回看第 3 章确认原始标注协议。

6. 从 zip 到“能报警”的检测闭环:训练、验证与时序修正

数据清理干净后,训练部分反而最简单。以 YOLO 系列为例,把前面整理好的数据集按images/train、images/val、labels/train、labels/val组织好,写一个 data.yaml 就能直接开训。

# fall_det.yaml path: ./datasets/fall_detection train: images/train val: images/val names: 0: fall 1: normal

训练命令按这一套跑通:

yolo detect train \ data=fall_det.yaml \ model=yolov8s.pt \ epochs=50 \ imgsz=640 \ batch=16

model=yolov8s.pt加载的是预训练权重,对跌倒检测这种中小规模数据集来说,比从头训收敛更快、泛化更好。imgsz=640是速度和精度的折中点,如果标签里小目标多,提到 768 或 832 会有效果,但显存消耗明显增加。训练完不要只看最后的 mAP,用验证命令输出混淆矩阵:

yolo detect val \ data=fall_det.yaml \ model=runs/detect/train/weights/best.pt

看混淆矩阵时重点看“normal 被误判成 fall”的比例。跌倒检测落地项目里,漏报是安全事故,误报是体验灾难,而误报的大头恰恰是把坐下、蹲下、躺下识别成跌倒的场景。单帧模型很难区分这些姿态,实际部署时我一般会加一个时序平滑:

# 滑动窗口投票,连续3帧中至少2帧报警才触发 def trigger(preds, window=3, threshold=2): if len(preds) < window: return False recent = preds[-window:] return sum(recent) >= threshold

这个函数preds是每帧模型输出的 0/1 结果,window=3表示只看最近 3 帧,threshold=2要求至少 2 帧同时报警才确认。真实病房项目里,有人只靠单帧高 mAP 模型做报警,一天误报上百次;加了这一层时序确认后,误报降到了个位数。这也是我反复强调“数据集处理比模型结构更关键”的原因——数据治理决定了模型的真实上限,时序后处理决定了它能不能从演示变成可用。希望帮到你。

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

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进