基于YOLOv8的跌倒检测实战:4500张数据集处理与训练避坑指南
发布时间:2026/10/2 4:10:56来源:尧图网络
简介面向跌倒检测模型训练的数据集适合使用YOLOv5等目标检测框架的开发者、研究者和安防/养老场景工程人员可有效弥补公开跌倒样本稀缺、人工标注耗时的痛点。资源由jpg原始图片与VOC格式xml标注文件配套构成xml中记录目标位置与类别信息既可基于VOC格式直接开展监督学习也可转换为YOLO格式后无缝接入YOLOv5训练流程。压缩包整体约236MB共包含2000个文件主要文件类型为jpg图像和xml标注文件数据覆盖室内外、不同光照、姿态角度和人员密度变化有助于提升模型在实际监控环境中的泛化能力。该规模在同类跌倒检测数据中较为适中适合作为入门实践、算法对比和预训练微调的基准数据集。目前已有1545人学习下载获取后即可自行划分训练集与验证集通过常见转换脚本完成格式适配并开展数据预览、模型训练、精度评估与调优迭代大幅节省数据采集和手工标注的重复投入。1. 跌倒检测数据集4500张已标注这个量级到底够不够用标题里写的跌到检测数据集实际要做的活是跌倒检测Fall Detection在养老院走廊、工地、独居老人家里从监控画面里识别出人摔倒这个动作。4500张左右已经标注的图片放在今天动辄几万张的公开数据集面前确实不算多但我的判断和大多数人的直觉相反——跌倒检测这类贴近具体场景的细粒度任务决定模型能不能用的往往不是总数而是跌倒类样本占多少、场景是否单一、标注框有没有认真抠边。4500张只要分布健康足够把YOLOv8s训练到mAP50超过85%的实用水平真正让它翻车的是类别失衡和验证集泄漏不是数量不够。这篇笔记就是围绕这套数据从盘清家底讲到训练评估再讲到坑在哪。2. 拿到4500张标注图先别急着训练花两小时盘清数据家底2.1 标注格式识别VOC、COCO、LabelMe还是YOLO txt先弄清楚一个最基础的问题这4500张已经标注标完的产物到底是什么格式。标注格式直接决定你后面要不要写转换脚本也决定你能不能直接喂给YOLO。常见的交付格式有这么几类每一类的文件长相完全不同。VOC格式一个JPEGImages文件夹放图一个Annotations文件夹放同名XML。XML里面有object节点每个object带一个name和bndboxbndbox里是xmin、ymin、xmax、ymax四个值。这是数据标注行业最常见的交付格式之一LabelImg默认导出就是它。判断方法最省事随便打开一个XML看到object和bndbox基本就是VOC没跑了。COCO格式所有标注汇总到一个JSON文件里面是images、annotations、categories三个数组每个标注对象的bbox字段是[x, y, width, height]从框左上角起算。CVAT导出COCO时还会附带一个labels.txt说明类别id。如果你拿到的是一整个大JSON十有八九是COCO。LabelMe格式每张图对应一个同名JSON里面有shapes数组每个shape有label和points。points可能是矩形四个点也可能是一圈多边形点。LabelMe标注工具导出就是这样一个图像文件夹配一个JSON文件夹。YOLO格式每张图一个txt每行是类别id cx cy w hcx、cy是归一化后的中心点坐标w、h是归一化后的宽高。这是YOLO系列训练直接能读的格式不需要转换。还有CVAT和Label Studio这类平台工具导出的格式更杂能选COCO、VOC也能选YOLO。判断方法别靠猜解压后在目录里看头十几个文件的扩展名再用编辑器打开一个标注文件看5秒钟结构比读什么文档都快。如果数据是别人转交的第一件事问清楚标注时用的什么工具。工具决定源格式源格式决定转换路径这一环问清楚能省一下午。2.2 用脚本统计类别分布、图像尺寸与标注框质量在动手训练前我会先写一个十几行的统计脚本把数据集的体检报告打出来。检查三个东西类别分布、图像尺寸分布、标注框面积占图像面积的比例。第三项最容易被忽略但它直接告诉你这个数据集里目标有多大决定后面训练时的imgsz设多少。以最常见的VOC XML为例脚本是这样import os import xml.etree.ElementTree as ET from collections import Counter def inspect_voc(ann_dir, img_dir): class_counter Counter() # 每个类别的目标数 size_counter Counter() # 图像分辨率分布 area_ratios [] # 每个框的面积 / 图像面积 for xml_name in os.listdir(ann_dir): if not xml_name.endswith(.xml): continue tree ET.parse(os.path.join(ann_dir, xml_name)) root tree.getroot() img_w int(root.find(size/width).text) img_h int(root.find(size/height).text) size_counter[f{img_w}x{img_h}] 1 for obj in root.iter(object): name obj.find(name).text class_counter[name] 1 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) box_area (x2 - x1) * (y2 - y1) area_ratios.append(box_area / (img_w * img_h)) print(类别分布:, dict(class_counter)) print(图像尺寸分布:, dict(size_counter)) if area_ratios: area_ratios.sort() med area_ratios[len(area_ratios) // 2] print(框面积占比中位数: %.4f % med) print(最小占比: %.6f, 最大占比: %.4f % (area_ratios[0], area_ratios[-1])) if __name__ __main__: inspect_voc(Annotations, JPEGImages)这段代码做的事情很朴素遍历每个XML统计每个类别的目标总数、图像分辨率、每个框相对整张图的面积占比。输出里我最看重的是框面积占比中位数。如果这个值在0.05以下说明目标在画面里偏小训练时要考虑把imgsz从640提到960或者引入切片推理如果在0.2以上说明是近景大目标640完全够用。图像尺寸分布也不能忽略如果数据集里混着1920x1080和640x480两种分辨率训练时letterbox会把小图拉大模型在两种分辨率上学到的特征尺度会不一致。类别分布这一项更要盯死一个健康的跌倒检测数据集跌倒这一类至少占全部目标的20%到30%。如果4500张图里只有300张含跌倒标注那后面必须做针对性的过采样和数据增强否则训练出来的模型大概率只会识别站立的人跌倒的人会被当成背景放过。顺便看一眼类别名很多数据集里跌倒类叫fall有的叫fallen还有的用中文跌倒等会儿写映射表的时候这些名字全都要处理。2.3 划分训练集与验证集按场景分别按文件随机分这是整个流程里最要命的一个决定很多翻车事故都出在这一步。跌倒检测数据集的来源通常是监控视频抽帧同一个摄像头同一段连续视频抽出来的帧相邻两帧的画面几乎一模一样。这时候如果用随机划分比如直接按文件列表随机切20%当验证集验证集里会有大量与训练集高度重复的画面。训练过程中val mAP能到0.9以上看起来非常漂亮可一旦把模型部署到一个没见过的摄像头画面里精度立刻崩掉。这不是模型玄学是数据泄漏。正确的做法是按视频源或场景分组再做组级别划分。假设文件名里有场景前缀比如scene01_00001.jpg这种脚本这样写import os import random from collections import defaultdict ann_files [f for f in os.listdir(Annotations) if f.endswith(.xml)] scene_map defaultdict(list) for f in ann_files: scene f.split(_)[0] # 取 scene01 作为分组键 scene_map[scene].append(f) scenes list(scene_map.keys()) random.shuffle(scenes) # 只打乱场景不打乱单张图 val_count max(1, int(len(scenes) * 0.2)) val_scenes set(scenes[:val_count]) train_txt open(train.txt, w) val_txt open(val.txt, w) for scene, files in scene_map.items(): for f in files: img_path os.path.join(JPEGImages, f.replace(.xml, .jpg)) if scene in val_scenes: val_txt.write(img_path \n) else: train_txt.write(img_path \n) train_txt.close() val_txt.close()如果文件名看不出分组信息就回头找视频源文件同一个视频抽出来的帧归到一组。哪怕分组粗糙一点也比纯随机切强。验证集的意义是模拟没见过的场景只有按场景隔离才能接近部署时的真实情况。顺手把每组的帧数打出来如果某个场景贡献了超过30%的帧最好限制它的占比防止模型被单一环境带偏。提示按场景划分后别急着开训。把验证集图像里随机抽几张和训练集做一次粗略的感知哈希对比确认没有重复帧混入。这一步十秒钟就能跑完能省掉后面几天排查指标虚高的时间。3. 把标注转成YOLO能吃的格式VOC/COCO到txt的转换脚本3.1 最小转换脚本XML到YOLO txtYOLOv8的训练管线默认读取txt标注每张图对应一个同名txt文件每行一个目标。如果你手里是VOC XML需要先转。转换的核心就一句话把框从左上角右下角绝对坐标变成归一化中心点坐标加宽高。下面这个脚本我一直在用去掉注释也就三十几行import os import xml.etree.ElementTree as ET # 类别名到id的映射顺序必须和后面的数据集YAML保持一致 CLASS_MAP {fall: 0, person: 1} def voc_to_yolo(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.iter(object): name obj.find(name).text 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) # 坐标越界防御标注工具偶尔会给出超出图像范围的框 x1 max(0, min(x1, img_w)) x2 max(0, min(x2, img_w)) y1 max(0, min(y1, img_h)) y2 max(0, min(y2, img_h)) if x2 - x1 2 or y2 - y1 2: continue # 过滤掉宽度或高度小于2像素的废框 cx (x1 x2) / 2.0 / img_w cy (y1 y2) / 2.0 / img_h w (x2 - x1) / img_w h (y2 - y1) / img_h lines.append(f{CLASS_MAP[name]} {cx:.6f} {cy:.6f} {w:.6f} {h:.6f}) with open(out_txt_path, w) as f: f.write(\n.join(lines)) for f in os.listdir(Annotations): if f.endswith(.xml): voc_to_yolo(os.path.join(Annotations, f), os.path.join(labels, f.replace(.xml, .txt)))代码里有两处不能省。第一处是坐标越界防御。LabelImg手标时偶尔会把框拖出图像边缘导致xmax大于img_w或者ymin小于0。不裁剪的话归一化后的w、h可能大于1YOLO训练时要么报错要么产生诡异的锚框匹配。第二处是小于2像素的过滤。这种框在跌倒检测里几乎都是误标或极小目标留着只会增加噪声清零反而干净。类别映射也要跟后面的数据集YAML对齐。CLASS_MAP里的顺序决定模型输出的类别id比如CLASS_MAP是{fall: 0, person: 1}YAML里就得写names: [fall, person]。顺序一错模型学出来的类别和实际含义就错位了这种错往往要到部署阶段才暴露排查起来很费劲。转完后务必检查一下labels目录下的txt数量和Annotations里的XML数量一致才算正常少一个都要回头找原因。3.2 转换时最容易改错的坐标与类别映射转换脚本出错一般集中在这几个地方我按踩过的频率排一下。第一COCO的bbox是[x, y, width, height]不是VOC的[x1, y1, x2, y2]。如果数据集交付的是COCO JSON转YOLO时先自己算x2 x widthy2 y height。很多人直接把拿到的x、y、width、height当成x1、y1、x2、y2填进去出来的框全往左上角缩mAP直接烂掉。第二多边形标注转外接矩形时别把points里的点顺序搞错。LabelMe的多边形可能几十个点取x_min、x_max、y_min、y_max包围盒即可最暴力也最不容易错的解法就是取min和max不要试图做旋转框。第三空标注文件不能省。一张图上确实没有跌倒的人、也没有别的目标时也要生成一个空txt。YOLO训练时需要每个标注文件都存在缺失会导致数据加载对不上。检查方法是遍历图像目录确保每张jpg都能在labels目录找到同名txt哪怕内容是空的。第四数据集YAML里的nc和names要跟转换脚本对齐。nc等于CLASS_MAP的长度names的每个字符串在CLASS_MAP里必须存在。这一步错了训练命令能跑但类别语义全乱。如果源格式是Label Studio导出的COCO转换前先确认它的categories里fall和person的id是不是从0开始。Label Studio默认从1开始编号COCO官方格式也习惯从1起但YOLO要求从0开始。统一在下游映射表里做偏移不要在转换脚本里hardcode两套编号不然第二次交付数据时容易翻车。转完之后强烈建议做一次可视化把YOLO txt读回来在图像上画框存成新图肉眼抽查20张。这一步能发现所有坐标换算错误比看数字靠谱得多。画框校验的代码很简短OpenCV的rectangle就能干。4. 用YOLOv8把4500张跌倒检测数据跑起来最小训练命令与关键参数4.1 数据配置YAML怎么写YOLOv8的数据配置是一个YAML文件字段很少但每个都影响训练能否启动。以我的目录习惯为例从VOC转好的标注放进labels/图像放在images/下再按train/val分好目录。数据集的YAML长这样# fall_dataset.yaml path: /data/fall_dataset # 数据集根目录写绝对路径 train: images/train # 相对 path 的训练图像目录 val: images/val # 相对 path 的验证图像目录 nc: 2 # 类别数 names: 0: fall 1: personpath字段我建议用绝对路径。相对路径在训练脚本和验证脚本的工作目录不一致时会各种找不到文件报错信息还不好懂。train和val写相对path的路径即可YOLO会自动拼接。names的索引顺序必须和转换脚本里的CLASS_MAP一致前面强调过这里再重复一遍因为这个错是黑匣子级别的训练时Loss能正常下降但模型输出的类别语义完全对不上评估的时候才会发现混淆矩阵一片乱。4.2 最小训练命令与五个必调参数数据集4500张左右建议从YOLOv8s起步而不是直接上m或l。小数据集上大模型容易过拟合训练时间也成倍增加。训练命令yolo detect train \ datafall_dataset.yaml \ modelyolov8s.pt \ epochs100 \ imgsz640 \ batch16 \ patience15 \ projectruns/fall \ nameexp1 \ device0五个必调参数逐个说。epochs设100。4500张图不算多100轮足够模型收敛。如果看到val mAP在40轮以后还在明显上升可以手动加到150轮但正常情况100轮配合patience早停是性价比最高的组合。imgsz设640。跌倒检测的目标是人人在监控画面里通常属于中大型目标640的输入尺寸能保持足够的细节训练速度也快。如果2.2节的体检报告显示框面积占比中位数低于0.05也就是人在画面里很小就要考虑imgsz960。加了之后显存占用大概是640的2.25倍8G显卡别硬撑。batch设16。这个值受显存约束YOLOv8s在640分辨率下batch16大概需要6到8G显存。如果CUDA out of memory降到8也行如果显存富余batch32会让训练更稳收敛曲线更平滑。patience设15。意思是验证集mAP连续15轮不提升就提前结束训练。这是后悔药设了它能避免你离开电脑后训练白白空跑几十轮。注意它配合epochs100使用实际训练很可能在60到80轮就停了这是好事。device0表示用第一张显卡训练。机器没有GPU又非要在本地练可以把device换成cpu但4500张图100轮CPU训练时间会长到没脾气建议还是找一台带N卡的机器。第一次跑之前确认一下yolo命令存在没装的话用pip install ultralytics装一下命令行工具就带上了。4.3 看验证结果mAP、PR曲线与混淆矩阵怎么读训练结束YOLO会在runs/fall/exp1/里生成weights/best.pt和weights/last.pt。先用best.pt跑一遍官方验证命令yolo detect val \ modelruns/fall/exp1/weights/best.pt \ datafall_dataset.yaml \ imgsz640输出里先看mAP50和mAP50-95两个指标。mAP50是IoU阈值取0.5时的平均精度跌倒检测这种框差一点没关系、关键是把人找出来的任务mAP50是最该盯的门槛指标。mAP50-95对框的位置精度更敏感跌倒检测场景里不需要追求它特别高除非你的下游要做轨迹分析或距离测量。4500张健康分布的数据集YOLOv8s练出来的mAP50至少在0.85附近才算正常低于0.7就要回头查数据。然后打开训练输出目录里的PR_curve.png和confusion_matrix.png。PR曲线里重点看fall这一类的曲线它在precision滑落到0.9时对应的recall能到多少。如果recall上到0.8之前precision就已经掉到0.7以下说明模型存在大量误报部署后会把蹲下捡东西、弯腰系鞋带都当成跌倒报警。混淆矩阵则直接告诉你fall被误判成什么如果fall和person之间有明显的错分说明这两类在视觉形态上重叠严重需要回头检查标注规范或者考虑把跌倒这个类别拆成正在倒下和已经倒地两种状态。训练过程的指标可以看runs/fall/exp1/results.csv里面有每一轮的train_loss、val_loss、mAP50用Excel或者pandas拉成曲线。如果你发现val mAP在某个轮次后突然暴跌大概率是学习率太高或者训练集里混入了脏标注先别调参回去看数据。5. 跌倒检测数据集训练的5个典型踩坑与排查清单5.1 跌倒样本占比过低损失函数被正常姿态带偏现象训练完模型对站立的人检测得很好mAP50看着不低但单独统计fall类recall只有40%上下。原因4500张图里可能只有300到500张含跌倒目标其余全是正常行走、站立、坐姿的人。YOLO的损失是逐目标累加的正常类目标数量碾压跌倒类梯度被大头带偏模型学成了一个偏科的检测器。解决先过采样把含fall的图片在每轮训练里多重复几次最简单的做法是把这些图复制几个副本注意副本要一起进训练集别一把复制完混进验证集。配合mosaic增强把跌倒的人和正常场景拼在一起让模型在同一张图里同时看到正反例。如果还不行给fall类单独加loss权重YOLOv8里可以用数据集的class_weight参数。我一般先做过采样效果不够再上权重上来就调权重容易掩盖数据本身的问题。5.2 标注框标准不统一框贴太紧或松一大圈都出问题现象mAP50能到0.86但在实际画面里框会抖同一帧里同一个跌倒的人框忽大忽小。原因数据集里跌倒的框有的只包住躯干有的把四肢全部展开的轮廓也包进去还有的把地面背景框了一截进去。训练时模型学到的框尺寸方差大NMS阶段互相抑制导致输出不稳定。这个坑在多人标注协作时特别常见三个人标出三套标准。解决重新约定标注规范并在转换后抽查。跌倒的人统一用外接矩形包住全身包括伸出的手臂和腿站立的人从头顶到脚底。执行层面写一个检查脚本把所有框的宽高比分布打出来。如果同一类别的宽高比出现好几个断层几乎可以断定标注标准没对齐。宽高比在0.8到1.2的区间内集中是正常现象如果既有0.3又有2.5说明有人只框了躯干有人框了全身需要手工挑出来重标。5.3 同一视频的连续帧泄漏进验证集指标虚高、部署翻车现象训练时val mAP50高达0.92换一个摄像头测试直接掉到0.5。原因随机划分导致验证集里全是训练集出现过的画面的近亲。监控视频抽帧的相邻帧背景完全一样模型哪怕没学会检测跌倒也能靠记住场景特征蒙对。验证集失去意义指标全线失真。解决严格按视频文件或场景ID划分保证同一段监控视频的帧全部在训练集或全部在验证集。排查时用感知哈希给图像算指纹把特征相似度超过阈值的图像列为嫌疑重复对看它们是不是跨集合出现。验证集还有个附带作用测试模型对新环境的泛化能力。如果验证场景和训练场景几乎一模一样这个验证集就名存实亡。宁可减少验证集数量也要保证场景隔离。5.4 跌倒瞬间的模糊帧模型在关键帧上反而最没把握现象人在跌倒过程中速度最快、画面最模糊的几帧模型要么漏检要么给出很低的置信度。原因跌倒检测最该报警的瞬间恰好是运动模糊和肢体遮挡最严重的瞬间。数据集里这类模糊帧如果太少或者干脆被清洗掉了模型就没见过这种输入分布。很多清洗流程把模糊帧当成脏数据删掉这在跌倒检测里是帮倒忙。解决训练时打开mosaic和随机模糊增强用OpenCV对图像做高斯模糊后混入训练批次。另外不要急着把模糊帧从数据里删掉把它们归为fall类保留让模型学会在模糊条件下也输出检测结果。报警逻辑可以稍微放宽置信度阈值到0.35再用时序确认过滤误报。模糊帧在验证集里的表现单独统计如果recall明显低于正常帧说明模型还没学会处理这个输入分布。5.5 单帧检测的先天不足把正在跌倒和已经倒地混为一谈现象模型能把倒地的人检出来也会把蹲下系鞋带、弯腰捡东西的人误报成跌倒。原因单帧图片本身没有时间维度跌倒是一个过程而倒地是一个状态。静态画面里蹲下和倒地终归有相似之处单帧模型只能猜猜错是正常的。这不是模型训练得不好是任务定义本身的边界。解决不要指望单纯靠模型解决一切。这一步有两条路要么给数据打上更细的状态标签把fall拆成falling和fallen两个类让模型做状态分类要么在模型后面加一个基于连续帧的时序判定逻辑。前者要重新标注一遍数据成本高后者轻量、见效快下一章就把这个做法展开。6. 从单帧检测到时序判断一个低成本的跌倒后静止判定技巧6.1 判定逻辑与阈值怎么定单帧模型是黑匣子但你可以在它外面套一层规则把过程和状态分开。跌倒动作有非常强的时序特征快速倒地然后静止不动。常见做法是在检测框输出后加一个轻量跟踪和状态机。用ByteTrack或者简单的IoU匹配都能做到核心判断逻辑是这样的prev_box None # 上一帧的人框 still_count 0 # 静止计数 def on_detect(box, fps10): global prev_box, still_count if prev_box is None: prev_box box return False # 中心点位移量 dx abs(box.cx - prev_box.cx) dy abs(box.cy - prev_box.cy) speed (dx dy) * fps # 像素/秒 hw_ratio box.h / box.w # 高宽比站立时1横躺时1 if speed 15 and hw_ratio 0.9: still_count 1 else: still_count 0 prev_box box return still_count int(fps * 1.5) # 连续1.5秒静止并横躺参数按场景调整speed阈值15像素每秒是按1080p、10fps估的画面里人如果很小就按比例放大hw_ratio取0.9意思是框的高度明显小于宽度代表人体水平展开。这个逻辑能挡住大部分误报比如蹲下系鞋带的人虽然也矮但不会在高速位移后突然长时间静止。多人场景要按跟踪ID维护各自的prev_box和still_count不能共用一个状态。我自己的教训是一开始阈值设太严静止3秒才报警导致真跌倒的老人躺了很久才触发后来改成1.5秒误报增加不多漏报少了很多。这种后处理逻辑不依赖额外模型适合单帧检测已经跑通、想快速提升实用性的阶段。先把模型调到一个可信的基线再往上叠时序规则别一上来就做复杂系统。希望帮到你。本文还有配套的精品资源点击获取
网站建设高端定制企业官网