简介:面向人工智能项目实践与姿态识别初学者的摔倒检测完整项目包,采用YOLOv5人体检测与OpenPose姿态估计相组合,能够支撑行人安全监控原型开发、姿态分类研究以及跌倒异常事件分析。压缩包内共183个文件,大小约40.24MB,内容以Python脚本、深度学习配置文件、jpg/jpeg训练图片、pt/jit模型权重为主,同时包含少量txt/xml标注、容器构建文件等,目录划分清晰,便于按模块查阅。项目中的关键点生成脚本可输出人体关键点图,并作为后续.jit模型训练数据;目标检测脚本先完成YOLO人体定位,再根据目标框宽高比做初步过滤,随后将截取的人体区域送入OpenPose进行姿态识别;动作分类训练脚本支持自定义姿势数据集,因而可方便地扩展到摔倒之外的更多动作。目前已有1064人学习/下载,特别适合希望快速上手目标检测与姿态估计组合应用的开发者,能够完整理解从数据准备、关键点生成、模型训练到实时摔倒判别的全流程。
1. 摔倒检测不是玄学:yolov5 框人、openpose 找骨头,双阶段才有得改
做监控报警的同事丢给我一个需求:养老院走廊摄像头要能自动报摔倒。市面上现成的摔倒检测案例不少,但多数要么只能框人,要么只给姿态,真正把 yolov5 人体检测和 openpose 姿态检测串成一条完整流水线的并不多。这份项目实践把两段接了起来:yolov5 先把画面里的人和框一起拿出来,再用 openpose 提人体关键点,摔倒、站立、行走这类动作最后由 action.jit 分类。检测和姿态分开,意味着你可以不动姿态模型就换掉检测器,也可以单独重训动作分类。适合正在做安防监控、老人看护,以及对 yolo 检测、姿态估计、摔倒检测三个方向都想上手跑一遍的人。
2. 为什么拆两段:yolov5 与 openpose 的职责边界和 .jit 部署形态
第一次拆这个项目的时候,我一度怀疑为什么要用两个模型,直接拿一个 pose 模型不是也能做摔倒检测吗?跑完一轮就明白了,摔倒检测看起来是一个任务,实际是两个子问题:人得先被找到,才有可能谈姿势。两个模型分开,每个模型的负担都小,出问题也好定位。
2.1 职责边界:yolo 管见人,openpose 管骨头
yolov5 是 one-stage 检测器,anchor 机制,一次前向就能回归出目标框、类别和置信度。它的优势在结构和后处理都很成熟,检测人这种类别根本不用专门训练,用预训练权重就行,推理也快。如果你看过 yolov5 的网络结构图,会发现它由 Backbone、Neck、Head 三段组成,Backbone 提取多尺度特征,Neck 做特征融合,Head 输出三个尺度的预测。对做摔倒检测来说,你不需要动这些结构,只需要知道人框从哪个输出拿,以及置信度怎么过滤。训练阶段才需要关心 yolov5 超参数,推理阶段真正影响结果的是 conf-thres 和 iou-thres 两个值。
openpose 走的完全是另一条路,核心是部分亲和场 PAF(Part Affinity Fields),网络输出的是关键点的置信度热图和部件关联向量场,要经过后处理才能解析成 18 点或 25 点骨架。所以你看到项目里的 openpose.jit,它不是直接给你坐标的普通分类模型,输出维度比分类模型大很多。工程上你不需要被这些细节吓到,.jit 文件已经把网络部分封装好了,你只需要把后处理的两三个函数调通。
把这两个模型串起来,摔倒检测才有可调的空间。单阶段端到端方案不是不能做,而是摔倒这个类别的视觉变体太多:遮挡、角度、光线、不同身材的人摔倒姿势差异很大,一个端到端模型要见够这些样本,训练成本高。拆成两段后,yolo 只负责“人在哪”,openpose 负责“姿势长什么样”,最后动作分类只看骨架图。任何一个环节效果不对,替换对应模型就行,不用整个推倒重来。
2.2 部署形态:openpose.jit 与 action.jit 的角色分工
项目里真正跑推理的不是训练脚本,而是 openpose.jit 和 action.jit 这两个 TorchScript 序列化模型。.jit 文件就是把训练好的模型和计算图打包在一起,加载时不依赖原来的 Python 类定义和训练代码,跨环境移动很省事。这是这套项目能给人最大便利的地方,换机器、换解释器版本,都不需要把训练代码一起搬走。
import cv2 import torch # 打开项目后先确认两个 .jit 文件的位置 pose_model = torch.jit.load("openpose.jit", map_location="cpu") action_model = torch.jit.load("action.jit", map_location="cpu") pose_model.eval() action_model.eval() # 假设你有一张从视频帧里抠出来的 ROI 图 roi = cv2.imread("roi.jpg") # 预处理成 openpose 要求的尺寸和通道顺序,这里用占位函数示意 tensor = preprocess(roi) # 常见做法是 BGR->RGB、HWC->CHW、归一化、加 batch 维 with torch.no_grad(): pose_out = pose_model(tensor) # pose_out 需要按项目里的后处理解析成关键点坐标 keypoints = postprocess(pose_out)代码说明:torch.jit.load 的两个模型都在 CPU 上加载,map_location 参数可以避免 GPU 环境下报显存错误;eval() 切到推理模式,关闭 dropout 和 batchnorm 的统计更新。preprocess 和 postprocess 函数名是示意,具体实现以项目里的工具函数为准,openpose 的输入尺寸和归一化方式直接决定关键点精度,不要随意改动。
要注意的是 torch.jit.load 出来的模型对 torch 版本有点敏感,跨大版本加载容易报错。项目里带了一个 Dockerfile,作用就是锁定环境,你在自己机器上复现遇到 torch 版本对不上,优先看它里面锁的是哪个版本。另外一个 labels.cache 是之前训练动作分类时留下的缓存文件,标注没变的情况下留着不影响推理。
2.3 三份核心文件的阅读顺序
yolov5.iml 是 IDE 模块文件,用来把项目结构索引起来,不用管它。真正要读的是 runOpenpose.py 和 detect.py:前者是数据准备和可视化入口,后者是联调主流程。学习顺序建议先 runOpenpose.py 再 detect.py,因为前者产出的关键点图,既给 action.jit 训练当数据,也让你肉眼检查 openpose 到底提没提对骨头。
| 文件 | 作用 | 备注 |
|---|---|---|
| runOpenpose.py | 单跑 openpose,出人体关键点图 | 结果默认存 data/test |
| detect.py | yolo 框人 + 宽高比粗筛 + openpose 判定 | 主流程入口 |
| openpose.jit | TorchScript 序列化姿态模型 | 吃 ROI 图,输出关键点 |
| action.jit | 动作分类模型 | 吃关键点图,输出动作类别 |
| Dockerfile | 锁定运行环境 | torch 版本不符时优先看它 |
提示:动代码之前先跑一遍原装流程,确认环境通、模型能加载,再开始改路径和参数。我见过不少人一上来就改 detect.py 的阈值,结果环境根本没好,白折腾半天。
3. 先跑通 runOpenpose.py:把原始帧变成可训练的人体关键点图
runOpenpose.py 做的事情可以概括为三行:读图、加载 openpose.jit、把关键点画出来保存。别小看这一步,它同时承担两个任务:验证姿态模型是否正常工作,以及为后面的 action.jit 准备训练数据。这一步跑不通,后面 detect.py 的联调就无从谈起。
3.1 单图推理:runOpenpose.py 的输入输出
单张图片跑关键点提取,核心流程是固定的:读图、预处理、推理、后处理、画骨架、存图。项目里简化后的逻辑可以写成下面这样,函数名以你手里的实现为准,但流程就是这条线。
import cv2 from pathlib import Path def run_single(image_path, save_dir="data/test"): img = cv2.imread(str(image_path)) if img is None: # 图片读失败直接跳过 print(f"skip {image_path}") return # 预处理到 openpose.jit 要求的尺寸和通道顺序 tensor = preprocess(img) # BGR->RGB, HWC->CHW, 归一化 with torch.no_grad(): out = pose_model(tensor) # openpose.jit 推理 # 后处理:把置信度热图解析成关键点坐标 keypoints = postprocess(out) # draw_skeleton 负责画点和连线,最终存成 jpg canvas = draw_skeleton(img.copy(), keypoints) out_path = Path(save_dir) / f"{Path(image_path).stem}_kp.jpg" cv2.imwrite(str(out_path), canvas)代码说明:draw_skeleton 是 pose.py 里的画图函数,它只负责把关键点画到图上,真正控制保存位置的是调用它的地方。preprocess 和 postprocess 是项目的核心工具函数,openpose.jit 输入尺寸大小直接影响推理速度和精度,一般项目里会固定写一个 resize 尺寸,你如果要改,两个模型相关的地方都要同步改。
如果你是自己找的测试图,建议先挑一张人比较正、没有遮挡的全身照,能正常出骨架再换复杂场景。一张侧身、被桌子挡了一半的图跑出来骨架是残缺的,这不代表模型坏了,而是输入本身不适合。
3.2 保存路径和 159 行限制:动手前先改这两个地方
踩坑最多的是保存路径。pose.py 里 draw 方法最下面决定了关键点图存到哪,摘要里也点名了这一点。常见做法是在 draw 方法末尾加一个 save 参数,默认落到 data/test。如果你跑完发现什么也没存,先查这个位置,八成是目录不存在,或者写死成了某个绝对路径。
# pose.py draw 方法最下面的保存逻辑示例 def draw(self, img, keypoints, save_dir="data/test"): skeleton_img = draw_skeleton(img, keypoints) save_dir = Path(save_dir) save_dir.mkdir(parents=True, exist_ok=True) # 确保目录存在 out_path = save_dir / f"{Path(self.name).stem}_kp.jpg" cv2.imwrite(str(out_path), skeleton_img) print(f"saved to {out_path}")代码说明:mkdir(parents=True, exist_ok=True) 是习惯写法,避免目录不存在时 imwrite 静默失败;print 出实际保存路径,比你自己猜存到哪里要省事得多。
runOpenpose.py 第 159 行加的限制也要看一眼。常见做法是过滤掉置信度低于某个阈值的关键点,或者对单张图检测人数做了上限。这些限制直接影响后续 action 训练的数据质量,比如说你把置信度调太高,低对比度帧里很多关键点会被丢掉,出来的骨架图缺胳膊少腿,训练出来的 action.jit 也跟着不准。阈值调太低也不行,背景里的噪点会被当成关键点,骨架图上会多出莫名其妙的线。建议先按默认值跑一批图,看整体效果再动。
3.3 批量生成:给 action 训练备料
单张调试没问题之后,批量跑几十张图片做数据集,核心逻辑就是循环。这个阶段不要追求自动化到底,跑完必须人工抽查,骨架线断得厉害的图直接删掉,这类残缺样本留在训练集里,反而会把动作分类器带偏。
from pathlib import Path image_dir = Path("raw_images") out_dir = Path("data/test") for img_path in sorted(image_dir.glob("*.jpg")): try: run_single(img_path, save_dir=out_dir) except Exception as e: print(f"failed: {img_path}, error: {e}")代码说明:sorted 保证处理顺序稳定,方便排查某张图失败时按顺序回放;try-except 包住单张处理,避免一张坏图中断整个批任务。批量跑完检查两件事:一是生成数量对不对得上,二是随机抽十张看骨架是否完整。这两步过了,再进训练环节。
4. 再看 detect.py 主链路:yolo 框人、宽高比过滤、姿态判摔倒
detect.py 和 runOpenpose.py 不一样,它先过 yolo。这一步的目的是把画面里可能存在的人框出来,剪裁后再喂 openpose。如果你直接把整张图给 openpose,一是慢,二是背景里的非人物体会产生虚假关键点。yolo 在这里相当于一个守门员,先把画面压缩到“人”的范围内。
4.1 主链路第一棒:yolov5 出人框
加载 yolo 模型,跑一轮前向,拿到 person 类别的框,这是整个 detect.py 的地基。项目里你有现成的加载逻辑,优先复用,下面只是示意常见做法。
import torch # 优先复用项目里现成的 yolo 加载逻辑,下面只是示意 model = torch.hub.load("ultralytics/yolov5", "yolov5s", pretrained=True) model.conf = 0.4 # 置信度阈值,按场景调 model.classes = [0] # COCO 里 person 是第 0 类 results = model(frame) df = results.pandas().xyxy[0] person_boxes = df[df["name"] == "person"][ ["xmin", "ymin", "xmax", "ymax"] ].values代码说明:model.conf 和 model.classes 是推理阶段最常用的两个参数。conf 设太低会混入大量误检框,设太高会漏掉被遮挡的人;classes=[0] 是 COCO 类别索引,不同权重类别顺序要自己核对一次,别想当然。关于如何用 yolov5 训练模型这种问题,在这个项目里其实不必纠结,人检测部分直接用预训练权重就够了,真正要训练的是动作分类那一层。
拿到框之后,别急着直接往 openpose 里送。先看一眼框的尺寸,太小的框说明人离摄像头很远,就算提关键点也提不准,不如直接过滤掉,给后面省算力。
4.2 169 行的宽高比粗筛:一个阈值省一半算力
拿到框之后到抠图之前,detect.py 里有一行针对宽高比的判断。它解决一个实际问题:站姿人体的框总是高大于宽,摔下去后框会变得宽扁。用这个比值做粗筛,可以直接跳过很多不需要姿态判断的画面。这个方法朴素,但非常管用,尤其在固定摄像头的场景下。
def ratio_filter(box): # box 格式: xmin, ymin, xmax, ymax x1, y1, x2, y2 = box h = y2 - y1 w = x2 - x1 if w < 1: return False # 防止除零 ratio = h / w return ratio, h, w # 示例阈值:小于 0.8 视为疑似摔倒,进入 openpose 环节 for box in person_boxes: ratio, h, w = ratio_filter(box) if ratio < 0.8: roi = frame[int(box[1]):int(box[3]), int(box[0]):int(box[2])] keypoints = run_openpose(roi)代码说明:ratio 是框的高宽比,不是宽高比,这点容易看反。站姿时 h/w 明显大于 1,摔倒后小于 1,所以 0.8 这个阈值是按 h/w 算的。具体阈值要根据摄像头俯仰角调:俯视视角下,站姿的高宽比没有平视那么大,躺平的框也会被拉长,建议先采样几十帧真实画面看一眼比值分布,再定阈值,别照抄。
| 姿势 | 框高宽比 h/w | 信号 |
|---|---|---|
| 站姿 | 明显大于 1,比如 1.5~3.0 | 正常 |
| 蹲坐 | 接近 1 | 候选,需要姿态确认 |
| 跌倒/躺平 | 小于 1 | 高疑似,进入 openpose |
表格说明:这只是经验值,实际场景里人斜靠着、弯腰捡东西,比值都会落到“接近 1”的区域。所以宽高比只负责粗筛,它的价值是让 openpose 少跑空,而不是代替姿态判断。
4.3 抠图进 openpose:关键点把摔倒做实
过了粗筛的框,才算真正进入姿态环节。把原图 ROI 按框裁剪,resize 到 openpose.jit 输入尺寸,推理出关键点坐标。这时候光看框已经不够了,要结合关键点的几何关系做判断。我一般会算髋、膝、踝三个关键点的夹角,加上髋关节中心的实时高度,两个信号一起看。
import math def angle_at(a, b, c): # a: 髋, b: 膝, c: 踝,算的是膝盖处的夹角 ba = (a[0] - b[0], a[1] - b[1]) bc = (c[0] - b[0], c[1] - b[1]) len_ba = math.hypot(ba[0], ba[1]) len_bc = math.hypot(bc[0], bc[1]) if len_ba < 1e-6 or len_bc < 1e-6: return 180.0 # 关键点重合时给一个安全值 cos_a = (ba[0]*bc[0] + ba[1]*bc[1]) / (len_ba * len_bc) cos_a = max(-1.0, min(1.0, cos_a)) # 钳位到 [-1, 1] return math.degrees(math.acos(cos_a)) hip = keypoints["hip"] knee = keypoints["knee"] ankle = keypoints["ankle"] knee_angle = angle_at(hip, knee, ankle) # 膝关节角度突然变小 + 髋关节中心高度快速下移,两个信号同时触发才报摔倒 if knee_angle < 100 and hip_height < threshold: alarm()代码说明:math.hypot 算向量长度,省得手写平方根;acos 前的钳位是为了防止浮点误差导致 cos_a 超出 [-1,1] 范围,这是角度计算里最常见的数值翻车点。膝关节角度变小说明腿在弯曲,髋关节高度下降说明重心已经在地面附近,两个条件同时满足,误报率会低很多。action.jit 在这种场景里派上用场,它把关键点图直接分类成你定义好的动作类别,相当于给宽高比和角度判断加了一层更稳的兜底。
5. 从复现到落地:yolo+openpose 避坑与常见问题排查
这套流程整体不难,但我实际复现时踩掉的坑一个没少。挑五个最常见的记录在这,都是现象、原因、解决一条线说清。每一条都是真实环境里会遇到的,不是理论推演。
5.1 现象:data/test 输出目录是空的
runOpenpose.py 跑完没有任何报错,控制台正常结束,但 data/test 里一张关键点图都没有。这问题我一开始以为是代码没执行到保存逻辑,打断点看了才发现保存路径根本不是我以为的那个目录。
原因:draw 方法末尾的保存路径没有生效,多数是目录不存在,或者代码里写死成了别的绝对路径。更隐蔽的情况是代码里用了相对路径,而你是从项目根目录以外的地方启动的脚本,路径就指歪了。
解决:先手动确认 data/test 存在,再在 pose.py 的 draw 方法最下面加一行 print,把实际的 out_path 打出来,看它落到了哪里。路径不对就直接改成绝对路径,或者按上一章给的方式改成由参数传入。
5.2 现象:.jit 加载直接报错
torch.jit.load("openpose.jit") 抛出类似“Can't load script module”的错误。这个报错信息很模糊,第一次遇到时我查了半天,最后发现是 torch 版本问题。
原因:本地 torch 版本和导出 .jit 时的版本差太多,TorchScript 的序列化格式跨大版本不保证兼容,小版本差异通常没大事,但主版本不一致大概率翻车。
解决:看 Dockerfile 里锁的 torch 版本,装一个同主版本的环境,或者干脆把 .jit 用你当前环境重新导出一次。如果你只是想在另一台机器上快速验证,直接在 Docker 容器里跑更省心。
5.3 现象:openpose 抠出来的是半个人脸
yolo 框看着没问题,但把框裁出来给 openpose,骨架全贴在半张脸和肩膀上,腿的关键点一个都没有。这种情况在监控视角特别常见,因为人可能被门框、桌子、其他行人挡住一半。
原因:你给的框带进了大量背景,或者裁剪时直接用框坐标而没做边缘扩展,肢体被截断。另一个常见原因是框太小,resize 到 openpose 输入尺寸后,人的细节被拉糊了。
解决:裁剪 ROI 时做一个小扩展,上下左右各加 10 像素左右,再保证输入尺寸和 openpose.jit 训练时一致。同时检查 yolo 框的置信度和宽高比,太小的框直接跳过,别硬送进 openpose。
5.4 现象:蹲下、坐下被误报成摔倒
画面里的人蹲下系鞋带,系统报摔倒;坐在椅子上玩手机,也报摔倒。这是摔倒检测项目里被问得最多的问题,也是宽高比方案最明显的缺陷。
原因:只用了框宽高比,蹲姿和躺姿都会让框变扁,宽高比分辨不了这两种姿势。摔倒的本质是重心失控后的快速姿态变化,而不是单纯的“框变扁了”。
解决:宽高比只做粗筛,摔倒判定必须加关键点角度和高度变化作为第二道闸;action.jit 的输出也不要直接取 argmax 用,给置信度设个阈值,比如概率大于 0.8 才报警。时序信息也很重要,连续三帧都满足条件再触发,能把瞬时蹲下误报压下去。
5.5 现象:换 GPU 推理反而更慢
同一段视频,在 GPU 上跑 detect.py,帧率还不如 CPU 高,很多人遇到这个现象都以为代码写错了,其实不是。
原因:yolov5 和 openpose 都不是大模型,单帧推理的 kernel 启动开销和数据拷贝时间占了很大比例,GPU 并行能力还没发挥就结束了。另外 .jit 模型没有做 TensorRT 加速,GPU 上的收益本来就不明显。在树莓派这类边缘设备上,这个问题会更突出,整图跑 openpose 基本跑不动。
解决:先用 CPU 验证逻辑,确认流程没问题再考虑加速。实时场景优先把 openpose 换成 TensorRT 导出,或者只对 yolo 框内的小图做姿态推理,不要整图喂。边缘设备部署时,尺寸开小一点、置信度阈值调高一点,比堆硬件更实际。
6. 进阶:换姿势就得重训 action.jit,三步走不返工
摔倒检测只是这套代码的默认玩法,换成其他姿态检测,摘要里给了完整三步:收集图片跑 runOpenpose.py 得关键点图,按标签分类放 data/train 和 data/test,再跑 action_detect/train.py。这套流程本质上是“如何用 yolov5 训练模型”的思路在姿态分支上的变体,只不过这里训练的不是 yolo 检测器,而是动作分类器。
6.1 三步重训 action.jit
第一步收集图片,跑 runOpenpose.py 生成关键点图,这一步的数据质量决定最终效果。第二步按标签把关键点图分类放好,目录名就是标签名。第三步训练,命令常见做法是:
python action_detect/train.py --data data/ --epochs 50 --batch-size 16 --lr 1e-3参数说明:epochs 设 50 是保守值,动作分类网络很小,几十张图每类也能收敛;batch-size 16 在普通显卡上足够,显存不够就降到 8;lr 1e-3 是这类任务比较通用的起点,loss 不降的时候再降到 1e-4。如果 train.py 不接收命令行参数,常见做法是改脚本里的配置变量,效果一样。
6.2 训练前强制检查:标签统一和样本质量
很多人在第二步翻车,不是图片跑不出来,而是目录命名不规范。文件夹名大小写混用,train.py 按目录名生成标签时会当成两个不同类别,评估矩阵直接乱掉。还有一类问题是样本不均衡,fall 500 张、walk 50 张,训练出来的模型大概率把所有动作都判成 fall。
我第一次重训时把 fall 和 Fall 两个目录混着放,训练完看混淆矩阵才发现多了两个标签,数据白跑一轮。从那以后,我每次准备姿态数据都强制走一遍:统一小写命名、逐张目检关键点图、确认每类样本量均衡,再喂给 action_detect/train.py,基本不再返工。这套检查流程花不了十分钟,但能省下后面调模型的半天时间,希望帮到你。
本文还有配套的精品资源,点击获取