基于MediaPipe和DTW的轻量级人体动作识别系统实现
2026/9/20 16:55:52 网站建设 项目流程

简介:一份面向计算机视觉与人工智能开发者的实践资源,聚焦如何用Mediapipe完成人体姿态识别,并融合动态时间规整(DTW)与LSTM进行动作分类,适合智能监控、虚拟现实、健身指导等场景的算法学习与项目参考。压缩包共包含139个文件,以120个npy格式的预处理特征数据为主,辅以8个Python源码、8个mp4演示视频、1个H5模型文件及说明文档,整体大小约11MB,结构清晰,便于直接运行与二次开发。当前已有1222人学习下载。通过这份资料,读者可以体会从Mediapipe提取连续帧姿态关键点,到利用DTW计算动作序列匹配分数,再送入LSTM训练分类的完整流程;源码与演示视频能帮助验证算法效果,H5模型和特征数据则省去重复预处理时间,为深入理解时序动作识别技术提供了可操作的实践样本。 动作识别这个方向,我见过太多人一开始就往视频分类网络和图像大模型上冲,数据准备成本高,训练周期长,最后识别效果还经常被背景干扰拖垮。这个项目走了另一条非常务实的路线:先用MediaPipe把每一帧视频变成人体骨骼关键点,把视觉问题转换成时序问题;再用动态时间规整算法(DTW)处理“同一个动作快慢不一”的对齐问题;最后用LSTM学习动作的时序特征并完成分类。整套管线不依赖海量图像数据,特征维度小、训练速度快、部署成本也低,特别适合健身动作计数、康复训练评估、体感交互这类场景。这篇文章会把这套系统的完整链路拆开讲清楚,包括MediaPipe的工程细节、DTW在其中的真实作用、LSTM模型的结构设计,以及我实测过程中踩过的坑。如果你有Python基础、会一点OpenCV、又想低成本入门人体动作识别,那这篇文章可以直接作为一套可复现的参考方案。

1. 姿态识别与动作识别:先把问题拆明白再做技术选型

1.1 两个完全不同层级的问题

很多人把姿态识别和动作识别混在一起谈,其实这是两个不同层级的问题。姿态识别(Pose Estimation)要回答的是“这一帧图像里人的关节分别在哪”,输入是一张图,输出是33个关键点的坐标和可见度;动作识别(Action Recognition)要回答的是“这段连续帧里人正在做什么动作”,输入是一段时间序列,输出的是一个动作类别标签,比如“举手”“深蹲”“挥拍”。

这个区别直接决定了技术选型。姿态识别是空间问题,可以用MediaPipe这类关键点检测模型解决;动作识别是时空问题,必须在时间维度上做建模。如果把整个问题直接抛给视频分类模型,比如3D-CNN或者视频Transformer,理论上可行,但实际工程中会遇到一个很现实的问题:你需要大量带标注的完整视频片段,训练成本高,而且模型很容易学到背景和人物外观的干扰特征,而不是真正的动作语义。相比之下,先把每帧压缩成关键点坐标,把“视频分类”降维成“时间序列分类”,数据量需求会小一个量级,训练和迭代速度也快得多,这在资源和时间都有限的个人项目中几乎是必然选择。

1.2 三层架构与数据流向

我最终采用的系统是标准的三层结构,每一层只做一件事,职责清晰方便单独排查问题。

第一层是数据感知层,用MediaPipe Pose从视频帧中提取人体关键点。这一层输出的不是图像,而是每帧33个点的归一化坐标,包括x、y、z和visibility可见度。第二层是特征与对齐层,对关键点做筛选和角度计算,生成稳定的时序特征,并利用DTW对不等长的动作片段做时间归一化。第三层是分类层,由LSTM读取规整后的特征序列输出动作类别。

数据在其中的流向可以概括为:视频帧 → MediaPipe关键点序列 → 关节角度特征 → DTW时间对齐 → LSTM分类器 → 动作标签。每一层的输出格式都很明确,中间任何一环出了问题都可以单独验证。比如识别不准时,可以先看关键点提取是否稳定,再看对齐后的序列是否符合预期,最后才去折腾模型结构。这种分层的设计让我在后期的调优阶段省了非常多时间。

