简介:基于深度学习OpenPose的人体姿态检测项目源码,面向老年人行为监护场景,可识别站、坐、躺及摔倒等状态,适合计算机视觉入门、智慧养老项目开发者参考。资源共580个文件,压缩包75.5MB,涵盖Python源码、JPG样本图像、模型配置与权重文件、文本说明及C++辅助模块等,既包含算法串联的线性推理demo,也提供多队列并发处理的webcam版本,方便按需运行。已有1316人学习/下载。资源附带详细操作说明,建议在Anaconda与PyCharm环境下运行,通过生成JSON文件输出人体框、关键点及当前行为状态,便于二次开发与结果可视化,适合用于跌倒检测、行为分析等课题。
1. 用 OpenPose 做老年人行为监护,为什么是这条技术路线
独居老人跌倒,是居家养老里最怕发生又最高发的事故。检测“站、坐、躺、摔倒”的思路很多,但基于深度学习的 OpenPose 人体姿态检测这条路线最好落地:一次推理输出 18 或 25 个关节点坐标,站、坐、躺由躯干和腿的几何关系直接推导,摔倒靠短时间内的关节点突变判断,老人不用佩戴任何设备,画面也不依赖云端处理。相比 YOLO 动作分类和 3D-CNN 动作识别,OpenPose 的骨架坐标可解释、可回放、误报好排查,一张中端 GPU 就能跑到 10 到 15fps。适合做智慧养老看护终端、独居老人监护设备的工程师,也适合拿人体姿态检测立项但还没定技术栈的开发者。
2. OpenPose 原理与关键参数:热图、PAF 与 BODY_25 关节定义
2.1 两分支网络:热图回答“关节在哪”,PAF 回答“谁和谁相连”
OpenPose 属于 bottom-up 姿态估计。它不先检测人框再在框内找关节点,而是把整张图直接送进网络,输出两类特征图。第一类是关节置信度热图(heatmap),每个关节对应一张,图上像素值表示该位置是某个关节的概率;第二类是部件亲和场(Part Affinity Fields,PAF),每个肢干对应一组向量场,编码相邻关节之间的连接走向。
两套特征由同一个网络的上下两个分支分别预测。前 10 层复用 VGG-19 的前段做共享特征提取,后面的多阶段结构交替细化热图和 PAF,每个 stage 都会把前一个 stage 的输出与共享特征拼接再预测,因此网络在遮挡情况下能借助全局上下文把关节补齐。后处理阶段先取热图峰值得到候选关节点,再用贪心匹配或匈牙利算法在 PAF 上做二分图匹配,把属于同一个人的关节连成骨架。这就是 OpenPose 天然支持多人检测的原因,也是它与 top-down 方法相比省掉一次目标检测开销的原因。
落到老年人行为监护场景,bottom-up 的设计有实际价值。看护画面里常有护理员半遮挡老人、老人撑着助行器、家属路过,top-down 方法在人物交叠时容易把骨架错配到最近的人框上;OpenPose 只要关节可见度足够,就能从热图里把点找全。代价是后处理匹配比单人回归复杂,同等精度下帧率低于 AlphaPose 这类方法,所以工程上通常用抽帧而不是逐帧全分辨率推理。动手前先明确这一点,后面定帧率预算才不会返工。
2.2 BODY_25 关节编号表:行为判读实际只用 10 个点
OpenPose 官方模型有两套输出约定:COCO 格式输出 18 个关键点,BODY_25 输出 25 个,多出的点包括髋部中点(midHip)以及大脚趾、小脚趾、脚跟等足部关键点。做站、坐、躺、摔倒四类判断,用不到手、眼、耳,真正有用的是脊柱链上的 10 个点。BODY_25 的编号要记熟,后面的角度计算和坐标索引都依赖它。
| 语义 | BODY_25 编号 | COCO 编号 | 在行为判读里的作用 |
|---|---|---|---|
| 鼻子 | 0 | 0 | 识别仰面倒地,辅助确认 |
| 颈部 | 1 | 1 | 躯干上端点,摔倒时下落速度的载体 |
| 左/右肩 | 2 / 5 | 2 / 5 | 计算肩线方向,区分坐姿与下蹲 |
| 髋部中点 | 8 | 无 | 躯干下端基准,重心估算用 |
| 左/右髋 | 12 / 9 | 11 / 8 | 与膝盖、脚踝构成腿部几何 |
| 左/右膝 | 13 / 10 | 12 / 9 | 膝关节角度,坐姿判定关键 |
| 左/右踝 | 14 / 11 | 13 / 10 | 与髋部组合判断支撑状态 |
| 左/右大脚趾 | 19 / 22 | 无 | 判断脚尖朝向,区分侧躺与仰躺 |
BODY_25 与 COCO 在髋、膝、踝的编号上整体错开一位,因为 BODY_25 把 8 号留给了髋部中点。这是它在行为判断上最大的优势:髋部中点是躯干下端和重心的基准,COCO 方案只能用左右髋取平均,单侧髋被遮挡时均值会跳变。还需要注意一个容易踩的坑:后处理返回的坐标基于网络输入尺寸,不是原图尺寸,输出时一定要乘回缩放系数,否则角度全偏,后面所有判据都作废。
2.3 四个关键参数:net_resolution、scale_number、scale_gap、keypoint_threshold
官方 C++ 版命令行把推理的三个可变维度暴露得很清楚:输入分辨率、多尺度策略、关键点阈值。用官方二进制跑一段视频并输出 JSON 骨架的典型命令是:
./build/examples/openpose/openpose.bin \ --video ./elderly_room.mp4 \ --model_folder ./models/ \ --model_pose BODY_25 \ --net_resolution 368x368 \ --write_json ./keypoints/ \ --write_video ./out_video.avi \ --keypoint_threshold 0.3命令的作用是:对输入视频逐帧做姿态估计,把每个人的关键点坐标与置信度写入指定目录的 JSON,同时输出叠加骨架的调试视频。--model_pose BODY_25决定输出 25 点还是 18 点,必须在加载模型前确定,因为两套模型的热图通道数不同,混用会直接报尺寸错误。--net_resolution是网络输入尺寸,官方默认 656x368,分辨率越高小目标关节越准,耗时近似线性增长。--keypoint_threshold是热图峰值筛选阈值,0.3 比较平衡;调到 0.5 噪点变少,但远处的人整段关键点都会丢,固定摄像头的看护场景通常会往低调而不是往高调。
多尺度用--scale_number 4 --scale_gap 0.25,官方代码按1 - i * scale_gap生成尺度序列,即 1.0、0.75、0.5、0.25 四档,把输入图逐级缩小后各推理一遍再融合热图与 PAF。多尺度融合能显著提升遮挡和小目标场景的召回率,但耗时随之翻倍;它本质是特征层的融合,和把多个模型结果做投票的模型融合不是一回事。固定摄像头画面里人物尺度变化不大,我会先关掉多尺度把算力让给帧率,再把 net_resolution 提到 432 补偿小目标损失。调参顺序建议固定:先定 net_resolution,再开多尺度看召回增量,最后用 keypoint_threshold 压噪点,每次只动一个变量,否则出了问题分不清是谁引起的。
3. 加载本地模型跑通人体姿态检测的单帧与视频推理
3.1 深度学习环境配齐,先解决模型文件对齐问题
用 OpenPose 的人多数卡在同一个地方:模型和网络定义对不上。官方发布的是 Caffe 权重(.caffemodel 配 .prototxt),社区常见的 PyTorch 实现把权重转成了 .pth。加载本地模型前必须确认三处一致:网络结构层名、输入归一化方式、关键点定义是 BODY_25 还是 COCO。如果model.state_dict()的 key 与权重文件对不上,出现size mismatch,优先怀疑网络定义混用;有些权重在 DataParallel 下保存,key 里带module.前缀,要先剥掉再加载。
提示:用
{k.replace("module.", ""): v for k, v in w.items()}去掉前缀后再load_state_dict,能解决八成以上的本地权重加载报错。
深度学习环境按最常见的组合配即可:Python 3.8 以上、PyTorch 1.10 左右的版本、opencv-python、numpy。没有 GPU 也能把单帧流程跑通,只是 368x368 输入在 CPU 上一帧要几百毫秒,适合调代码;跑实时视频建议用 N 卡 CUDA,Apple Silicon 可以试 MPS 后端,但要逐算子验证兼容性。
3.2 单帧推理代码:缩放、归一化与坐标回流
下面是一段可运行的单帧推理骨架代码,以社区常见的 PyTorch OpenPose 实现结构为准;网络定义与后处理模块的导入路径,按你实际下载的版本替换,核心流程不变:
import cv2 import torch import numpy as np from model import OpenPoseNet # 网络定义文件,按实际实现导入 from postprocess import extract_keypoints # 热图峰值 + PAF 匹配 model = OpenPoseNet(BODY_25=True).eval() weights = torch.load("openpose_body25.pth", map_location="cpu") weights = {k.replace("module.", ""): v for k, v in weights.items()} model.load_state_dict(weights) device = "cuda" if torch.cuda.is_available() else "cpu" model.to(device) def infer_frame(frame, net_h=368): h0, w0 = frame.shape[:2] scale = net_h / h0 # OpenPose 输入要求宽高都是 8 的倍数,跳过这步热图坐标会错位 nw = int(w0 * scale // 8) * 8 nh = int(h0 * scale // 8) * 8 img = cv2.resize(frame, (nw, nh)) blob = cv2.cvtColor(img, cv2.COLOR_BGR2RGB).astype(np.float32) blob = (blob / 255.0 - 0.5) / 0.5 # 与训练一致的 [-1, 1] 归一化 blob = torch.from_numpy(blob.transpose(2, 0, 1)).unsqueeze(0).to(device) with torch.no_grad(): heatmaps, pafs = model(blob) # 网络输出两组特征图 keypoints = extract_keypoints(heatmaps, pafs, thr=0.3) # 坐标基于网络输入尺寸,按各自方向映射回原图 keypoints[..., 0] = keypoints[..., 0] * (w0 / nw) keypoints[..., 1] = keypoints[..., 1] * (h0 / nh) return keypoints逻辑说明:infer_frame先对原图做等比缩放,再把宽高对齐到 8 的倍数,这是 OpenPose 卷积下采样结构对输入尺寸的硬性约束,跳过这步热图坐标会整体偏移。(blob / 255.0 - 0.5) / 0.5把像素归一化到 [-1,1],必须与训练时完全一致,漏掉这步再好的权重精度都会崩。模型前向只产出热图和 PAF,真正的骨架拼装在后处理里;extract_keypoints内部包含阈值过滤、非极大值抑制、PAF 积分与匹配,是整条链路最耗 CPU 的部分,多目标画面里经常比前向推理还慢。
参数说明:net_h=368对应官方默认输入高度,1080p 画面缩到 368 高后宽约 600,画面远端的小目标只剩十几个像素,关节容易丢;提到 432 或 512 能明显改善小目标召回,推理时间同步上涨,配合抽帧策略使用。返回值形状是 [人数, 25, 3],最后一维是 (x, y, score),score 低于阈值的关节坐标通常为 0,后面算角度前要做掩码过滤,否则会拿原点当真实关节参与计算。
3.3 视频流上的帧率预算与抽帧策略
实时逐帧跑 368x368 推理,中端 GPU 大约 15 到 30fps,CPU 只有 1 到 3fps。固定画面里人的动作变化没那么快,摔倒过程本身也不过一两秒,工程上常见的做法是每 3 帧取 1 帧推理,等效 10Hz。10Hz 对“站起来、摔倒、躺平”这个过程足够,瞬时突变至少跨越两三帧,不会漏。产品化时可以在推理前加一层帧差预判,画面无变化就降到 5Hz,检测到大面积变化再切回逐帧,省电也省算力。
| 输入高度 net_h | 中端 GPU 参考帧率 | CPU 参考帧率 | 适用场景 |
|---|---|---|---|
| 256 | 25~35fps | 2~3fps | 只做人存在性检测 |
| 368 | 15~25fps | 0.5~1.5fps | 单人行为状态机 |
| 432 | 10~15fps | 小于 1fps | 画面远端老人、多目标 |
表里的数值是同硬件条件下的量级参考,不同品牌显卡差异很大,用它做预算规划而不是验收指标。如果不想在 Python 里维护视频解码和跳帧逻辑,用 ffmpeg 先抽帧、再送入官方 C++ 版 OpenPose 批量推理,是省去手写视频循环的常见做法:
ffmpeg -i elderly_room.mp4 -vf "fps=10,scale=640:-2" -q:v 3 sampled.mp4 ./build/examples/openpose/openpose.bin \ --video sampled.mp4 --model_folder ./models/ \ --model_pose BODY_25 --net_resolution 368x368 \ --write_json ./keypoints/ --keypoint_threshold 0.3这段命令先把 30fps 视频统一抽到 10fps 并缩窄到 640 宽,再逐帧推理。scale=640:-2中 -2 表示高度自动计算并保证偶数,避免编码器报奇偶尺寸错误;-q:v 3控制画质,数值越小越好。抽帧后再推理的好处是 JSON 时间戳天然对应 10Hz 采样,行为状态机按固定间隔读数据,不用处理视频帧率漂移。画面里有多个老人时,JSON 会出现多个 person 对象,必须按“髋部中点与上一帧距离最近”的规则做跨帧 ID 匹配,否则每个人的状态机是断开的,一次连续的站立、摔倒、躺平会被切成两半。
4. 从 OpenPose 骨架到站、坐、躺与摔倒判别:角度、速度与状态机
4.1 姿态几何判据:从坐标到四分类的归一化处理
拿到 25 个关节点的像素坐标后,第一件事是消除个体差异。身高不同、离摄像头远近不同,直接比较像素距离没有意义,因此所有长度量都要除以骨架自身的尺度。我习惯把颈部(编号 1)到髋部中点(编号 8)的距离记为 torso_len,作为基准单位,位移、速度、高度差全部用它归一化。基于这个基准,四分类判据可以收成一张表:
| 行为 | 躯干角(相对竖直) | 髋膝关系 | 附加条件 |
|---|---|---|---|
| 站立 | 小于 30° | 髋部基本在踝部正上方 | 膝角大于 150° |
| 坐姿 | 小于 40° | 髋与膝接近等高,髋高于踝 | 髋踝水平距大于 40% 腿长 |
| 躺姿 | 大于 60° | 肩与髋高度差小于 25% 躯干长 | 持续 2 秒以上 |
| 蹲姿(不告警) | 30°~60° | 髋低于肩,膝角小于 90° | 髋踝水平距小,与坐姿区分 |
注意“坐”和“蹲”是误报重灾区。两者躯干角区间重叠,单看角度分不开,必须叠加髋踝水平距离:坐姿大腿前伸,髋到踝的水平投影距离大;蹲姿脚踝基本在髋正下方。判断顺序建议先判躺、再判坐、最后判站,因为躺的躯干角特征最明显,最先排除能挡掉大部分误报。轮椅上的老人坐姿躯干角偏大,通用阈值会把久坐判成半躺,需要单独收一段轮椅样本调阈值。
4.2 摔倒检测不能只看角度:颈部下落速度与归一化速度
很多初版实现只用“躯干角超过 60°”判摔倒,结果把老人弯腰捡东西、系鞋带全部误报。摔倒的本质是受控姿态的快速失去:躯干角在 0.3 到 0.8 秒内从小于 30° 突变到大于 60°,同时颈部高度快速下降。角度阈值必须搭配角速度与颈部下落速度一起用。颈部下落速度按帧间像素位移计算,再除以 torso_len 归一化:
from collections import deque class FallDetector: def __init__(self, fps=10, angle_thr=55, speed_thr=1.2, hold_sec=2.0): self.fps = fps self.angle_thr = angle_thr # 倒地后躯干角的维持阈值(度) self.speed_thr = speed_thr # 归一化颈部下落速度(躯干长/秒) self.hold_sec = hold_sec # 倒地滞留确认时长(秒) self.history = deque(maxlen=fps * 2) self.fall_start = None def update(self, neck_y, torso_angle, torso_len): now = len(self.history) / self.fps self.history.append((now, neck_y, torso_angle)) if len(self.history) < 3: return False _, y0, a0 = self.history[-3] # 取前 2 帧做差分,抗单帧抖动 drop = (neck_y - y0) / torso_len # 图像坐标 y 向下增长,落下时为正 speed = drop * self.fps / 2.0 # 换算成每秒下落多少个躯干长 angle_delta = torso_angle - a0 falling = speed > self.speed_thr and angle_delta > 25 if falling and self.fall_start is None: self.fall_start = now if self.fall_start is not None: if now - self.fall_start > self.hold_sec and torso_angle > self.angle_thr: self.fall_start = None return True # 两个条件都满足才输出告警 if torso_angle < self.angle_thr - 15: self.fall_start = None # 角度回落,视为弯腰或猛坐下 return False逻辑说明:update维护一个 2 秒的环形缓冲,用当前帧与前 2 帧的颈部位置做差分,比相邻帧差分抗抖动。speed 的单位是“每秒下落多少个躯干长度”,1.2 表示老人以每秒超过一个身高的速度向下移动,远高于下蹲速度,又低于自由落体下半程的速度范围,是一个跨摄像头距离都通用的阈值。angle_delta > 25保证只响应角度突变;hold_sec的滞留确认是防误报最关键的机制,先触发下落事件,再观察 2 秒内躯干角是否维持大于 55°,两个条件都满足才置位告警,能滤掉“猛坐进沙发”这类高度突变但角度不维持的动作。
参数调整顺序:先用离线回放调 speed_thr,把正常蹲起、弯腰全部放掉;再用 angle_thr 收紧“倒地”的定义;最后用 hold_sec 平衡响应速度与误报率。1 秒响应更快,但容易把猛坐误报;2 秒是看护场景里常见的折中。躯干长在人物侧身时会被压缩,计算出的速度会偏大,所以骨架平均置信度低于 0.5 的帧不参与速度计算,避免抖动被放大。
4.3 帧间 ID 跟踪与遮挡兜底策略
多人画面里,状态机必须绑定到具体的人。常见做法是卡尔曼滤波跟踪每个骨架的髋部中点,下一帧所有检测目标按髋部中点距离做最近邻匹配;匹配不上先让预测值顶一帧,连续丢失超过 1 秒就删除该人的状态,防止“幽灵骨架”的静止坐标停在原地被判成躺姿。兜底还有一条:整副骨架平均置信度低于阈值时视为完全遮挡,不回填任何状态。宁可丢一帧检测,也不要让状态机卡在“躺”上持续发假告警。告警触发时把前 3 秒和后 2 秒的原始视频片段落盘,供人工复核,这一步在真实产品里比算法本身更能决定误报率能不能被接受。
5. 自建看护数据集微调 OpenPose 模型的验证技巧
官方权重的训练数据是 COCO、MPII 这类通用数据。直接拿来做老人在室内的行为监护,会遇到三类典型失败:穿深色衣服时关节热图置信度整体偏低、拄拐与轮椅造成躯干角长期偏大、低矮床铺上坐卧边界模糊。常见做法是自采几小时老人活动视频,用现成模型自动标注一遍,再人工修正出错帧,导出成 COCO 格式 JSON,按 8:2 切训练和验证。微调时把网络前 10 层共享特征冻结,只解冻后 3 个 stage,学习率从 1e-4 起,约两万步收敛。显存不够就把训练尺寸降到 256x256,验证时再回到 368 推理。在通用权重上做迁移学习、训练自己的数据集,成本和难度都远低于从零训练,这是自建看护行为数据集的标准路径。
微调效果的验证不要只看 COCO AP。行为监护的真实指标是误报与漏报。具体技巧是把一天 8 小时的监控录像按 10Hz 抽帧离线跑完整链路,把状态机输出的告警时间点和人工标注的跌倒事件做对齐,统计“事件发生后 5 秒内是否告警”的召回率,以及非事件时段的告警次数。这个回放脚本要能一键切换 speed_thr、angle_thr、hold_sec 三个参数,每改一组就重跑一遍,结果写成 CSV 对比:
python replay_validation.py \ --keypoints_dir ./keypoints/ \ --annotations ./fall_events.json \ --speed_thr 1.2 --angle_thr 55 --hold_sec 2.0 \ --output ./report.csv这个脚本只读骨架 JSON 与人工标注,不依赖摄像头实时流,整个过程可复现,适合做调参回归。报告里除了整体准确率,还要单独看“低速缓慢倒地”这一类:老人扶着桌沿慢慢滑坐再躺下,颈部速度达不到阈值,只有躯干角最终越过 60°,需要把 angle_thr 降到 50 并用更长的 hold_sec 兜住,这是摔倒检测和日常监护最后一道磨合。当验证集上 30 帧滞留确认的准确率达到 95% 以上、8 小时录像误报不超过 1 次时,这套参数再迁移到实时链路。现场部署时把三个阈值做成可远程调整的配置项,误报频发时先动 hold_sec 再动 speed_thr,避免每次现场调试都要重新触发一次模型推理。
本文还有配套的精品资源,点击获取