拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

基于MediaPipe Holistic的八段锦动作识别:75个关键点与DTW匹配实战

基于MediaPipe Holistic的八段锦动作识别:75个关键点与DTW匹配实战 简介基于计算机视觉的八段锦智能辅助训练系统选用MediaPipe Holistic模型可同时检测33个身体关键点和42个手部关键点在自建测试集上对8个标准动作的识别准确率达92%。资源面向动作识别与姿态估计方向的开发者、科研人员可落地于居家场景的八段锦动作纠偏、训练评分与反馈。压缩包共包含10个文件主要有项目Python源码、运行依赖与测试说明txt、技术文档docx/md以及字体文件整体约13.87MB便于快速查看结构和复现实验。目前已有140人学习浏览。通过源码和配套文档可完整理解MediaPipe关键点提取、动作特征分析到准确率验证的流程同时获得配置文件与依赖清单降低环境搭建门槛。对于想将计算机视觉应用于传统体育标准化教学或同类姿态辅助训练的人员有直接参考价值。1. 八段锦辅助训练系统的切入点先解决“动作对不对”再谈“练得好不好”过去大半年我在帮一位做运动康复的朋友搭动作评估工具试过一版基于 YOLOv8 姿态估计的方案后来整个推倒重来换成了 MediaPipe Holistic。原因很简单八段锦这类缓慢的导引功法评分重点不在“人有没有出现在画面里”而在手指、手掌朝向、膝盖曲直这些细枝末节。YOLO 的姿态分支只给 17 个身体关键点手部信息完全缺失而这套基于计算机视觉的八段锦智能辅助训练系统标题里明确写了用 MediaPipe Holistic 同时做 33 个身体关键点和 42 个手部关键点检测再用自建测试数据集验证 8 个标准动作。这套组合解决的是“对着视频练八段锦没人告诉你手有没有翻到位、膝盖有没有挺直”这个具体问题。它适合做课程设计、运动康复小工具或者健身房里的轻量级动作纠正设备也适合刚入门的计算机视觉从业者拿来练手——因为你不需要 GPU不需要标注几十万张图片一套开源模型加一个自建数据集就能跑起来。2. 关键点选择与模型落地为什么是 MediaPipe Holistic 而不是 OpenPose 或 YOLO2.1 33 42 75 个关键点这套骨架到底覆盖了什么MediaPipe Holistic 本质上是三套模型的组合Pose 模型输出 33 个身体关键点Hand 模型对左右手各输出 21 个关键点Face 模型输出 468 个面部关键点。像标题里说的“33 个身体关键点和 42 个手部关键点”就是只取了 Pose 和 Hand 的部分一共 75 个点没有算人脸。这 33 个身体点里0 号是鼻子11 和 12 是左右肩13 和 14 是左右肘15 和 16 是左右腕23 和 24 是左右髋25 和 26 是左右膝27 和 28 是左右踝。手部的 21 个点分布更有讲究0 号是手腕1 到 4 是拇指5 到 8 是食指9 到 12 是中指13 到 16 是无名指17 到 20 是小指。这套手部拓扑和 OpenPose 的手部模型完全一致是学术界通用的手势标注规范。八段锦里为什么一定要手部关键点看“两手托天理三焦”这一式要求两臂上举、手掌翻转朝天。如果只看肘关节角度手臂伸直的人和不完全伸直的人分数差不多但看手腕到指尖的方向向量手掌有没有翻到位一目了然。“攒拳怒目增气力”要求握拳手指有没有真的卷起来也只能靠手部关键点判断。这就是 75 个点相比纯身体 33 个点的核心价值动作的“末梢细节”决定了功法质量。2.2 MediaPipe Holistic 的调用代码与输出结构安装和调用并不复杂MediaPipe 官方 pip 包直接支持 Python也不需要独立下载模型文件。安装命令在多数环境下就是一条 pip 指令开发调试用 PyCharm 或者 VS Code 都可以区别不大。pip install mediapipe opencv-python numpy下面是逐帧提取关键点的最小可用代码。import cv2 import numpy as np import mediapipe as mp mp_holistic mp.solutions.holistic def extract_keypoints(results): 把 Holistic 输出转成定长特征向量: 33*4 21*3 21*3 258 维 pose [] if results.pose_landmarks: pose [[lm.x, lm.y, lm.z, lm.visibility] for lm in results.pose_landmarks.landmark] else: pose [[0, 0, 0, 0]] * 33 # 漏检帧用零填充后续会过滤 left_hand [] if results.left_hand_landmarks: left_hand [[lm.x, lm.y, lm.z] for lm in results.left_hand_landmarks.landmark] else: left_hand [[0, 0, 0]] * 21 right_hand [] if results.right_hand_landmarks: right_hand [[lm.x, lm.y, lm.z] for lm in results.right_hand_landmarks.landmark] else: right_hand [[0, 0, 0]] * 21 return np.array(pose left_hand right_hand, dtypenp.float32).flatten() holistic mp_holistic.Holistic( static_image_modeFalse, model_complexity1, min_detection_confidence0.5, min_tracking_confidence0.5, smooth_landmarksTrue ) cap cv2.VideoCapture(0) while cap.isOpened(): ok, frame cap.read() if not ok: break rgb cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) results holistic.process(rgb) features extract_keypoints(results) # 形状: (258,) # 这里把 features 送到后面的动作分类器 / DTW 匹配器这段代码里有几个参数直接影响后续识别的稳定性。model_complexity接受 0 或 10 是轻量模型速度快但关键点抖动明显1 是完整模型精度更高在离线构建数据集时我建议用 1。min_detection_confidence是初始检测阈值低于这个置信度就认为画面里没有人min_tracking_confidence是帧间跟踪阈值实际做识别时可以适当放宽到 0.4因为相比重新检测跟踪更便宜。smooth_landmarks默认开启自带轻量平滑能压掉一部分手部抖动噪声。2.3 与 OpenPose、MoveNet、YOLO 姿态估计的方案对比这里摆一张直观对比表是我在这些动作捕捉方案里反复横跳后的结论。方案身体点手部点推理成本适合八段锦辅助训练吗MediaPipe Holistic3342双手笔记本 CPU 可实时适合手部全轻量MediaPipe Pose33无最低不适合缺手指信息OpenPose2542双手需要 GPU适合但部署太重MoveNet17无低不适合点数少YOLOv8-pose17无GPU 友好不适合末梢缺失OpenPose 是学术界的老牌方案手部关键点质量很高但在 CPU 上跑实时推断基本不现实还得自己管理多路模型的生命周期。常见做法是用 OpenPose 做离线数据标注等数据准备好了线上推断反而用 MediaPipe。MoveNet 虽然轻量但 17 个点只有躯干四肢手部为零。至于 YOLOv8-pose更多是目标检测加姿态的统一框架适合做多人、多目标的场景用来单独抠手型就有点杀鸡用牛刀。MediaPipe Holistic 作为主力方案的真正理由是它把身体和手放在同一个推理管道里关键点坐标系一致不需要单独标定摄像头也不用手动拼接两套模型输出。这对自建数据集来说是省事且少出错的选择。3. 建立八段锦动作数据集自建数据集的采集与标注流程3.1 八个标准动作的定义与采集规范标题里提到“8 个标准动作”对应八段锦的八个招式。做数据集之前必须把这 8 个动作的起止界定清楚否则后面标注会乱。我一般这样定义从起势静立开始到该式结束回到起势为止算一个完整动作样本。两手托天理三焦左右开弓似射雕调理脾胃须单举五劳七伤往后瞧摇头摆尾去心火两手攀足固肾腰攒拳怒目增气力背后七颠百病消采集时用手机横拍或普通摄像头即可但有几个硬性要求。背景不要有其他人避免多目标干扰人物最好站在画面中央占画面高度的三分之二以上摄像头高度和胸口齐平不要俯拍也不要仰拍。每个动作至少录 10 个完整样本推荐 15 到 20 个因为后面要留出跨人验证的余量。录制人最好找 3 到 5 个不同身材的人而不是只录一个人——如果你自己练准确率可能会很好看但换个人识别率会掉这种坑我下面专门讲。3.2 视频转关键点序列批量抽取的完整脚本采集完视频后第一步是把每个视频转成关键点时间序列。我习惯按“每 3 帧抽 1 帧”的采样率来处理这样既能保留动作细节又不会让序列太长拖慢匹配速度。import os import cv2 import numpy as np import mediapipe as mp mp_holistic mp.solutions.holistic SAMPLE_INTERVAL 3 # 每 3 帧抽 1 帧 def video_to_keypoint_sequence(video_path, intervalSAMPLE_INTERVAL): 读取视频输出关键点序列形状 (N, 258) holistic mp_holistic.Holistic( static_image_modeTrue, # 抽帧模式不做帧间跟踪 model_complexity1, min_detection_confidence0.5 ) cap cv2.VideoCapture(video_path) seq [] frame_idx 0 while True: ok, frame cap.read() if not ok: break if frame_idx % interval 0: rgb cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) results holistic.process(rgb) seq.append(extract_keypoints(results)) frame_idx 1 cap.release() holistic.close() return np.array(seq) # (N, 258) # 目录结构: raw_videos/动作名/人名_序号.mp4 data_root raw_videos output_root landmark_sequences for action in os.listdir(data_root): action_dir os.path.join(data_root, action) if not os.path.isdir(action_dir): continue os.makedirs(os.path.join(output_root, action), exist_okTrue) for video in os.listdir(action_dir): if not video.endswith((.mp4, .avi, .mov)): continue seq video_to_keypoint_sequence(os.path.join(action_dir, video)) name os.path.splitext(video)[0] np.save(os.path.join(output_root, action, name .npy), seq)这段代码有意识地设了static_image_modeTrue。视频转序列和实时识别不一样不需要跟踪逐帧独立检测更稳因为跟踪一旦丢了就很难自己恢复。代价是每一帧都要完整走一遍检测模型速度慢一些但构建数据集属于离线任务慢一点无所谓。输出文件是.npy每份是一条完整的动作序列后续无论是做 DTW 匹配还是喂给 LSTM都是现成的格式。3.3 数据清洗与归一化决定 92% 准确率的是数据处理不是模型数据集建好之后不能直接拿去训练或匹配先要做清洗和归一化。这里的翻车点极多我逐个说。清洗的第一件事是过滤漏检帧。上面extract_keypoints里因为手部漏检会返回全零向量所以要用置信度或非零比例做一次过滤。常见做法是把含 21 个手部点全是零的帧直接剔除如果一段序列剔掉后还剩不到总帧数 70% 的长度这段样本直接作废。第二件事是坐标归一化。直接拿原始 x/y 坐标做距离计算会被镜头内位置和人物身高严重干扰。我一般会以髋部中心为原点、肩宽为尺度做一次归一化。def normalize_sequence(seq): seq: (N, 258), 前 132 维是身体(33*4)后面 126 维是左右手(42*3) pose seq[:, :132].reshape(-1, 33, 4) normalized np.zeros_like(seq) for i in range(len(seq)): landmarks pose[i][:, :3] # 只取 x,y,z left_hip landmarks[23] right_hip landmarks[24] hip_center (left_hip right_hip) / 2.0 shoulder_width np.linalg.norm(landmarks[11] - landmarks[12]) if shoulder_width 1e-6: continue # 肩宽退化跳过 normalized[i, :132] ((landmarks - hip_center) / shoulder_width).reshape(-1) normalized[i, 132:] seq[i, 132:] # 手部坐标保持原尺度 return normalized解释一下为什么手部要保持原尺度。手部 21 个点的 z 值本身噪声很大而且手和肩宽不在同一个尺度体系里强行用手部坐标除以肩宽会把手指信息压没。所以这里只对身体做平移和缩放手部保持原始归一化坐标后续特征计算只取 x 和 y。最后是训练验证集的划分。这里有一个几乎所有人都会踩的坑如果同一个人的数据同时出现在训练集和测试集里测试准确率会虚高。正确做法是按“人”来切而不是按“样本”切。用GroupShuffleSplit可以保证同一个人的所有样本只进一侧。from sklearn.model_selection import GroupShuffleSplit X all_sequences # (总样本数, N, 258) y all_labels # 0~7 groups all_person_ids # 每个样本对应的录制人编号 gss GroupShuffleSplit(n_splits1, test_size0.2, random_state42) train_idx, test_idx next(gss.split(X, y, groupsgroups))这一步做完你会看到跨人准确率比同源准确率低 5 到 15 个百分点这是正常现象。标题里那个 92% 的准确率只有在按人划分测试集的前提下才有横向比较的价值。4. 动作识别实现基于关键点序列的匹配与分类方案4.1 特征工程的三个层次静态角度、动态曲线、时序窗口拿到 75 个关键点的原始坐标不能直接比欧氏距离。不同的人臂长、肩宽、身高差异很大即使归一化之后动作执行幅度也会带来很大偏差。我按三个层次做特征组织。第一层是关节角度。把 33 个身体点换算成肘关节角、肩关节角、膝关节角、髋关节角、躯干倾角。这些角度对身材比例不敏感是动作识别最稳的特征。计算方式就是向量夹角的余弦公式左手肘角用 11左肩、13左肘、15左腕三个点算。第二层是方向向量。比如“左右开弓似射雕”中前臂要和躯干保持接近垂直此时可以用肘到腕的向量与肩到髋的向量的夹角作为关键判据。这一层对“动作做得标不标准”更敏感适合后面做评分。第三层是时间窗口。八段锦每个动作持续 5 到 10 秒单帧判断没有意义必须把连续 20 到 30 帧的角度变化曲线当作一个整体。这一层就是给 DTW 或 LSTM 用的。4.2 用动态时间规整做动作匹配以及模板投票机制小数据集上最可靠的动作匹配方案是动态时间规整。它的核心能力是允许两个动作序列在时间轴上伸缩对齐哪怕一个人做得快、一个人做得慢也能找到最小累积距离。八段锦的节奏本来就因人而异而且学练者的动作速度可能比标准示范慢很多所以 DTW 是这个场景下的首选。实现上用fastdtw库是最省事的。from fastdtw import fastdtw from scipy.spatial.distance import euclidean def angle_sequence_from_landmarks(seq): 把原始关键点序列压成角度序列, 避免直接比坐标 angles [] for frame in seq: # 这里假设 frame 已经是 258 维向量, 还原成 75 点 pts frame[:132].reshape(33, 4) # 只用身体点 ang [] ang.append(angle_3pts(pts[11], pts[13], pts[15])) # 左肘 ang.append(angle_3pts(pts[12], pts[14], pts[16])) # 右肘 ang.append(angle_3pts(pts[23], pts[25], pts[27])) # 左膝 ang.append(angle_3pts(pts[24], pts[26], pts[28])) # 右膝 angles.append(ang) return np.array(angles) # (N, 4) def action_distance(template_seq, query_seq): 模板与查询序列的 DTW 距离, 按路径长度归一化 distance, path fastdtw(template_seq, query_seq, disteuclidean) return distance / len(path) # 每个动作存多个模板, 识别时取最小距离 scores {} for action in action_names: # action_names 是 8 个动作名 templates template_db[action] # list of (T, 4) 角度序列 scores[action] min(action_distance(t, query_angles) for t in templates) predicted_action min(scores, keyscores.get)action_distance里除以路径长度的原因是 DTW 原始距离会随序列长度膨胀同样一个动作录了 8 秒和录了 10 秒累积距离会差很大。除以路径长度后得到的单位距离相当于“平均每帧的匹配代价”横向可比性要好得多。模板投票机制也很重要。每个动作不要只录一条标准动作当模板而是录 5 条覆盖正常速度、慢速、幅度偏大三种情况。识别时取这 5 条模板中的最小距离相当于“这句动作只要接近任何一个合法示范就算匹配上”。我在项目里常用 5 条模板加连续 5 帧多数投票的组合先逐帧算距离再对一个滑动窗口内的预测结果投票取票数最多的动作作为最终识别结果。窗口长度设在 15 到 25 帧之间太短会把单帧噪声算进去太长会吞掉动作边界。4.3 数据量充足时的备选方案LSTM 分类器与参数设置如果自建数据集扩大到了每动作 50 条以上可以考虑用一个小型 LSTM 替代 DTW。优点是推理更快不需要和每条模板都比一遍缺点是需要训练而且对数据划分更敏感。import tensorflow as tf from tensorflow.keras import layers, models model models.Sequential([ layers.Input(shape(None, 258)), # 变长序列 layers.Masking(mask_value0.0), # 过滤掉漏检帧的零向量 layers.LSTM(64, return_sequencesFalse), layers.Dropout(0.3), layers.Dense(32, activationrelu), layers.Dense(8, activationsoftmax) # 8 类动作 ]) model.compile(optimizeradam, losssparse_categorical_crossentropy, metrics[accuracy]) history model.fit( train_seq_padded, train_labels, validation_data(val_seq_padded, val_labels), epochs30, batch_size8, callbacks[tf.keras.callbacks.EarlyStopping(patience5, restore_best_weightsTrue)] )关键参数有三个Masking层、LSTM 单元数、batch_size。mask_value0.0对应标记提取时的零填充让 LSTM 跳过这一段而不是把它当成真实的零坐标去学习LSTM 单元我用 64数据量不到 500 条时 128 单元过拟合非常明显batch_size8是因为视频序列样本本身很长batch 太大会把显存或内存吃满。数据量不够大时不要迷信深度学习。我在早期版本里试过 LSTM效果反而不如上面那套 DTW 模板匹配稳定。后来总结出一条经验样本量低于每类 30 条时DTW 几乎总是赢样本量过百且换人验证时LSTM 才有机会反超。所以最后交付给朋友用的版本还是跑在 DTW 方案上。5. 避坑与排查八段锦识别系统里的 6 个常见问题5.1 手部关键点漏检导致序列长度参差不齐现象同一动作的不同样本关键点序列长度能差出 40% 以上DTW 距离整体偏大识别结果随机跳动。原因MediaPipe 的手部模型在手掌张开时识别很好但“攒拳怒目增气力”里的握拳动作手指卷曲加上掌心朝向不定置信度很容易跌到阈值以下手部关键点直接整段丢失。解决在数据清洗时加入质量过滤帧内 42 个手部点中有超过 5 个点坐标为全零该帧直接跳过。如果跳过帧超过序列总长 30%整个样本重新录。另外把min_detection_confidence从 0.5 降到 0.3可以让手部检测少丢一半远端的腕点会抖一些但总比没有强。5.2 归一化基准选错导致高个子和小个子动作距离悬殊现象同一个动作用 A 的模板去匹配 B 的同款动作距离远大于 A 匹配自己的距离。原因初期我用“髋中心到头顶的距离”作为尺度因子。但八段锦多处低头、仰头头顶点的检测受发型和低头姿态影响非常大尺度因子本身就在抖动。解决改用肩宽作为尺度因子。左右肩点 11 和 12 的检测稳定性远高于头顶而且肩膀几乎不会出现在复杂姿势变化中。实践下来归一化后的跨人身高差异至少被压掉了 60%。5.3 一个动作只录一条模板换人后识别失真现象单人测试准确率还行换成第二个测试者所有动作都往同一个类上偏。原因这是典型的过拟合。一条模板只代表了录制者本人的动作习惯比如“左右开弓”这一式有的人马步扎得很低有的人只是微蹲DTW 会把马步深的动作归到距离更近的其他类里。解决每个动作至少录 5 条模板尽量找不同身高、不同熟练程度的人录制。匹配时取 5 条模板中的最小距离等于放宽了该类动作的合法范围。我后来还试过取中位距离对异常模板的鲁棒性更好了。5.4 关键点抖动让尾部动作误判现象平时识别稳定一到“背后七颠百病消”这种身体上下弹动的动作预测结果在 2 秒内反复横跳时而识别成“两手托天”时而识别成“摇头摆尾”。原因这个动作本身是全身小幅振动关键点每一帧都在正常范围内微动单帧角度特征在这种情况下没有判别力。加上弹动时手部位置和起势姿势接近容易混淆。解决一是对关键点坐标做 Savitzky-Golay 滤波窗口设 11能保留动作趋势又压掉高频抖动二是针对这个动作在特征里增加“质心垂直位移的累计量”而不要只看瞬时位置。这样过滤后“颠”这个动作的判别信号从微动变成了整体位移幅度。5.5 训练集和测试集来自同一个人92% 是虚高的现象自测准确率 92%数据按 80/20 随机切分后训练换一个没参与录制的人来测准确率掉到 70% 出头。原因随机切分会把同一个人的多条相似样本分到训练和测试两侧。这个人习惯性的抬手高度、转身幅度、节奏快慢都成了模型学到的“捷径”。换个人这些习惯全变了。解决见 3.3 节的GroupShuffleSplit以后只要是动作识别项目一律按人物 ID 切分。如果要做严格评估训练集里完全不放录制者 A 的数据全部用其他人训练A 只出现在测试集里。这个做法会逼着你多采集几个人但做出来的系统才真有实用价值。我这里有个血泪经验印证凡是拿自己录的数据自测并对外报高准确率的活动识别项目落地时基本都要回炉重造。5.6 手部坐标在背景区域漂移现象人物靠近画面边缘时手部关键点飞到背景墙上或者身体轮廓附近形成明显的“鬼手”。原因MediaPipe 的手部检测器是通过裁剪人体区域来定位手的当人物占比小、手部超出检测框时定位框不准确手部点就会漂移到纹理丰富的背景区域。解决录制时保证人物占画面高度三分之二以上镜头尽量固定。代码层可以对手部关键点做一个合理性检查手部点距离腕点超过前臂长度的 2.5 倍视为异常直接丢弃该帧。这个启发式规则只用几何约束不需要额外模型成本很低但很有效。6. 从 92% 到落地动作评分与跨设备验证的进阶细节识别准确率解决的是“这是哪一式”但辅助训练系统真正要回答的是“这式做得标不标准”。我会把输出从类别改成评分对每个动作定义两到三个关键角度比如“两手托天”看肘关节伸展度、肩关节上抬角度、手掌朝向上的翻掌角。每一帧把当前角度和模板角度序列对齐后求差值取整段动作的加权平均偏差映射成百分制分数。这一步不复杂但设计角度时最好是每式单独定义比如“攒拳怒目”看握拳紧度和出拳方向“两手攀足”看膝关节伸直程度和躯干前倾角度一套通用角度打不了天下。验证系统是否真的可用我的习惯是坚持跨人加跨设备。同一个动作用笔记本内置摄像头录一份再用手机横拍录一份观察同一人的分数波动是否超过 10 分。超过就检查是不是画面比例或者摄像头高度带来的视角差异——这是落地最常见的变量。模型本身尽量导出成 OpenVINO 或 TFLite 格式再部署加载体积更小推理延迟明显下降这对 CPU-only 的辅助训练设备是划算的工程投入。我现在的个人习惯是每当要在一个新动作库上训练先把数据采集方案定死再碰模型。录制视角、人物占比、按人划分验证集这三件事提前定好后面能省掉大量返工。做动作识别这几年最深的体会是视觉得分和主观感受不一致时先怀疑关键点抖动再怀疑打分角度定义大部分翻车都出在这两处。希望这篇基于计算机视觉的八段锦智能辅助训练系统的拆解能帮你把从 MediaPipe Holistic 到 92% 准确率的这条路径走通。本文还有配套的精品资源点击获取
返回列表