☰
双模型协作实现精准摔倒检测:YOLOv5框人+OpenPose看骨架
2026/10/10 11:41:01 网站建设 项目流程

简介:一份面向人工智能学习者的YOLOv5人体检测与OpenPose姿态检测综合项目包,聚焦摔倒检测场景,适合有Python基础、希望实践目标检测与姿态估计结合应用的开发者。压缩包共183个文件,包含75张jpg/jpeg样本图、39个py源码脚本、21个pyc缓存、17个yaml配置、7个txt说明及5个xml标注,另有预训练pt模型与OpenPose的jit模型,总大小40.24MB,目录划分清晰便于直接运行和二次开发。已有1064人学习下载,项目覆盖从数据准备到模型训练的全流程:运行runOpenpose.py可提取人体关键点图并自动保存至data/test,为后续jit模型训练提供标注数据;detect.py先利用YOLO检测行人,再依据框宽高比筛选并裁剪人体区域,交由OpenPose完成姿态识别,实现摔倒判断。若需扩展其他姿势分类,可自行采集图片、生成关键点图并按类别放置data/train与data/test,执行action_detect/train.py即可训练;其中关键裁剪与限制逻辑均留有修改注释,便于初学者复现与调优。

1. 摔倒检测不是玄学:yolov5 框人、openpose 看骨头,双模型分工才是正解

做过监护类项目的人都有体会:摔倒检测看起来是「视频里判断一个人是不是倒了」,真上手才发现,视频里人姿态千奇百怪,蹲下系鞋带、弯腰捡东西、躺下休息,全都长得像摔倒。直接拿分类网络去识别「摔倒」这个动作,结果就是误报率高到没法用。这个项目给的思路很实在——不直接识别摔倒,而是把任务拆成两步:先用 yolov5 做人体检测,把人从画面里框出来,再用 openpose 做姿态检测,提取出人体关键点的坐标和骨架结构。摔倒这个动作,本质上是人体关键点在空间上的瞬间重构:躯干从竖直变成水平,髋关节和肩关节的相对位置发生剧烈变化。yolov5 负责「人在哪」,openpose 负责「人是什么姿势」,两步各干各的活,比端到端的摔倒分类网络可控得多。这个资源适合正在做安防告警、老人看护、康养设备、甚至是健身房动作识别的开发者,拿到手不是跑通一个 demo 就完事,而是能从检测框架到姿态分类训练,完整走一遍。关键是它还留了训练接口——你想识别的不只是摔倒,哪怕是举手求救、蹲地不起,都能按它给的流程自己收数据、自己训分类器。整个工程文件不复杂,核心就几个入口文件,但逻辑链完整,属于那种「拿到就能跑、跑完能改、改完能部署」的实战包。

2. 项目文件拆解与实际运行:先分清哪个文件是入口,哪个是训练产出的模型

2.1 压缩包里到底有什么:文件角色一目了然

项目目录里没有那么多花哨的东西,但文件角色特别容易搞混。很多人第一次解压,看到labels.cache、Dockerfile、yolov5.iml以为是项目核心,其实那些是 PyCharm 工程配置和缓存文件,真正要盯紧的是这几个:detect.py、runOpenpose.py、pose.py,以及action_detect/train.py。9fe027d4f9638c7f25673dc70312518d.jpeg这一串哈希命名的图片是测试图,不用管。openpose.jit是 openpose 的 TorchScript 模型文件,action.jit是已经训练好的动作分类模型——这两个jit文件的区别必须搞清楚:openpose.jit是把图像变成人体关键点坐标的模型,action.jit是把关键点序列分类成「正常 / 摔倒 / 其他姿态」的模型。前者是骨架提取,后者是动作判定,两个模型串联才是完整的摔倒检测链。

2.2 运行起来的第一步:先跑 openpose 看关键点效果

我建议任何人拿到这个项目,别急着跑完整检测,先单独跑runOpenpose.py。这一步看起来只是「获得人体关键点图」,实际上是验证你的环境能不能正常加载openpose.jit、CUDA 能不能用、关键点绘制是不是正常。命令很简单:

python runOpenpose.py --source data/test/xxx.jpg

这个文件会读取图片,加载 openpose 模型,然后把检测到的关键点画在图上,保存到data/test目录。很多人在这一步就翻车——不是模型加载失败,而是关键点图保存路径不对,后面训练动作分类器时根本找不到数据。关键点在pose.py的draw方法最下面,那里有一行保存逻辑:

