ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

视频与摄像头下的人体形态识别:OpenPose原理与工程实践

视频与摄像头下的人体形态识别:OpenPose原理与工程实践

简介:一套以 Python 语言为核心、结合 OpenCV 与 OpenPose 的人体姿态识别实战资源,面向计算机视觉学习者、AI 开发者和需要快速搭建动作捕捉方案的工程师。资源针对视频与摄像头两种输入方式,实现了从实时视频流读取、人体关键点提取到结果可视化的完整流程,并包含训练脚本、模型定义、关键点后处理工具与自定义数据集说明,便于二次开发与扩展;源码中还涉及单欧元滤波等稳定性优化技巧,可直接借鉴。压缩包共40个文件,主体是19个 Python 源码,覆盖训练、推理、演示与工具模块,同时包含已编译的 pyc 缓存、说明文档、演示视频和预览图,整体仅 8.12MB,目录结构清晰。目前已有7000余人学习下载,是入门人体姿态识别与 OpenPose 应用的实用参考,可支撑运动分析、健身指导、虚拟现实交互等场景,适合希望快速上手关键点检测的开发者。

1. 视频和摄像头下跑通人体形态识别,为什么比想象中难

很多人第一次用 Python+OpenCV+OpenPose 做人体形态算法识别,都会卡在同一个地方:骨骼点倒是出来了,但离“识别形态”还差很远。视频文件里能画出 18 个关键点,换成摄像头实时推流就疯狂掉帧;离线跑得好好的程序,换到另一台机器上直接编译不过。这个方向真正的门槛不在调用 OpenPose 生成骨架,而在把“关键点坐标”换算成“站立 / 下蹲 / 跌倒 / 举手”这种可用的形态判断,以及让同一套流程同时扛住视频文件和摄像头两种输入。我接下来会先把 OpenPose 的原理和选型讲清楚,再给出视频和摄像头统一接入的最小流程、必调参数和踩坑记录,最后落到一个能直接抄走的形态判断实现。

2. 人体形态识别为什么离不开 OpenPose:PAF 原理与模型选型

2.1 关键点检测是“形态识别”的地基

“人体形态”落到算法层面,其实就是关节点的几何关系:站立时膝关节接近 180 度,下蹲时膝关节小于 90 度,跌倒时躯干与地面的夹角突然变小。所以形态识别的第一步永远是姿态估计——把每个人身上的关键点坐标找出来,后面所有判断都建立在坐标之上。

OpenPose 输出的不是检测框,而是一个形状为 (人数, 关键点数, 3) 的数组,最后一维是 x、y 和置信度。它的核心设计是两分支网络:一个分支预测关键点置信图,回答“每个关节在画面哪个位置”;另一个分支预测部分亲和场,回答“哪些关键点属于同一个人、应该怎么连起来”。PAF 的做法是对每对相邻关键点(比如肩和肘)在连线区域编码一个方向向量,解码时沿着向量做积分,得到两个关键点之间的关联分数,最后用贪心匹配把零散的点组装成完整骨架。这套机制让它天然支持多人场景,不会因为两个人靠近就把骨架接错。

2.2 COCO 18 与 BODY_25:两张模型的适用边界

OpenPose 官方提供 COCO 和 BODY_25 两套主流模型,选错会直接影响后续角度计算。COCO 模型输出 18 个关键点,序号 0 到 17 依次是鼻子、颈部、右肩、右肘、右腕、左肩、左肘、左腕、右髋、右膝、右踝、左髋、左膝、左踝、右眼、左眼、右耳、左耳。这套覆盖了躯干和四肢,做跌倒检测、动作分类、安防监控足够。BODY_25 则在此基础上加了中髋点和双脚的各三个点,适合步态分析、运动捕捉这类需要脚部细节的场景,代价是模型体积更大、推理更慢。

模型关键点数覆盖部位适合场景
COCO 1818躯干、四肢、面部五官通用姿态、跌倒检测、动作分类
BODY_2525COCO 全部 + 中髋 + 双脚各 3 点步态分析、舞蹈动捕、精细运动分析
MPI 1515躯干、四肢,无面部老项目兼容,新项目不推荐