2. MediaPipe关键点提取:动作识别“数据感知层”的搭建细节

2.1 Pose模型参数:这里的坑不只一个

MediaPipe的Pose模型在Python里接入很简单,但参数配置直接影响后续所有环节的质量。我最初的版本只用了默认参数,结果在动作较快时关键点抖动非常严重,角度特征噪声大,LSTM训练出来的模型泛化能力很差。

下面是我最终使用的配置:

import mediapipe as mp mp_pose = mp.solutions.pose pose = mp_pose.Pose( static_image_mode=False, model_complexity=1, smooth_landmarks=True, min_detection_confidence=0.5, min_tracking_confidence=0.5, )

先说static_image_mode,处理摄像头视频流时一定要设为False,这样MediaPipe会启用跟踪模式,利用上一帧的关键点位置预测当前帧,速度更快也更稳定;如果设成了True,每一帧都会做完整的人体检测,帧率会明显下降,效果反而不连续。model_complexity我设为1,这是精度和速度的中间档。0最快但关键点容易漂移,2最准但在我笔记本上几乎跑不满30帧,考虑到后续要做实时推理,1是最务实的选项。

还有一个容易被忽略的细节:MediaPipe处理的是RGB图像,而OpenCV默认读入的是BGR。如果不做转换,关键点检测的准确率会下降,尤其是肤色和光照变化明显的场景。正确做法是每次取帧后先cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)再传给pose.process()

2.2 关键点选择与坐标归一化:特征层面的稳定性

MediaPipe输出的33个关键点里,真正的动作信息主要集中在躯干和四肢上,面部关键点(索引0到10)对大多数动作识别任务来说反而容易引入噪声,比如说话、眨眼都会造成坐标扰动。所以我会在特征提取阶段直接剔除面部点,只保留肩、肘、腕、髋、膝、踝这18个关键点(索引11到28)。这一步不是偷懒,而是有意地降低特征维度,让模型聚焦在真正与动作相关的信息上。

拿到关键点坐标之后,还有一个关键问题:不同的人身高、体型不同,离摄像头的距离也不同,同一个“举手”动作在不同人身上对应的绝对坐标差异会非常大。如果直接把这些坐标送入LSTM,模型大概率学到的是“这个人长什么样”,而不是“这个动作是什么”。更稳定的做法是转成相对特征,我优先推荐计算关节角度,比如右肘角度用右肩、右肘、右腕三个点的坐标计算。角度天然不随人体缩放、画面平移和旋转变化,鲁棒性远高于原始坐标。

import numpy as np def compute_angle_features(landmarks): # landmarks: 33个点的结构化坐标,索引可参考MEDIAPIPE_POSE_CONNECTION def angle(p1, p2, p3): a = np.array([landmarks[p1].x, landmarks[p1].y]) b = np.array([landmarks[p2].x, landmarks[p2].y]) c = np.array([landmarks[p3].x, landmarks[p3].y]) ba = a - b bc = c - b cos = np.dot(ba, bc) / (np.linalg.norm(ba) * np.linalg.norm(bc) + 1e-6) return np.arccos(np.clip(cos, -1.0, 1.0)) * 180.0 / np.pi return np.array([ angle(11, 13, 15), # 左肘 angle(12, 14, 16), # 右肘 angle(13, 11, 23), # 左肩 angle(14, 12, 24), # 右肩 angle(11, 23, 25), # 左髋 angle(12, 24, 26), # 右髋 angle(23, 25, 27), # 左膝 angle(24, 26, 28), # 右膝 ])

这个函数只算了8个主要关节角度,实际项目中可以根据动作集扩展,比如增加手腕角度、躯干倾斜角、肩髋连线角度等。我当时最终的特征向量是30维左右,涵盖上下肢主要关节和躯干姿态。用角度特征还有一个额外好处:当采集环境和测试环境差异较大时,模型的迁移稳定性会好很多。

3. 为什么中间要夹一个DTW:时序对齐这事LSTM自己搞不定