# pose.py dip.draw 方法尾部 cv2.imwrite(os.path.join(save_dir, filename), out_image)

save_dir这个变量就是关键点图的输出目录,默认可能是空字符串,结果图片直接写到当前工作目录,后面归类训练数据时找不到。我第一次跑就吃过这个亏,找了半天图在哪,最后发现全堆在项目根目录。先把save_dir改成data/test,和训练时的data/test对齐,否则后面train.py读不到数据。

2.3 完整检测主链路:detect.py 里人框裁剪与宽高比判断

跑通了 openpose 单测环节,再进主链路detect.py。这个文件的逻辑是:先做 yolo 目标检测,把所有检测到的人框出来,然后对每个人框做一次预处理,把人的图片区域抠出来,送到 openpose 做姿态估计。整个流程的核心代码在这个位置:

# detect.py 约 160-180 行,核心逻辑 for *xyxy, conf, cls in det: if int(cls) == 0: # 只处理 person 类别 x1, y1, x2, y2 = [int(i) for i in xyxy] w = x2 - x1 h = y2 - y1 ratio = h / w if ratio < 0.8: # 宽高比阈值,摔倒时人体框趋近于横向 # 把这个人的区域裁剪出来,送 openpose person_img = frame[y1:y2, x1:x2] key_points = openpose_infer(person_img)

这里有个非常实用的工程决策:摔倒时人体是横躺的,人框的宽高比会从正常的 1.5~2.5 掉到 0.8 以下。所以detect.py先用宽高比做一次粗筛,宽高比异常的框才送 openpose 做精细姿态判定。这不是 yolo 的标准能力,是作者在这个项目里加的条件判断,后续你想调整灵敏度,改的就是这个0.8。注意它不是只靠这个粗筛就下结论,而是宽高比异常后仍然要送 openpose 看关键点,双重确认。我在实际部署时,把这个阈值改成 0.9 试过,误报多了不少,因为有的人坐着翘二郎腿,人框也是偏横向的。这个值调起来要看你的摄像头安装角度,俯视视角和水平视角差别很大。

2.4 修改之后重新加载:两套模型串起来的推理链路

整个detect.py跑完一次,相当于执行了「yolo 检测→裁剪→openpose 关键点提取→action 分类」四步。后面两步的衔接在runOpenpose.py的 159 行附近,那里加了限制条件——具体是过滤置信度低的关键点,还是限制最小检测框尺寸,要看你自己改。我一般会在这一步加一个「关键点数量下限」:openpose 输出 25 个关键点(BODY_25 模型),如果检测到的有效关键点少于 10 个,说明这个框里的画面质量太差,直接丢弃,不送进动作分类器。这样能挡掉一部分遮挡严重的场景,比如人躲在桌子后面只露半个头,那种情况本来也判断不了姿态。

这个双模型管线跑通后,性能瓶颈几乎都在 openpose 上。yolo 检测 1080p 画面用 yolov5s 大概 20-40ms,openpose 的 jit 模型跑一次姿态估计在 GPU 上大约 60-100ms,合起来单帧大约 100ms 出头,也就是 8-10 FPS,做实时告警够用,做流畅视频分析会卡。想要提速,可以把 yolo 的输入尺寸从 640 降到 416,或者把 openpose 的输入分辨率从 368x368 降到 256x256,代价是远端小目标的姿态识别准确率下降。这里没有免费的午餐,调的时候要看你的监控距离。

3. 核心检测逻辑拆解:宽高比粗筛 + 骨架精判,两层把关更稳

3.1 人体框宽高比的理论依据与动手调整

我之前说过,正常站立的人体框宽高比(h/w)在 1.5~2.5 之间,这个数不是拍脑袋定的,而是人体直立姿态下,肩宽约 40-50cm,身高约 160-180cm,再加上 yolo 框会包含一些边缘空间,算下来自然落在这个区间。一旦摔倒,躯干水平,框的宽度变成原来的身高尺度,高度变成肩宽尺度,宽高比直接翻转,变成 0.3~0.7。所以宽高比是一个计算成本极低的强特征,几乎不消耗 GPU,就能把画面里 90% 的正常站立人形排除掉,让 openpose 只处理可疑目标。

