ARTICLE DETAIL

资讯详情

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

基于PyQt5与OpenPose的姿态识别可视化:从骨架提取到关节角度分析

基于PyQt5与OpenPose的姿态识别可视化:从骨架提取到关节角度分析 简介基于PyQt5与OpenPose实现的太极拳姿态识别可视化系统是一份完整可运行的毕业设计项目源码包面向计算机相关专业正在准备毕设、课程设计或期末大作业的学生也适合需要项目实战练习的Python学习者可作为课程模板或毕业设计参考。项目经导师指导获得九十九分评价代码结构清晰覆盖姿态检测、关键点提取、结果可视化、界面交互等核心环节配套模型与数据集开箱即用部署难度低。压缩包共一百一十六个文件其中以八十张图片样本为主另配有Python源码、编译文件、配置文件及说明文档整体大小约一点七三兆字节轻量且方便迁移调试。目前已有一百二十七人学习下载。通过该资源可掌握OpenPose姿态识别与PyQt5界面整合的完整工程思路获得可直接复用的项目模板与排错经验大幅节省从零搭建的时间。1. 把太极拳拆成关节角度一套用得上手的姿态识别可视化方案教拳的老师傅最头疼的不是动作记不住而是三十个学员同时练云手他只有一双眼睛。这类体测与教学场景正是 PyQt5 OpenPose 姿态识别系统的用武之地OpenPose 先把你从视频帧里的人体骨架关键点抠出来PyQt5 负责把骨架、关节角度和比对得分实时画到屏幕上。这套系统解决的是“动作到底偏了多少”这个可量化问题适合做体态评分、教学辅助和运动示范展示。哪怕只有一块 CPU按本文把推理线程与输入分辨率调好也能跑到可用的帧率。2. 系统框定OpenPose 把骨架提出来PyQt5 把姿态画出来2.1 从摄像头到得分贯穿这套系统的四层数据流把标题里的几个词拆开看这套系统本质上是一条四层流水线。第一层是视频输入摄像头或录制好的太极拳教学视频都可以第二层是 OpenPose 推理从每一帧里检测出人体关键点的像素坐标第三层是动作特征提取把坐标换算成关节夹角比如肘角、膝角、肩髋连线与竖直方向的夹角第四层是 PyQt5 界面把原图、骨架连线、关节角度和动作名称实时显示出来。姿态识别综述里通常按检测策略分成两大类自顶向下是先检测出人体框再在框内回归关键点自底向上是在整张图上一次性预测所有关键点再通过 PAF部分亲和场把属于同一个人的关键点关联成骨架。OpenPose 属于后者也是这类项目里最容易复现的公开方案。PAF 的巧妙之处在于它不只是找“哪里有手肘”还编码了骨骼的方向和位置关系所以多人场景下也不容易把隔壁人的手腕接到你身上。我一般会把项目拆成两个独立模块来开发推理模块只负责“从图像到关键点坐标”界面模块只负责“从坐标到可视化与打分”。这样做的直接好处是调试时可以不开摄像头拿一张静态图片先验证推理结果界面出问题了也能单独排查绘图层不用每次都在完整链路里找毛病。2.2 关键点定义怎么选COCO 18 与 BODY_25 的取舍OpenPose 官方给了两套常用的关键点定义一套是 COCO 的 18 点一套是 BODY_25 的 25 点。两套模型都能从官方仓库的 release 里下载到对应权重区别在于 BODY_25 额外包含了脚趾和脚跟的关键点对步法细节比较敏感但推理开销也更大。索引部位在太极拳评定里的用途0鼻头部朝向的辅助参考1颈部躯干中轴基准点2 / 5右肩 / 左肩沉肩坠肘时的肩部高度3 / 6右肘 / 左肘肘部夹角决定动作幅度4 / 7右腕 / 左腕手腕相对躯干的位置8 / 11右髋 / 左髋重心高低与开胯程度9 / 12右膝 / 左膝弓步屈膝角度10 / 13右踝 / 左踝支撑脚位置如果目标只是识别“野马分鬃”“云手”这类以上肢为主的太极拳动作COCO 18 点足够了CPU 上推理速度比 BODY_25 快接近一倍。如果还想分析虚步、蹬脚这类对脚部位置敏感的动作再升级到 BODY_25 也不迟。我的建议是界面代码里把关键点连线表定义成独立的常量这样切换模型时只需要改一行表定义。2.3 界面层为什么锚定 PyQt5做可视化界面有 Tkinter、PyQt5、Web 前端三条常见路线。Tkinter 上手快但做视频流刷新和自定义绘图很别扭Web 前端用 Flask 加 Canvas 也能画骨架但每次要起服务、处理端口和跨域教学场景里显得重。PyQt5 的优势在于三个点QThread 线程模型可以优雅地处理 OpenPose 这种耗时推理信号槽机制天然适合把推理结果从子线程传回界面再加上 QLabel 直接显示 QImage和 OpenCV 的集成几乎是无缝的。找“pyqt5界面设计”相关资源时你会看到很多基于 PyQt5 的桌面工具实践套路其实一致界面线程只负责刷新控件绝不执行耗时计算。这点对姿态识别项目尤其关键因为 OpenPose 在 CPU 上单帧推理可能要几百毫秒到一两秒把它放进主线程窗口会直接卡成“未响应”。我在项目里还习惯用 QTextBrowser 做动作说明区直接调用 setHtml 方法加载一段带样式的富文本说明这就是“pyqt5显示html”的常见用法。比起用 QLabel 拼字符串代码更整洁界面也更像正规的教学工具。3. 环境落地pyqt5 安装、模型权重与最小推理链路3.1 pyqt5 安装与 Python 版本匹配先说最容易劝退新手的一步pyqt5 安装本身不复杂复杂的是版本和 Python 版本的匹配。PyQt5 5.15 系列要求 Python 3.6 以上如果你用的是较新的 Python 3.11 或 3.12pip 会自动选择已经适配的版本一般不会出问题。真正容易翻车的是在旧项目虚拟环境里装 PyQt5因为环境中其他包可能把可用的 PyQt5 版本范围锁死了。# 推荐用虚拟环境隔离避免污染系统 Python python -m venv venv source venv/bin/activate # Windows 下用 venv\Scripts\activate # 安装 PyQt5 与常用工具 pip install --upgrade pip pip install PyQt55.15.9 pip install PyQt5-tools这里的版本号 5.15.9 是 5.15 系列的最后一个补丁版兼容性和稳定性都经过了大量验证。PyQt5-tools 提供了 Qt Designer 等辅助工具如果你不打算用 Designer 拖控件可以跳过这个包。网上搜“pyqt5安装”时如果遇到下载速度慢把 pip 源换成国内镜像而不是强行用默认源反复重试。安装完成后可以用一行命令验证python -c from PyQt5.QtWidgets import QApplication; print(PyQt5 OK)如果输出 PyQt5 OK说明环境正常。这一步能过滤掉绝大多数“代码没错但跑不起来”的情况值得先做。3.2 两种 OpenPose 落地方式源码编译与 OpenCV DNN 轻量加载OpenPose 官方源码基于 Caffe 实现编译流程对新手不太友好需要安装 CMake、Caffe 依赖、CUDA 和 cuDNN仅环境准备就可能耗掉一整天。如果你的目标是把这个系统跑起来并专注在界面和动作判定上我强烈建议先跳过源码编译直接用 OpenCV 的 DNN 模块加载官方发布好的 Caffe 模型。import cv2 # 从 OpenPose 官方仓库 models 目录获取这两个文件 # COCO 模型对应 pose_deploy_linevec.prototxt 与 pose_iter_440000.caffemodel proto_file pose_deploy_linevec.prototxt weight_file pose_iter_440000.caffemodel net cv2.dnn.readNetFromCaffe(proto_file, weight_file)这种做法的好处是只需要 pip install opencv-python不需要安装任何 Caffe 相关的编译环境而且 OpenCV DNN 在 CPU 上的推理性能经过优化对单路视频流足够用。代价是无法使用 OpenPose 官方源码里的多线程流水线优化多人检测时帧率会下降。3.3 先用 30 行代码验证关键点能提出来拿到模型文件后先不碰界面用一段最小代码验证推理链路。这一步能尽早暴露模型文件对齐、路径错误、图像维度不对等问题我每次搭新环境都是这么自检的。import cv2 import numpy as np proto_file pose_deploy_linevec.prototxt weight_file pose_iter_440000.caffemodel net cv2.dnn.readNetFromCaffe(proto_file, weight_file) # OpenPose 训练时的标准输入尺寸是 368x368 in_width, in_height 368, 368 frame cv2.imread(taichi_frame.jpg) height, width frame.shape[:2] # 构造 blob除以 255 归一化mean 为 0不交换 RGB 通道 blob cv2.dnn.blobFromImage(frame, 1.0 / 255, (in_width, in_height), (0, 0, 0), swapRBFalse, cropFalse) net.setInput(blob) outs net.forward() print(outs.shape) # 期望输出 (1, 57, 46, 46)输出张量的形状是 (1, 57, 46, 46)其中 57 19 个热图通道加 38 个 PAF 通道。前 19 个通道里18 个分别对应 COCO 18 个关键点的置信度热图最后 1 个是背景。后 38 个通道是 19 条骨骼的 PAF 向量场每条骨骼有 x 和 y 两个分量。特征图分辨率 46x46 是输入图像 368x368 经过网络下采样得到的。要拿到某个关键点的像素坐标就在对应热图上找最大值位置。我常用的写法是for part_idx in range(18): prob_map outs[0, part_idx, :, :] _, conf, _, point cv2.minMaxLoc(prob_map) if conf 0.3: x int(point[0] * width / 46) y int(point[1] * height / 46) cv2.circle(frame, (x, y), 4, (0, 0, 255), -1)这里 point 的坐标是特征图坐标需要按比例放大回原图尺寸缩放因子就是原图宽高除以 46。置信度阈值 0.3 是我经验里的常用值对太极拳这种动作幅度大、遮挡少的场景够用如果画面里人离镜头远关键点置信度普遍偏低可以降到 0.2。4. PyQt5 界面线程拆分、骨架绘制与实时预览4.1 界面布局与信号槽别让推理堵住主线程界面布局我按三个区域来排左侧是摄像头预览区核心控件是 QLabel右侧上方是动作判定结果区包括动作名称、得分和关节角度右侧下方是动作说明区用 QTextBrowser 显示当前动作的文字讲解和练习要点。整个界面的运行逻辑依赖 Qt 的信号槽机制。工作线程每完成一次推理就发出一个携带图像和关键点数据的信号主线程的槽函数收到信号后只做两件事把图像转成 QImage 显示到 QLabel 上把角度和得分更新到文本控件里。工作线程绝不直接操作任何界面控件。这样设计的好处是界面永远不会被推理阻塞。即使用户在慢速机器上跑也只是预览画面变卡窗口拖动、关闭按钮依然是灵敏的不会出现系统提示“无响应”的尴尬场景。4.2 摄像头循环里的推理任务与帧回传摄像头读取和 OpenPose 推理都是耗时操作我习惯把它们统一放进一个 QThread 子类里。核心代码如下from PyQt5.QtCore import QThread, pyqtSignal import cv2 import numpy as np class VideoWorker(QThread): # 信号携带三个对象绘制好骨架的帧、关键点列表、判定结果字典 frame_ready pyqtSignal(object, object, dict) def __init__(self, net, src0, interval2, parentNone): super().__init__(parent) self.net net self.cap cv2.VideoCapture(src) self.interval interval # 每隔 interval 帧做一次推理 def run(self): frame_id 0 while not self.isInterruptionRequested(): ok, frame self.cap.read() if not ok: break frame_id 1 if frame_id % self.interval ! 0: continue keypoints, skeleton_img self.detect_skeleton(frame) result self.judge_action(keypoints) self.frame_ready.emit(skeleton_img, keypoints, result) self.cap.release() def detect_skeleton(self, frame): # 这里执行 OpenPose 推理并绘制骨架返回关键点数组和绘制后的图像 pass def judge_action(self, keypoints): # 关节角提取与动作比对返回动作名和得分 pass关键参数是 interval它控制每隔多少帧做一次推理。在 CPU 上跑 368x368 输入时单帧推理约 0.3 到 1 秒interval 设为 2 表示每两帧推理一次画面大约每秒更新 1 到 3 次作为教学演示可以接受。如果你的机器配置好可以改成 1如果画面太卡调到 3 或 4 会明显改善流畅度。线程退出用的是 isInterruptionRequested 这个标志位。关闭窗口时在主线程调用 worker.requestInterruption()run 循环会在下一次循环判断到标志后正常退出并释放摄像头比直接调用 terminate 安全得多。直接 terminate 杀死线程可能导致摄像头句柄没释放下次启动程序时摄像头被占用打不开这是个很隐蔽的坑。4.3 骨架绘制与画面缩放坐标对齐是关键推理得到的关键点坐标是归一化的特征图坐标绘制到界面前要先映射到帧的实际尺寸。我习惯先把骨架画在原始帧上再把整帧图像转换成 QImage 显示这样画面和骨架天然对齐。SKELETON [(1, 2), (1, 5), (2, 3), (3, 4), (5, 6), (6, 7), (1, 8), (8, 9), (9, 10), (1, 11), (11, 12), (12, 13), (1, 0), (0, 14), (14, 16), (0, 15), (15, 17), (2, 9), (5, 12)] def draw_skeleton(img, keypoints, threshold0.3): height, width img.shape[:2] for idx1, idx2 in SKELETON: x1, y1, conf1 keypoints[idx1] x2, y2, conf2 keypoints[idx2] if conf1 threshold and conf2 threshold: px1 int(x1 * width / 46) py1 int(y1 * height / 46) px2 int(x2 * width / 46) py2 int(y2 * height / 46) cv2.line(img, (px1, py1), (px2, py2), (0, 255, 0), 2) return imgSKELETON 列表里存的是 COCO 18 关键点的连线关系。比如 (3, 4) 表示右肘到右腕(9, 10) 表示右膝到右踝。这里用关键点索引而不是坐标是因为索引是固定的坐标每帧都在变。上面的坐标除以 46 再乘以原图尺寸是因为推理结果的特征图分辨率是 46x46这样换算后坐标就落回原图空间了。显示到界面时还要注意 OpenCV 和 Qt 之间的图像格式差异。OpenCV 读入的图像是 BGR 通道顺序Qt 的 QImage 默认按 RGB 解释直接把 OpenCV 图像转 QImage 会导致画面整体偏蓝。def cv2_to_qimage(img): rgb_image cv2.cvtColor(img, cv2.COLOR_BGR2RGB) h, w, ch rgb_image.shape bytes_per_line ch * w qimg QImage(rgb_image.data, w, h, bytes_per_line, QImage.Format_RGB888) return qimgQLabel 显示时我用 scaled 方法缩放到控件大小保持宽高比避免画面被拉伸变形。注意 QImage 的 data 指向的是原始 buffer如果后续还要修改这张图需要先拷贝否则可能出现显示内容跟着 buffer 变化的问题。5. 避坑搭建与运行阶段最容易翻车的 5 个位置5.1 labelme 装不上 pyqt5界面库依赖打架现象在新环境里执行 pip install labelme 时报错提示与 PyQt5 的版本约束冲突或者反向操作时先装好了 PyQt5再装 labelme 被拒绝。原因labelme 这类标注工具的依赖文件里写死了 PyQt5 的版本区间比如限定在 PyQt5 5.15 以下的某个范围内和手动装的 5.15.9 冲突。pip 在解析依赖时发现无法同时满足两边要求就会直接拒绝安装报 dependency conflict。解决先建一个干净的虚拟环境按工具依赖顺序安装。先装 labelme让它自动把匹配的 PyQt5 带进来然后再看看其他需要 PyQt5 的包能不能兼容这个版本。非要指定版本的话用 pip install labelme 不加版本号让 pip 自己决定通常比手动推导依赖关系更省事。5.2 预览画面颜色偏蓝BGR 与 RGB 顺序没转现象摄像头画面在 PyQt5 界面上整体偏蓝红色组件变成暗紫色。原因OpenCV 读图像默认是 BGR 顺序而 QImage 的 Format_RGB888 按照 RGB 解释数据。数据没转就直接交给 QImage相当于把三通道的顺序整体错位了。解决转换 QImage 前调用 cv2.cvtColor(img, cv2.COLOR_BGR2RGB)这行代码写在 cvt_to_qimage 函数里所有图像统一走这一个入口。你可以在显示前把像素右下角的颜色值打出来如果 BGR 三个通道值是 (255, 0, 0) 但显示为蓝色那就一定是顺序问题。5.3 界面卡成幻灯片推理活全压在主线程现象窗口能打开但鼠标拖动窗口时画面卡顿明显点击关闭按钮后要等好几秒才有响应系统甚至提示程序未响应。原因把 OpenPose 推理这段耗时操作直接写在了某个槽函数或定时器回调里。推理执行期间Qt 的事件循环被阻塞鼠标事件、重绘事件全部排队等待界面就表现为“假死”。解决把推理放进 QThread 的 run 方法里通过信号把结果传回主线程更新界面。这是 Qt 并发编程的基本准则耗时任务绝不跑在 GUI 线程。如果不想继承 QThread也可以用 QObject moveToThread 的写法效果一样只是组织方式不同。5.4 骨架线斜穿人体COCO 与 BODY_25 索引串用现象画出来的骨架连线乱七八糟有的线从右肩直接穿到左胯有的把鼻子和脚踝连在一起。原因COCO 18 和 BODY_25 的关键点索引顺序完全不一样。我在项目里把 COCO 的连线表套在了 BODY_25 模型吐出的关键点坐标上右边肩膀索引对应到的实际部位就变了画出的线自然串位。解决每次切换模型连线表和关键点索引都要一起换。我在代码里把 SKELETON 常量定义在独立文件里注释里标明适用的是哪套关键点定义。如果画出的线不对先把模型切换到 COCO 18 验证界面代码确认无误后再研究 BODY_25 的索引布局。5.5 中文路径读不到视频OpenCV 后端的兼容性问题现象cv2.VideoCapture(测试视频.mp4) 能返回对象但 read() 一直返回 False换成英文文件名就正常。原因OpenCV 的视频读取后端在某些系统上不支持非 ASCII 路径中文字符在解码时出错文件根本打不开。解决单帧图片可以用 np.fromfile 配合 cv2.imdecode 读取视频没有直接的解码替代方案我一般会在程序启动时检测路径是否含中文如果有就先复制到一个临时英文路径再打开。你要是嫌复制麻烦就定个项目规范所有资源文件一律英文命名。6. 动作判定进阶用 DTW 与阈值校准验证这套识别准不准6.1 把骨架转为关节角特征序列关键点坐标会随着人离摄像头远近而变化直接用坐标做比对不稳定。关节角度是尺度无关的人站远站近角度不变所以我把关键点换算成一组关节角向量。def joint_angle(p1, p2, p3): v1 p1 - p2 v2 p3 - p2 cos_theta np.dot(v1, v2) / (np.linalg.norm(v1) * np.linalg.norm(v2) 1e-6) return np.degrees(np.arccos(np.clip(cos_theta, -1.0, 1.0)))这里取右肘、左肘、右膝、左膝四个关节角具体是肩-肘-腕和髋-膝-踝的组合。置信度低于阈值的关键点直接返回 None丢弃这一帧因为不准确的关键点算出的角度会严重污染后续比对。6.2 模板比对与阈值选择动作是一个时间过程拿单帧角度和模板比没有意义。我用 DTW 算法把待识别序列和标准动作序列做非线形对齐算出距离分数。DTW 的核心思想是允许两段动作节奏有快慢差异这正好匹配太极拳“慢而不停”的特点。def dtw_distance(seq_a, seq_b): n, m len(seq_a), len(seq_b) dp np.full((n 1, m 1), np.inf) dp[0, 0] 0 for i in range(1, n 1): for j in range(1, m 1): cost np.linalg.norm(seq_a[i - 1] - seq_b[j - 1]) dp[i, j] cost min(dp[i - 1, j], dp[i, j - 1], dp[i - 1, j - 1]) return dp[n, m]阈值怎么定我一般会录 20 遍标准动作当正样本再录 20 遍错误动作当负样本分别计算距离画柱状图看分布。两组分布明显分开时取中间值作为阈值重叠严重时说明角度特征选得不够好换关节组合再试。实测里阈值取正样本平均距离的 1.2 倍附近比较稳既能容忍学员动作的轻微偏差又不会把明显错误放进来。我的习惯是把校准过程做成一键脚本每次换动作或换摄像头位置后重新跑一遍避免凭感觉调阈值。这套流程跑顺之后识别系统就不再是玄学而是每个分数都能回溯到具体的角度偏差上。希望帮到你。本文还有配套的精品资源点击获取
返回列表