简介:基于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.12,pip 会自动选择已经适配的版本,一般不会出问题。真正容易翻车的是在旧项目虚拟环境里装 PyQt5,因为环境中其他包可能把可用的 PyQt5 版本范围锁死了。
# 推荐用虚拟环境隔离,避免污染系统 Python python -m venv venv source venv/bin/activate # Windows 下用 venv\Scripts\activate # 安装 PyQt5 与常用工具 pip install --upgrade pip pip install PyQt5==5.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), swapRB=False, crop=False) 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, src=0, interval=2, parent=None): 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, threshold=0.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 倍附近比较稳,既能容忍学员动作的轻微偏差,又不会把明显错误放进来。
我的习惯是把校准过程做成一键脚本,每次换动作或换摄像头位置后重新跑一遍,避免凭感觉调阈值。这套流程跑顺之后,识别系统就不再是玄学,而是每个分数都能回溯到具体的角度偏差上。希望帮到你。
本文还有配套的精品资源,点击获取