yolov5+openpose摔倒检测实战:人体检测与姿态估计的联合推理
2026/9/11 15:24:03 网站建设 项目流程

简介:面向计算机相关专业毕业设计、课程设计与期末大作业场景,这套结合YOLOv5人体检测与OpenPose姿态检测的摔倒检测项目,是评审98分的高分毕业设计资源。项目融合目标检测与人体姿态估计两条技术路线,涵盖人体框检测、关键点提取、摔倒行为判断等核心环节,既能用于智能安防、老人监护等场景演示,也有助于理解深度学习模型从推理到实际应用的完整路径。资源以zip压缩包形式提供,整体约40.25MB,含Python源码与模型文件,便于本地运行和二次开发,代码组织方式适合动手实践与功能扩展。目前已有163人浏览学习,学习者可将它作为项目模板,参考整体架构、模块划分与实现思路,迁移到自己的课题、课程设计或期末大作业中。

1. 为什么摔倒检测要拆成 yolov5 人体检测 + openpose 姿态估计两段来做

监控画面里“慢慢坐下”和“突然摔倒”在二维像素上的差别,往往只有两三帧的时间窗口。只靠目标检测框做判断,人一蹲下宽高比就剧变,误报率会高到没法用;只靠姿态点做判断,又容易把背景里路过的行人、桌椅遮挡出来的假骨架算进去。yolov5 负责把人找出来,openpose 负责把人体的骨架点估计出来,两者各管一段,摔倒判定才有可解释的特征来源——髋部高度变化、躯干与垂直轴夹角、人体框宽高比,这些都是从业者验证过的最稳定信号。这套组合适合做养老院、独居监护、工业安防里的行为分析,对刚做完目标检测想往行为识别方向扩展的团队来说,是最容易复用的路线:检测器可以换成更轻量的模型,姿态估计也可以根据硬件条件替换,摔倒判定逻辑完全不用动。本文按“模型分工 → 摔倒特征 → 推理管线 → 误报调优”的顺序,把每一步怎么落地讲清楚。

2. 先理清两个模型的输出:yolov5 检测框与 openpose 关键点对齐

2.1 yolov5 人体检测:网络结构只负责“人在哪”,不负责“人怎么了”

yolov5 网络结构本身是一套单阶段目标检测器,输出的是目标框、置信度和类别编号。在摔倒检测这个场景里,它最重要的产出不是框的坐标本身,而是三个后续要用到的信息:人体的位置区域、人体框的宽高比、以及“这个目标是不是人”的置信度。类别编号为 0 即 person,这是 COCO 数据集上的固定约定。

把检测和姿态估计分开而不是用一个端到端模型直接识别摔倒,原因在于摔倒样本太少。自己做数据采集时,正常走路的帧能攒几万张,摔倒的帧可能只有几百张,端到端分类器很容易过拟合到背景上,换个房间就不准。yolov5 用通用检测权重做人,openpose 用通用权重做骨架,两者都不需要见过摔倒样本才能工作,摔倒判定完全交给后续的规则逻辑,这是这套方案在工程上最稳的原因。

2.2 openpose 姿态估计:拿到的是带置信度的 COCO 18 点骨架

openpose 输出的是一组关节点坐标,常用权重采用 COCO 18 点格式(也有 BODY_25 格式),每个点包含 x、y 和置信度。与摔倒判定直接相关的点有这么几个:0 鼻子、1 颈部、8 右髋、11 左髋、9 右膝、12 左膝、10 右踝、13 左踝。

实际处理时,一般把 8 和 11 的平均位置当作髋部中心,把 1 号颈部当作躯干顶部。髋部中心的垂直位置是摔倒过程中变化最剧烈的量,躯干角度则由颈部和髋部中心的连线与垂直轴夹角计算。置信度字段容易被忽略但非常关键:遮挡、快速运动、目标在画面边缘时,openpose 会给出低置信度的坐标,如果不加过滤直接参与角度计算,一次错误点就能让整个判定失真。

2.3 检测框与关键点如何对齐:单人场景取最大框,多人场景做中心点匹配

