恒美微站
首页
关于我们
建站服务
主题模板
案例展示
资讯中心
联系我们
基于MediaPipe和DTW的轻量级人体动作识别系统实现
首页
资讯中心
/
基于MediaPipe和DTW的轻量级人体动作识别系统实现
基于MediaPipe和DTW的轻量级人体动作识别系统实现
发布时间:2026/9/20 16:55:53
简介一份面向计算机视觉与人工智能开发者的实践资源聚焦如何用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_modeFalse, model_complexity1, smooth_landmarksTrue, min_detection_confidence0.5, min_tracking_confidence0.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对齐与时间归一化把不规整的序列变成规整的输入DTWDynamic 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, disteuclidean) # path 是 [(i, j), ...] 的匹配对列表i 为 seq_a 的帧索引j 为 seq_b 的帧索引拿到path后我用插值的方式把原始特征序列映射到一个统一的时间轴。比如目标长度是target_len30就用等间隔的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_size30, hidden_size128, num_layers2, num_classes5, dropout0.3): super().__init__() self.lstm nn.LSTM( input_sizeinput_size, hidden_sizehidden_size, num_layersnum_layers, batch_firstTrue, dropoutdropout, ) 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_decay1e-4做L2正则能明显压住过拟合。criterion nn.CrossEntropyLoss() optimizer torch.optim.Adam(model.parameters(), lr1e-3, weight_decay1e-4) scheduler torch.optim.lr_scheduler.StepLR(optimizer, step_size20, gamma0.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_modeFalse, model_complexity1) 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(fdata/action_{label}_{sample_id}.npz, featurenp.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负责最终的序列分类每一步都在解决一个明确的工程问题组合起来才形成了一个训练成本低、部署简单、效果可控的完整系统。如果你正在尝试类似的人体动作识别项目建议先把这条最小管线跑通再根据具体动作集和场景逐步加特征、调参数避免一上来就追求复杂模型。本文还有配套的精品资源点击获取