简介:一份面向Python及计算机视觉学习者的实战教程,围绕MediaPipe框架讲解如何调用预训练模型,实时完成面部关键点、手部跟踪与全身姿态估计。教程从环境依赖安装、网络摄像头视频流读取讲起,逐步覆盖面部检测、Holistic模型下的多部位地标绘制,并配有可直接运行的示例代码、效果说明和应用场景分析(如手势控制、健身指导、AR交互),整体结构清晰,适合计算机视觉入门者快速上手,也方便有经验的开发者做原型验证。文档还梳理了各模型的适用场景:面部关键点可用于表情分析,手部跟踪服务于手势交互,全身姿态估计应用于动作比对。资源包共1个docx文档,体积约16KB,轻量便于下载阅读。已有357人学习浏览,文档中对MediaPipe跨平台特性、预训练模型能力及代码实现均有讲解,是一份可操作性强、重点突出的技术笔记。
1. 用 Python 和 MediaPipe 做三合一姿态检测:先从一次翻车说起
摄像头前面站着一个人,想让程序实时说出他有没有抬手、扭头、眨眼——这个需求听起来不难,但真上手时会发现,靠 OpenCV 凭空算关键点根本是玄学:肤色分割在复杂背景下一碰就碎,帧率掉到个位数,一顿操作下来连「手在哪」都稳定不了。后来换用 MediaPipe,才把这条链路走通。MediaPipe 是 Google 开源的机器学习推理框架,内置了人脸网格、姿势估计和手部关键点三套预训练模型,Python 调用非常直接,CPU 上就能跑到实时帧率。这篇笔记就把「怎么用 Python 把三套检测串起来、参数怎么设、哪些地方容易翻车」一次讲透,给正准备做行为分析、交互控制或动作纠正的从业者一条能直接复现的路径。
2. 为什么选 MediaPipe:方案对比与最小环境搭建
2.1 三套模型分别解决什么问题:人脸、姿态、手部关键点
MediaPipe 在 Python 侧最常用的三件套是 FaceMesh、Pose 和 Hands,它们各自解决的是完全不同的检测任务。
FaceMesh 输出 468 个人脸关键点,覆盖眉毛、眼睛、嘴唇、面部轮廓甚至瞳孔位置。它不是为了做人脸识别(那是 FaceNet 那类模型的活),而是给出「脸在画面里长什么样」的稠密几何描述。基于这 468 个点,你可以算眼睛开合度、头部朝向、嘴部动作,这些是专注度分析、疲劳提醒、表情驱动类应用的基础。
Pose 基于 BlazePose 架构,输出 33 个身体关键点,从鼻子、肩膀、肘、腕一路到脚踝。它定位的是「人的骨架」,不是人形框。有了骨架坐标,可以算肘关节角度、判断深蹲幅度、识别跌倒姿态,这是动作计数、健身纠正、安防异常行为检测里最常用的输入。
Hands 输出每只手 21 个关键点,并且能区分左右手。它内部是两段式管线:先用一个轻量 palm detector 在整图上找到手掌位置,再在裁剪区域里精确定位 21 个关键点。这意味着手部检测比人体姿态更依赖「手在画面里够不够大」。
三套模型的输出结构很统一,都是 normalized landmark(0~1 浮点数),格式一致是它们能拼进同一条推理链路的关键前提。
2.2 MediaPipe 与 OpenPose、YOLO-pose 的边界:实时单机场景谁更合适
从业者常在这三个方案之间犹豫,我先说结论:实时单机、CPU 优先、跨平台,选 MediaPipe;追求学术级精度、有多卡 GPU 资源,考虑 OpenPose;已经重度依赖 YOLO 生态、需要目标框和关键点同时输出,YOLO-pose 更顺手。
OpenPose 是姿态估计领域的老牌方案,关键点定义细致,精度上限高,但它对算力的要求是另一个量级:原版在 CPU 上跑单人实时几乎不现实,通常需要 CUDA 加速,部署成本明显更高。如果你的业务是离线批处理、对精度极度敏感,它才有优势。
YOLO-pose 把关键点检测做成了目标检测的附属输出,每个检测框自带对应关键点。这种方案在「画面里有多人、需要先框人再取点」的语义下很自然,模型权重也轻。但它的关键点数量通常是人体的 17 点或 28 点版本,手部、面部细节远不如 MediaPipe 细,做手势或面部动作分析时不够用。
MediaPipe 的优势恰恰在工程落地上:模型小、推理快、CPU 可跑,官方还同时维护 Python 和移动端接口。代价是精度上限不如学术方案,关键点偶尔会抖动。对绝大多数实时交互、行为判断类的产品需求来说,这个精度足够了,而且省下来的部署成本是实打实的。
2.3 最小依赖清单与安装验证:Python 环境、mediapipe 安装、跑通第一段代码
安装这一步本身不难,但环境搭配上有一些需要注意的地方。建议用 Python 3.9~3.11 的 64 位环境,新版本 mediapipe 对 Python 版本的支持有明确上限,装之前先确认一下自己环境里的 Python 版本,避免遇到 wheel 不匹配的报错。
# 创建虚拟环境(Windows / Linux / macOS 通用) python -m venv mp_env # Windows 激活 mp_env\Scripts\activate # Linux / macOS 激活 # source mp_env/bin/activate # 安装核心依赖 pip install mediapipe opencv-python numpy # 验证安装 python -c "import mediapipe as mp; print(mp.__version__)"这段命令先建了一个独立的虚拟环境,这是为了避免 mediapipe 和项目的其他包互相污染依赖。pip install 一行把三样东西装齐:mediapipe 本身、opencv-python 用来读摄像头和图像处理、numpy 用来做数组运算。最后一句验证命令会打印出版本号,看到输出就说明基本环境没问题。如果打印失败,大多数情况是 Python 版本超出 wheel 支持范围,换一个 3.10 左右的版本基本能解决。
装完库之后,先用一张静态图做冒烟测试,比直接上摄像头好排错。下面这段代码读入本地图片,跑一次姿势检测并打印出关键点数量:
import cv2 import mediapipe as mp mp_pose = mp.solutions.pose pose = mp_pose.Pose(static_image_mode=True, model_complexity=1) image = cv2.imread('test.jpg') image_rgb = cv2.cvtColor(image, cv2.COLOR_BGR2RGB) results = pose.process(image_rgb) if results.pose_landmarks: print(f"检测到 {len(results.pose_landmarks.landmark)} 个关键点") else: print("未检测到人体姿态") pose.close()这段代码的逻辑是:把图像从 OpenCV 默认的 BGR 色彩空间转到 MediaPipe 要求的 RGB,然后调用 process 方法推理。static_image_mode=True 表示对单张静态图做完整检测,model_complexity=1 表示使用完整模型(比 0 更准,比 2 更快)。最后记得调用 pose.close() 释放资源,这是新手很容易漏掉的一步,不释放的话在循环场景里会累积内存占用。
3. 从单模型到三合一:人脸、身体、手部检测的实现步骤
3.1 人脸检测:FaceMesh 的 468 关键点与注意力方向判断
FaceMesh 在 Python 侧的接口设计得很简单,初始化时调整好参数,送入 RGB 帧就能拿到多张脸的 468 点坐标。实际业务里用得最多的不是全部 468 点,而是眼睛周围那几个点——通过它们可以算出眼纵横比(EAR),用来判断睁眼还是闭眼。
FaceMesh 初始化有三个关键参数:static_image_mode 控制是静态图模式还是视频跟踪模式;max_num_faces 限制一帧最多检测几张脸,默认是 1,业务场景经常需要调大;min_detection_confidence 是检测阶段的最低置信度阈值,默认 0.5,调高会减少误检但可能漏掉模糊的小脸。
对于视频流,我一般强烈建议把 static_image_mode 设为 False,让模型进入跟踪模式。跟踪模式不会每帧都做全图人脸检测,而是基于上一帧的结果做局部跟踪,速度更快、抖动也更小。代价是如果脸在画面里突然大幅移动或转侧,跟踪可能丢失,模型会重新启动全图检测。在实际产品里这通常比每帧全量检测的体验好得多。
眨眼判断是 FaceMesh 最经典的应用,核心是指标 EAR(Eye Aspect Ratio)。计算方式是用眼睛六个关键点中的两组垂直距离取平均,再除以水平距离。人正常睁眼时 EAR 在 0.2~0.35 之间,闭眼时会显著下降到 0.1 以下。按视频帧序列统计 EAR 的持续低值,就能判断一次眨眼的开始和结束。持续低值超过某个时长(比如 0.5 秒),可以判为闭眼超过正常值,这是疲劳驾驶提醒系统的常见做法。
3.2 身体姿势检测:Pose 的 33 关键点与骨架连线绘制
Pose 模型用 BlazePose 算法,输出 33 个关键点,比 COCO 的 17 点多了面部区域和手脚的一些点,实际做动作分析时信息更充分。它同样有两种使用模式:静态图和视频跟踪,参数上多了一个 model_complexity。
model_complexity 是 Pose 独有的性能开关,取值 0、1、2。0 是最轻量模型,速度快但精度弱,适合低功耗设备;1 是平衡档,大多数产品场景我会默认用 1;2 是最大模型,精度最好,但 CPU 推理耗时明显增加。在 PC 上跑 model_complexity=2 还能接受,在树莓派这类设备上就要谨慎了。
绘制骨架线是调试阶段的刚需。MediaPipe 提供了现成的绘图工具,把 Pose 输出的 landmark 按预设的连接关系画出来,骨骼走向一目了然。但要注意传进去的图必须是 RGB 的,如果你直接把 BGR 帧传过去,绘制结果的颜色会偏蓝调,看着很别扭。
关键点绘制有一个容易被忽略的点:landmark 坐标全部是归一化的浮点数,范围在 0~1 之间,表示相对于图像宽高的比例。绘图工具内部会自动换算成像素坐标。你如果要自己算关节角度,也别忘了先乘上图像的宽和高,再算几何关系,否则角度数值会完全失真。
3.3 手势识别:Hands 的 21 关键点与左右手判定逻辑
Hands 模型输出的 21 个点从手腕(0 号点)开始,沿手掌到五个指尖分布。它在设计上有一个有意思的细节:模型同时输出左右手标签,但这个左右是基于「画面里的人自己的左右」来定义的,不是基于观众视角。如果你直接把 label 当成观众视角的左右手,在镜像画面里一定会反,这在做手势交互时是典型的坑。
Hands 初始化时有两个参数需要留意:max_num_hands 控制一帧最多检测几只手,默认是 2,如果你要处理多人或者一人双手的特殊场景,记得调大;min_detection_confidence 默认 0.5,新手往往会为了拉高检测率把它调低到 0.3 甚至 0.2,结果误检大量增加,背景里的晃动物体也被当成手。
手部检测对目标大小非常敏感,因为前置的 palm detector 是在缩略图上做检测的。手在画面里占的面积太小,直接检测不到;占得太大,超出检测框范围,关键点也会漏。这是一个需要靠部署经验来调的问题,后面避坑章节会展开讲。
手势的语义判断一般不用模型,而是基于 21 个关键点的几何关系自己写规则:判断手指伸直还是弯曲,看的是指尖点到手腕的距离与该手指指根到手腕距离的比值;判断握拳,看五个指尖是否都贴近手掌中心。这套规则在大多数场景下够用,而且逻辑透明、容易调试。
3.4 把三个模型串进同一路视频帧:同步推理与结果整合
把三套模型拼进同一循环,最直接的做法是每一帧都依次调用三个 process,但这样 CPU 占用会比较高。更常见的做法是控制推理频率,比如姿势检测每帧跑、手势检测每两帧跑一次,画出上一帧的结果。下面这段代码演示了完整的整合流程:
import cv2 import mediapipe as mp mp_face_mesh = mp.solutions.face_mesh mp_pose = mp.solutions.pose mp_hands = mp.solutions.hands mp_drawing = mp.solutions.drawing_utils mp_drawing_styles = mp.solutions.drawing_styles face_mesh = mp_face_mesh.FaceMesh( static_image_mode=False, max_num_faces=1, min_detection_confidence=0.5) pose = mp_pose.Pose( static_image_mode=False, model_complexity=1, min_detection_confidence=0.5, min_tracking_confidence=0.5) hands = mp_hands.Hands( static_image_mode=False, max_num_hands=2, min_detection_confidence=0.5, min_tracking_confidence=0.5) cap = cv2.VideoCapture(0) cap.set(cv2.CAP_PROP_FRAME_WIDTH, 640) cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 480) frame_count = 0 while cap.isOpened(): ret, frame = cap.read() if not ret: break frame_count += 1 frame_rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) frame_rgb.flags.writeable = False # 每帧跑人脸和姿势,每 2 帧跑一次手势 face_results = face_mesh.process(frame_rgb) pose_results = pose.process(frame_rgb) if frame_count % 2 == 0: hand_results = hands.process(frame_rgb) else: hand_results = None # 绘制结果 if face_results.multi_face_landmarks: for face_landmarks in face_results.multi_face_landmarks: mp_drawing.draw_landmarks( image=frame, landmark_list=face_landmarks, connections=mp_face_mesh.FACEMESH_TESSELATION, landmark_drawing_spec=None, connection_drawing_spec=mp_drawing_styles .get_default_face_mesh_tesselation_style()) if pose_results.pose_landmarks: mp_drawing.draw_landmarks( image=frame, landmark_list=pose_results.pose_landmarks, connections=mp_pose.POSE_CONNECTIONS, landmark_drawing_spec=mp_drawing_styles .get_default_pose_landmarks_style()) if hand_results and hand_results.multi_hand_landmarks: for hand_landmarks in hand_results.multi_hand_landmarks: mp_drawing.draw_landmarks( image=frame, landmark_list=hand_landmarks, connections=mp_hands.HAND_CONNECTIONS, landmark_drawing_spec=mp_drawing_styles .get_default_hand_landmarks_style(), connection_drawing_spec=mp_drawing_styles .get_default_hand_connections_style()) cv2.imshow('MediaPipe Multi-Task Detection', frame) if cv2.waitKey(1) & 0xFF == ord('q'): break cap.release() cv2.destroyAllWindows()这段代码里有几个细节值得拆开说。frame_rgb.flags.writeable = False 这一行是性能关键,它告诉 numpy 这块内存不允许原地修改,MediaPipe 内部就能跳过一些数据拷贝,推理速度能快不少,但代价是你不能再原地往 frame_rgb 上画图,所以要画的时候得用原始 BGR 帧。这是官方惯例的坑点,很多教程不提。
手势检测隔帧执行的策略是有依据的:Hands 的 palm detector 比较耗时,而手部动作在相邻两帧之间变化通常不大,隔一帧取一次结果完全够用。我用了 frame_count % 2 == 0 这个取模条件控制频率,你可以随手改成 3 或 5,观察一下帧率和精度的平衡点在哪里。
绘制人脸网格用的是 FACEMESH_TESSELATION 连接关系,这个是密集三角网格,画出来很炫但视觉上会糊成一片。如果你只看眼睛或嘴部,建议换成 FACEMESH_CONTOURS 或者自己挑几个关键点画,视觉会清爽很多,CPU 也能省一点。
4. 实战避坑:MediaPipe 三合一检测的 5 个高频问题
4.1 CPU 占用居高不下:三个模型一起跑,风扇狂转
现象:代码跑起来后 CPU 占用直接冲到 80% 以上,笔记本风扇声音明显变大,帧率降到 10fps 以下,画面肉眼可见地卡顿。
原因:三个模型默认都是每帧全量推理,且输入分辨率如果按摄像头默认的 1280×720 甚至更高,每个人脸、手势检测都要在较大画面上做缩放计算,整个链路叠加起来的算力消耗非常可观。
解决:把摄像头分辨率强制设为 640×480 或更低,这是最有效的一招,检测精度损失很小但帧率提升明显;再按前面的代码把 Hands 推理频率降为每 2~3 帧一次;Pose 的 model_complexity 从 1 降到 0,如果只是粗略判断姿态而不是做精细角度分析,几乎无感知。做完这三步,通常能把占用从 80% 压到 40% 以下。
4.2 手一离远就检测不到:小目标检测失效
现象:手在摄像头前 30 厘米内检测正常,稍微往后一放,手部关键点就消失了;人在画面里站着,手自然垂在身体两侧,也时常检测不出来。
原因:Hands 模型的前置 palm detector 在缩略图上工作,手在图像里占比小、纹理弱,检测器在缩略图上容易把它漏掉。这是两段式管线在物理层面的局限,不是代码写错。
解决:两个思路。一是做大手在画面里的成像面积,可以物理上让摄像头离人更近,或者在代码里把画面中手所在的 ROI 区域裁剪出来再送进 Hands 模型。二是给手部检测加引导,利用 Pose 输出里手腕点的位置,裁剪出以手腕为中心的方形区域放大后再检测,这种方法效果比单纯调 confidence 阈值明显得多,我在实际项目里用它把手部检测的有效距离从 30 厘米拉到 80 厘米左右。另外,min_detection_confidence 不建议低于 0.5,调太低会大面积误检,背景里的杂志、键盘、纸团都可能被当成手。
4.3 左手右手搞反了:镜像与身体坐标的冲突
现象:画面里人举起右手,程序输出的 label 是 Left;或者对着镜子做测试,左右手判断完全颠倒。
原因:Hands 模型输出的 handedness 是基于「画面中人自身的左右」来标注的。视频画面如果不做镜像处理,人举起右手时,在图像坐标里这只手出现在画面左侧,模型按图像内容判断,就可能标成 Left。镜面场景则相反,人的左右和画面里的左右发生了翻转。
解决:如果是视频通话或自拍场景要做镜像显示,先对帧做水平翻转,再送进模型,这样显示和识别结果就一致了。如果是普通场景,你需要知道系统里「左右」用在哪里:如果是让用户看到自己,用镜像;如果是让后台程序做动作语义判断(比如判断举的是左手还是右手),不能用镜像,而且要按照身体的全局朝向把影像坐标系转换到人体坐标系。具体做法是取 Pose 输出的左右肩坐标算出身体朝向,再根据手相对身体的位置来修正左右标签。纯靠 Hands 自带的 label 做业务判断,几乎一定会出问题。
4.4 static_image_mode 用错场景:视频里关键点疯狂抖动
现象:视频流里一个人安静坐着,头部和手的关键点却在小幅高频抖动,像开了抖动特效;有时候明明没动,关键点却在几个位置之间跳变。
原因:这是典型的把 static_image_mode=True 用在视频流里的后果。静态图模式每帧都做全量检测,帧与帧之间没有时序关联,检测框的微小波动直接反映到关键点上,就会变成肉眼可见的抖动。跟踪模式下模型会参考上一帧结果做平滑,抖动量能小一个量级。
解决:视频流场景统一用 static_image_mode=False,并显式设置 min_tracking_confidence 参数,默认 0.5 够用。如果抖动仍然明显,可以在代码侧对关键点坐标做一阶低通滤波,也就是把当前帧坐标和上一帧坐标按 0.7 比 0.3 的权重混合。这招对姿态和手部都管用,但注意混合系数不能太小,否则动作响应会有明显的迟滞感。我把这种事后平滑看作最后一道保险栓,如果模型本身的跟踪模式已经稳定,就不要再叠一层,避免过度平滑把快速动作吃掉。
4.5 中文路径图片读不进来:OpenCV 与操作系统编码的冲突
现象:cv2.imread 的图片路径里带了中文,返回值总是 None,图片显示为空;换英文路径一切正常。
原因:OpenCV 的 imread 底层用的是 C++ 的文件读取接口,不处理操作系统层面的 Unicode 编码,中文路径在 Windows 上尤其容易直接失败。这是 OpenCV 的历史遗留问题,跟 MediaPipe 本身无关,但做项目时文件路径经常带中文名,所以踩到的人不少。
解决:不要用 cv2.imread 直接读中文路径,改用 numpy 和 cv2.imdecode 组合:
import cv2 import numpy as np def imread_unicode(filepath): # 用 numpy 从字节流读文件,再交给 cv2 解码,绕开编码问题 data = np.fromfile(filepath, dtype=np.uint8) return cv2.imdecode(data, cv2.IMREAD_COLOR) image = imread_unicode('姿态数据/实验_01.jpg')这段代码的思路是先用 numpy 的 fromfile 以字节方式读入整个文件,此时文件内容已经是二进制数组,跟路径编码无关了,再交给 cv2.imdecode 去解析图像格式。这样处理之后,中文目录和中文文件名都能正常读取,我在处理批量数据集时一般直接封装这个函数统一调用,能省掉不少无谓的排查时间。注意反过来写文件时也有同样的问题,cv2.imwrite 写中文路径也会失败,解决办法是用 cv2.imencode 先编码成字节流,再用 tofile 写入,套路和读取一致。
5. 让检测跑得更快:FPS 优化与关键点数据落地
5.1 分辨率、置信度阈值与推理频率:三合一链路的性能调整参数
三合一检测的性能瓶颈基本固定在这三个旋钮上:输入分辨率、置信度阈值、推理频率。它们各自影响检测结果的不同侧面,组合起来才是一条完整的调优路径。
输入分辨率对 MediaPipe 这类模型来说不是越高越好。模型内部会先把图像缩放到固定尺寸再推理,你送 1080p 的帧进去,它也得先缩小,白白浪费了缩放耗时,而关键点精度并不会因此显著提升。我一般把摄像头分辨率设在 640×480,这是性价比很高的档位。如果业务确实需要看远处的小目标(比如三四米外的手部动作),再考虑升到 1280×720,但这时候 CPU 占用会明显上去。
min_detection_confidence 这个参数是检测阶段的闸门,调高它,误检变少,但漏检会增加;调低它,召回率提升,但容易把背景噪声当成目标。我通常用 0.5 作为起步值,然后根据真实场景微调:如果现场光线差、人动得快,降低到 0.4 能救回一些检测;如果误检多到影响业务逻辑,就往上提到 0.6 或 0.7。注意不要低于 0.3,再低就是拿稳定性和性能换那一点点召回,不划算。
推理频率的错峰是性能优化里最容易见效的一招。三个模型不一定要每帧都跑:人脸和姿势是全局信息,每帧跑;手部隔 2~3 帧跑一次,用最近一次结果回填。因为姿势模型已经把人体框出来了,手的位置变化都可以通过姿态的腕部点粗估,两帧之间的手部误差通常不超过一个手掌宽度,对大多数交互和计数场景完全够用。
这三个参数的调整顺序我建议是:先固定分辨率,再调 confidence,最后再动推理频率。分辨率影响一切,confidence 只影响检测边界,推理频率是最后的手段。不要一上来就隔帧跑,那样丢了精度还不容易定位问题。
5.2 关键点坐标归一化:从模型输出到业务数据的转换
MediaPipe 输出的 landmark 坐标是归一化的浮点数,x、y 在 0~1 之间表示相对图像宽高的比例,z 是相对深度的估计值。这个设计让推理结果跟输入分辨率无关,同一段视频在不同分辨率下跑出来的 landmark 数值一致,方便做清洗和比对。但实际写业务逻辑时,往往需要把归一化坐标还原成像素坐标,或者按自己的需求存储。
下面这段代码把 Pose 的关键点转成 numpy 数组并保存为 CSV,是后续做动作分析前的标准预处理步骤:
import numpy as np import cv2 import mediapipe as mp mp_pose = mp.solutions.pose pose = mp_pose.Pose(static_image_mode=True, model_complexity=1) def pose_to_array(results, frame_width, frame_height): """把 MediaPipe 的 Pose 结果转成 (33, 3) 的数组,前两列是像素坐标,第三列是相对深度""" if not results.pose_landmarks: return None pts = [] for lm in results.pose_landmarks.landmark: x = lm.x * frame_width y = lm.y * frame_height z = lm.z pts.append([x, y, z]) return np.array(pts) image = cv2.imread('test.jpg') image_rgb = cv2.cvtColor(image, cv2.COLOR_BGR2RGB) results = pose.process(image_rgb) h, w = image.shape[:2] pts = pose_to_array(results, w, h) if pts is not None: # 保存为 CSV,表头标注每一列的含义 np.savetxt('pose_landmarks.csv', pts, delimiter=',', header='x,y,z', comments='') print(f"已保存 {pts.shape[0]} 个关键点坐标")这段代码的核心是把归一化坐标乘以图像的宽和高,还原为像素坐标,z 保持不变。第三列 z 是模型预测的相对深度值,单位不是米,而在多个关节之间具有相对意义:z 越大代表离镜头越远。你如果要做关节角度的计算,直接用 x、y 像素坐标算二维角度就够了,z 主要用在需要区分肢体前后关系的场景,比如判断手臂是向前伸还是向侧面平举。
保存成 CSV 只是数据落地的起点,我一般会在此基础上继续加工,把每一帧的 33×3 数组拼接成时间序列,再按时间窗提取特征,比如肘关节角度的滑动平均值、腕部坐标的位移速度。这些时序特征才是动作识别模型真正消费的输入。
5.3 坐标平滑的正确姿势:防抖但别拖影
landmark 序列里的抖动分为两类:一类是检测器本身的随机游走,幅度小、频率高;另一类是目标在快速运动时检测结果出现跳变,幅度大、偶尔丢失一帧。两类问题处理方式不同。
小幅度高频抖动适合用指数移动平均(EMA)平滑。公式是 smoothed = alpha * current + (1 - alpha) * previous,alpha 取 0.3 到 0.5 之间。alpha 越大越跟手,alpha 越小越平滑,我常用 0.4 起步。
大幅度跳变适合用中值滤波或丢失帧补齐。如果某一帧检测不到手,不要直接输出空数据,而是沿用上一帧结果并做一个简单的速度外推。补帧逻辑可以写成一个轻量函数:当前帧位置 = 上一帧位置 + (上一帧位置 - 上上一帧位置)。这个一阶线性外推对打羽毛球、抬手这类动作够用,但在突然转向的动作里会存在轻微过冲,用的时候注意观察。
平滑处理最怕的就是过度,alpha 调到 0.1 以下后,你会发现手已经抬起来了,屏幕上关键点还在原处磨蹭,这种拖影感在交互场景里比抖动更让人难受。平滑只解决检测噪声问题,不解决模型本身丢目标的问题,后者要靠前面说的 ROI 引导和阈值调整来做。
6. 从检测到应用:把三合一结果用起来的三个方向
检测本身不产生价值,关键点落到具体业务里才算闭环。我按实际落地的频率整理三个方向,每个方向都可以直接基于前面的代码延展。
第一个方向是专注度分析,用的是 FaceMesh 的关键点序列。通过 EAR 判断眨眼频率和闭眼时长,通过鼻尖和两只耳朵的连线角度估算头部朝向,如果头部持续偏转超过 30 度且眨眼频率显著下降,就可以判定为专注度下降。这个思路在在线教育、远程监考、驾驶疲劳提醒里都适用。落地时注意每 15 秒做一次统计窗,不要用单帧下结论,单帧误判率太高。
第二个方向是动作计数与姿态纠正,用的是 Pose 的关节序列。统计深蹲次数时,只需要计算髋关节、膝关节、踝关节三点形成的夹角,当角度从接近 180 度连续下降到小于 90 度再回到 180 度,计一次完整动作。俯卧撑、引体向上同理,只是换关节组合。这套逻辑的实现成本很低,但需要处理动作速度差异:同样的角度变化,快速做完和慢速做完的时间窗不同,计数逻辑里要加一个时间容忍度参数,否则会漏计或重计。
第三个方向是手势交互,用的是 Hands 的关键点序列。识别数字 1 到 5 的规则很简单:统计除拇指外四个手指中伸直的数量,加上拇指的状态。食指和中指伸直判定为 V 字手势,在界面翻页场景里很常用。更复杂的可以识别点击动作,也就是某个指尖在短时间内的位移变化和速度曲线,模拟鼠标点击。手势可以在两帧内完成识别,没有明显延迟感,适合做演示翻页、缩放控制的替代方案。
三个方向里我踩过最深的坑是「先做了识别,再想业务逻辑」。曾经有段时间,我把姿态、手势、人脸全部识别出来并展示在画面上,视觉效果很炫,可到了交付阶段发现业务侧根本不知道拿这些坐标干什么。后来我调整了顺序,先定义清楚业务需要什么动作特征,再倒推需要哪几个关键点,然后才去配置模型。模型选型和参数调整都是跟着业务需求走的,先有动作定义,再有关键点选择,最后才有模型配置。每次接新项目我都提醒自己先想清楚这一步,能省掉大量返工的时间。希望这篇笔记能帮你把这条路走顺。
本文还有配套的精品资源,点击获取