yolov5 负责框选目标,openpose 在整帧上推理得到所有骨架点,两者之间需要建立对应关系。单人监控场景直接取面积最大的检测框作为目标,然后从 openpose 的全部骨架点里选一个“骨架中心”落在这个框内的即可。多人场景下,常见做法是用颈部点(或其他中间点)与每个检测框的中心做距离匹配。

# 检测框与骨架点对齐:单人场景取最大框,多人场景匹配最近距离 import numpy as np def match_person_to_pose(det_boxes, keypoints): # det_boxes: [[x1, y1, x2, y2, conf, cls], ...] # keypoints: [[x, y, conf] * 18, ...],假设已按 COCO 索引排列 persons = [box for box in det_boxes if int(box[5]) == 0] if not persons: return None, None # 多人时用颈部点(索引1)做匹配置信度更高的近似 target_box = persons[0] target_kpt = keypoints if len(persons) > 1: # 计算每个骨架的颈部中心到每个检测框中心的距离 neck = keypoints[1] min_dist = float("inf") for box in persons: cx = (box[0] + box[2]) / 2 cy = (box[1] + box[3]) / 2 dist = (neck[0] - cx) ** 2 + (neck[1] - cy) ** 2 if dist < min_dist: min_dist = dist target_box = box return target_box, target_kpt

代码里的匹配逻辑是先用类别筛选出 person,多人场景下把每个检测框的中心点和颈部点的欧氏距离平方作比较,取距离最近的组合。这里用颈部而不是髋部做匹配,是因为颈部相对髋部在大多数姿态下更靠近画面中心区域,框的定位也更稳定。注意该匹配只解决框和骨架的对应问题,不做跨帧跟踪,连续帧的关联由后面的速度计算自己维护。

2.4 两个模型输出格式的对照

模型每帧输出关键字段在摔倒判定中的用途
yolov5若干组检测结果x1, y1, x2, y2, conf, cls人体框宽高比、目标裁切区域、多目标定位
openpose18 或 25 个关键点x, y, confidence髋部垂直速度、躯干角度、角度突变率

对照表能看出一个分工原则:yolov5 输出的框是“目标级”信息,openpose 输出的是“部件级”信息,摔倒判定的三个核心特征里,宽高比来自 yolov5,角度和速度来自 openpose。后续调参时如果误报增多,第一步就是看误报发生在哪个特征上,这比盲目改整个模型靠谱得多。

3. 摔倒判定算法:从关键点几何特征到速度加速度阈值

3.1 为什么不训练一个摔倒分类器,而是用规则判定

摔倒检测的公开实测数据很少,自己标注的成本又高,这是直接用分类网络做动作识别的主要障碍。规则判定的好处在于特征可解释、阈值可调、样本需求低;坏处是阈值需要按实际场景标定。从业者通常会把三个信号组合使用,而不是依赖单一阈值:髋部垂直速度是主信号,躯干角度是确认信号,人体框宽高比是环境抗干扰信号。

三个信号分别捕捉摔倒的不同侧面:髋部快速下坠是“摔”的过程;躯干与垂直轴夹角变大是“倒”的结果;宽高比变小说明人体从直立态变为水平态。单一信号都有明显误报场景——弯腰捡东西让角度变大但速度不快,快速坐下让速度达标但角度不大,所以实际代码里用“速度为主、角度为辅、宽高比做二次确认”的投票式逻辑。

3.2 核心判定特征与代码实现

实现时先定义一个特征提取函数,输入 openpose 的 18 点骨架、上一帧髋部高度和检测框高度,输出当前帧的三个特征和一个布尔判定结果。