对绝大多数“人体形态算法识别”项目,我建议从 COCO 18 起步。关键点少,计算量低,绘制和角度计算的代码也简单。等确认 COCO 满足不了脚部需求时再切 BODY_25,切换时只需改模型配置和绘制连接表,算法逻辑不用动。

2.3 OpenPose、MediaPipe、MoveNet:三选一怎么拍板

现在姿态估计方案不少,但 OpenPose 在“多人 + 可解释 + 稳定姿态输出”这条路上依然是最稳的选择。MediaPipe Pose 轻量、对 CPU 友好,但官方侧重单人或少量人,多人交叉时骨架容易串;MoveNet 速度快、适合嵌入式设备,但在遮挡和多人场景下不如 OpenPose 稳。OpenPose 框架比较重,安装编译也麻烦,换来的是学术界验证过的 PAF 关联机制和明确的关键点置信度,这对形态识别非常关键——你拿到每个点都带置信度,可以在代码里主动过滤不可靠的点,而不是黑匣子式地相信输出。

方案多人支持模型体积CPU 友好度输出置信度
OpenPose强大低每个点都有
MediaPipe弱小高有但不细
MoveNet中小高有

简单说:如果做单人互动类应用,MediaPipe 或 MoveNet 更省事;如果做安防、康复、运动分析这类需要同时处理多人、又要根据关节角度做精确判断的项目,直接选 OpenPose,不要犹豫。

3. 安装与模型加载:先把 OpenPose 从黑匣子变成可调用组件

3.1 环境核对清单:CUDA、cuDNN、OpenCV 的版本搭配

OpenPose 安装最大的玄学不是代码本身,而是机器环境。先别急着编译,花五分钟核对三样东西:NVIDIA 驱动版本、CUDA Toolkit 版本、cuDNN 版本。驱动决定显卡能不能被识别,CUDA Toolkit 决定编译期工具链,cuDNN 决定运行时神经网络加速是否生效。任何一层对不上,都会在编译或启动阶段报出莫名其妙的错误。

nvidia-smi nvcc --version python -c "import cv2; print(cv2.__version__)"

nvidia-smi 看的是驱动支持的 CUDA 版本,nvcc 看的是当前默认激活的 CUDA Toolkit 版本。这两个数字允许不一样,但编译 OpenPose 时用到的必须是 nvcc 对应的那套,否则后面 cmake 会找到错误的 CUDA 路径。OpenCV 版本建议直接看 python 里 import 的结果,系统里如果有多个 OpenCV,要保证编译 OpenPose 时链接的和你运行时 import 的是同一个。我见过最典型的翻车现场是:系统里有 conda 的 OpenCV 4.x,也有 apt 装的 OpenCV 3.x,编译时链接到旧的,运行时加载新的,直接段错误。

3.2 源码编译与 Python 绑定:两种做法怎么选

OpenPose 官方源码编译是生产环境最可靠的路线。虽然要装 CMake、GCC,编译过程可能让你怀疑人生,但编译一次之后 Python 绑定基本不会再出问题。另一种做法是直接用第三方基于 TensorFlow 的移植版,pip 就能装,API 更符合 Python 习惯,但模型精度和时效性都不如官方 Caffe 实现,遇到问题也没人管。我一般会这样选:跑 demo 图快用第三方移植版;要做完整项目、要稳定多人姿态输出,老老实实编译官方源码。

git clone <官方仓库地址> cd openpose mkdir build && cd build cmake -DBUILD_PYTHON_API=ON \ -DBUILD_EXAMPLES=OFF \ -DBUILD_CAFFE=ON \ -DUSE_CUDA=ON \ -DUSE_NCCL=OFF \ -DCMAKE_BUILD_TYPE=Release \ .. make -j$(nproc)

这里几个参数按顺序解释:BUILD_PYTHON_API 决定是否生成 Python 绑定,不开这个你只能拿 C++ 写;BUILD_EXAMPLES 关掉能省大量编译时间,示例代码你根本看不过来;BUILD_CAFFE 让工程自己编译内置 Caffe,避免去匹配系统里的 Caffe 版本;USE_CUDA 为 ON 走 GPU,如果你的机器没有 NVIDIA 显卡,改成 OFF 走 CPU 模式,但帧率会低很多,实时摄像头场景基本不现实。编译完成后,Python 绑定产物在 build/python 目录下,使用前需要把它加进 PYTHONPATH。

