ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

yolov5+openpose摔倒检测实战:人体检测与姿态估计的联合推理

yolov5+openpose摔倒检测实战:人体检测与姿态估计的联合推理 简介面向计算机相关专业毕业设计、课程设计与期末大作业场景这套结合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, fps30): 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_frames6, cooldown_sec5.0, fps30): 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, fps30): # yolov5 常见用法加载本地权重 self.detector torch.hub.load(ultralytics/yolov5, custom, pathdet_weights) self.pose_model pose_model # openpose 的 torch 实现按权重对应接口加载 self.feature FallFeatureExtractor(fpsfps) self.state FallStateMachine(need_frames6, cooldown_sec5.0, fpsfps) self.frame_queue Queue(maxsize2) self.result_queue Queue(maxsize2) 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(maxsize2)是给后面的多线程用的缓冲区容量 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(indexFalse))回放脚本的价值在于把“感觉误报变多了”变成“看到报警前 5 帧到底发生了什么”。如果报警时髋部速度才 -0.4说明速度阈值过松如果角度只有 50 度就报警说明角度和速度的组合逻辑可能有问题。把日志按天留存还能观察昼夜间同一个摄像头的阈值漂移情况这种稳定性问题只有回放日志才能暴露。5.3 一个更稳的时序判据先加速后倒地单帧阈值容易把“快速弯腰”和“摔倒”混在一起一个更稳的判据是检查“先加速后倒地”的时序模式先出现髋部速度低于 -1.0 框高/秒的快速下坠并在随后的 0.5 到 1.0 秒内出现角度大于 60 度的水平姿态才触发报警。快速弯腰时角度同样会变大但髋部速度不会出现同样的先峰。实现时只要在状态机里增加一个fast_drop标志记录过去 15 帧内是否出现过超阈值的速度再和当前的角度条件取与。这比单纯拉高阈值更能区分动作意图也是实测中误报率下降最明显的一处改动。阈值标定的完整闭环应该是收集一小时监控片段→跑日志→看误报分布→调整对应特征的阈值→再跑同一段视频验证而不是在线上边看边改。本文还有配套的精品资源点击获取
返回列表