这个阈值的调节方法,我建议不要拍脑袋定,而是用一段小脚本统计自己场景下的数据。找一个部署现场的摄像头,录 10 分钟正常视频,用 yolo 跑一遍,把所有检测到的人框宽高比落下来,看分布:

python -c " import torch model = torch.hub.load('ultralytics/yolov5', 'yolov5s', pretrained=True) # 统计一批画面中所有 person 框的 h/w 分布 # 正常站立者多集中在 1.5-2.5,若你的安装角度偏俯视,这个分布会整体变小 "

俯视摄像头底下,正常站立的人框宽高比也会偏小,可能掉到 1.0 左右,这时候阈值设 0.8 就太高了,得跟着现场调。我做过一个卫生间跌倒检测项目,摄像头装在墙角高处俯视,正常站立的人框宽高比只有 1.1-1.3,摔倒时反而变成 1.5——整个判断逻辑都得反过来。所以这个 0.8 阈值必须按「现场拍一段视频,统计自己场景的分布」去定,不能照抄。

3.2 openpose 关键点提取:从图像到骨架坐标的映射

粗筛过了之后,进入姿态精判。openpose 接收裁剪出的人体图像,输出 18 个(COCO 模型)或 25 个(BODY_25 模型)关键点的坐标和置信度。每个关键点的结构是(x, y, confidence)三元组。关键部位如下:鼻子(0)、颈部(1)、左右肩(2/5)、左右肘(3/6)、左右腕(4/7)、左右髋(8/11)、左右膝(9/12)、左右踝(10/13)。摔倒检测最常用的是髋关节和肩关节的连线方向,以及踝关节和髋关节的垂直距离。站立时,肩到髋的连线基本垂直于地面;倒地时,这条线趋于水平。

在实际工程里,我不会直接把原始坐标拿去喂分类器,而是会做一次归一化——把人体框的尺寸作为基准,把人脸坐标转换成相对坐标,避免远处的人和近处的人因为像素尺度不同造成误判。常见做法是把所有关键点减去颈部坐标,再除以肩宽(左肩到右肩距离),得到一个尺度不变的特征向量。这个向量会作为动作分类器的输入。整个推理片段像这样:

# 将 openpose 输出的关键点转化为尺度不变特征 neck = key_points[1] shoulder_l = key_points[2] shoulder_r = key_points[5] shoulder_width = dist(shoulder_l, shoulder_r) + 1e-6 feat = [] for kp in key_points: feat.append((kp[0] - neck[0]) / shoulder_width) feat.append((kp[1] - neck[1]) / shoulder_width) # feat 是一个 50 维向量(BODY_25 则为 75 维),送入 action.jit

这里的1e-6是防除零的,虽然实际中 shoulder_width 几乎不可能为 0,但写推理代码要养成这个习惯。这个特征向量计算是纯 CPU 操作,耗时可以忽略不计。有了这个稳定的特征形式,后面训练和推理用的数据就统一了。

3.3 动作分类器与推理逻辑:不只是看骨架角度

拿到尺度不变特征后,最后的判定交给action_detect/train.py训练出的动作分类器。这个分类器用的是全连接网络,输入维度是关键点数的两倍(x/y 坐标),输出维度是姿态类别数。项目自带的action.jit训练的类别是「正常 / 摔倒」二分类,你如果要扩展「举手求救」这类动作,就得重新训练。训练数据怎么来?就是第 4 章将详细讲的三步流程:收集图片→生成关键点图→分类归档。说一句题外话:在这个分类任务里,我踩过的坑是「二分类太粗暴」。蹲下捡东西和摔倒,关键点特征高度重合,只有时序信息才能区分——蹲下是「慢速下移然后上移」,摔倒一般是「快速下移然后静止」。如果你只做单帧分类,这两个动作就是几乎一样的输入。所以我在自己项目里会对 action 分类器的输出做时间维度的二次过滤:连续 3-5 帧被判为摔倒才触发告警,单帧触发几乎必然是误报。这个项目本身没做时序建模,但它的单帧关键点提取稳定,你完全可以在它的输出之上加一个 3 帧滑窗投票,误报率能下降一个数量级。

4. 训练自己的动作分类器:从收图到 jit 导出的完整流程与避坑记录

4.1 第一步:收集图片并生成关键点图

