OpenCV + MediaPipe:从零实现实时人体姿态追踪系统

1. 为什么你需要一个自己的实时人体姿态追踪系统?

想象一下,你正在家里跟着健身视频做深蹲,但总感觉动作不到位,又怕伤到膝盖。或者,你是一个游戏开发者,想做一个体感游戏,却苦于动作捕捉设备昂贵且复杂。再或者,你只是对AI如何“看懂”人体动作感到好奇。如果你有这些想法,那么今天这个用OpenCV和MediaPipe搭建的实时人体姿态追踪系统,就是为你量身定做的。

简单来说,这个系统就像一个“数字眼睛”,它能通过普通的摄像头(比如你的笔记本摄像头),实时地“看到”画面中的人,并精准地标出你身上33个关键关节点的位置——从头顶到脚踝,包括肩膀、手肘、手腕、膝盖、脚踝等等。这听起来很科幻,但实现起来,比你想象的要简单得多。我刚开始接触时也觉得这得是多复杂的算法,但实际用上MediaPipe之后,发现它把最难的深度学习模型训练和优化都打包好了,我们只需要像搭积木一样调用几个接口,就能得到一个效果相当不错的实时追踪程序。

这套系统能做什么?应用场景远超你的想象。除了开头说的健身动作纠正(比如判断深蹲时膝盖是否超过脚尖、手臂是否抬平),它还能用于体感交互游戏舞蹈动作学习安防监控中的异常行为检测,甚至是短视频特效(比如给你的关节加上炫酷的光效)。最关键的是,它的实时性非常好,在我的普通办公笔记本上跑起来,帧率轻松超过30FPS,完全感觉不到延迟。

所以,无论你是编程新手想找个有趣的AI项目练手,还是有一定经验的开发者想快速为你的应用添加“视觉”能力,跟着我一步步走下来,一两个小时你就能拥有一个完全在自己掌控之下的、可定制的人体姿态追踪demo。我们不用从零开始研究数学公式和神经网络,而是站在Google巨人的肩膀上,用最实用的工具,解决最实际的问题。接下来,我们就从准备“工具箱”开始。

2. 手把手搭建你的开发环境

万事开头难,但配置环境这一步,我会让你觉得异常简单。我们的核心工具就两个:OpenCVMediaPipe。你可以把它们理解为一个“万能图像处理助手”和一个“专属人体姿态识别专家”。

2.1 安装核心依赖:一行命令搞定

我强烈建议你使用 Python 3.8 或 3.9 的版本,这是目前兼容性最稳定的选择。打开你的命令行终端(Windows上是CMD或PowerShell,Mac/Linux上是Terminal),创建一个新的虚拟环境是个好习惯,可以避免包版本冲突。这里我用conda举例,用venv也一样。

# 创建并激活一个名为 pose_tracking 的虚拟环境
conda create -n pose_tracking python=3.9
conda activate pose_tracking

环境激活后,安装那两个核心库:

pip install opencv-python mediapipe

对,就这么简单。opencv-python 是OpenCV的Python版本,负责所有图像的读取、显示、绘制和保存。mediapipe 则是Google出品的宝贝,里面内置了现成的人体姿态估计模型。这里有个小坑我踩过:MediaPipe的新版本有时会有API变动,如果你追求极致的稳定,可以指定一个我实测很稳的版本,比如 pip install mediapipe==0.10.0。但通常直接安装最新版也没问题。

安装完成后,可以写两行代码验证一下:

import cv2
import mediapipe as mp
print(cv2.__version__)
print(mp.__version__)

如果没有报错,并且打印出了版本号,那么恭喜你,最基础的环境已经就绪了。

2.2 准备你的“素材”:摄像头还是视频文件?

系统需要输入源来“看”。你有两个选择:

  1. 使用电脑摄像头(实时流):这是最互动的方式。OpenCV能直接调用你的摄像头。
  2. 使用本地视频文件:适合做离线分析,或者你想处理一段特定的运动视频。

为了后续代码清晰,我们先统一一下素材路径。如果你用视频文件,比如你手机拍的一段健身视频,把它放在项目文件夹里,我们假设它叫 my_workout.mp4。如果直接用摄像头,就更简单了。

这里我插一个非常重要的经验:处理视频时,一定要在代码开头就获取它的尺寸和帧率信息。这是因为后续保存处理结果视频时,输出视频的参数必须和输入源匹配,否则要么保存失败,要么画面扭曲。我们会在代码里具体体现这一点。

