简介:面向人体姿态识别与动作分类任务,这份配套资料包集中演示了Mediapipe姿态估计、动态时间规整(DTW)序列匹配和LSTM时序建模的组合应用。资源共139个文件,压缩包仅11.04MB,主体包括120个npy格式的姿态特征数据、8个py格式的完整训练与推理脚本、8个mp4演示视频,以及h5权重模型、README说明文档等,便于对照源码复现识别流程并快速验证效果。已有1200余人学习下载。从内容结构看,npy数据覆盖了不同动作样本的姿态序列,py脚本串联起Mediapipe关键点提取、DTW模板匹配和LSTM分类器训练,mp4视频则直观展示采集与测试场景,适合计算机视觉初学者、算法工程师以及智能健身、安全监控等应用开发者参考。通过运行该项目,读者可以掌握姿态关键点提取、序列对齐打分与时序分类的完整链路,并基于内置模型和数据进行二次扩展与优化。 最近在做一个动作识别的项目,技术方案正好踩在“传统时序匹配+深度学习”的结合点上:用mediapipe提取人体骨骼关键点,再用DTW和LSTM两条路线分别做动作分类。项目难度不算高,但把这两套方法完整跑通、对比、再融合的过程很有代表性。这篇就把整体思路、关键代码和踩坑记录整理出来,给正在做姿态识别或者动作分类的朋友做参考。
1. 项目总体方案与选型思路
1.1 为什么选mediapipe做姿态识别
人体动作识别的第一步,是把视频里的人和动作“数字化”。行业内有很多方案,比如OpenPose、AlphaPose、MediaPipe,我最终选了MediaPipe,主要看中三点。
一是部署成本低。MediaPipe的Pose模块在CPU上就能跑实时推理,单帧延迟在普通笔记本上能到30ms以内。我的项目初期是在本地摄像头跑,后期要迁移到边缘设备,这个特性很重要。
二是输出稳定性。MediaPipe输出的是33个人体关键点的归一化坐标(x, y, z),关键点定义和COCO风格基本一致。相比自己训练关键点检测模型,它的跨平台一致性做得很好,省掉了大量数据标注和模型训练的功夫。
三是生态成熟。MediaPipe同时支持Python、Android、Web,同一个Pose模型可以多端复用。动作识别真正上线时往往要覆盖手机端,选它风险最小。
这里要补充一点,MediaPipe的关键点坐标分为x、y、z三个分量,但z并不代表真实深度,而是以髋部中心为原点的相对深度,单位和x/y不一致。这会在后续特征工程里带来不少麻烦,下面会专门说。
1.2 为什么同时用DTW和LSTM两条路线
确定了用MediaPipe获取关键点之后,动作识别层我特意对比了两类算法。
DTW(动态时间规整)属于传统时序匹配算法,核心能力是计算两条长度不一致的序列之间的相似度。动作数据天然存在“同一个人做同一动作,速度快慢不同、骨架大小不同”的问题,DTW能从时间轴上“伸缩”地比较序列,非常适合处理这类变长匹配。
LSTM则是循环神经网络的代表,能够学习时序数据中的长期依赖关系。对于“坐下”这种包含连贯骨骼运动轨迹的动作,LSTM能隐式学到动作的时序特征,不需要手工设计距离度量。
之所以两条路线都做,是因为它们在项目中的定位完全不同:DTW适合小样本、模板明确、需要实时响应的场景;LSTM适合样本充足、动作种类多、需要泛化能力的场景。两者不是替代关系,而是互补关系。项目后期我还试了分数融合,效果比单独使用任意一个都要稳。
1.3 系统整体架构与数据流
整个系统分为四个模块:视频输入与关键点提取、动作序列预处理、DTW分类器、LSTM分类器。数据流是这样的:
摄像头或视频文件逐帧输入,经过MediaPipe Pose,得到每帧的33个关键点坐标;把连续帧的关键点组合成动作序列;然后经过滑动窗口切分和归一化;最后分别送入DTW模板库或LSTM模型做分类预测。
我先用Python脚本从视频里批量抽帧,存成关键点序列的npy文件,再离线训练LSTM。实时预测时则直接用摄像头输入,每10帧滑动一次窗口输出当前动作类别。这样开发和测试能够解耦,训练数据也能复用。
2. 环境准备与mediapipe姿态识别落地
2.1 环境依赖与安装
我的开发环境是Python 3.9、OpenCV 4.5、MediaPipe 0.10。安装命令很简单:
pip install opencv-python mediapipe numpy如果要做LSTM训练,还得装TensorFlow:
pip install tensorflow这里有个容易踩的坑:MediaPipe不同版本的API有细微差异,比如0.9.0之后Pose接口的参数改名了。建议直接装最新版,网上很多教程是基于旧版写的,照着抄经常报参数错误。
2.2 基于摄像头/视频的姿态提取核心代码
姿态提取的核心逻辑并不复杂,关键是把MediaPipe的输出正确转成自己的数据格式。我封装了一个工具函数:
import cv2 import mediapipe as mp mp_pose = mp.solutions.pose pose = mp_pose.Pose( static_image_mode=False, min_detection_confidence=0.5, min_tracking_confidence=0.5 ) def extract_keypoints(video_path): cap = cv2.VideoCapture(video_path) sequences = [] while cap.isOpened(): ret, frame = cap.read() if not ret: break frame_rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) results = pose.process(frame_rgb) if results.pose_landmarks: h, w = frame.shape[:2] keypoints = [] for lm in results.pose_landmarks.landmark: keypoints.append([lm.x * w, lm.y * h, lm.z * w]) sequences.append(keypoints) cap.release() return np.array(sequences)逐帧存原始像素坐标是有意为之:像素坐标受摄像头分辨率影响,但特征归一化时我会再转成相对坐标,所以原始数据怎么存都行,保留越多原始信息越好。
2.3 关键点选择与置信度处理
MediaPipe的33个关键点里,不是每个都适合作为动作分类特征。比如面部关键点对肢体动作几乎无信息量,反而引入噪声。我最终只保留了四肢和躯干的22个关键点,相当于在源头做了一次特征筛选。
另一个关键点是置信度过滤。当人体部分遮挡时,MediaPipe会输出低置信度的关键点,这些点的坐标噪声极大。我的做法是:当某个关键点的visibility低于0.5时,用前后帧的同位置点线性插值补上。
def interpolate_missing(points, visibility, threshold=0.5): points = points.copy() for i in range(1, len(points)-1): if visibility[i] < threshold: points[i] = (points[i-1] + points[i+1]) / 2.0 return points这么做的前提是动作帧率足够高,前后帧差异不大。如果视频本身只有15帧以下,这个方法的误差就会很大。
3. 动作序列特征工程与数据集构建
3.1 归一化与相对坐标系转换
这一步是整个项目里对最终精度影响最大的一个环节。MediaPipe输出的关键点是基于图像尺寸的归一化坐标,但它没有消除人体骨架大小、站在画面不同位置的差异。如果直接用原始坐标送入模型,模型会把“人离摄像头远近”也当成特征,导致同一种动作因为距离不同被判成不同类别。
解决办法是把每个点都转换到以髋部中心为原点的相对坐标系。髋部中心我用左右髋的中点近似,然后以两肩距离作为尺度参考做等比例缩放。这样处理后,同一个人在不同位置、不同距离做同一动作,特征序列基本一致。
def normalize_pose(landmarks): hip_center = (landmarks[23] + landmarks[24]) / 2.0 shoulder_left, shoulder_right = landmarks[11], landmarks[12] scale = np.linalg.norm(shoulder_left - shoulder_right) normalized = [] for lm in landmarks: normalized.append((lm - hip_center) / scale) return np.array(normalized)注意z坐标的处理。MediaPipe的z值本身是相对坐标,单位和x/y不一致,不能直接除scale。我的经验是:在归一化之后,单独给z乘一个较小的权重,比如0.2,降低它对分类的干扰。因为大部分动作在侧向深度上的信息量远低于平面上的信息量。
3.2 滑动窗口与动作序列切分
连续的摄像头流不能直接进模型,需要切成固定长度的窗口。我用的窗口策略是:窗口长度30帧(相当于1秒的30fps视频),滑动步长10帧。每个窗口提取一次特征。
这里有一个平衡点:窗口太短,动作信息不完整;窗口太长,实时性差,延迟高。实际测试中,30帧对“蹲下”“举右手”“走路”这类动作足够,对更复杂的多阶段动作(比如“鞠躬后坐下”)可能需要60帧。可以先做预实验,对不同动作计算关键点序列的持续时间分布,再定窗口长度。
3.3 数据集的采集与标签设计
数据集中每个样本的标签是动作类别。我采集了6类动作:站立、走路、坐下、举起右手、弯腰、踢腿。每个动作由一个人重复20次,每次持续3秒钟,共采集3个人,最终得到360组动作样本。
采集时最容易忽略的问题是:数据要覆盖不同人和不同速度。如果只采集自己一个人的动作,模型会严重过拟合到个人的运动习惯。后来我专门让身高差异较大的同事也录了一遍,模型在跨人测试中的准确率提升了约10个百分点。
数据存储格式如下:每个样本是一个numpy数组,形状为(30, 66),30是窗口长度,66是22个关键点乘以3维坐标。标签用one-hot编码,与LSTM的softmax输出对齐。
4. DTW动作识别的实现细节
4.1 DTW原理与匹配流程
DTW的核心思想是:对两条序列,逐点计算距离,然后找一条从左上角到右下角的最短路径。路径的含义是“把序列A的第i帧对齐到序列B的第j帧”,允许一对多或者多对一,从而消除时间长度不一致带来的影响。
实际使用中,我先为每个动作构造一个“标准模板”。做法是把该类动作的所有训练样本按时间轴做平均,得到一条代表该类动作的模板序列。待测序列与每个模板计算DTW距离,距离最小的类别就是预测结果。
4.2 DTW核心代码实现
DTW的实现本身很经典,直接照搬动态规划即可:
import numpy as np def dtw_distance(seq1, seq2): n, m = len(seq1), len(seq2) 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(seq1[i-1] - seq2[j-1]) dp[i, j] = cost + min(dp[i-1, j], dp[i, j-1], dp[i-1, j-1]) return dp[n, m]这里用欧氏距离作为单帧两向量之间的距离度量。需要注意的是,seq中每一行是当前帧的66维特征向量。在Python里最耗时的部分是这个双重循环,我实测30帧序列对比一个模板大概需要5ms,6个模板共30ms,对于离线识别够用,实时场景有点勉强。优化的思路是用矩阵运算一次算出所有单帧距离,再在GPU或向量化层面做动态规划。
4.3 阈值判定与误报控制
DTW分类只输出“距离最近的模板”,但“最近的模板”不等于“就是该动作”。如果待测序列和所有模板的距离都很大,说明这根本是没见过的动作。此时如果依然输出最近标签,就会把任意随机动作强行归为训练集中的某一类,这是DTW最容易出现的误报。
我的方案是给每个模板设定一个拒识阈值。阈值根据训练集内同类样本与该模板的DTW距离分布来定,公式是:
threshold = mean_distance + 2 * std_distance待测序列与该模板的距离如果超过阈值,直接判定为“未知动作”。这里用均值加两倍标准差,大约覆盖95%的正常波动。阈值设得越严,误报越少,但漏报也会增加。实际项目中我倾向于“宁漏勿错”,毕竟动作分类错比不识别更影响体验。
5. LSTM动作识别的实现细节
5.1 模型结构与参数设计
LSTM模型我采用了两层LSTM加两层全连接的结构。输入形状是(30, 66),第一层LSTM返回完整序列,第二层只返回最后一个时间步的输出,再接两个全连接层,最后softmax输出6个类别概率。
from tensorflow.keras.models import Sequential from tensorflow.keras.layers import LSTM, Dense, Dropout model = Sequential([ LSTM(64, input_shape=(30, 66), return_sequences=True), Dropout(0.3), LSTM(32, return_sequences=False), Dropout(0.3), Dense(32, activation='relu'), Dense(6, activation='softmax') ]) model.compile(optimizer='adam', loss='categorical_crossentropy', metrics=['accuracy'])LSTM单元数量设置成64和32是在精度和过拟合之间折中的结果。单元数太少模型学不到复杂时序模式,太多则在小样本集上很容易过拟合。Dropout放在每层LSTM之后,随机丢弃30%的节点,明显提升了测试集上的表现。
5.2 训练过程与调参心得
训练数据一共360个样本,按7:2:1划分为训练集、验证集和测试集。这里有个关键细节:同一人的同一动作样本不能同时出现在训练集和测试集里,否则会严重高估模型效果。我按“人”拆分数据,保证测试集里的人从未出现在训练过程中。
训练时设置了早停机制,监视验证集loss,连续5个epoch不下降就停止训练。实际训练中大约在第20个epoch收敛,准确率稳定在95%以上。batch_size我用了32,lr用了默认的0.001,其他超参数没有过多搜索。
这里分享一个调参经验:如果发现训练集精度很高但验证集精度低,第一件事不是调模型结构,而是检查数据增广和归一化。我最初就是在数据归一化上做错了,导致跨人测试效果崩了。
5.3 模型评价与实时预测集成
模型评价除了整体准确率,我还会看每个类别的召回率。某些动作比如“站立”和“坐下”在过渡阶段非常相似,容易被误判。我统计了混淆矩阵,发现误判主要集中在“站立”和“坐下”的起止帧。这其实和数据标注的边界模糊有关,不完全是模型的问题。
实时预测时,我把模型导出为tflite格式,再加上滑动窗口,每次预测使用最近30帧的关键点数据。整个流程在CPU上大约每100ms能输出一次预测结果,基本满足实时应用要求。导出的方式:
converter = tf.lite.TFLiteConverter.from_keras_model(model) tflite_model = converter.convert()6. DTW与LSTM对比、融合与踩坑实录
6.1 实测结果对比
我在相同测试集上对比了两条路线的表现。准确率方面,LSTM达到96%左右,DTW约88%。LSTM明显更强,但它的强建立在充足训练样本的基础上。当我只保留每个人5次动作数据时,DTW的准确率是82%,LSTM降到74%,说明LSTM在小样本下发挥不出优势。
实时性方面,DTW每帧消耗大约30ms,LSTM约10ms。这里有个反直觉的点:LSTM虽然计算量大,但TensorFlow的推理引擎优化得好;反而是DTW的Python双重循环成为瓶颈。如果能把DTW换成C++或用numba加速,二者差距会缩小。
适用场景上,DTW非常适合动作种类少、样本少、需要快速上线的项目;LSTM适合动作种类多、样本充足、追求泛化效果的项目。如果你的动作是固定的几个姿势,用DTW就够了,完全没必要上深度学习。
6.2 融合策略与最终效果
项目最后我尝试了分数融合,把DTW的“距离相似度”和LSTM的“类别概率”结合起来。做法是把DTW距离转换成相似度分数,然后和LSTM概率做加权平均:
similarity = 1.0 / (1.0 + dtw_dist) final_score = 0.35 * similarity + 0.65 * lstm_prob权重0.35和0.65是通过网格搜索确定的。融合后准确率提升到97.5%左右,更重要的是拒识效果变得更稳:当动作属于未知类别时,LSTM会输出一个比较平均的概率分布,DTW则给出一个偏大的距离,两者的分数都低,就能正确触发“未知动作”判定。
6.3 实操中遇到的最多的三个坑
第一个坑是MediaPipe的z坐标。不同距离下z值的分布完全不同,我一开始没处理,LSTM训练时loss降到0.1但验证集准确率始终上不去,排查了很久才意识到是z轴权重问题。最终方案是归一化后对z乘0.2,问题随即消失。
第二个坑是动作边界难切分。用户在“弯腰”动作开始前必然有一小段“站立”的过渡帧,这部分包含在窗口里会拉低准确率。我最后参考了别人的做法,对每个动作模板的首尾各截掉10%的帧,只保留动作最核心的区间,效果立竿见影。
第三个坑是数据集采集时动作速度太规范。我录数据时很谨慎,每个动作都做得又慢又标准,结果模型在真实环境里遇到快节奏动作时经常误判。后来我特意加入了一些“快做”和“做得随意”的样本,模型鲁棒性才提上来。
这个项目跑完之后我最大的感受是:技术选型不是越新越好,而是要看数据量、实时性和部署环境。先用DTW这类传统方法快速跑通流程验证可行性,再上LSTM提升上限,最后用融合策略弥补各自短板,是成本最低也最稳妥的推进路径。如果你正在做类似的动作识别项目,建议照着这个顺序走一遍,能少走不少弯路。
本文还有配套的精品资源,点击获取