如果要识别的姿势不只是摔倒,比如想加一个「举手求救」类别,流程第一条就是收集图片跑runOpenpose.py。作者原话说得很清楚:

1. 收集图片,跑 runOpenpose.py 文件获得人体的关键点图 2. 对人体的关键点图根据自己想要的进行分类放在 data/train 和 data/test 3. 跑 action_detect/train.py

这个流程的关键是:训练数据不是原始图片,而是 openpose 输出的关键点图——画好骨架的图。骨架图是训练集,不是原图。为什么这么做?因为骨架图已经剔除了衣服、背景、光照的干扰,分类器学习的是纯粹的姿态结构,泛化能力更强,数据量需求也更小。我实测下来,每个类别准备 200-300 张骨架图,训练出来的分类器在真实场景里就够用了,但如果用原图训练,这个数量级根本不够。收集图片时注意姿势多样性:不同身高的人、不同距离、不同拍摄角度,这样骨架图的长宽比例会更丰富。关键点图保存在data/test中,位置由pose.pydraw方法的最下面控制,建议把save_dir参数统一设置为你的训练集根目录,免得后面整理文件时还得手动搬。

4.2 第二步:按类别归档训练数据

data/train和data/test的目录结构直接决定train.py怎么读数据。作者的设计意图很直白——train.py是从这两个目录扫描图片,用子文件夹名作为类别标签,目录应该这样组织:

data/ ├── train/ │ ├── normal/ # 正常站姿、行走的骨架图 │ │ ├── 001.jpg │ │ ├── 002.jpg │ ├── fall/ # 摔倒、躺地的骨架图 │ │ ├── 001.jpg ├── test/ │ ├── normal/ │ ├── fall/

注意保持 train 和 test 的类别结构完全一致,子文件夹名字就是类别 ID,训练完保存 jit 时类别映射就是按这个目录树来的。如果 train 里有两个类别,test 里有三个,训练会直接报错。我在整理时还有个习惯——每个类别的图片数量尽量均衡,二分类时两边都保持在 300 张左右,最多不要超过 1:2 的比例,否则小样本类别会被大样本淹没。

4.3 第三步:训练流程与train.py核心参数

action_detect/train.py是完整的 PyTorch 分类训练脚本。核心代码结构大致如下:

# action_detect/train.py 核心训练逻辑 model = SimplePoseClassifier(num_classes=len(classes)).to(device) criterion = nn.CrossEntropyLoss() optimizer = torch.optim.Adam(model.parameters(), lr=1e-3) for epoch in range(50): for images, labels in train_loader: outputs = model(images) loss = criterion(outputs, labels) optimizer.zero_grad() loss.backward() optimizer.step() # 训练完导出为 TorchScript traced_model = torch.jit.trace(model, example_input) traced_model.save('../action.jit')

参数含义:num_classes是类别数,二分类就是 2;lr=1e-3是初始学习率,一般 50 个 epoch 里会在第 30 轮降到 1e-4,防止后期震荡;epoch=50对于几千张图的小数据集完全够用。我一般会把 batch size 设成 16 或 32,视显存而定。重点说下torch.jit.trace导出这一步——这就是action.jit的来历。trace 方式导出的模型要求输入尺寸固定,如果你的推理输入是 1x3xHxW 的图,导出时的example_input必需是同一个 size。睁大眼睛看:如果导出用的example_input是 1x3x64x64,但detect.py推理时给的是 1x3x128x128,运行会直接报 shape 不匹配错误。这是 jit 导出最高频的坑。

4.4 避坑记录:这五个问题几乎人人都会遇到

项目跑起来很容易,但跑顺很难,我梳理了五条高频坑,按「现象→原因→解决」给全。

坑一:runOpenpose.py跑完没有任何输出图现象:程序正常结束,控制台无报错,但data/test目录下没有新图片。原因:pose.pydraw方法里的保存路径指向的是当前工作目录,不是data/test。解决:打开pose.py找到cv2.imwrite,把保存路径改成os.path.join('data/test', filename),并且确保data/test目录存在。

坑二:detect.py检测结果没有把摔倒的人框出来现象:人明明倒地了,yolo 检测框也出来了,宽高比条件不满足,代码根本没进入 openpose 流程。原因:宽高比阈值 0.8 不适用于你的摄像头安装角度。解决:统计自己场景的人框宽高比分布,用第 3.1 节的脚本统计,根据实际数据重新设阈值。我在俯视场景下就把ratio < 0.8改成了ratio > 1.2,判断逻辑方向完全不同。