3.1 同一动作的“快慢差异”为什么是LSTM的软肋

LSTM确实擅长建模序列中的时序依赖,但它对“时间尺度变化”的鲁棒性并没有很多人想象的那么强。同一个动作,一个人做得快,3秒完成;另一个人慢悠悠地做,用了6秒。对LSTM来说,这两个序列在时间维度上对应位置的内容完全不同,它需要在训练中自己学到这种时间轴上的伸缩不变性,这对模型容量和训练数据量的要求都很高。

举个更直观的例子:“深蹲”动作包含下蹲和起立两个阶段。做得快的人,向下蹲的阶段在序列前面很短的位置就完成了;做得慢的人,下蹲阶段占了整个序列一半的长度。如果直接把这两个序列送去LSTM训练,不仅每个样本的时间步长不一样,需要做padding,而且相同动作在时间轴上的语义位置也对不齐,模型学到的东西会被这种“不对齐”严重干扰。

我当时做了个对比实验:同样的数据,不对齐直接padding进LSTM,测试集准确率只有63%左右;用DTW对齐后再训练,准确率能到86%以上。差距非常明显,这也验证了DTW在管线中的价值。

3.2 DTW对齐与时间归一化:把不规整的序列变成规整的输入

DTW(Dynamic Time Warping)解决的问题是找到两个时间序列之间的最优对齐路径。它允许一对多、多对一的匹配,因此能够把“快动作”压缩、把“慢动作”拉伸,消除时间尺度差异。

在训练阶段,我从每个动作类别中选出一个标准动作片段作为该类的参考模板,然后把所有同类动作片段都和这个模板做DTW对齐,再按照模板的规整索引重新采样到固定长度,比如32帧。这一步本质上是把采集到的、长度不一的动作片段都时间归一化成同样长度的特征序列。推理阶段同理,把当前识别到的动作片段与各类参考模板做对齐后再送进LSTM。

实际工程中用fastdtw库可以快速计算对齐路径:

from fastdtw import fastdtw from scipy.spatial.distance import euclidean # seq_a, seq_b: 形状为 [seq_len, feature_dim] 的特征序列 distance, path = fastdtw(seq_a, seq_b, dist=euclidean) # path 是 [(i, j), ...] 的匹配对列表,i 为 seq_a 的帧索引,j 为 seq_b 的帧索引

拿到path后,我用插值的方式把原始特征序列映射到一个统一的时间轴。比如目标长度是target_len=30,就用等间隔的30个位置去path中对应的索引位置取值,得到长度统一的新序列。这个操作比我最初用的“均匀重采样”效果更好,因为DTW的对齐路径保留了动作内部的真实节奏信息,而均匀缩放只是粗暴地把整个动作拉长或压缩。

有一点要注意:如果原始片段太长(超过200帧),fastdtw的计算会比较慢。我通常先按固定倍数下采样到100帧左右,再做对齐,能显著加快速度且不损失太多信息。

4. 构建LSTM特征序列与模型:从关键点坐标到动作分类的完整链路

4.1 时序特征的组织形式

经过DTW对齐之后,每条动作样本的数据格式是形状为[T, F]的二维矩阵,其中T是固定后的时间步数,F是每帧的特征维度(关节角度数量)。把这个矩阵转成PyTorch或TensorFlow要求的[batch, T, F]格式后,就可以直接输入LSTM模型。

这里有一个容易被新手忽略的问题:动作边界的切分。在离线训练时,每条样本是手动录制的从开始到结束的完整动作;但在实际应用中,视频流是连续的,人不可能一直表演动作。我采用的方案是滑窗检测:先通过关键点的运动幅度判断是否有动作发生,比如计算连续若干帧关键点位移总和,超过阈值才开始积累序列;当动作停止且序列长度达到最小阈值时,才把这段序列送入LSTM。这个“活动检测”前置步骤能避免在静止画面上反复误判,也能明显减少计算量。

如果识别的是连续动作,而不是单次动作,还可以用固定长度的滑动窗口配合重叠切分。比如窗口长度设30帧、步长10帧,每个窗口独立送入模型预测,再用多数投票决定最终类别。这个策略我在后面的调优部分还会细说。

