我要提问
ARTICLE DETAIL

资讯详情

前沿编程新知与开发实战干货的深度解读。

基于YOLOv5与OpenPose的摔倒检测系统:双阶段姿态估计实战解析

基于YOLOv5与OpenPose的摔倒检测系统:双阶段姿态估计实战解析 简介这是一份基于YOLOv5与OpenPose的摔倒检测完整项目面向具备一定深度学习基础、希望快速入手人体姿态识别与异常行为检测的开发者。项目中YOLO负责行人定位OpenPose负责关键点提取两者结合后可对跌倒动作进行判断。运行runOpenpose.py能生成并保存人体关键点图关键点图会存放到指定目录作为后续训练动作识别模型的数据集detect.py则在YOLO检测到人后先依据目标框宽高比做初步筛选再裁剪人体区域送入姿态检测流程整体思路清晰、便于二次开发。若想检测其他姿势可按流程收集图片、生成关键点图、分类存入data/train与data/test再训练自己的分类模型。压缩包共183个文件约40.24MB主要包含75张jpg样例、39个Python脚本、17个YAML配置、2个JIT模型、2个PT权重及XML标注等覆盖数据准备、模型推理、动作分类与配置调节等环节。目前已有1063人学习下载适合用于跌倒监测实验、安全监控原型验证以及姿态识别相关课题的拓展研究。1. 摔倒检测的双阶段架构yolov5 人体框 openpose 关键点为什么分开跑一个人摔倒在地和一个人正常弯腰捡东西单看某一帧画面连人眼都可能误判。如果只用 yolov5 框人体只能知道「有人」不知道「人的姿态」只用 openpose 做姿态估计在多人场景下会漏检而且对画面分辨率要求高。这个项目把两者串起来yolov5 负责快速找到人并裁剪出人体区域openpose 对裁剪结果做姿态估计输出关键点数据再由关键点的坐标关系判断是否摔倒。三个脚本对应三条链路——runOpenpose.py 单独出关键点图、detect.py 走完整检测、train.py 训练自定义动作分类模型。适合正在做智慧养老、工地安全这类落地场景的开发者也适合想快速把姿态检测跑通再二次开发的从业者。2. 先跑 openposerunOpenpose.py 收集关键点数据喂给后续模型2.1 工程结构先摸清三个 .py、两个 .jit、data 目录各管什么拿到压缩包先别急着跑先把文件角色分清楚。labels.cache 是 yolov5 训练过程中的标签缓存文件Dockerfile 说明这个项目可以用 Docker 构建运行环境yolov5.iml 是 PyCharm 的项目文件可以忽略。核心是三个 Python 脚本和一串图片样本。openpose.jit 是 torch.jit 序列化后的 openpose 模型负责从单张图片提取人体关键点action.jit 是动作分类模型负责把关键点序列分类成具体动作。runOpenpose.py 调的是 openpose.jitdetect.py 先调 yolov5 找人体框、再调 openpose.jit 做姿态检测。图片文件是姿态检测的样本素材用于验证效果。data 目录下会用到 train 和 test 两个子目录分别存放用于训练和验证的关键点图。data/test 是 runOpenpose.py 默认的关键点输出位置。需要留意的是 openpose.jit 和 action.jit 的差别前者输出的是人体骨骼关键点坐标后者输出的是动作类别。整个项目里openpose.jit 是基础后面训练自定义动作分类模型时openpose.jit 负责把原始图片转成关键点图再交给 action 模型。2.2 runOpenpose.py 跑通先出关键点图别急着调 detect.py从摘要描述看作者建议先运行 runOpenpose.py只跑 openpose 获取人体关键点图用于后续 .jit 模型训练。这个思路是对的——先把特征提取链路单独验证通再串完整检测流程排错时会省很多时间。# runOpenpose.py 核心流程简化示意实际以项目内代码为准 import torch from pose import draw # 项目内的绘图模块 model torch.jit.load(openpose.jit) # 加载序列化后的 openpose 模型 model.eval() img load_image(test.jpg) # 读取一张测试图 keypoints model(img) # 前向推理得到关键点坐标 save_path data/test draw(img, keypoints, save_path) # 将关键点绘制在原图上并保存 print(f关键点图已保存到 {save_path})这里 torch.jit.load 加载的是 TorchScript 模型好处是不需要保持和训练时完全一致的 Python 环境部署时依赖更少。实际跑之前先确认 PyTorch 版本和模型序列化时的版本兼容性常见做法是直接用 CPU 跑通一遍确认模型输出维度正常后再上 CUDA。关键点图的保存路径在 pose.py 的 draw 方法最下面控制想改目录就直接改那里的输出路径参数。为什么要先跑这一步而不是直接跑 detect.py因为 detect.py 是两段检测串联如果最终结果不对你分不清是 yolov5 没检测到人、还是 openpose 姿态估计失败。先把 openpose 单链路验证通过相当于先确认后半段没坑。2.3 pose.py 的 draw 方法与输出路径控制runOpenpose.py 能出图依赖的是 pose.py 里的 draw 方法。这个方法负责两件事把 openpose 输出的 18 个或 25 个关键点画到原图上然后按指定路径保存。如果想批量处理一张图一个人可以但如果是多人场景且没有裁剪openpose 会为每个人输出一组关键点这时候保存逻辑要处理多组数据。# pose.py 中 draw 方法的关键参数说明 def draw(img, keypoints, save_path): for person_id, kpts in enumerate(keypoints): for x, y, conf in kpts: if conf 0.3: # 置信度阈值低于该值的关键点不绘制 cv2.circle(img, (int(x), int(y)), 3, (0, 0, 255), -1) # 保存路径在函数最下方改动这里即可更换输出目录 cv2.imwrite(f{save_path}/keypoint_{person_id}.jpg, img)置信度阈值 0.3 是一个经验值可以根据实际画面质量上下浮动。画面清晰、光照均匀时可以调到 0.5 过滤掉低质量关键点画质差时阈值要降否则大量关键点被过滤后续动作分类会缺少输入特征。保存路径改动只需要修改 cv2.imwrite 的第一个参数。跑完这一步data/test 目录下会积累一批带骨骼关键点的标注图这些图就是后续训练动作分类模型的原始素材。收集够用之后手动把人体的关键点图按你想要的类别进行区分放进 data/train 和 data/test然后跑 action_detect/train.py 就能训练出针对特定动作的 .jit 模型。3. yolov5 做人体检测detect.py 里如何框出人并传给 openpose3.1 完整链路yolo 找人宽高比过滤再裁剪给 openposerunOpenpose.py 直接对整张图做姿态估计问题是当画面里有多个人、或者人在远处只占很小面积时openpose 容易丢关键点。detect.py 的思路是先用 yolov5 检测出所有人再把每个人的区域裁剪出来单独送进 openpose这样每个被剪出来的人都是画面主体姿态估计准确率高很多。这是典型的级联式双阶段检测第一阶段的检测结果直接决定第二阶段的输入质量。如果 yolov5 漏检一个人openpose 根本看不到这个人如果 yolov5 的框太松、框住大片背景openpose 的关键点也会被干扰。所以第一阶段的质量直接影响全过程。3.2 detect.py 第 169 行的宽高比判断调参逻辑与适用边界输入描述里明确提到detect.py 的 169 行加了根据框的宽高比判断后续可自行修改。这一行在做什么对行人检测来说正常站立或行走的人检测框通常是高大于宽宽高比w/h大约在 0.3 到 0.5 之间而一个人摔倒后平躺或侧躺检测框变成宽大于高宽高比会大于 1。用宽高比做一次粗过滤可以快速筛掉「不像是站着的目标」减少后续送入 openpose 的计算量。# detect.py 第 169 行附近的宽高比判断逻辑示意写法 for det in detections: x1, y1, x2, y2 det[:4] w, h x2 - x1, y2 - y1 ratio w / h # 关键判断宽高比阈值 # 小于 0.4 意味着高显著大于宽符合站姿特征 # 大于 0.8 则框趋于横向疑似摔倒或弯腰 if ratio 0.4: continue # 跳过站姿目标不送入 openpose elif ratio 0.8: person_img crop_and_resize(img, (x1, y1, x2, y2)) keypoints openpose(person_img) # 只对有嫌疑的目标做姿态估计阈值 0.4 和 0.8 不是绝对标准要根据场景调。摄像头俯视时一个正常站立的人框宽高比会和仰视时差别很大。常见做法是先采集一段目标场景的监控视频统计正常行走和摔倒两种情况下检测框宽高比的分布再取两类分布的分界值设置阈值。如果场景里经常有人弯腰工作弯腰时宽高比也会变大光靠宽高比区分弯腰和摔倒不够需要结合后面的关键点数据。这个预过滤的价值在于降载openpose 是对每个裁剪区域做推理如果每帧都有七八个人全部送进 openpose 会拖慢帧率。先用宽高比筛掉明显站立的人只有少数「姿态异常」的目标才做完整姿态估计。3.3 裁剪与衔接把人的图片扣出来给 openposeyolov5 的输出是每个人的边界框坐标不是图像块。要送给 openpose得先把框内的像素裁剪出来做缩放、归一化再转成 openpose 的输入张量。这一步的代码在 detect.py 中衔接 yolov5 和 openpose 两个模型的部分。# 裁剪与预处理示意写法 def crop_and_resize(frame, box, target_size(368, 368)): x1, y1, x2, y2 [int(v) for v in box] # 边界裁剪防止越界 x1 max(0, x1); y1 max(0, y1) x2 min(frame.shape[1], x2); y2 min(frame.shape[0], y2) person frame[y1:y2, x1:x2] # openpose 输入一般要求 368x368但实际以模型要求为准 person cv2.resize(person, target_size) person person.astype(np.float32) / 255.0 tensor torch.from_numpy(person).permute(2, 0, 1).unsqueeze(0) return tensortarget_size 设为 368x368 是 openpose 的经典输入尺寸但 .jit 序列化模型可能内置了预处理逻辑实际输入尺寸要看模型的 forward 方法定义。如果输入尺寸不对模型不会报错但输出的关键点坐标会整体偏移因为坐标是按输入分辨率归一化的。坐标映射回去的时候要注意缩放比例原图坐标 关键点坐标 / 368 × 裁剪区域宽高 裁剪区域左上角坐标。这一节的调试有一个通用做法把裁剪后送进 openpose 的图像单独保存到磁盘和最终关键点绘制结果放在一起对比。如果裁剪图正常但关键点乱飘问题在 openpose 输入预处理如果裁剪图本身就有大量背景干扰问题在 yolov5 的检测框太松。4. 摔倒检测的判定逻辑从关键点到动作分类阈值不能拍脑袋4.1 关键点坐标到摔倒判断y 轴相对关系是第一信号openpose 输出的人体关键点包含鼻子、颈部、左右肩、左右肘、左右腕、左右髋、左右膝、左右踝等位置每个关键点有 x、y 坐标和置信度。摔倒检测最直接的特征是正常站立时颈部、髋部、膝部、踝部的 y 坐标依次递增且整条身体线接近垂直摔倒后这条线的 y 坐标差急剧缩小身体主轴线接近水平。# 摔倒判定核心逻辑示意写法 def is_fall(keypoints, frame_height): neck keypoints[1] # 颈部 hip keypoints[8] # 左髋或右髋按模型输出定义 ankle keypoints[10] # 左踝 # 颈部到踝部的垂直距离与水平距离 dy abs(neck[1] - ankle[1]) dx abs(neck[0] - ankle[0]) # 身体主轴线与水平方向的夹角 angle math.atan2(dy, dx) * 180 / math.pi # 角度小于 30 度身体接近水平判定为摔倒 # 同时要求关键点置信度达到阈值防止误判 if angle 30 and neck[2] 0.3 and ankle[2] 0.3: return True return False角度阈值取 30 度是一个保守值宁可漏判也不误报。实际项目里30 到 45 度都需要根据摄像头安装高度和俯仰角调整。摄像头装在高处俯视时一个人正常站立时身体主轴线在画面里也接近 40 度倾角这时阈值就得提高摄像头平视时站立和摔倒的角度差异非常明显阈值可以收紧。这里有一个很容易翻车的问题直接用原始关键点坐标做判断不区分画面中人的身高和距离。同一套角度阈值近处的人和远处的人在同一摔倒姿态下画面里的夹角会差很多。常见做法是先对关键点做归一化——以颈部为原点把其他关键点的坐标除以颈部到髋部的距离再做角度判断。这个归一化过程本质上是把人从「像素坐标空间」映射到「姿态空间」之后再设阈值才有跨距离的通用性。4.2 时序维度单帧误判太多连续多帧投票是刚需单帧角度小于 30 度就报摔倒误报率会很高。一个人正常弯腰系鞋带、蹲下捡东西单帧角度都可能小于 30 度。但摔倒和弯腰蹲下有一个本质区别摔倒后短时间内姿态不会恢复到直立。所以落地时不能只看单帧要看连续 N 帧的判定结果。# 连续帧投票示意写法 FRAME_THRESHOLD 5 # 连续 5 帧判定为摔倒才报警 fall_votes 0 while True: frame get_frame() keypoints detect_person_and_pose(frame) if is_fall(keypoints, frame.shape[0]): fall_votes 1 if fall_votes FRAME_THRESHOLD: trigger_alarm() else: fall_votes 0 # 一旦出现非摔倒帧清零重新计数FRAME_THRESHOLD 取 5 到 10 帧比较合适具体看帧率。30fps 下 5 帧约等于 0.17 秒正常人摔倒从开始到完全倒地也就 0.5 到 1 秒0.17 秒的确认延迟完全可接受。但如果是慢动作倒下、或者老人倒地后还有轻微挣扎帧数阈值可以放宽到 10 到 15 帧。这个参数没有绝对最优只能在真实场景里跑一段录像统计误报率和漏报率的平衡点。另外关键点置信度要参与判定。摔倒瞬间人体部分被遮挡时openpose 会输出低置信度的关键点这些点的坐标基本是模型猜的。我一般在角度判断前加一道过滤参与计算的关键点置信度全部大于 0.4 才进入判断否则跳过这一帧。这个做法能有效减少「黑影被误认成人」的假阳性。4.3 误检率控制把「角度 宽高比 置信度」三个维度叠起来单一特征判摔倒任何阈值都会在某个场景翻车。宽高比分辨不出弯腰和摔倒角度分辨不出躺地上休息和摔倒置信度分辨不出真实遮挡和模型失效。把三个维度叠起来能大幅压低误报。假设逻辑是yolov5 检测框宽高比大于 0.8横向框→ openpose 关键点计算身体主轴角度小于 30 度 → 参与计算的关键点置信度均高于 0.4 → 连续 5 帧满足以上三者 → 触发摔倒报警。三个阶段逐级过滤前面阶段不满足就提前结束不进后续阶段。这也是这个项目把 detect.py 的宽高比判断放在 169 行、把 runOpenpose.py 的限制放在 159 行的原因——每一层过滤都有独立调参入口。5. 常见问题与避坑从 jit 模型加载到关键点乱飘5.1 现象torch.jit.load 加载 openpose.jit 报错加载 .jit 模型时提示Unsupported operator或RuntimeError通常发生在 PyTorch 版本不一致时。TorchScript 模型虽然是序列化格式但算子注册表和版本强相关旧版本序列化的模型用新版本加载时会遇到算子不兼容。解决优先使用项目要求的 PyTorch 版本来加载不要用最新版本。我一般先看 Dockerfile 里 FROM 的 PyTorch 基础镜像版本直接按那个版本建 conda 环境。如果实在找不到原版本用torch.jit.load时加上map_locationcpu参数先排除设备问题再逐个排查算子报错。5.2 现象关键点图输出正常但坐标整体偏移在检测框内画关键点点的位置和真实关节对不上普遍原因有两个。一是裁剪后的图像在送入 openpose 前做了 resize但关键点坐标没有按 resize 比例映射回原图。二是 openpose 的输入尺寸和模型内置的预处理尺寸不一致。解决先确认 openpose.jit 的期望输入尺寸——打印模型的 forward 方法或者检查输入张量的 shape。按照这个尺寸做裁剪图的预处理而不是默认 368x368。关键点回映射时用输入尺寸做缩放比例计算。一个验证技巧把 openpose 输出的关键点画在裁剪图上先看是否正确再画回原图分步定位问题出在哪一步。5.3 现象yolov5 检测到人但 openpose 没有关键点输出输入描述里提到 runOpenpose.py 的 159 行加了限制这个限制很可能就是过滤掉小尺寸目标或低置信度目标。如果裁剪出来的人太小关键点置信度全局偏低就会被过滤掉。解决打开 runOpenpose.py 第 159 行附近检查过滤条件。典型条件是目标框面积小于某像素值或关键点平均置信度低于阈值。调低这两个值代价是更多目标进入姿态估计流程帧率下降。如果是在室内固定摄像头场景建议保留过滤条件只调低阈值如果画面里人经常走远建议直接取消过滤靠后续角度判断兜底。5.4 现象程序能跑但 CPU 推理每帧要好几秒这个不是 bug是资源瓶颈。openpose 本身是计算密集型模型CPU 推理一张 368x368 的图要 1 到 3 秒配合 yolov5 串联帧率很难看。解决模型加载后调用model.cuda()移到 GPU 上推理前后把输入输出张量显式移动设备。如果只有 CPU控制送入 openpose 的帧数——每 5 帧做一次姿态检测中间帧直接用上一帧的结果。还可以在宽高比过滤阶段调高阈值让更少目标进入 openpose。这个项目的两个 .jit 模型明显是为了部署准备的但部署推理性能优化还是得靠设备。5.5 现象多人场景下关键点互相串一帧里站了两个人openpose 输出的关键点组数和检测框数量对不上甚至关键点坐标混在一起。解决openpose 对多人场景用 part affinity fields 做人-关键点匹配这个匹配在遮挡严重时本来就会出错。项目给的方案是把 yolov5 检测框裁剪成单人区域只让 openpose 处理单人从源头避开多人匹配。如果裁剪后依然出错检查裁剪区域是否包含了旁边人的肢体——把检测框向内收缩 10% 到 20% 再裁剪能减少邻人干扰。6. 自定义动作分类与部署验证把摔倒检测扩展成通用姿态识别6.1 runOpenpose.py 第 159 行的限制调整什么时候改、怎么改这个限制存在的意义是提高关键点质量——只保留置信度高、骨骼完整的目标筛掉模糊的、残缺的、过小的目标。但限制条件是死的场景是活的。摄像头距离远、画面里人占比小默认限制会筛掉几乎所有目标导致下游没有数据可用。调整思路是先打印日志看哪些目标被过滤了、各自的置信度和尺寸是多少再按实际分布调阈值。比如限定目标宽度大于 100 像素就改成 60 关键点平均置信度要求大于 0.5就改成 0.3。每次只调一个参数跑一段视频确认效果参数要记录方便回溯。6.2 训练自己的动作分类模型从收集数据到产出 action.jit这个项目最有价值的部分是作者给了完整的路径收集图片 → runOpenpose.py 生成关键点图 → 分类放入 data/train 和 data/test → 跑 train.py 训练。这意味着你是可以用这个资源训练自定义动作的 — 摔倒只是默认分类举手机、抬手、踢腿等都可以学。# action_detect/train.py 训练流程示意写法 # 1. 读取 data/train 目录下的关键点图按子目录名作为类别标签 # 2. 可以用 ResNet 等分类网络微调也可以直接用全连接 多层感知机 # 3. 训练完成后转成 TorchScript 保存为 action.jit from torchvision.models import resnet18 model resnet18(pretrainedTrue) model.fc nn.Linear(512, num_classes) # num_classes 是动作类别数 criterion nn.CrossEntropyLoss() optimizer torch.optim.Adam(model.parameters(), lr1e-4) for epoch in range(50): for inputs, labels in train_loader: outputs model(inputs) loss criterion(outputs, labels) optimizer.zero_grad() loss.backward() optimizer.step() # 转 TorchScript 保存 script_model torch.jit.script(model) script_model.save(action.jit)训练关键点在分类还是回归如果把摔倒检测做成分类输入是关键点图类别是「正常 / 摔倒」甚至更多动作如果做回归输入是关键点序列输出是摔倒概率。项目给的做法是前者——先生成关键点图再做图像分类。这个路径的优点是复用 openpose 特征提取能力不用自己设计关键点特征工程缺点是分类结果依赖 openpose 绘制的关键点图质量openpose 输出的点位抖动会影响分类准确性。6.3 部署验证用一段真实监控视频做回归测试不要只测单张图片我跑这类项目有个习惯最后一定会做一次「时间维度回归」录一段 2 到 3 分钟的真实场景视频里面包含正常行走、短暂弯腰、真的摔倒、部分遮挡等片段标注好每一段的时间戳然后跑完整检测链路对比检测结果和时间戳的匹配度。验证标准一般是三个数漏报率真实摔倒没识别出来、误报率正常动作被识别成摔倒、响应延迟从摔倒开始到触发报警的秒数。这三个数也和前面每章的阈值直接对应——宽高比阈值影响漏报率角度阈值和置信度阈值影响误报率连续帧投票数影响响应延迟。改任何参数都要重跑回归视频不能只看单帧效果。从那以后我每次碰这类系统都强制走一遍回归视频流程参数改没改、改了多少一跑便知。希望帮到你。本文还有配套的精品资源点击获取
返回列表