3.3 模型成对放好:prototxt 和 caffemodel 加载闭环

编译只是第一步,模型文件才是真正的“运费大头”。OpenPose 不把模型打进仓库,需要单独下载后放到 models 目录。下载慢的问题在国内环境尤其明显,我建议用支持断点续传的下载工具,别用浏览器硬扛。模型文件必须成对出现:prototxt 是网络结构描述,caffemodel 是训练好的权重,少一个都起不来。COCO 对应 pose_deploy_linevec.prototxt 和 pose_iter_440000.caffemodel,BODY_25 对应 pose_deploy_linevec_body25.prototxt 和 pose_iter_584000.caffemodel。文件名里的迭代数字不同,千万别混着用。

from openpose import pyopenpose as op params = { "model_folder": "./models/", "net_resolution": "656x368", "num_gpu": 1, } op_wrapper = op.WrapperPython() op_wrapper.configure(params) op_wrapper.start()

这段代码是加载模型的最小闭环。model_folder 指向存放 prototxt 和 caffemodel 的目录,OpenPose 启动时会扫描目录里有哪些文件,自动选择合适的模型。net_resolution 是网络输入分辨率,这里用的 656x368 是官方默认值,后面章节会细讲怎么调。num_gpu 设为 1 表示只用第一块 GPU。如果启动时读取模型失败,八成是文件没放对位置,或者 prototxt 和 caffemodel 不是一对。

注意:运行时如果提示找不到 pyopenpose,先把 build/python 目录加入 PYTHONPATH,或者用 sys.path 手动插入路径。这是编译版最常见的启动报错。

4. 视频与摄像头统一帧管线:一个入口、两路输入、一套绘制

4.1 VideoCapture 的统一入口:路径还是设备索引

OpenCV 的 VideoCapture 最方便的地方在于:视频文件和摄像头用的是同一个读取接口,传入字符串就是文件路径,传入整数就是摄像头设备索引。这个统一入口让我们可以把输入源抽象成参数,整个识别流程不需要关心画面来自哪里。摄像头索引 0 通常是内置摄像头,1 和 2 是外接 USB 摄像头,笔记本上有时候内置摄像头反而是 -1,具体要看设备枚举。代码里不要硬写,先写一个探测函数把所有候选索引都试一遍。

import cv2 def open_source(source): cap = cv2.VideoCapture(source) if not cap.isOpened(): raise IOError(f"无法打开视频源: {source}") return cap def find_camera_index(): for idx in range(3): cap = cv2.VideoCapture(idx) if cap.isOpened(): cap.release() return idx raise IOError("没有找到可用摄像头")

source 既可以是 "/path/to/video.mp4",也可以是整数索引。find_camera_index 逐个尝试 0、1、2,返回第一个能打开的索引,避免用户在命令行里瞎猜。注意 VideoCapture 构造函数本身不抛异常,摄像头打不开时 isOpened() 返回 False,所以必须显式检查。有些开发者在 Windows 上遇到摄像头被占用的情况,isOpened() 照样返回 True 但 read() 返回空帧,这种只能在读取循环里靠 ret 标志兜底。

4.2 跳帧推理:为什么实时场景不能每帧都算

OpenPose 对单帧的推理时间远大于 OpenCV 读取一帧的时间。视频文件还好,最多慢一点;摄像头场景下如果逐帧推理,画面延迟会持续累加,人走过去两秒后骨架才跟上,这种延迟直接让形态识别失去意义。解决办法是跳帧:每 N 帧只对其中一帧做推理,其余帧直接丢弃或沿用上一帧的骨架结果。跳帧数要结合推理速度和业务实时性要求来权衡,一般从 2 开始试。

import time FPS_TARGET = 15 # 目标输出帧率 INFERENCE_SKIP = 2 # 每 3 帧推理 1 帧 cap = open_source(camera_index) frame_idx = 0 fps_timer = time.time() fps_count = 0 while True: ret, frame = cap.read() if not ret: break if frame_idx % (INFERENCE_SKIP + 1) != 0: frame_idx += 1 continue # 这里执行 OpenPose 推理和形态判断 pose_results = inference(frame) fps_count += 1 if time.time() - fps_timer >= 1.0: print(f"当前帧率: {fps_count} FPS") fps_timer = time.time() fps_count = 0 frame_idx += 1