3. 第一步:让OpenCV“动起来”——视频流的读取与显示

在请出MediaPipe这位“专家”之前,我们先得让OpenCV这个“助手”把图像流管道打通。这一步的目标是创建一个能流畅读取并显示视频画面的程序,这是所有视觉应用的基础。

3.1 打开视频源并创建显示窗口

我们直接上代码,我会逐段解释每一行是干什么的:

import cv2
import time # 用于计算帧率

# 初始化摄像头。如果参数是0,代表使用电脑默认摄像头。
# 如果你想用视频文件,把0换成文件路径,比如 'my_workout.mp4'
cap = cv2.VideoCapture(0)

# 设置摄像头分辨率(可选,但建议设置以获得稳定尺寸)
cap.set(cv2.CAP_PROP_FRAME_WIDTH, 1280)
cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 720)

pTime = 0  # 上一帧的时间,用于计算FPS

while True:
    # 读取一帧图像。success是个布尔值,表示是否读取成功;img是这一帧的图像数据。
    success, img = cap.read()
    
    if not success:
        print("无法读取视频流。")
        break  # 如果读取失败(比如视频播完了),就退出循环

    # --- 计算并显示实时帧率(FPS) ---
    cTime = time.time()  # 当前时间
    fps = 1 / (cTime - pTime) if (cTime - pTime) > 0 else 0  # 计算帧率,避免除零错误
    pTime = cTime  # 更新“上一帧时间”

    # 把FPS数值画到图像上
    cv2.putText(img, f'FPS: {int(fps)}', (50, 70), cv2.FONT_HERSHEY_PLAIN, 3, (0, 255, 0), 3)

    # 显示图像,窗口名称为 “Pose Tracking”
    cv2.imshow('Pose Tracking', img)

    # 等待1毫秒,并检测是否按下了‘q’键。如果按下,则退出循环。
    if cv2.waitKey(1) & 0xFF == ord('q'):
        break

# 释放摄像头资源,并关闭所有OpenCV创建的窗口
cap.release()
cv2.destroyAllWindows()

把这段代码保存为 video_stream.py 并运行。你应该能看到一个显示你摄像头画面的窗口,左上角有绿色的FPS数值。按 Q 键可以退出程序。这就是我们系统的“地基”。如果这一步摄像头没打开,可能是权限问题(Mac/Linux常见)或者摄像头被其他软件占用,检查一下就好。

3.2 添加视频保存功能(可选但实用)

很多时候,我们不仅想实时看,还想把处理结果保存下来,用于分享或后续分析。给上面的循环里加上保存功能非常简单:

# 在while循环之前,定义视频写入器
frame_width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
frame_height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
# 参数:输出文件名、编码器、帧率、画面尺寸
out = cv2.VideoWriter('output.avi',
                      cv2.VideoWriter_fourcc(*'XVID'), # 编码器,'MJPG'或'XVID'常用
                      30, # 保存的帧率
                      (frame_width, frame_height))

# 在while循环内部,显示图像(cv2.imshow)之后,写入这一帧
out.write(img)

# 在退出循环后,释放写入器
out.release()

注意,cv2.VideoWriter_fourcc(*'XVID') 这里的 *'XVID' 是一种写法,等价于 cv2.VideoWriter_fourcc('X','V','I','D')。不同编码器在不同系统上支持度不同,如果在Windows上保存有问题,可以试试 'MJPG';在Mac上可以试试 'mp4v' 并将文件名后缀改为 .mp4。保存功能加上后,你运行程序,处理完按Q退出,就会在文件夹里生成一个 output.avi 文件,记录了你刚才的全部操作。

4. 核心环节:请出MediaPipe,绘制你的身体“骨架”

现在,地基打牢了,该请出我们的大杀器MediaPipe了。它会分析我们每一帧的图像,找出人体的33个关键点,并告诉我们每个点的坐标。

4.1 初始化姿态估计模型并处理单帧

MediaPipe的使用模式非常清晰:初始化模型 -> 送入图像 -> 获取结果。我们先看看如何修改之前的代码,加入姿态估计:

import cv2
import mediapipe as mp
import time

# 初始化MediaPipe的绘图工具和姿态解决方案
mp_drawing = mp.solutions.drawing_utils
mp_pose = mp.solutions.pose

