☰
人体动作识别实战:从骨架特征提取到滑窗模型避坑
2026/10/3 18:27:23 网站建设 项目流程

简介:面向机器学习与深度学习学习者的人体动作识别项目压缩包,适合作为毕业设计、课程设计或期末大作业的参考。项目完整覆盖从数据集整理、数据预处理、特征工程到模型训练与评估的流程,并比较了随机森林、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×4=132维,经过归一化后常用维度只有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_mode=False, min_detection_confidence=0.5, min_tracking_confidence=0.5 ) def extract_pose_features(video_path, stride=2): 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控制采样间隔,动作缓慢时stride=1保留全部帧,动作快速时适当增加到stride=2或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, window=12, step=3): 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, window=12, step=3) X_train, X_test, y_train, y_test = train_test_split( Xw, yw, test_size=0.2, random_state=42 ) clf = RandomForestClassifier(n_estimators=200, min_samples_leaf=3, n_jobs=-1) clf.fit(X_train, y_train) pred = clf.predict(X_test) print(classification_report(y_test, pred))

这里有两个容易被新手忽略的决策。标签取窗口中心帧而不是最后一帧:因为动作是渐变的,窗口前面一半可能还在准备动作,后面一半已经在恢复,中心帧最接近这个窗口的语义主题。step控制窗口与窗口的重叠量,step=3配合window=12表示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_sequences=True), layers.LSTM(32), layers.Dense(num_classes, activation="softmax") ]) model.compile( optimizer=tf.keras.optimizers.Adam(learning_rate=1e-3), loss="sparse_categorical_crossentropy", metrics=["accuracy"] ) history = model.fit( X_seq, yw, epochs=30, batch_size=32, validation_split=0.2, callbacks=[tf.keras.callbacks.EarlyStopping(patience=5, restore_best_weights=True)] )

LSTM模型有三个关键点:特征必须先标准化,随机森林不需要标准化但LSTM需要,否则坐标特征的值域差异会拖慢收敛;return_sequences=True让第一层LSTM输出完整的时间步序列,供第二层继续捕捉时序;EarlyStopping的patience=5表示连续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, annot=True, fmt="d", xticklabels=classes, yticklabels=classes) 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_weight="balanced"),或者手动传一个字典精确控制比例。

调参不建议手动一个个试,可以先用下面这段网格搜索锁定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, cv=3, scoring="f1_weighted", n_jobs=-1) grid.fit(X_train, y_train) print(grid.best_params_)

scoring="f1_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, window=12, low=0.6, high=0.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

这段代码的要点在双阈值:high=0.8负责"开始",low=0.6负责"结束",之间留了0.2的迟滞带。如果只用一个0.7的阈值,模型在动作边界处的置信度容易来回摆动,一个挥手会被切割成"开始、结束、再开始",出现大量虚假片段。我在实际项目里通常从high=0.8、low=0.6起步,然后根据误报率调整。

最后还有一个我自己踩出来的习惯:在线推理接好之后,别急着调模型超参数,先在录制的长视频上跑一遍,把误报片段剪出来看。模型离线指标高只说明它在"整齐切好的数据上表现好",而真实动作流的价值判断标准是:该报警的没漏,不该报警的没乱报,这个标准在滑窗推理跑通之前是看不见的。每次拿到动作识别的压缩包,我都先把这套在线推理框架搭起来,再回头慢慢调模型,这个顺序帮我避开了很多"离线指标好看、上线就翻车"的尴尬,希望帮到你。

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

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

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

立即咨询