INFERENCE_SKIP 是跳帧核心参数,设 2 表示每 3 帧推理一次。如果你的机器推理一帧要 100 毫秒,不跳帧输出帧率只有 10 FPS 且延迟递增;跳帧后推理开销摊到 3 帧里,输出能稳定在 20 FPS 以上,延迟也不累积。帧率统计用 time.time 做每秒计数,能直观看到跳帧策略有没有生效。调这个参数时观察两个指标:一是 FPS 数字,二是画面里骨架和实景的重合程度,后者比前者更能反映实际体验。

4.3 从关键点到骨架:置信度过滤与绘制

OpenPose 返回的每个关键点都带置信度,阈值以下的点基本是乱预测的,画出来就是满屏鬼点。绘制时要先过滤,再连线。骨架连接对要按模型标注来,COCO 18 的连接关系是固定的:颈部连双肩和双髋,双肩连双肘,双肘连双腕,双髋连双膝,双膝连双踝,左右髋之间也连一条线。画线用 cv2.line,画点用 cv2.circle,颜色用 BGR 三元组。

COCO_SKELETON = [ (1, 2), (1, 5), (2, 3), (3, 4), (5, 6), (6, 7), (1, 8), (1, 11), (8, 9), (9, 10), (11, 12), (12, 13), (8, 11) ] def draw_skeleton(frame, keypoints, threshold=0.3): # keypoints shape: (人数, 18, 3) for person in keypoints: visible = {} for idx, (x, y, conf) in enumerate(person): if conf > threshold: visible[idx] = (int(x), int(y)) cv2.circle(frame, (int(x), int(y)), 3, (0, 255, 0), -1) for (a, b) in COCO_SKELETON: if a in visible and b in visible: cv2.line(frame, visible[a], visible[b], (0, 255, 255), 2) return frame

threshold 是关键点置信度阈值,设太高考察不到低置信度的关节,设太低会把错误点也画出来。四肢末端的腕和踝在遮挡时置信度会明显下降,如果业务场景经常有人手持物品,这两个点的阈值要单独放宽。骨架连接表相当于一份“人体解剖学固定配置”,使用其他模型时必须换成对应的连接对,这是新手最容易忽略的:BODY_25 的关键点序号和 COCO 不一样,直接套用会画出从肩连到脚趾的离谱骨架。

5. 必调参数与常见问题排查:识别不准先改这里

5.1 四个直接影响形态识别效果的参数

OpenPose wrapper 的配置参数很多,但真正需要手工调的就四个。net_resolution 控制网络输入分辨率,默认 656x368,值越大细节越清楚,但推理时间几乎随像素线性增长。我实际项目里如果跑 GPU,保持默认;跑 CPU 直接降到 320x176,骨架照样能用。scale_number 和 scale_gap 控制多尺度推理,scale_number 设为大于 1 时会对同一帧做多个尺度的推理再合并,能提高小目标检测率,但推理时间成倍增加,实时场景千万别开。

参数作用精度优先设置速度优先设置
net_resolution网络输入分辨率656x368320x176
scale_number多尺度推理数量31
nms_threshold关键点非极大值抑制保持默认保持默认
thre_1 / thre_2关键点/PAF 置信度阈值调低到 0.1 左右保持默认

nms_threshold 控制关键点去重力度,多人密集场景下如果出现同一个关节被识别成两个点,把这个值略微调高;单人场景基本不用动。thre_1 是关键点置信度阈值,thre_2 是 PAF 连接置信度阈值,识别骨架时发现“骨架断裂”“多出一条腿”这类问题,优先把 thre_2 调低,让连接更容易成立。调参的正确顺序是:先固定 net_resolution,再调 thre_2 看骨架完整性,最后调 nms_threshold 解决多人重叠,不要一上来就动 scale_number。

5.2 避坑记录:四类高频翻车现场

现象一:摄像头打不开,cap.isOpened() 返回 False。原因:设备索引不对,或者系统权限没给到终端程序。Linux 下摄像头设备在 /dev/video0,当前用户不在 video 组就打不开;macOS 下终端首次访问摄像头会弹权限请求,误点拒绝后不会再自动弹。 解决:先用 find_camera_index 遍历索引,同时检查系统权限。Linux 用 usermod 把用户加进 video 组后重新登录;macOS 去系统设置里把终端的摄像头权限打开。不要自己在代码里猜索引,让程序去探测。