# 创建Pose模型实例。这里可以设置一些参数,我们先用默认的。
# static_image_mode=False 代表我们处理的是视频流,模型会利用帧间信息优化结果,速度更快。
# model_complexity=1 是模型复杂度(0,1,2),1是平衡精度和速度的好选择。
# smooth_landmarks=True 平滑关键点,让输出不会抖动得太厉害。
# min_detection_confidence=0.5 检测置信度阈值,低于这个值认为没检测到人。
# min_tracking_confidence=0.5 跟踪置信度阈值,用于视频流中维持跟踪稳定性。
pose = mp_pose.Pose(static_image_mode=False,
                    model_complexity=1,
                    smooth_landmarks=True,
                    min_detection_confidence=0.5,
                    min_tracking_confidence=0.5)

cap = cv2.VideoCapture(0)
pTime = 0

while True:
    success, img = cap.read()
    if not success:
        break

    # MediaPipe模型需要RGB格式的图像,但OpenCV默认读取的是BGR格式,所以必须转换。
    img_rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
    
    # 为了提升性能,可以设置图像不可写(这是一个可选的优化)
    img_rgb.flags.writeable = False
    
    # 关键一步:将图像送入模型进行处理!
    results = pose.process(img_rgb)
    
    # 处理完后,再把图像改回可写状态,方便我们画图
    img_rgb.flags.writeable = True
    
    # 再把图像从RGB转回BGR,因为OpenCV显示用的是BGR
    img_bgr = cv2.cvtColor(img_rgb, cv2.COLOR_RGB2BGR)

    # --- 如果检测到了人体关键点 ---
    if results.pose_landmarks:
        # 最爽的一行代码:用MediaPipe自带的工具绘制所有关键点和连接线!
        mp_drawing.draw_landmarks(
            img_bgr,                # 要画在上面的图像
            results.pose_landmarks, # 检测到的关键点数据
            mp_pose.POSE_CONNECTIONS, # 预定义的关键点连接关系(就是画骨架)
            mp_drawing.DrawingSpec(color=(0, 0, 255), thickness=2, circle_radius=2), # 关键点样式(蓝色,2像素粗,半径2)
            mp_drawing.DrawingSpec(color=(0, 255, 0), thickness=2) # 连接线样式(绿色,2像素粗)
        )

    # 计算并显示FPS(同上)
    cTime = time.time()
    fps = 1 / (cTime - pTime) if (cTime - pTime) > 0 else 0
    pTime = cTime
    cv2.putText(img_bgr, f'FPS: {int(fps)}', (50, 70), cv2.FONT_HERSHEY_PLAIN, 3, (0, 255, 0), 3)

    cv2.imshow('Pose Tracking', img_bgr)
    if cv2.waitKey(1) & 0xFF == ord('q'):
        break

pose.close() # 释放模型资源(好习惯)
cap.release()
cv2.destroyAllWindows()

运行这段代码,奇迹发生了!你的身体轮廓被一套清晰的“骨架”覆盖了,关节处是红色小点,骨头是绿色线条。即使你做一些快速移动,这个骨架也能比较稳定地跟着你。这就是MediaPipe的强大之处,它用一行 draw_landmarks 就完成了最复杂的可视化工作。

4.2 深入关键点数据:获取每个关节的精确坐标

光是画出来还不够,我们经常需要知道某个特定关节的具体位置,比如左手腕的坐标,来计算手臂的角度。results.pose_landmarks 里就藏着这些宝藏数据。

if results.pose_landmarks: 的代码块里,我们可以添加以下代码来遍历和访问每一个关键点:

# 获取图像的尺寸,用于将归一化坐标转换为像素坐标。
h, w, c = img_bgr.shape