# 摔倒特征提取与判定核心逻辑 import numpy as np class FallFeatureExtractor: def __init__(self, fps=30): self.fps = fps self.prev_hip_y = None self.prev_time = None def extract(self, kpts, bbox_h): # kpts 按 COCO 18 点排列,取左右髋关节的平均作髋部中心 hip_y = (kpts[8][1] + kpts[11][1]) / 2 hip_conf = min(kpts[8][2], kpts[11][2]) # 躯干角度:颈部到髋部中心连线与垂直轴的夹角 neck_x, neck_y = kpts[1][0], kpts[1][1] hip_x = (kpts[8][0] + kpts[11][0]) / 2 dx = hip_x - neck_x dy = hip_y - neck_y angle = np.arctan2(abs(dx), abs(dy)) * 180.0 / np.pi # 髋部垂直速度,用框高做归一化,乘以 fps 转成“框高/秒” v_y = 0.0 if self.prev_hip_y is not None: v_y = (self.prev_hip_y - hip_y) / bbox_h * self.fps self.prev_hip_y = hip_y # 简单判定:速度低于阈值且角度高于阈值,才认为是一次摔倒 is_fall = (v_y < -0.6 and angle > 55.0) return { "hip_v": v_y, "angle": angle, "hip_conf": hip_conf, "is_fall": is_fall, }

速度公式里(prev_hip_y - hip_y)之所以这样写,是因为图像坐标系的 y 轴向下:人往下倒时 hip_y 增大,prev_hip_y 减 hip_y 为负,负值越大表示下落越快。除以 bbox_h 后,速度变成“每秒移动了几个自身身高”,这样可以避免远处的人因为像素位移小而漏判,也避免近处的人因为像素位移大而误判。fps参数让阈值不受摄像头帧率影响,换摄像头时不用重新标定阈值。

3.3 三个特征的起始标定值与常见误判场景

阈值不能照抄任何项目的默认值,但可以按下面这组经验起始点来标定。髋部垂直速度的起始值设成 -0.6 框高/秒,正常坐下时髋部下沉速度约 0.3 至 0.5,快速摔倒时可到 1.5 以上,这个区间分得很开。角度阈值设成 55 度,弯腰捡东西时躯干角度也会有 40 到 60 度,所以速度条件必须同时成立。

特征计算公式起始标定值常见误判场景
髋部垂直速度帧间髋部位移 / 框高 × fps-0.6 ~ -0.8 框高/秒快速坐下、跳起落地瞬间
躯干角度arctan(水平偏移 / 垂直偏移)55 ~ 65 度弯腰系鞋带、低头看手机
人体框宽高比框高 / 框宽0.5 ~ 0.8蹲姿、坐轮椅

宽高比单独拿出来说:检测框本身就受目标检测模型的输出影响,人摔倒后又经常触发检测框的剧烈变化,所以不建议把宽高比作为主信号。更好的用法是把它作为二次确认条件——当速度和角度都触发时,再看宽高比是否明显小于该场景的站立均值,是则确认报警,否则延迟几帧再判断,能有效挡掉一部分“快速坐下并弯腰拿东西”的复合动作误报。

3.4 状态机与去抖:连续帧判定和报警冷却

单帧的特征判定不能直接触发报警,因为快速起身、弯腰再直起、画面中的瞬时抖动都可能造成单帧误判。实际工程里必须加状态机:连续 N 帧满足摔倒条件才进入报警状态,报警后进入冷却期,冷却期内不再重复报警。N 的取值跟帧率相关,25 到 30 帧的视频流建议取 5 到 8 帧,对应约 0.2 秒,既能滤掉瞬时抖动,又不会漏掉 0.5 秒内完成的摔倒动作。

# 连续帧确认与报警冷却 class FallStateMachine: def __init__(self, need_frames=6, cooldown_sec=5.0, fps=30): self.need_frames = need_frames self.cooldown_frames = int(cooldown_sec * fps) self.pending = 0 self.cooldown = 0 def update(self, is_fall): if self.cooldown > 0: self.cooldown -= 1 return False if is_fall: self.pending += 1 if self.pending >= self.need_frames: self.pending = 0 self.cooldown = self.cooldown_frames return True else: self.pending = 0 return False

冷却期的存在很关键:一次摔倒事件在现实里会持续好几秒,如果不加冷却,报警逻辑会在同一事件期间反复触发,消息队列会被刷爆。冷却结束后,如果被检测人已经被扶起或者离开画面,pending 计数值会清零,不会立即再报。这就是摔倒检测里“事件边沿触发”和“电平触发”的区别,工程上必须用边沿。