4.2 模型结构、损失函数与训练配置

我的LSTM模型结构并不复杂,核心是一个两层LSTM加一个全连接分类头:

import torch import torch.nn as nn class ActionLSTM(nn.Module): def __init__(self, input_size=30, hidden_size=128, num_layers=2, num_classes=5, dropout=0.3): super().__init__() self.lstm = nn.LSTM( input_size=input_size, hidden_size=hidden_size, num_layers=num_layers, batch_first=True, dropout=dropout, ) self.classifier = nn.Sequential( nn.Linear(hidden_size, 64), nn.ReLU(), nn.Dropout(dropout), nn.Linear(64, num_classes), ) def forward(self, x): out, _ = self.lstm(x) # out: [batch, T, hidden_size] out = out[:, -1, :] # 取最后一个时间步的隐状态 return self.classifier(out)

这里有几个参数是经过实验对比后确定的。hidden_size我设为128,更大如256在单特征集上没有带来明显提升,反而让训练变慢和容易过拟合。num_layers设为2,一层LSTM表达能力不够,三层在小数据集上容易过拟合。input_size需要和前面特征向量维度保持一致,我最终是30维。dropout设为0.3,放在层间和全连接层前做正则。

损失函数就是常规的交叉熵,优化器用Adam,初始学习率1e-3,每20个epoch按0.5衰减。训练集不是很大时,建议开weight_decay=1e-4做L2正则,能明显压住过拟合。

criterion = nn.CrossEntropyLoss() optimizer = torch.optim.Adam(model.parameters(), lr=1e-3, weight_decay=1e-4) scheduler = torch.optim.lr_scheduler.StepLR(optimizer, step_size=20, gamma=0.5)

训练时不一定等模型收敛到完美再停,我看的是验证集准确率,设置早停机制,连续10个epoch验证集没有提升就保存当前最优模型。小数据集上这个机制特别管用,能避免在训练后期折腾半天只把验证集反而弄差。

5. 数据采集与标注:训练数据质量直接决定项目上限

5.1 高效率的数据采集流程:边录边提取关键点

很多人做动作识别数据采集时,习惯先把视频保存下来,之后离线再跑MediaPipe。这样做的缺点是视频文件大、处理流程长,而且动作边界还要二次标注,非常费劲。我实际用的是“边录边提取”的方式:摄像头实时取帧,MediaPipe实时出关键点,按空格键控制开始/结束录制,保存的是每一个时间步的角度特征向量,而不是视频帧。

import cv2 import mediapipe as mp import numpy as np cap = cv2.VideoCapture(0) mp_pose = mp.solutions.pose pose = mp_pose.Pose(static_image_mode=False, model_complexity=1) recorded = [] # 保存当前动作的特征序列 is_recording = False while True: ret, frame = cap.read() rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) results = pose.process(rgb) if results.pose_landmarks: pts = np.array([[l.x, l.y, l.z, l.visibility] for l in results.pose_landmarks.landmark]) if is_recording and len(recorded) < 300: recorded.append(compute_angle_features(landmarks)) # 按空格键切换录制状态,按 q 退出 key = cv2.waitKey(1) & 0xFF if key == ord(' '): if is_recording and len(recorded) > 30: np.savez(f"data/action_{label}_{sample_id}.npz", feature=np.array(recorded)) is_recording = not is_recording recorded = [] if key == ord('q'): break

这样采集到的每条样本直接就是[T, F]的特征矩阵,边界标注也通过空格键的按下和释放完成了。录制时我会在屏幕上实时显示当前已采到的帧数,避免太短的无效样本混进去。每类动作至少采80到100条,动作集涉及5个类别的话,整个数据采集过程大约一两个小时就能完成。

5.2 数据增强与数据集划分的实操建议

小数据集上,数据增强是提升泛化能力性价比最高的手段。结合关键点特征,我用了几种有效的增强方式。