现象二:程序启动直接崩溃,报 Check failed 或 shape mismatch。原因:prototxt 和 caffemodel 不匹配。最常见的是网上东拼西凑下载的模型,COCO 的 prototxt 配了 BODY_25 的权重,网络输出维度对不上,加载到一半就崩。 解决:模型文件成对下载、成对存放,文件名里已经标了模型类型。加载前检查 models 目录里是否同时存在 pose_deploy_linevec.prototxt 和 pose_iter_440000.caffemodel,或者 body25 那对。工程上我习惯给两套模型建子目录,靠目录区分,不让文件散在一起。

现象三:GPU 运行时报 no kernel image is available for execution on the device。原因:显卡架构太老,或者 CUDA 编译参数与显卡算力不匹配。OpenPose 默认编译目标覆盖较新的计算能力,老显卡不在列表里,运行时驱动拒载。 解决:先查显卡算力,cmake 时指定对应架构重新编译。实在没法重编译就切 CPU 模式跑,或者换一台带新显卡的机器。这个报错本质是硬件和软件代差问题,不是代码 bug,补丁也救不了。

现象四:实时识别卡成 PPT,延迟两秒以上。原因:逐帧推理 + 默认分辨率,导致推理时间远超帧间隔。很多人以为加了个 GPU 就高枕无忧,实际上 4K 输入缩放到 656x368 也要时间,再加上渲染和显示开销,延迟就失控了。 解决:先把 INFERENCE_SKIP 调到 2,再把 net_resolution 降到 320x176,最后用多进程把推理和显示拆开。注意跳帧丢的不是检测精度,是时间分辨率——你只是降低了判断频率,不是降低了判断质量,对形态识别这种连续状态变化来说影响很小。

6. 从骨架到形态判断:角度计算、阈值与帧级平滑

6.1 角度计算核心函数

有了稳定骨架,形态判断就变成几何计算了。最常用的是三点夹角:通过髋、膝、踝三点算膝关节角度,通过肩、肘、腕三点算肘关节角度。角度用余弦定理算,向量点乘除以模长再取反余弦,这个函数在任何姿态项目里都能复用。

import math def calc_angle(a, b, c): # b 是夹角顶点,a、c 是两端点 v1 = (a[0] - b[0], a[1] - b[1]) v2 = (c[0] - b[0], c[1] - b[1]) dot = v1[0] * v2[0] + v1[1] * v2[1] m1 = math.hypot(v1[0], v1[1]) m2 = math.hypot(v2[0], v2[1]) if m1 * m2 == 0: return 0.0 cos_val = max(-1.0, min(1.0, dot / (m1 * m2))) return math.degrees(math.acos(cos_val))

m1 或 m2 为 0 表示两个关键点重合,返回 0 度避免除零崩溃。计算前最好确认三个点都通过了置信度过滤,否则角度值会剧烈跳动。拿到角度后先别急着定阈值,打印出正常站立、慢走、下蹲各状态的角度范围,再画分界线。

6.2 形态判断不能只看单帧

单帧的角度值波动很大,直接拿它做判断会频繁误触发。我习惯的做法是连续 N 帧满足条件才切换状态,相当于一个简单的状态机。比如判定“下蹲”要求膝关节角度连续 5 帧小于 100 度,判定“站立”要求连续 5 帧大于 150 度。这样能把抖动过滤掉,代价是状态切换有 1 到 2 帧延迟,对安防和康复场景完全可以接受。

state = "standing" angle_buffer = [0] * 5 if len([a for a in angle_buffer if a < 100]) == 5: state = "squatting" elif len([a for a in angle_buffer if a > 150]) == 5: state = "standing"

angle_buffer 是最近 5 帧的膝关节角度,窗口越长越稳,但反应越慢;窗口太短又会误判。这个参数没有通用解,取决于你的业务允许多大延迟。我自己调试这类项目时,一定先离线录一段 30 秒视频,把角度曲线打印出来看一眼,再决定阈值和窗口大小,而不是直接对着摄像头调,这样能省一半调试时间。希望帮到你。

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

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进