坑三:torch.jit.load('openpose.jit')报错或加载后推理异常现象:加载模型报RuntimeError: Unexpected block或直接段错误。原因:PyTorch 版本不兼容。openpose.jit 是用特定 PyTorch 版本 trace 导出的,不同版本之间 TorchScript 序列化格式可能不兼容。解决:查看项目环境文件里的 PyTorch 版本,严格按那个版本装环境。通用方案是检查Dockerfile里锁定的基础镜像,照着配环境最稳。

坑四:训练损失不下降,准确率一直在 50% 左右现象:train.py跑了几十轮,loss 在高位震荡,val accuracy 没有明显上升。原因:骨架图数据质量差,或者是训练集和测试集的类别划分反了——train 和 test 目录下图片内容几乎一样,分类器学到的是记忆而非泛化。解决:检查训练集里每个类别的图是否来自不同的人、不同的动作幅度。如果所有「摔倒」图都来自同一个人同一个角度,模型根本学不到通用摔倒特征。另外,确认 train 和 test 的图片不是一份文件复制两份。

坑五:部署到树莓派或 RK 平台后帧率掉到 1 FPS 以下现象:本地 GPU 跑得好好的,换到边缘设备上卡到没法用。原因:openpose 模型计算量大,在 CPU 上单帧推理要几百毫秒到几秒。解决:先把 openpose 的输入分辨率从 368x368 降到 256x256,再把 yolo 模型从yolov5s换成yolov5n。这样精度会掉,但至少能跑到 3-5 FPS。如果要做量化(比如 RK3568 平台),优先量化action.jit分类器,它结构简单,量化后精度损失很小;openpose.jit量化后关键点坐标会偏,不建议动。

5. 验证召回与误报:trace 输入灰度图统计,精度与速度之间的取舍技巧

把整个管线跑通之后,最容易被忽视的是验证环节。很多人只看一眼视频里能不能框出摔倒的人,就急着部署,结果到了现场误报率惨不忍睹。我习惯做一个专门的「阈值标定实验」:从部署现场录一段包含正常行走、弯腰、蹲下、躺下、坐下多种姿态的测试视频,用detect.py跑一遍,把每次触发告警的宽高比值和动作分类置信度全部输出成 CSV,然后统计不同阈值下的召回率和误报率。做法是给detect.py加一个--debug_csv参数,把检测到的每个目标的ratio、action_score、final_result都写入文件。然后跑完视频看分布:final_result == 1的样本中,有多少是真正的摔倒,有多少是弯腰捡东西。如果误报集中在ratio在 0.7 到 0.9 区间的样本,那说明宽高比阈值要收紧到 0.7;如果误报集中在action_score刚过阈值(比如 0.6-0.7)的样本,那就要调分类器的判定阈值。这套验证流程做下来,比凭感觉调参有用得多。另有一个很容易被忽略的技巧:openpose 对灰度和彩色图的敏感度不同——如果你部署的摄像头是红外夜视黑白画面,一定不要用白天彩色图训练出的分类器直接上,至少得补一批灰度图做数据增强。做法很简单:训练前在数据加载器里加一个随机灰度化:

# 训练数据增强:灰度图增强,适配夜视场景 if random.random() < 0.3: img = cv2.cvtColor(img, cv2.COLOR_RGB2GRAY) img = cv2.cvtColor(img, cv2.COLOR_GRAY2RGB)

30% 的概率随机转灰度,能让分类器在黑白夜视摄像头下不掉精度。这是个成本极低但收益明显的技巧。速度与精度的取舍,最终体现在你想部署的平台和帧率需求上。如果只需要 1 帧/秒的检测频率(摔倒告警场景通常足够),那么在树莓派 4B 上可以保持 yolo 输入 640、openpose 输入 368 的完整精度;如果需要实时 10 FPS 以上,那就只能缩小输入,接受误报率小幅上升。从那以后我每次拿到这类双模型检测项目,都会强制走一遍「先统计宽高比分布、再标定动作分类阈值、最后做灰度增强验证」的流程,不跳过任何一步。希望帮到你。

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

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

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

立即咨询