时间缩放是最直接的一种:把序列在时间轴上拉长或压缩20%到30%。虽然DTW后面会做对齐,但训练时引入速度变化能帮LSTM学到更稳定的时间特征。水平翻转也很有用,相当于把左臂和右臂交换,只需要在角度特征上做对应维度的交换即可。再就是加噪声,在每个角度值上叠加一个均值为0、标准差1到2度的高斯噪声,模拟关键点抖动带来的测量误差。

数据集划分上,我按“人”来划分,而不是按“样本”随机划分。比如5个人采集的数据,4个人的样本作为训练集,1个人的样本作为测试集。这样做才能评估模型跨人的泛化能力。如果随机划分,同一个人的动作片段可能同时出现在训练集和测试集里,测试结果虚高,到了真实部署场景就会露馅。

还要检查类别均衡。动作类别之间样本数差异过大的话,训练很容易偏向样本多的类别。我的做法是先统计每类样本数量,把少的类别通过增强补到和多的类别接近,不做成本太高的过采样。

6. 实测调优记录:识别率停在60%之后的三个排查阶段

6.1 先查特征和数据,而不是先动模型

我第一次把整套流程跑通后,测试集准确率停留在61%左右,和预期差得很远。当时第一反应是加大模型、调低学习率,折腾了几天几乎没有进展。后来我冷静下来,先把分类错误的样本全部打印出来,做混淆矩阵分析,才发现了真正的问题:不是模型不够强,而是“深蹲”和“站起”两个动作频繁互相误判。

原因很快锁定了:数据采集时,我把“站起”的起始帧标得太晚,导致前几帧其实还处在深蹲姿态,这些带错误标签的样本把模型的决策边界彻底搅浑了。把这两类样本的边界重新整理、剔除标签不准的片段之后,准确率立刻跳到80%以上。这个经验非常值得分享:模型效果差时,先可视化错误样本,检查数据是否有问题,然后再去调模型。数据问题没解决之前,调模型基本都是白费力气。

6.2 预测平滑与实时性优化

数据问题解决后,准确率到了85%左右,但实际部署到实时视频流时,又出现了新的问题:预测结果在相邻帧之间抖动,一个动作会连续输出几个“举手”夹杂一个“其他”再回到“举手”。这种情况用单帧预测很容易出现。

我采用的方案是滑动窗口加多数投票。以30帧为一个窗口,每次移动10帧,对窗口内所有帧的LSTM预测结果做投票,票数最多的类别作为当前窗口的输出。这样做之后,预测序列平滑了很多,动作切换的响应延迟也还能接受,大约在300毫秒左右。如果对实时性要求更高,可以把窗口缩短或者投票改成指数加权平均,给近期预测更高的权重。

6.3 跨人跨环境的泛化提升

在同一个摄像头、同一个人的测试环境里,准确率已经能到90%以上,但我让另一个同事在不同光照、不同背景的环境里测试,准确率直接跌到70%。这个落差说明模型存在比较明显的过拟合,主要原因是训练数据采集自单一环境。

针对这个问题,我从三个方向做了改进。第一是数据层面,增加第二个人、第二个环境的采集数据,同时强化水平翻转和角度噪声增强。第二是特征层面,把原始的绝对关键点坐标改为躯干中心的相对坐标,再把主要特征换成角度,减少摄像头距离和人体尺寸的影响。第三是模型层面,把LSTM的dropout从0.3提到0.4,让模型更“保守”。这三步下来,新环境下的准确率回升到85%左右,虽然比同环境测试低一些,但是在可接受范围内。

这类跨人跨环境的泛化问题没有捷径,数据多样性和特征稳定性是根本。模型结构在其中的作用相对有限,不要把希望完全寄托在更复杂的网络上。

整套系统最终跑下来,我最大的感触是:动作识别项目的瓶颈往往不在模型选型,而在数据质量和特征表达。MediaPipe把图像问题转成时序问题,DTW处理了时间尺度变化,LSTM负责最终的序列分类,每一步都在解决一个明确的工程问题,组合起来才形成了一个训练成本低、部署简单、效果可控的完整系统。如果你正在尝试类似的人体动作识别项目,建议先把这条最小管线跑通,再根据具体动作集和场景逐步加特征、调参数,避免一上来就追求复杂模型。

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询