4. 视频推理管线:yolov5 与 openpose 联合推理的完整实现

4.1 最小的推理管线设计:抽帧、检测、姿态、判定

整个管线可以拆成四个串行阶段:从视频流取出帧,交给 yolov5 处理得到检测框,把包含人的区域或整帧交给 openpose 得到关键点,最后把关键点和检测框输入摔倒判定模块。最简单的情况下可以串行执行,但实际项目中 openpose 的推理耗时要远大于 yolov5,所以一般会把 openpose 的输入从整帧改成检测框裁剪出来的区域,大幅缩小输入尺寸。

裁剪区域不能紧贴检测框边界,否则骨架点会集中在图像边缘,造成明显的坐标偏移。常见做法是把检测框外扩 20% 到 30% 再裁剪,外扩的 padding 能保证手、脚这些容易超出框的关节点留在画面内。这个改动对摔倒场景尤其重要,因为人倒地后四肢伸展范围大,紧贴框的裁剪大概率丢掉脚踝关节点,而脚踝正是判断倒地后姿态的重要依据。

4.2 完整推理循环代码骨架

下面给出一个可运行的管线骨架,涵盖从读到帧到输出报警的完整闭环。骨架里省略了具体模型权重路径和预处理细节,实际接入时替换为自己的调用方式即可。

import cv2 import torch from queue import Queue from threading import Thread class FallDetectionPipeline: def __init__(self, det_weights, pose_model, fps=30): # yolov5 常见用法:加载本地权重 self.detector = torch.hub.load("ultralytics/yolov5", "custom", path=det_weights) self.pose_model = pose_model # openpose 的 torch 实现,按权重对应接口加载 self.feature = FallFeatureExtractor(fps=fps) self.state = FallStateMachine(need_frames=6, cooldown_sec=5.0, fps=fps) self.frame_queue = Queue(maxsize=2) self.result_queue = Queue(maxsize=2) def process_frame(self, frame): # 阶段 1: yolov5 检测 results = self.detector(frame) boxes = results.xyxy[0].cpu().numpy() # [x1, y1, x2, y2, conf, cls] # 阶段 2: 取最大 person 框,外扩后裁剪 persons = results.pandas().xyxy[0] persons = persons[persons["name"] == "person"] if persons.empty: return None target = persons.iloc[0] x1, y1, x2, y2 = self._expand_box(target, frame.shape, 0.25) crop = frame[int(y1):int(y2), int(x1):int(x2)] # 阶段 3: openpose 姿态估计 kpts = self.pose_model(crop) # 返回 18x3 的关键点数组 kpts = self._map_crop_to_frame(kpts, x1, y1) # 关键点坐标映射回原图 # 阶段 4: 特征提取 + 状态机判定 features = self.feature.extract(kpts, bbox_h=(y2 - y1)) if features["hip_conf"] < 0.3: return None # 置信度过低,不参与判定 alarm = self.state.update(features["is_fall"]) return alarm, features

代码里的_expand_box负责把检测框外扩并限制在画面边界内,_map_crop_to_frame把裁剪区域里计算出的关键点坐标加上裁剪偏移量映射回原图坐标系。最容易被忽略的是hip_conf过滤:髋部置信度低于 0.3 时,速度计算毫无意义,此时正确行为是跳过本帧而不是输出一个不可靠的报警。Queue(maxsize=2)是给后面的多线程用的缓冲区,容量 2 保证了背压机制——推理慢时不会无限堆积旧帧。

4.3 多线程切分与推理耗时优化

用两个线程可以显著提高吞吐:一个线程只做 yolov5 检测和裁剪,另一个线程做 openpose 推理和摔倒判定。两个线程之间用队列传递数据,队列长度限制为 2,避免延迟累积导致报警滞后。openpose 推理是瓶颈,把它和检测器放在不同线程后,检测结果可以提前准备好,openpose 一有空闲就能拿到最新的裁剪图。