# 遍历33个关键点
for landmark_id, landmark in enumerate(results.pose_landmarks.landmark):
    # landmark.x, landmark.y, landmark.z 是归一化坐标(0到1之间),
    # 分别表示相对于图像宽度、高度和深度(粗略估计)的比例。
    # landmark.visibility 是可见性置信度,表示这个点在当前视角下是否可见。
    
    # 将归一化坐标转换为图像上的实际像素坐标
    cx, cy = int(landmark.x * w), int(landmark.y * h)
    
    # 现在我们有了像素坐标(cx, cy),可以做一些自定义绘制。
    # 例如,高亮显示鼻子(关键点ID为0)和左右手腕(ID为15, 16)
    if landmark_id in [0, 15, 16]:
        cv2.circle(img_bgr, (cx, cy), 10, (255, 255, 0), cv2.FILLED) # 画一个亮青色的实心圆
        # 在旁边显示ID号
        cv2.putText(img_bgr, str(landmark_id), (cx+10, cy), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (255, 255, 255), 1)
    
    # 你也可以打印出来看看
    # print(f"ID: {landmark_id}, X: {cx}, Y: {cy}, Visibility: {landmark.visibility}")

MediaPipe的33个关键点有固定的ID编号。比如,0是鼻子,11是左肩,12是右肩,15是左手腕,16是右手腕,23是左髋,24是右髋。知道每个ID对应哪个关节,是你进行高级应用(如姿势判断)的基础。我建议你打印出几个点的坐标,动一动身体,观察数值变化,这样感受最直接。

5. 从“看到”到“理解”:实现简单的姿态判断逻辑

系统能实时画出骨架,这已经很酷了。但让它真正“理解”姿势,比如判断一个人是否举手、是否深蹲,才是价值的体现。这需要我们基于关键点的坐标,进行一些几何计算。

5.1 计算关节角度:以判断手臂是否平举为例

人体姿态分析中,计算关节角度是最常见的操作。我们以判断“双臂是否侧平举”为例。这需要用到肩膀、手肘和手腕三个点形成的夹角。

在数学上,我们可以用向量点积公式来计算角度。但别怕,我们不用自己推导,直接用一个函数来实现:

import math

def calculate_angle(a, b, c):
    """
    计算由三点a, b, c构成的角abc的角度(以b为顶点)。
    参数a, b, c是包含x, y坐标的元组或列表。
    返回角度值(0到180度之间)。
    """
    a = np.array(a) # 点a的坐标
    b = np.array(b) # 点b(顶点)的坐标
    c = np.array(c) # 点c的坐标
    
    # 计算向量 ba 和 bc
    ba = a - b
    bc = c - b
    
    # 计算向量点积和模长
    dot_product = np.dot(ba, bc)
    norm_ba = np.linalg.norm(ba)
    norm_bc = np.linalg.norm(bc)
    
    # 计算夹角的余弦值,并防止除零或数值溢出
    cosine_angle = dot_product / (norm_ba * norm_bc + 1e-10) # 加个极小值避免除零
    cosine_angle = np.clip(cosine_angle, -1.0, 1.0) # 将值限制在[-1,1]范围内
    
    # 将弧度转换为角度
    angle = np.degrees(np.arccos(cosine_angle))
    
    return angle

注意,这个函数用了NumPy库,所以需要在代码开头 import numpy as np,并安装NumPy (pip install numpy)。现在,我们可以在主循环中,获取左肩(11)、左肘(13)、左手腕(15)的坐标,并计算角度:

# 在 if results.pose_landmarks: 内部,获取坐标后
landmarks = results.pose_landmarks.landmark

# 获取左臂三个关键点的像素坐标
shoulder = [landmarks[mp_pose.PoseLandmark.LEFT_SHOULDER.value].x * w,
            landmarks[mp_pose.PoseLandmark.LEFT_SHOULDER.value].y * h]
elbow = [landmarks[mp_pose.PoseLandmark.LEFT_ELBOW.value].x * w,
         landmarks[mp_pose.PoseLandmark.LEFT_ELBOW.value].y * h]
wrist = [landmarks[mp_pose.PoseLandmark.LEFT_WRIST.value].x * w,
         landmarks[mp_pose.PoseLandmark.LEFT_WRIST.value].y * h]

# 计算角度
left_arm_angle = calculate_angle(shoulder, elbow, wrist)

# 在图像上显示这个角度
cv2.putText(img_bgr, f'L-Arm: {int(left_arm_angle)}', 
            tuple(np.multiply(elbow, [1, 1]).astype(int)), # 在肘部位置显示
            cv2.FONT_HERSHEY_SIMPLEX, 0.7, (255, 255, 255), 2)

# 判断:如果角度在160度到180度之间(接近伸直),且手腕在肩膀下方(防止误判),则认为手臂平举
if 160 < left_arm_angle < 180 and wrist[1] > shoulder[1]:
    cv2.putText(img_bgr, "ARM RAISED!", (100, 150), 
                cv2.FONT_HERSHEY_SIMPLEX, 1.5, (0, 0, 255), 3)

