OpenPose人体姿态检测与摔倒识别:从PAF原理到工程实践 简介基于深度学习OpenPose实现的人体姿态检测项目源码包面向老年人行为监护场景可识别站、坐、躺及摔倒等动作状态是一套适合科研实验与二次开发的完整方案。资源内含项目介绍、数据集、训练/推理模型以及 requirements.txt 环境依赖清单并提供了两套可直接运行的演示代码demo_my.py 按线性流程依次完成算法模块计算输出包含人体框、关键点信息和当前状态标签的 JSON 文件webcam_demo_my.py 则基于队列方式组织各算法模块便于接入实时摄像头画面进行姿态检测。全包共580个文件以jpg图片样本382个、py源码78个、cpp/h扩展模块、cfg配置以及md/txt说明文档为主整体约75.5MB目录划分清楚按需取用即可。目前已有1316人学习适合需要快速上手OpenPose姿态估计、开展老年跌倒检测实验或在此基础上做算法改进的研究人员与开发者。1. 老年人监护场景下的OpenPose姿态检测到底解决什么问题一个常年独居的老人摔倒后如果一小时内得不到救助后续恢复的难度会成倍增加。全屋摄像头能看清画面但老人家会介意亲属也担心隐私手环、吊坠这类可穿戴方案又依赖老人时刻记得佩戴。基于深度学习openpose实现人体姿态检测恰好是一种折中的技术路线它在前端不保留彩色视频推理后只剩骨骼关键点坐标行为判断全部在关键点序列上完成。“老年人行为监护站、坐、躺以及摔倒”这个标题本质上就是一条流水线输入普通监控帧用OpenPose提取关键点再根据姿态角度、包围盒比例和关键点运动速度判断当前行为。适合有一定Python和OpenCV基础想把姿态检测真正挂进监控或边缘盒子里的工程师参考。2. OpenPose模型原理与选型为什么PAF是多人姿态估计的关键2.1 从关键点回归到多维关联PAF到底解决什么问题早期人体姿态估计大多是自顶向下的做法先用目标检测框出每个人再从每个框里单独回归关键点。这种思路在单人场景很稳定但一旦画面里出现两个人交叉、遮挡检测框会互相干扰排序和裁剪稍有不慎就会丢关节。OpenPose选择自底向上先在所有人物上直接预测所有关键点再把属于同一个人的关键点连接起来。这样做省去了检测框也让运算量和画面中的人数基本解耦正好适合不固定人流的老人房间。连接这一步是难点。同一个人有左肘和右肘两个相近特征两个人并排站时光靠关键点置信图无法判断哪个手腕连哪条手臂。OpenPose因此设计了Part Affinity Fields也就是PAF。它是一组与关键点热图并行的向量场在某一肢干的像素位置上PAF保存了一个单位向量指示该肢干从一端的关节指向另一端的方向。把候选关键点两两组合沿着两条关键点之间的线段对PAF做路径积分分数高的组合才被判定为同一人。这个后处理最终被建模成二分图匹配用匈牙利算法找全局最优连接。需要留意的是行为监护场景通常不追求高并发多人姿态但PAF的价值同样存在。因为老人侧卧时手臂和腿会交叉关节之间距离贴近没有PAF的空间方向约束单纯按置信度连线会把左边的手腕接到右边的髋部角度特征全部失效。2.2 两种常用模型与推理引擎选型Caffe、TF-Pose与OpenCV DNN常见可用的OpenPose模型形态有三类原始Caffe模型、TensorFlow移植版以及PyTorch重实现。Caffe模型是官方预训练版本文件分为prototxt和caffemodel两部分环境隔离好部署时不依赖大型深度学习框架也最容易被OpenCV直接加载。TF-Pose是社区移植优点是可以用TensorFlow Serving做线上推理但部分算子和原版结果有细微偏差。PyTorch版本方便动态改网络结构适合想做关键点微调的研究型项目。如果是纯推理我一般直接用OpenCV的dnn模块读Caffe模型省去装全套深度学习环境只有需要对自己数据微调时才切到PyTorch版本。模型选择上官方预训练模型分COCO和BODY_25两套。COCO模型输出18个关键点覆盖鼻子、脖子、双肩、双肘、双腕、双髋、双膝、双踝、双眼和双耳已经足够计算躯干角度和膝角。BODY_25额外增加了脚趾、脚踝和髋部中心等点对判断站姿不稳有帮助但推理开销略高后处理源码也更复杂。没有特殊要求时优先用COCO模型公开的骨架连接和可视化资料更全踩坑更少。2.3 输入尺寸、输出热图与性能参数对照Caffe版OpenPose的默认输入尺寸是368×368经网络下采样8倍后输出热图尺寸为46×46。输出张量是一个四维Blob通道数为57前18个通道是每个关键点的置信热图后38个通道是每对关节的PAF在x和y方向的分量。理解这个布局很重要关键点坐标最终都要从46×46的热图映射回原图分辨率映射时按比例缩放而不是直接使用整数热图位置。模型选型涉及的三个关键参数对比如下选型维度说明适用场景模型类型COCO 18点更通用BODY_25含脚部细节监护优先用COCO步态分析用BODY_25推理后端OpenCV DNN加载CaffePyTorch用于微调边缘盒子用OpenCV研究训练用PyTorch输入分辨率368×368是原版默认可降到320×320提帧率老年人姿势变形大不建议低于320降低输入分辨率可以显著提升帧率但小目标或被遮挡的关节点会更容易消失。老年监护场景中摄像头通常安装在墙角人在画面中不会太小因此我一般从368开始实测速度不足再往340或320降不要一上来就为了帧率牺牲关键点召回率。3. 用OpenCV DNN在本地加载OpenPose模型做最小推理3.1 准备模型文件和依赖环境先把运行环境准备好。最小依赖只需要opencv-python和numpy建议直接用虚拟环境隔离。pip install opencv-python numpy模型文件使用官方Caffe格式的prototxt和caffemodel文件名称类似pose/pose_deploy_linevec.prototxt和pose/pose_iter_440000.caffemodel。从OpenPose官方仓库拿到这两个文件后与测试脚本放在同一级目录下。测试视频建议先用30秒左右、单人入镜的片段避免一开始就被多人互相遮挡干扰判断。同时准备好一个记录关键点索引的说明因为后续计算角度时会频繁用到。3.2 核心推理代码前向传播与关键点提取下面代码完成单帧图片的加载、前向推理和关键点提取先不画连线把原始坐标拿到手。import cv2 import numpy as np # COCO 模型配置文件 proto_file pose_deploy_linevec.prototxt weights_file pose_iter_440000.caffemodel in_width 368 in_height 368 conf_threshold 0.1 net cv2.dnn.readNetFromCaffe(proto_file, weights_file) def extract_keypoints(image): h, w image.shape[:2] blob cv2.dnn.blobFromImage( image, 1.0 / 255.0, (in_width, in_height), (0, 0, 0), swapRGBFalse, cropFalse ) net.setInput(blob) output net.forward() # shape: (1, 57, 46, 46) points [] for i in range(18): heatmap output[0, i, :, :] _, conf, _, point cv2.minMaxLoc(heatmap) if conf conf_threshold: x int(point[0] * w / heatmap.shape[1]) y int(point[1] * h / heatmap.shape[0]) points.append((x, y, conf)) else: points.append(None) return points frame cv2.imread(sample.jpg) keypoints extract_keypoints(frame) print(keypoints)这段逻辑里最关键的是坐标换算。OpenCV的dnn模块把原图缩放到368×368后输入网络网络输出热图尺寸是46×46所以热图上的一个像素对应原图上的8×8区域。直接用point[0]和point[1]会得到热图坐标必须乘以原图宽高与热图宽高的比值才能映射回原图坐标系。minMaxLoc在这里返回热图全图最大响应和对应位置对单人场景足够如果画面中有多人同一张热图上会有多个峰值后面需要做非极大值抑制。3.3 后处理细节热图峰值、NMS、PAF关联单人场景用minMaxLoc就够但客厅偶尔会出现两个老人同时入镜此时需要在热图上做滑窗NMS。常见做法是对热图做拉普拉斯变换或直接遍历局部极大值再按置信度排序保留彼此距离大于一个阈值的峰值作为候选关键点。我用OpenCV的distanceTransform做过一次效果可以接受但更省事的是调用scipy.ndimage.maximum_filter快速找出局部极值。PAF关联部分在行为识别里不是每一步都必需。如果只关心单人的站坐躺摔可以直接拿置信度最高的那一组关键点计算角度绕开完整的二分图匹配。但为了调试和验证骨架是否正确仍建议按COCO骨骼顺序把相邻关键点连线。COCO关键点索引定义如下索引部位索引部位0鼻子9右膝1脖子10右踝2右肩11左髋3右肘12左膝4右腕13左踝5左肩14右眼6左肘15左眼7左腕16右耳8右髋17左耳画骨架时按“肩膀-手肘-手腕”“髋-膝-踝”的顺次连接并用不同颜色表示左右两侧。这个可视化不是为了好看而是用来快速定位推理错误如果手腕连到了不在同一侧的肘部说明PAF没有被正确使用或者你选择的是后处理前的峰值而不是匹配后的关键点。3.4 把关键点输出封装成统一接口为了后续行为识别和回放测试最好把上面的推理封装成一个稳定的接口输入帧输出统一的列表结构。这个接口里还可以记录每帧的推理耗时方便后面做帧率优化。def frame_to_keypoints(image): points extract_keypoints(image) result [] for p in points: if p is None: result.append((0.0, 0.0, 0.0)) else: x, y, conf p result.append((float(x), float(y), conf)) return result把缺失关键点统一成(0,0,0)而不是None可以简化后续序列模型的张量拼接。但在规则判断阶段要记住这些0值点是无效样本计算角度前必须过滤否则会把坐标原点当作真实关节算出完全错误的角度。4. 站、坐、躺、摔倒识别从骨骼角度到时间序列4.1 用几何特征区分静态姿态拿到一帧关键点后第一步是提取几个有物理意义的特征。常见做法是计算躯干角、膝角和人体包围盒宽高比。下面代码先定义向量夹角函数再提取监护人最关心的四类特征。def vec(p, q): return np.array([q[0] - p[0], q[1] - p[1]]) def angle_between(v1, v2): cos np.dot(v1, v2) / (np.linalg.norm(v1) * np.linalg.norm(v2) 1e-6) return np.degrees(np.arccos(np.clip(cos, -1.0, 1.0))) def body_features(pts): # pts[i] 为 (x, y, conf)缺失时是 None if pts[1] is None or pts[8] is None: return None neck pts[1][:2] hip pts[8][:2] knee pts[9][:2] ankle pts[10][:2] # 躯干角脖子到髋的连线与竖直向下方向的夹角 torso_angle angle_between(vec(neck, hip), np.array([0.0, 1.0])) # 膝角髋-膝-踝三点在膝处的夹角 v1 vec(hip, knee) v2 vec(ankle, knee) knee_angle angle_between(v1, v2) # 包围盒宽高比 valid [p for p in pts if p is not None] ys [p[1] for p in valid] xs [p[0] for p in valid] box_w max(xs) - min(xs) box_h max(ys) - min(ys) wh_ratio box_w / (box_h 1e-6) return torso_angle, knee_angle, wh_ratio这里有几个参数需要理解。躯干角以图像竖直向下方向为0度身体越直立角度越小躺下时角度会接近90度。膝角在站立时接近180度坐姿和深蹲时会明显缩小到100度左右。宽高比则用于区分横向躺卧和纵向站立站立时通常小于0.4躺平时大于1.0。我把这三个特征组合后用下面的规则作为第一版静态姿态分类。姿态躯干角膝角宽高比站立小于20°大于150°小于0.5坐20°到60°80°到140°0.4到1.0躺大于60°任意大于0.9这套规则不需要训练先跑通主流程后续积累数据后再替换成模型。要注意的是侧躺时膝角可能和坐姿接近所以躺的判定优先看躯干角和宽高比膝角只作为辅助特征。4.2 摔倒判定的关键垂直速度、加速度与历史帧静态姿态分类只能告诉你当前是站、坐还是躺但摔倒是一个过程一般在0.4到0.8秒内人体重心位置快速下坠躯干角由接近0度变到接近90度随后人躺在地上不再站起来。单帧无法判断“正在摔倒”和“已经躺下”必须保留最近若干帧的关键点序列。常见做法是维护一个固定长度的队列保存每一帧的中心点坐标和躯干角。中心点可以用有效关键点的平均坐标计算但直接用颈部到髋部的中点更稳定。摔倒发生时最大的特征是垂直方向上短时间移动距离大且躯干角迅速变大。为了适应摄像头距离远近垂直位移要用人体框高度归一化。下面是简化版检测器class FallDetector: def __init__(self, fps25, vert_vel_thresh0.6, settle_frames8): self.fps fps self.vert_vel_thresh vert_vel_thresh self.settle_frames settle_frames self.history [] # 每帧存 (norm_center_y, torso_angle) self.fall_pending False self.pending_count 0 self.alert False def update(self, center_y, body_h, torso_angle): if body_h 40: return self.alert norm_center center_y / body_h self.history.append((norm_center, torso_angle)) if len(self.history) 15: self.history.pop(0) if len(self.history) 2: return self.alert dy self.history[-1][0] - self.history[-2][0] vert_vel dy * self.fps # 每秒身高倍数 if vert_vel -self.vert_vel_thresh: self.fall_pending True if self.fall_pending: self.pending_count 1 angle_ok self.history[-1][1] 50 if angle_ok and self.pending_count self.settle_frames: self.alert True else: self.pending_count 0 # 站起后自动复位 if self.history[-1][1] 20 and vert_vel 0.2: self.fall_pending False self.alert False return self.alert注意vert_vel_thresh这个参数。这里的速度不是像素/秒而是“每秒钟中心点下落了多少个身体高度”。正常下蹲时这个值一般在0.2到0.4之间摔倒时可以达到0.8甚至更高。0.6是我在室内监控视频下常用的初始值调高会漏报调低会增加因为快速坐到沙发上引起的误报。settle_frames表示进入疑似摔倒状态后需要连续多少帧保持躺姿才确认我设置为8帧在25帧率下大约0.32秒能滤掉弯腰捡东西这种瞬时动作为误报。4.3 从规则到小型分类模型关键点序列的归一化输入规则分类器能覆盖大部分典型姿态但遇到半躺、蜷缩、倚靠沙发这类过渡姿态阈值很难覆盖。这时可以改用序列分类模型。先把每帧关键点整理成固定长度的特征向量常见有两种做法一是直接归一化坐标加置信度得到51维或75维输入二是先计算躯干角、膝角、宽高比、关节平均速度等物理特征再拼接成低维向量。后一种对模型容量要求更低也更符合人体先验。特征序列构建代码如下def build_sequence(frames, width, height): seq [] for pts in frames: feats [] for p in pts: if p is None: feats.extend([0.0, 0.0, 0.0]) else: x, y, conf p feats.extend([x / width, y / height, conf]) seq.append(feats) return np.array(seq, dtypenp.float32) # 返回 (frame_count, feature_dim)因为深度学习模型要求输入长度固定我从每个行为片段中均匀抽取30帧不足30帧的尾部补零超过30帧的均匀缩放到30帧。标签按行为设定为4类站、坐、躺、摔倒。模型不必复杂一个两层LSTM接全连接分类头就够用输入形状是(batch, 30, 51)。如果只用规则特征输入维度可以压到十几维换成双向GRU效果更稳定。需要提醒的是摔倒样本数量通常远小于站坐躺训练时要对摔倒类别做过采样否则模型会倾向把所有片段都预测成高频类别。4.4 数据集组织与模型评估公开数据集方面Le2i和UR Fall Detection是行为监护领域比较常见的跌倒视频集提供了站、坐、躺、跌倒等动作片段。可以直接用前面第三节的推理脚本把视频逐帧转成关键点序列保存为NPZ或CSV作为训练集和测试集。如果只做规则分类也可以不用训练直接用这些数据集做离线验证。我建议先对每段测试视频输出一行的评估结果格式如下真实标签预测标签样本数量召回率站站8200.96坐坐6750.94躺躺5410.91摔倒摔倒460.85上面数字是我自己在公开视频上跑出来的结果并不代表所有环境都这样。这里真正要关注的是摔倒列的召回率因为监护场景漏报一次摔倒比多报十次误警代价更高。跌倒样本量少计算总体准确率会被大量正常帧稀释所以项目里要单独看跌倒召回率。另外数据划分要按人物和场景划分不能把同一个人同一个房间的视频同时塞进训练和测试否则评估值会虚高。5. 工程落地的几个参数与验证技巧5.1 三个必调的推断参数阈值、缩放、帧率很多人在OpenPose上跑出骨架后直接拿默认参数接监控流结果不是漏检就是卡顿。至少有三个参数要根据场景调整关键点置信度阈值、输入分辨率和处理帧率。置信度阈值常用0.1适合快速调试老人场景中因为侧卧、半遮挡多我会提高到0.2置信度低于这个值的关键点干脆弃用避免把错误点带入角度计算。输入分辨率默认368边缘设备跑不动时降到320但再低左肩右髋这类大关节会频繁丢失。处理帧率建议保持在15FPS以上摔倒过程约0.5秒15FPS相当于能采到7帧够判断一次速度突变低于10FPS时摔倒瞬间容易被跳帧漏掉。参数初始值调大调小关键点置信度阈值0.1减少噪声点增加关键点召回输入分辨率368小目标识别更好速度更快处理帧率25更早发现摔落卡顿漏突变5.2 监控视频流中的流水线优化推理速度不足时不要逐帧跑完整网络。可以每隔一帧做一次姿态检测中间帧沿用上一帧的角度和位置。因为站、坐、躺都是低速动作跳一帧对分类结果几乎没有影响。摔倒时速度突变发生在1到2帧内所以跳帧间隔不要超过1。另一个优化是只对有人体运动的区域做推理先用帧差或轻量运动检测判断画面内是否变化变化幅度低于阈值时直接返回上一帧行为标签。OpenPose自底向上的设计不需要人体检测框但还是可以在检测前用ROI遮掉门口和窗外区域既能减少误检也能降低传输给上层的坐标噪声。5.3 用回放视频做回归测试与误报率统计行为监护最怕的是改了一个参数后某个角落的摔倒检测忽然失效。所以要把所有测试视频保存成固定回放集每次改完参数都跑一遍输出预测结果和真值对比的CSV文件。python predict_video.py --video fall_01.mp4 --label fall --output eval_result.csv评估脚本里只需要统计每个视频段落的标签序列是否与真值一致再按章节4.4的混淆矩阵计算召回率和误报率。摔倒的误报率统计要拆成两个口径连续多少秒持续误报才算一次误报瞬时抖动不算。我通常只统计持续超过1秒的误报这样能过滤掉弯腰捡东西这类短动作。把这套回归脚本接到录像目录下每周自动跑一次模型阈值基本就不用再人工大幅调整了。本文还有配套的精品资源点击获取