线程负责模块输入输出
视频采集线程读帧、队列写入摄像头/视频流原始帧
检测线程yolov5 检测、裁剪原始帧裁剪后的人体区域
姿态与判定线程openpose、特征提取、状态机裁剪图报警结果
主线程结果展示、消息推送报警结果通知

实际部署时,如果 GPU 显存有限,可以考虑把 yolov5 和 openpose 分开用不同的推理引擎运行,比如一个用 PyTorch 一个用 ONNX Runtime,让两个模型在显存占用和计算开销上解耦。这个优化对只有 6G 显存的显卡特别有效,PyTorch 的 CUDA 缓存机制会把两个模型的权重都留在显存里,换成 ONNX Runtime 后显存占用可以明显下降。实时性优化要遵守一个原则:不牺牲判定准确率去换取帧率,摔倒检测的核心不是画面流畅,而是关键帧不丢。

5. 落地调优:摔倒检测的置信度过滤与误报排查脚本

5.1 低置信度关键点的处理优先级高于阈值调优

任何姿态估计算法在遮挡、快速运动、运动模糊下都会输出质量下滑的关键点。摔倒瞬间恰恰是运动最快的时刻,所以置信度过滤在摔倒检测里比阈值本身更值得优先处理。我通常的做法是:把髋部、颈部、左右踝的置信度单独取出来,任一关键区域的置信度低于 0.3 时,要么跳过该帧,要么用最近有效帧的速度值补充,两者选其一。补充逻辑比想象中容易写:保存上一帧的有效骨架点,当前帧置信度低就用上一帧的坐标计算速度,再用当前帧的低置信度坐标计算角度,两个特征各自用各自质量最高的数据来源。

5.2 一个能定位误报发生时刻的回放脚本

阈值调优最怕拍脑袋:今天把角度阈值从 55 调到 60,明天误报少了但漏报也多了,根本不知道改动影响了哪一帧。解决这个问题的办法是给每帧写一份特征日志,然后离线回放误报位置。

# 特征日志分析与误报定位 import pandas as pd # fall_log.csv 列为: time, hip_v, angle, wh_ratio, fall_flag df = pd.read_csv("fall_log.csv") # 找出 fall_flag 从 0 变为 1 的时刻,即报警起始帧 df["prev_flag"] = df["fall_flag"].shift(1).fillna(0) alerts = df[(df["fall_flag"] == 1) & (df["prev_flag"] == 0)] # 打印每次报警前后的特征变化,用于判断阈值是否合理 for _, row in alerts.iterrows(): window = df.iloc[max(0, int(row.name) - 5): int(row.name) + 10] print("报警时间:", row["time"]) print(window[["hip_v", "angle", "wh_ratio"]].to_string(index=False))

回放脚本的价值在于把“感觉误报变多了”变成“看到报警前 5 帧到底发生了什么”。如果报警时髋部速度才 -0.4,说明速度阈值过松;如果角度只有 50 度就报警,说明角度和速度的组合逻辑可能有问题。把日志按天留存,还能观察昼夜间同一个摄像头的阈值漂移情况,这种稳定性问题只有回放日志才能暴露。

5.3 一个更稳的时序判据:先加速后倒地

单帧阈值容易把“快速弯腰”和“摔倒”混在一起,一个更稳的判据是检查“先加速后倒地”的时序模式:先出现髋部速度低于 -1.0 框高/秒的快速下坠,并在随后的 0.5 到 1.0 秒内出现角度大于 60 度的水平姿态,才触发报警。快速弯腰时角度同样会变大,但髋部速度不会出现同样的先峰。实现时只要在状态机里增加一个fast_drop标志,记录过去 15 帧内是否出现过超阈值的速度,再和当前的角度条件取与。这比单纯拉高阈值更能区分动作意图,也是实测中误报率下降最明显的一处改动。阈值标定的完整闭环应该是:收集一小时监控片段→跑日志→看误报分布→调整对应特征的阈值→再跑同一段视频验证,而不是在线上边看边改。

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询