同样的逻辑,你可以计算右臂角度、膝盖角度等。通过组合多个角度的判断,你就能定义出“深蹲”、“开合跳”、“平板支撑”等复杂动作的规则。

5.2 优化性能与提升稳定性

当你把所有这些功能都加进去后,可能会发现帧率有所下降,或者检测在某些时候(比如侧身、快速运动)会丢失。这里分享几个我调试后很有效的技巧:

  1. 降低输入图像分辨率:MediaPipe处理高分辨率图像会更慢。你可以在 cap.read() 后,用 img = cv2.resize(img, (640, 480)) 将图像缩小。模型在640x480的分辨率下精度损失很小,但速度提升非常明显。
  2. 调整模型参数:创建 mp_pose.Pose 对象时,降低 model_complexity 从1到0,会牺牲一点点精度换取速度。提高 min_detection_confidencemin_tracking_confidence(比如到0.7或0.8)可以减少误检,但可能会让检测变得更“迟钝”。
  3. 利用跟踪而非每帧检测:在视频流模式下 (static_image_mode=False),MediaPipe会自动在帧间跟踪关键点,这比每一帧都重新检测要快得多。确保这个参数为False。
  4. 多线程处理:对于更极致的需求,可以考虑将图像采集和姿态估计放在不同的线程中,避免I/O等待。但这会显著增加代码复杂度,新手可以先不考虑。

在我的ThinkPad T14上,处理640x480的摄像头输入,绘制骨架和计算一个角度,FPS能稳定在35以上,完全满足实时交互的需求。如果你的机器性能更强,体验会更好。

6. 举一反三:将你的系统应用到具体场景

有了这个可运行、可定制的代码框架,你就可以像玩乐高一样,为它添加不同的功能模块,适配各种场景。

场景一:健身教练助手 你可以预先定义标准动作(如深蹲)下,髋关节、膝关节、踝关节的理想角度范围。当用户做动作时,实时计算这些角度并与标准范围比较。如果膝盖内扣(角度异常),就在屏幕上用醒目的文字或声音提示“注意膝盖方向!”。你甚至可以设计一个得分系统,根据动作的贴合度给出实时反馈。

场景二:体感游戏控制器 获取双手腕(ID 15, 16)的坐标,映射到屏幕上的某个区域。当双手在胸前合拢时,触发“格挡”事件;当右手快速从上向下挥动时,触发“劈砍”事件。用这些事件去控制游戏角色。MediaPipe的3D坐标(landmark.z)虽然只是相对深度,但也能粗略判断手的前后位置,实现“推”、“拉”的交互。

场景三:动画与特效 将检测到的33个关键点坐标,通过Socket或共享内存发送给3D动画软件(如Blender)或游戏引擎(如Unity、Unreal)。这样,你的身体动作就能实时驱动一个虚拟卡通角色,实现低成本的动作捕捉。或者,直接在OpenCV里,在关键点坐标上叠加火焰、闪电等粒子特效,制作有趣的短视频内容。

我最初做这个项目,就是为了给一个社区活动做一个简单的“姿势模仿游戏”。参与者需要模仿屏幕上的目标姿势并保持几秒钟。就是靠计算几个主要关节的角度相似度来判断是否成功。虽然逻辑不复杂,但现场效果非常好,大家都玩得很开心。这让我深刻感受到,技术不在于多高深,而在于能否巧妙地解决一个具体的问题。

从安装环境到实现动作判断,我们一步步构建的这个系统,就像一个功能齐全的“工具箱”。MediaPipe提供了精良的“探测器”,OpenCV提供了灵活的“显示和操作面板”,而你的逻辑代码则是赋予这个系统灵魂的“大脑”。希望你在运行起代码、看到自己的动作被实时分析的那一刻,能感受到和我当初一样的兴奋。剩下的,就是发挥你的想象力,去创造属于你自己的应用了。如果在尝试的过程中遇到任何问题,比如某个角度计算不准,或者想实现某个特定功能但不知从何下手,回顾一下我们这几个章节的基础操作,多调整参数,多打印中间数据看看,问题总能一步步解决。编程和做项目就是这样,在动手和调试中,成长最快。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符  | 博主筛选后可见
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值