资讯动态

人体动作识别实战:从骨架特征提取到滑窗模型避坑

发布时间:2026/10/3 18:27:35 来源:尧图企业网站定制
简介面向机器学习与深度学习学习者的人体动作识别项目压缩包适合作为毕业设计、课程设计或期末大作业的参考。项目完整覆盖从数据集整理、数据预处理、特征工程到模型训练与评估的流程并比较了随机森林、AdaBoost、逻辑回归、SVM等多种算法在动作识别任务上的表现通过穷举搜索获得各自最佳参数可帮助读者快速掌握动作识别系统的构建与调优方法。压缩包共7个文件以Jupyter Notebook、Matlab脚本、数据集及Markdown文档为主整体大小约2.28MB其中Notebook完整记录开发过程Matlab脚本提供各模型参数优选的实现README则包含背景、说明与环境指引。目前已有41人学习资料体量精简但脉络清晰适合需要参考完整实验流程或进行算法对比分析的研究者与开发者。1. 人体动作识别到底在识别什么.zip包里真正值钱的部分拿到一个基于机器学习的人体动作识别.zip解压之后多数情况是几十个视频、几个CSV标注文件和若干Python脚本。很多第一次做机器学习项目的人会急着把代码跑起来但动作识别这类时序任务的第一个坑不在模型而在数据怎么切、特征怎么提。真正决定这个项目能不能落地到课堂作业、毕设或者跌倒检测这类真实场景靠的不是神经网络的深度而是你对一个动作到底占多少帧、特征怎么对齐的理解。这类任务解决的是给定一段视频或一个连续动作流系统判断人在做什么——走、跑、跳、挥拳、跌倒。与传统图像分类不同人体动作识别要求模型同时看到空间姿态和前后帧的时序关系。适合入手的人群包括做课程设计或毕业设计的学生、想用机器学习尝试运动分析的爱好者以及做安防与康复预研的一线工程师。下面这套流程同样适用于传感器笔记、康复训练评估等更宽泛的场景。2. 先整理数据再谈模型动作切分与特征选型2.1 动作不是一帧是一段时间先搞清序列长度动作识别与图像分类最大的区别是图像看单帧就够了动作必须看一段连续帧。一个挥手动作在30fps视频里通常持续2到3秒即60到90帧一个跌倒动作可能只有0.5秒也就是15帧左右。如果特征窗口取得太短模型无法区分蹲下和站起一半取得太长一个窗口里混入两个动作标签就乱了。所以拿到.zip包的第一步永远是打开标注文件统计每个动作的持续时间分布。常见做法中标注文件可能是一个CSV或JSON记录视频名、动作类别、起始帧、结束帧。我一般先写一个统计脚本把每个类别的帧数范围打出来再决定用什么序列长度。import pandas as pd df pd.read_csv(annotations.csv) df[duration] df[end_frame] - df[start_frame] print(df.groupby(label)[duration].describe())这段代码统计每个动作类别的帧数分布describe()会给出最小值、最大值、中位数。如果某个动作类别的中位数只有25帧你把序列长度设成64就会让多数样本被丢进填充或无意义的重复采样里。这个统计结果直接决定后续滑窗和模型输入的长度是整条流程里最容易被跳过的关键一步。2.2 特征选型骨架坐标为什么比原始像素更适合小项目特征提取得好不好比模型选什么更重要。在人体动作识别里常见特征有三类原始像素帧、密集光流、人体骨架关键点。三者取舍直接决定训练成本、泛化能力和调试难度。特征类型维度计算成本小样本表现是否需要GPU原始像素帧高224×224×3高需CNN极易过拟合建议GPU密集光流中高双通道图高提取慢对光照鲁棒但数据量要求高建议GPU人体骨架关键点低33点×坐标低CPU可跑在几百个样本上表现稳不需要在机器学习框架里骨架坐标的性价比最高。以MediaPipe提供的姿态估计为例单帧输出33个关键点的x、y、z坐标和可见度一帧33×4132维经过归一化后常用维度只有x、y与可见度。维度低意味着随机森林、逻辑回归这类传统机器学习算法可以直接吃不需要大量数据就能训练。骨架特征还有一个隐形优势它天然剔除了背景纹理和颜色干扰。模型学到的是人体姿态结构而不是这个人穿了红衣服所以是跑步这对换场景、换人之后的泛化帮助很大。2.3 视频转特征数据集一份可照抄的提取脚本下面这份脚本把视频转成骨架坐标CSV是动作识别项目里最通用的前置步骤。它使用MediaPipe的Pose模块做姿态估计按帧读取视频每隔stride帧采样一次每个关键点的坐标相对于髋中心做归一化让不同身高的人在同一尺度下可比。import cv2 import mediapipe as mp import csv mp_pose mp.solutions.pose pose mp_pose.Pose( static_image_modeFalse, min_detection_confidence0.5, min_tracking_confidence0.5 ) def extract_pose_features(video_path, stride2): cap cv2.VideoCapture(video_path) rows [] frame_idx 0 while cap.isOpened(): ret, frame cap.read() if not ret: break if frame_idx % stride 0: rgb cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) results pose.process(rgb) if results.pose_landmarks: landmarks results.pose_landmarks.landmark hip_x (landmarks[23].x landmarks[24].x) / 2 hip_y (landmarks[23].y landmarks[24].y) / 2 row [frame_idx] for lm in landmarks: row [round(lm.x - hip_x, 4), round(lm.y - hip_y, 4), round(lm.visibility, 3)] rows.append(row) frame_idx 1 cap.release() return rows with open(pose_features.csv, w, newline) as f: writer csv.writer(f) writer.writerow([frame, x0, y0, v0, x1, y1, v1, x2, y2, v2]) for row in extract_pose_features(sample.mp4): writer.writerow(row)这个脚本有三个参数值得按需调整。stride控制采样间隔动作缓慢时stride1保留全部帧动作快速时适当增加到stride2或3避免前后帧重复度过高。min_detection_confidence控制首次检测阈值低于0.5时容易把模糊帧漏检高于0.7则可能无谓地丢掉大量有效帧在运动模糊明显的视频里尤其明显。归一化时把坐标系原点平移到两髋中心这是骨架特征里最常见也最有效的预处理技巧能抵消人的绝对位置和身高差异。还要注意漏检帧的处理。上面的简化脚本直接跳过了没有人体的帧这会破坏时序连续性。实战里我会用上一帧的关键点填空或者给这一帧打一个检测失败标记这样后续模型可以通过可见度特征学到此刻可能没看到人而不是被硬截断。3. 训练第一个动作识别模型从帧级特征到动作标签3.1 为什么先跑传统机器学习打底很多人的第一反应是直接上深度学习模型但在动作识别这类数据量通常只有几百到几千个样本的任务里深度学习很容易把训练集背下来在真实场景一用就碎。我自己的习惯是先从传统机器学习算法开始把每帧的66维特征按窗口堆叠喂给随机森林先拿到一条能用的基线再考虑要不要换深度学习模型。随机森林在这个任务里有三个优点是深度学习短期内替代不了的对特征尺度不敏感不需要精细标准化在小样本上不容易过拟合因为每棵树只看到特征的随机子集训练速度快CPU上几分钟就能跑完几百棵树的网格搜索。这里说的就是机器学习实战项目里最常用到的套路先有基线再谈提升。如果你拿到一个动作识别压缩包脚本里已经给了随机森林的代码我建议不要急着删掉换LSTM先把基线跑出来换模型的决策才有依据。3.2 滑窗构造训练样本给随机森林喂时序信息随机森林看不到时间顺序所以要把连续帧拼成一个窗口向量。下面这段代码把每条视频的帧特征序列切成长度为window的片段每个片段取窗口中间帧的动作类别作为标签然后训练随机森林分类器。import numpy as np import pandas as pd from sklearn.ensemble import RandomForestClassifier from sklearn.model_selection import train_test_split from sklearn.metrics import classification_report from scipy import stats def build_windows(X, y, window12, step3): Xs, ys [], [] for i in range(0, len(X) - window 1, step): Xs.append(X[i:i window].reshape(-1)) ys.append(y[i window // 2]) return np.array(Xs), np.array(ys) df pd.read_csv(pose_features_with_label.csv) X df.drop(columns[frame, label]).values y df[label].values Xw, yw build_windows(X, y, window12, step3) X_train, X_test, y_train, y_test train_test_split( Xw, yw, test_size0.2, random_state42 ) clf RandomForestClassifier(n_estimators200, min_samples_leaf3, n_jobs-1) clf.fit(X_train, y_train) pred clf.predict(X_test) print(classification_report(y_test, pred))这里有两个容易被新手忽略的决策。标签取窗口中心帧而不是最后一帧因为动作是渐变的窗口前面一半可能还在准备动作后面一半已经在恢复中心帧最接近这个窗口的语义主题。step控制窗口与窗口的重叠量step3配合window12表示75%重叠样本数量大约是逐帧不重叠的4倍能明显提升模型稳定度。值得警惕的是上面这段代码里的train_test_split会随机洗牌在动作识别场景里这样写是有问题的。同一段动作视频的相邻窗口会被随机分到训练集和测试集导致测试成绩虚高。正确的做法是按视频文件或受试者分组这个坑在第5章单独展开。3.3 用LSTM对比时序建模到底值不值如果你手里有几千条以上样本或者动作类别之间的边界很模糊可以试试LSTM模型。它的输入不再是扁平化窗口向量而是(样本数, 帧数, 特征维度)的三维张量让模型自己学习帧与帧之间的先后依赖关系。import tensorflow as tf from tensorflow.keras import layers X_seq Xw.reshape((-1, 12, 66)) num_classes len(set(yw)) model tf.keras.Sequential([ layers.Input(shape(12, 66)), layers.LSTM(64, return_sequencesTrue), layers.LSTM(32), layers.Dense(num_classes, activationsoftmax) ]) model.compile( optimizertf.keras.optimizers.Adam(learning_rate1e-3), losssparse_categorical_crossentropy, metrics[accuracy] ) history model.fit( X_seq, yw, epochs30, batch_size32, validation_split0.2, callbacks[tf.keras.callbacks.EarlyStopping(patience5, restore_best_weightsTrue)] )LSTM模型有三个关键点特征必须先标准化随机森林不需要标准化但LSTM需要否则坐标特征的值域差异会拖慢收敛return_sequencesTrue让第一层LSTM输出完整的时间步序列供第二层继续捕捉时序EarlyStopping的patience5表示连续5个epoch验证集不提升就回滚权重没有这个回调小数据集上很容易训练集准确率99%、验证集准确率60%。一个务实的判断标准是随机森林跑出来的加权F1在0.9以上换LSTM未必看得见提升反而会引入更大的调参成本随机森林F1卡在0.8以下才值得用LSTM或时序卷积去替换。4. 评估与调参别让准确率骗了你4.1 混淆矩阵比准确率诚实得多在动作识别里整体准确率是最容易骗人的指标。假设数据里站立占80%模型把所有样本都预测成站立准确率也有80%但这个模型在真实场景里完全没有用处。所以评估时第一件事是打印每个类别的召回率尤其是跌倒这类风险动作宁可把其他动作误判成跌倒也不能让跌倒被漏掉。from sklearn.metrics import confusion_matrix, f1_score import seaborn as sns import matplotlib.pyplot as plt classes sorted(set(yw)) cm confusion_matrix(y_test, pred) sns.heatmap(cm, annotTrue, fmtd, xticklabelsclasses, yticklabelsclasses) plt.xlabel(Predicted) plt.ylabel(Actual)混淆矩阵里重点看两条对角线以外的邻居误判。比如挥手和举手互相混淆说明窗口长度太短模型只看见抬手动作没看到后续的摆动走和跑互相混淆多半是帧率归一化没做或者两类动作的序列长度差异太大。这些误判模式会直接告诉你下一步调参数的方向而不是盲目调模型结构。4.2 三个必调参数窗口长度、步长、类别权重动作识别项目里下面这三个参数对最终效果的影响比换算法大得多它们分别是window、step和class_weight。参数常用范围调整方向典型症状窗口长度 window8~30帧取最短动作持续帧数的1/3到1/2混淆相邻动作 → 加大边界处误判 → 减小滑动步长 step1~5帧越小样本越多越大越防过拟合训练测试都差 → 减小过拟合 → 增大类别权重 class_weight1:1到1:10少数类样本量比例少数类recall低 → 加大少数类权重窗口长度的调整有一条经验先统计每个动作类别的中位帧数取最小类别的中位数作为初始window。比如跌倒中位数只有15帧window就设15如果设成30跌倒样本会被大量周边动作稀释模型学不到跌倒的完整特征。class_weight在随机森林里直接传参即可RandomForestClassifier(class_weightbalanced)或者手动传一个字典精确控制比例。调参不建议手动一个个试可以先用下面这段网格搜索锁定window和step的组合再按结果调类别权重。from sklearn.model_selection import GridSearchCV param_grid { n_estimators: [100, 200], max_depth: [10, 20, None], min_samples_leaf: [1, 3, 5] } grid GridSearchCV(clf, param_grid, cv3, scoringf1_weighted, n_jobs-1) grid.fit(X_train, y_train) print(grid.best_params_)scoringf1_weighted比默认的accuracy更适合不均衡数据因为它按每个类别的样本量加权少数类对模型分数的贡献不会被淹没。交叉验证折数设3而不是5因为动作数据通常按受试者分组后组数有限5折容易让某些类别在验证折里一个样本都没有。4.3 小样本兜底数据增强和迁移特征当某些类别只能收集到几十条样本时有两个兜底方案。第一个是序列层面的数据增强把同一段动作的坐标序列做时间拉伸比如把速度变慢到0.8倍或加速到1.2倍再给关键点坐标加少量高斯噪声模拟肌肉抖动和检测误差。具体到随机森林这里增强后的样本直接拼进训练集就能撑大部分类别数量到可训练水平。第二个是迁移思路。骨架特征本身就是一种迁移——MediaPipe这类姿态估计模型已经在百万级图像上预训练过你拿到的关键点已经编码了人的身体结构知识机器学习算法只需学习动作类别。你不需要也从零训练一个姿态估计器这是小项目能站稳脚跟的核心理由。5. 人体动作识别避坑指南五条血泪经验5.1 训练准确率和测试准确率都95%一上真实视频就崩现象离线评估很好看模型放到另一台电脑、另一个房间录的视频上F1直接掉到60%以下。原因训练集和测试集随机洗牌同一段动作视频的相邻窗口被分到了两边。模型看到的是同一段视频的不同片段测试自然开卷考试。解决按视频文件或受试者分组做数据划分。确保同一个人的动作片段只出现在训练集或只出现在测试集。如果标注文件里有video_id字段用GroupShuffleSplit之类的分组切分代替随机拆分宁肯让训练集小一点也要切断信息泄漏。5.2 模型预测的动作边界总是慢半拍现象模型判断站起到行走的切换点比人工标注晚0.3到0.5秒。原因人工标注的起点不一定准确。标注者看到人开始站起并按下按键时动作已经发生了两三百毫秒标签边界附近其实是一段供参考但不可靠的监督信号模型被逼着学习错误的边界。解决对窗口中心的标签做平滑处理。在动作边界前后各留5帧把硬标签从独热改成软标签比如窗口中心帧落在边界附近时把前一动作和后一动作的标签分别赋0.8和0.2。这样模型不用强行学习一个事实上不存在但标注存在的精确切换帧。5.3 同一个动作换个拍摄帧率就认不出来现象训练集视频是30fps测试集视频是24fps整体手势识别率下降超过10%。原因滑窗长度固定为12帧时在30fps下覆盖0.4秒在24fps下覆盖0.5秒。同一个动作在两种帧率下对应的时间比例变了模型学到的时序形状对不齐。解决特征提取前把所有视频统一重采样到约定帧率。用OpenCV的cap.set(cv2.CAP_PROP_FPS, target_fps)做不到真正的重采样正确做法是逐帧读取后固定间隔抽取或者用FFmpeg统一转码。最简单可靠的办法不依赖视频自带的帧率而是在extract_pose_features里直接按时间戳取帧每0.05秒取一帧保证所有序列的时间分辨率一致。5.4 少数类样本量差10倍recall直接归零现象分类报告里跌倒这一类别的recall是0所有跌倒样本都被预测成下蹲或坐下。原因类别极度不均衡。模型只要放弃少数类整体准确率都不会受到太大影响于是决策边界被多数类完全吞掉。解决先加类别权重让少数类的误判成本升高再用第4章讲的时序增强把这几个类别的样本量撑到跟多数类同一个数量级。还有一种经常被忽略的兜底在滑窗采样时对少数类用更小的step多数类用更大的step相当于对少数类过采样不动坐标数据本身也能把样本量拉上来。5.5 人体关键点坐标归一化做了换个视角照样翻车现象训练集全是正面拍摄的视频测试集有一个侧面视角的视频模型完全失效。原因归一化解决了人的位置和高矮差异但没有解决视角差异。侧面视角下右肩的x坐标与正面视角完全不同模型学习到的是手往右挥这类绝对方向信息而不是手肘从屈到伸这类相对结构变化。解决在归一化基础上增加角度特征。对每个关键点计算它与相邻关键点的关节角度比如肩膀到肘部再到手腕的夹角这类角度特征对视角旋转相对稳定。更彻底的做法是在数据增强阶段用左髋和右髋轮流做参考点模拟左右视角翻转让模型看到更多视角变化。6. 进阶滑窗推理与置信度阈值策略前面训练和评估都假设视频已经分段好每个窗口只含一个动作。但真实应用里模型是连续看到视频流的没法等到动作结束才给结果。这时就需要滑窗推理与双阈值策略实时维护一个最近window帧的特征缓冲每来一帧就预测一次只有当置信度穿过高阈值时才判定动作开始低于低阈值时才判定动作结束。def sliding_predict(model, feature_stream, window12, low0.6, high0.8): buf [] state idle active_class None for feat in feature_stream: buf.append(feat) if len(buf) window: continue window_feat np.concatenate(buf[-window:]).reshape(1, -1) proba model.predict_proba(window_feat)[0] idx int(np.argmax(proba)) if state idle and proba[idx] high: state active active_class idx elif state active: if idx active_class and proba[idx] low: state idle active_class None elif idx ! active_class and proba[idx] high: active_class idx这段代码的要点在双阈值high0.8负责开始low0.6负责结束之间留了0.2的迟滞带。如果只用一个0.7的阈值模型在动作边界处的置信度容易来回摆动一个挥手会被切割成开始、结束、再开始出现大量虚假片段。我在实际项目里通常从high0.8、low0.6起步然后根据误报率调整。最后还有一个我自己踩出来的习惯在线推理接好之后别急着调模型超参数先在录制的长视频上跑一遍把误报片段剪出来看。模型离线指标高只说明它在整齐切好的数据上表现好而真实动作流的价值判断标准是该报警的没漏不该报警的没乱报这个标准在滑窗推理跑通之前是看不见的。每次拿到动作识别的压缩包我都先把这套在线推理框架搭起来再回头慢慢调模型这个顺序帮我避开了很多离线指标好看、上线就翻车的尴尬希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