
简介这是一套基于Python与MediaPipe_Pose库开发的羽毛球训练视频分析系统面向运动员、教练及体育科研人员。系统可对训练视频进行实时姿态估计识别手、肘、肩、膝等二十一个骨骼关节点并与预设动作模板对比评价姿势标准性同时借助球场参考线辅助定位追踪运动员移动轨迹并计算跑动距离为体能分配和战术执行提供量化依据。资源包共二十七个文件以九个Python脚本为核心配有五个Markdown文档、三个Shell部署脚本及配置说明等压缩包仅有八十一KB结构紧凑便于快速部署与二次开发。目前已有一百七十二人学习使用适合希望以低成本构建运动分析原型或研究姿态识别落地的开发者。随包附带的系统设计文档、快速上手指南和示例脚本能帮助用户从环境搭建到功能扩展顺利上手。1. 把训练视频拆成“动作坐标”为什么羽毛球教练越来越需要盯屏幕算数据训练视频攒了一大堆教练却越来越难一帧帧看完这拍杀球手腕是不是又压不下去刚才那二十拍到底跑了多少米。这套基于Python与MediaPipe Pose库的羽毛球运动员训练视频姿势识别与移动距离计算系统解决的就是这件事——把训练视频变成一组带时间的骨骼关节点标注用实时姿态估计跑出每帧的人体姿势再判断动作是否标准、借用球场参考线辅助定位最后把移动距离和移动轨迹一起追出来。系统适合两类人一类是给队员做量化训练的教练和体育科研人员另一类是刚接触人体姿态估计、想拿真实场景练手的Python开发者。它能替代的是秒表和肉眼回放替代不了教练的现场判断。2. MediaPipe Pose 凭什么是首选姿态估计选型逻辑与最小跑通环境2.1 对比 OpenPose、YOLO 姿态估计、MMPose为什么最后选 MediaPipe人体姿态估计这个方向市面上不缺现成方案。学术界用得多的 OpenPose关键点检测质量确实高但模型权重动辄几百 MB部署依赖也多给羽毛球训练视频做离线分析有点杀鸡用牛刀。经常被拿来对比的 YOLO 姿态估计优点是能同时做人框检测和关键点回归速度也不错但你需要自己准备带关键点标注的训练数据对大部分教练团队来说这个门槛太高。MMPose 是实验室工具箱灵活性强可换成自己想要的骨干网络代价是配置文件和训练流程要花不少时间折腾。MediaPipe Pose 恰好卡在中间位置。它把姿态估计做成了开箱即用的推理管线输出人体 33 个关键点权重是预训练好的CPU 上跑 720p 视频也能维持每秒二十帧以上一个 pip install 就能进 Python。真正让它适合羽毛球场景的是两个细节一是它默认启用了帧间跟踪处理连续视频时关键点会跟随上一帧的结果走比每帧重新检测快不少二是输出的 x、y 是归一化坐标z 是相对深度估计附带 visibility 置信度做二次计算非常直接。那什么时候不选它如果要做精细的多人姿态估计或者需要训练自己的动作分类我会混用 YOLO 那一路做人体框检测再对每个框单独跑 MediaPipe Pose。双打训练视频里两个队员交错跑位原生 Pose 会把最靠近镜头的人当作追踪目标这个问题后面专门讲。单打视频和以单人为主的教学视频MediaPipe Pose 本体就是性价比最高的方案换模型前的调参成本几乎为零。提示MediaPipe 的推理后端默认走 CPU。装了 TensorFlow 不代表 GPU 会被自动用上先别指望它后面有专门一节讲性能。2.2 搭运行环境从 Python 安装到 vscode 里第一次加载 mp.solutions.pose环境本身不复杂但版本踩坑率很高。我建议用 Python 3.10 或 3.11太新的 3.12 偶尔会遇到 mediapipe 轮子没跟上反而浪费时间。装 Python 时勾选 Add to PATH然后在 vscode 里建一个虚拟环境三行命令解决依赖python -m venv .venv .venv\Scripts\activate pip install mediapipe opencv-python numpy这段做了三件事创建隔离环境避免把系统 Python 搞乱激活环境让后续安装落在项目里装齐三个核心包。mediapipe 负责姿态估计opencv-python 负责视频读写和绘图numpy 负责坐标运算。装完验证一下是否真的可用python -c import mediapipe as mp; print(mp.__version__)能打印出版本号就说明导入成功。这一步最常见的问题是环境没激活就 pip install装到了别的 Python 里vscode 里跑起来还是报 ModuleNotFoundError。所以装完先看终端左侧是不是显示 .venv再跑验证命令。2.3 最小命令读一帧视频并输出 33 个关键点坐标跑通环境后的第一个里程碑是从视频里取出一帧让 MediaPipe 输出 33 个关键点坐标。下面这段代码是整套系统的地基import cv2 import mediapipe as mp mp_pose mp.solutions.pose pose mp_pose.Pose( static_image_modeFalse, model_complexity1, min_detection_confidence0.5, min_tracking_confidence0.5, ) cap cv2.VideoCapture(badminton.mp4) ret, frame cap.read() if not ret: raise SystemExit(视频流没有读出来) frame_rgb cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) results pose.process(frame_rgb) if results.pose_landmarks: for i, lm in enumerate(results.pose_landmarks.landmark): if lm.visibility 0.5: print(f{i:2d}: x{lm.x:.3f} y{lm.y:.3f} z{lm.z:.3f} vis{lm.visibility:.2f}) cap.release()逻辑说明VideoCapture 默认读出来的是 BGR 顺序MediaPipe 的输入要求 RGB必须用 cvtColor 转一次。pose.process 是真正的推理入口输入一帧图输出 results 对象。results.pose_landmarks 里按固定索引排了 33 个关键点从鼻子0、双肩11/12、双肘13/14、双腕15/16到髋、膝、踝和脚趾全套都在。参数说明static_image_modeFalse 表示走视频跟踪模式连续帧之间会复用上一帧的结果比每帧独立检测快很多代价是极速转身时偶尔会丢点model_complexity 取 1是精度和速度的中间档0 最快但关键点漂移明显2 最准但帧率掉一半min_detection_confidence 和 min_tracking_confidence 都是 0.5低于这个置信度的点会被丢弃。注意 lm.z 是模型猜出来的相对深度不是真实深度拿它算距离会出大笑话后面讲移动距离时再说。3. 骨架关节点标注与实时姿态估计从视频帧到骨骼画面的完整流水线3.1 帧循环与推理参数static_image_mode、model_complexity、置信度怎么配处理完整视频不能只读一帧需要一个循环。但一场 40 分钟的训练视频有三万多帧每帧全量推理是自找苦吃。我一般把视频抽帧到 810 fps对姿势标准性检测来说已经足够对移动距离计算也不会把轨迹压得太失真。抽帧逻辑是每隔 N 帧处理一次把有效推理量压到几千帧。fps_target 10 # 期望的处理帧率按视频实际帧率换算跳帧数 video_fps cap.get(cv2.CAP_PROP_FPS) every_n_frames max(1, round(video_fps / fps_target)) frame_idx 0 while cap.isOpened(): ret, frame cap.read() if not ret: break frame_idx 1 if frame_idx % every_n_frames ! 0: continue frame_rgb cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) results pose.process(frame_rgb) # 这里把结果交给后面的角度计算/轨迹计算模块逻辑说明先拿视频原始帧率除以目标帧率算出每隔几帧取一帧。比如 30 fps 的视频要压到 10 fpsevery_n_frames 就是 3每三帧处理一帧。这样做可以把一小时视频的推理量从十多万帧降到三万多帧配合缩分辨率一台普通笔记本也能跑完。参数上static_image_mode 要根据输入源切换处理跳帧后的视频流保持 False 以便启用帧间跟踪如果你后期要把单张图片或照片喂进去就必须改成 True否则模型还在等上一帧的跟踪结果。model_complexity 我建议固定 1模型 2 在右手快速杀球时对肘关节的定位确实更稳但推理时间涨得太多得不偿失。置信度阈值前期用默认值跑 200 帧统计一下哪些关键点频繁低于 0.5再针对性地调高到 0.6 或 0.7。3.2 画骨骼draw_landmarks 与 POSE_CONNECTIONS 的调用方式坐标算出来以后要让教练看得见。骨架关节点标注就是把 33 个点和它们之间的连线画到原始帧上MediaPipe 自带绘图工具两行代码就能出图。mp_drawing mp.solutions.drawing_utils mp_drawing_styles mp.solutions.drawing_styles annotated frame.copy() mp_drawing.draw_landmarks( annotated, results.pose_landmarks, mp_pose.POSE_CONNECTIONS, landmark_drawing_specmp_drawing_styles.get_default_pose_landmarks_style(), connection_drawing_specmp_drawing_styles.get_default_pose_connections_style(), ) cv2.imshow(Pose, annotated) cv2.waitKey(1)逻辑说明draw_landmarks 接收三个核心参数——画布、关键点对象、连接关系。POSE_CONNECTIONS 是 MediaPipe 预置的骨架拓扑定义了哪些关键点之间应该画线比如左肩连左肘、左肘连左腕。landmark_drawing_spec 控制点的颜色和大小connection_drawing_spec 控制线的样式。第 4 章也会用到它用不同颜色标出角度超限的关节教练一眼就能看出是哪拍出了问题。注意这段代码画在 frame 的副本上不污染原始帧。如果后面要做视频输出用 VideoWriter 把 annotated 逐帧写进去按 10 fps 合成剪辑软件直接能播。此时标注的可信度取决于 visibility画之前先把低于阈值的点过滤掉否则会画出一些完全随机的手腕位置。3.3 归一化坐标还原成像素坐标先乘宽还是先乘高的细节与边界MediaPipe 给出的 x、y 都是 01 的归一化值要画框或算像素距离必须还原成图像上的真实像素坐标。这里有个顺序问题我见过不少人在这一步翻车。h, w frame.shape[:2] px int(lm.x * w) py int(lm.y * h)逻辑说明x 是相对图像宽的比例必须乘以宽 wy 是相对图像高的比例必须乘以高 h。写反了坐标就跑到画面外了。shape 返回的是 H、W、C第一维是高我特意把 h、w 一起取出来再分别乘就是怕写混。参数补充如果对帧做了 resize 预处理比如把 1280 的宽压到 640那 w 应该用缩放后的宽度不是原视频宽度。这地方最容易出现“标注点偏移”、和骨骼线错位的现象多数是宽高写反或没跟着缩放。另外lm.z 在这里要特别提醒它是一个和摄像机到人体距离相关的相对深度值单位不是米不同人被拍到画面不同位置时 z 的差异没有物理意义。硬拿 z 参与三维重建会得到完全没谱的结果这套系统里 z 最多用来判断“这个点是否朝向镜头”不参与距离计算。提示快速杀球时手腕会经历从脑后到身前的高速运动单帧 30 fps 都容易丢点。做骨骼关节点标注时要给手腕、脚踝这类高频部位单独设一条置信度下限宁可缺一帧也不要用脏点算角度。4. 姿势标准性判断与球场参考线辅助定位把“打得好不好”变成可计算的量4.1 用三关节向量夹角量化动作肘关节、膝关节的角度计算姿势标准性判断的数学基础很简单三个关键点组成两条向量用余弦公式求夹角。比如判断杀球时肘关节有没有伸直就看肩、肘、腕三点构成的肘角大小判断准备姿势重心得稳不稳看髋、膝、踝三点构成的膝角。import numpy as np def calc_angle(a, b, c): 计算以 b 为顶点的角度a、b、c 是关键点坐标 [x, y] a np.array(a[:2]) b np.array(b[:2]) c np.array(c[:2]) v1 a - b v2 c - b cos np.dot(v1, v2) / (np.linalg.norm(v1) * np.linalg.norm(v2) 1e-6) cos np.clip(cos, -1.0, 1.0) return np.degrees(np.arccos(cos)) lm results.pose_landmarks.landmark left_elbow calc_angle(lm[11], lm[13], lm[15]) # 左肩-左肘-左腕 right_elbow calc_angle(lm[12], lm[14], lm[16]) # 右肩-右肘-右腕 left_knee calc_angle(lm[23], lm[25], lm[27]) # 左髋-左膝-左踝 right_knee calc_angle(lm[24], lm[26], lm[28]) # 右髋-右膝-右踝逻辑说明calc_angle 里先取三个点的 x、y以 b 点为顶点构造两条向量用点积除以模长得到夹角的余弦值再通过 arccos 转成角度。1e-6 是防止向量模长恰好为零时除零报错clip 限制余弦值在 [-1, 1] 之间避免角度输出 NaN。参数说明这里用 a[:2] 只取 x、y刻意丢掉 z因为 z 是相对深度、误差大算夹角会把真实角度带偏。利用的是两脚踝和两髋左髋索引 23、左膝 25、左踝 27右髋 24、右膝 26、右踝 28。不用记死打印一下 PoseLandmark 枚举挨个看更直观。4.2 标准性打分与实时提示判定区间、异常分支与可视化反馈角度算出来之后怎么叫“标准”需要定区间。我一般以羽毛球动作的基本规律为起点再拿队员自己的合格动作视频做标定录十个高质量动作算出角度分布的均值和标准差取均值加减两倍标准差作为正常区间。下面是一组常用参考值适用于成年业余队员动作阶段观察关节正常角度区间常见错误正手高远球击球点持拍侧肘关节160°175°屈肘过多发力不完整接杀准备姿势双侧膝关节140°155°膝盖过直或蹲得太低反手挑球引拍持拍侧肘关节110°135°肘部架太高挑球下网上网跨步落地前跨腿膝关节120°140°膝盖超过脚尖伤膝判断分支建议写成函数返回状态和角度方便后面叠加颜色标注def judge_joint(angle, low, high, name): if low angle high: return good, angle if angle high: return over, angle return under, angle status, ang judge_joint(right_elbow, 160, 175, right_elbow)参数说明low 和 high 来自上文表格实际项目里要用队员自己的动作标定修正。很多教程直接给死值我建议你把它做成配置文件不同队员用不同区间。反馈端把 status 分别映射成绿、黄、红三个颜色画骨骼时给对应关节单独着色教练看标注视频就能知道第几拍出了问题。4.3 球场参考线辅助定位从像素坐标到球场坐标系的透视变换羽毛球视频几乎都是斜视角拍摄这时画面远处的 100 像素和近处的 100 像素对应的实际距离完全不同。直接用像素坐标算移动距离误差能到 50% 以上。球场参考线辅助定位就是解决这个问题的利用场地线的标准长度把像素坐标变换到以米为单位的俯视球场坐标系。羽毛球场地标准尺寸是长 13.4 米、宽 6.1 米半场长 6.7 米、宽 3.05 米。我在画面上点选四个场地角点对应到俯视图坐标求一个透视变换矩阵src np.float32([[206, 402], [884, 398], [1206, 612], [38, 608]]) # 画面上四个场地角点 dst np.float32([[0, 0], [6.7, 0], [6.7, 3.05], [0, 3.05]]) # 俯视图球场坐标系单位米 M cv2.getPerspectiveTransform(src, dst) point_px np.array([[foot_x, foot_y]], dtypenp.float32) point_m cv2.perspectiveTransform(point_px.reshape(-1, 1, 2), M).reshape(2)逻辑说明getPerspectiveTransform 需要至少四组对应点src 是图像上场地四个角点dst 是按实际米数排布的俯视图坐标。M 是一个 3×3 单应矩阵把画面上的任意像素点映射到“像从正上方往下看球场”的坐标。变换后得到的就是以米为单位的坐标可以直接算距离和轨迹。参数说明src 四个点要选在场地平面上比如底线与边线的交点、发球线与边线的交点。注意这是平面变换它假设脚踝、脚跟这些关键点都在场地平面上所以只对这些点做透视变换手部、头部高度离场地平面远套同一个 M 会变形。角点选择不准是整个系统误差的主要来源我建议先把标注界面做出来逐帧拖动微调四个点直到变换后场地线对齐再批量跑数据。5. 从安装到距离换算5 个反复踩过的坑与排查方法5.1 视频一卡一卡、GPU 没生效MediaPipe 的推理后端默认是 CPU现象720p 视频跑起来只有七八帧每秒风扇狂转但任务管理器里 GPU 占用率基本是零。原因mediapipe 的 TFLite 推理后端默认在 CPU 上运行不会自动调度到 GPU。你之前装过 TensorFlow、PyTorch 都不影响这个链路它们和 mediapipe 的推理引擎是两套东西。解决离线分析场景硬扛显存不划算。先把视频抽帧压到 10 fps再把每帧画面缩到宽度 640process 之前做一次 resize推理耗时能降到原来的四分之一。如果一定要实时走 mediapipe 的 C 接口或 TFLite 委托 GPU 分支Python 端的 GPU 支持在不同版本上表现不稳定我不建议生产环境依赖它。实测下来CPU 上 640 宽度 10 fps 的抽帧方案处理一小时视频大约二十分钟内跑完完全够用。5.2 关键点抖动得像“帕金森”置信度阈值和时空平滑都没做现象同一个静止站姿左肩坐标在相邻帧里上下跳 35 个像素计算出来的肘角在 80° 到 110° 之间乱摆。原因min_detection_confidence 太低模型把低质量检测结果也输出来了同时没有对坐标做平滑。视频流模式下 MediaPipe 自带轻量平滑但剧烈动作下仍然不够。解决把 min_detection_confidence 调到 0.7min_tracking_confidence 调到 0.6。对角度和坐标再做一次滑动窗口平均窗口取 5 帧。注意快速启动和急停瞬间过度平滑会把距离峰值抹平所以移动距离计算里我不要全局平滑只对角度做轻量一阶滤波smoothed_angle 0.7 * prev_angle 0.3 * current_angle逻辑说明当前帧角度占 0.3上一帧平滑后的角度占 0.7。这是最简单的低通滤波可以有效压掉高频抖动又不会把动作边缘钝化得太厉害。如果你要处理的是杀球这类极快动作0.7 权重可以降到 0.5自己跑一遍看角度曲线以曲线不炸毛为准。5.3 移动距离算出来是“像素米”没标定就上了轨迹计算现象直接累加相邻帧脚踝的像素欧氏距离算出来一趟训练“跑了几十万”除以一个拍脑袋的比例还是对不上。更隐蔽的是同一个动作站在近处和远处算出的“跑动距离”完全不同。原因透视把实际距离在画面里压缩了近处放大、远处缩小。像素距离不是物理距离两者不成线性比例。解决必须先做球场参考线标定也就是第 4 章里的透视变换把像素坐标换算成米以后再算距离。如果拍摄机位固定、角度没变标定一次就能复用到整段视频机位一旦移动或焦距变了必须重新标定。我自己的习惯是每段视频启动后先强制做一次四点标定不做完不让后续流程开始。5.4 多人同框时骨骼线互相串多人姿态估计的边界现象双打训练视频里左边队员的手腕连线直接连到右边队员的肩膀上骨骼线画得乱七八糟移动轨迹也混在一起。原因MediaPipe Pose 原生是单人姿态估计模型在同一帧出现多个人时它倾向于输出画面中最显著或最近的那个人即使偶尔换到另一个人也没有稳定的身份区分。解决最稳妥的方案是先用目标检测模型把人框出来裁出单个人的区域分别做 MediaPipe Pose再把人框的多路结果合并回原坐标系。具体到你自己的项目可以先跑 YOLO 检测人框裁切后每个框单独过 pose.process最后把关键点坐标加回框的偏移量。如果不想引入第二个模型退而求其次的做法是限制追踪目标始终选择画面中心点距离上一帧目标最近的那个人分配一个临时 ID丢失后再重新捕获——这个方法在两人交叉跑位时仍然可能切换目标做移动距离统计时要有心理准备。5.5 参考线在逆光下检测失效固定灰度阈值经不起光照变化现象同一套 HSV 阈值早晨拍视频时能干净地提取出白色底线下午窗帘一拉同一个阈值什么都检测不到场地线跟地板融为一体。原因场地线是白色但材质和亮度在不同光照下的反射差异很大。固定阈值相当于在赌“拍摄条件不变”这在室内运动场馆几乎不可能。解决先做光照归一化对帧做 CLAHE 自适应直方图均衡化再提取场地线。更稳的做法是干脆不做全自动检测程序启动时人工点选四个场地角点之后用光流跟踪这些角点逐步修正透视矩阵。实测下来人工点选加光流跟踪的稳定性远高于纯阈值分割尤其是场馆灯光开一半、场地投影来回晃的情况反而是最简单可靠的路径。6. 移动距离与轨迹追踪最小代码验证“跑动距离”算得准不准前面的标定工作做完移动距离计算反而只是“累加坐标差”而已。这里给一个可直接落地的最小实现把脚踝坐标经透视变换后按时间顺序累计欧氏距离total_m 0.0 prev_pt None 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 not results.pose_landmarks: continue lm results.pose_landmarks.landmark h, w frame.shape[:2] left_ankle np.array([lm[27].x * w, lm[27].y * h], dtypenp.float32) pt_px left_ankle.reshape(-1, 1, 2) pt_m cv2.perspectiveTransform(pt_px.astype(np.float32), M).reshape(2) if prev_pt is not None: total_m np.linalg.norm(pt_m - prev_pt) prev_pt pt_m print(ftotal distance: {total_m:.2f} m)逻辑说明每一帧取左脚踝关键点还原成像素坐标后套用第 4 章的透视矩阵 M得到以米为单位的球场坐标拿当前帧坐标和上一帧坐标求欧氏距离并累加就是总移动距离。参数和细节上注意三点。第一脚踝关键点在快速跑动中容易丢失如果你发现 27 号 left_ankle 的 visibility 经常低于阈值就改用脚跟或脚趾关节也可以取左脚踝和左脚的均值规则是固定取一只脚不要左右脚来回切否则距离会从头翻倍到尾。第二抽帧会截弯取直10 fps 的采样间隔下跑动轨迹里的曲线段会被算短实测总距离约偏低 10%。要修正就做线性插值在每两个有效帧之间按时间补 24 个插值点再累加能挽回一部分误差但做不到完全还原。第三验证方法很重要——沿场地底线从一边走到另一边底线标准长度是 13.4 米对比计算值和实际值误差在 ±5% 以内这套标定和参数才算合格。我习惯在每个视频跑完后都打印一次“标定验证距离”当作系统自检。我自己第一次把整套流程跑通时跳过了标定直接把像素距离报给了教练结果算出来的跑动距离比真实情况少了快一半问题全出在透视上。后来我把四点标定改成启动时必做的步骤才彻底改掉这个偷懒的坏毛病。这类系统的精度不是靠模型而是靠标定和数据纪律撑起来的模型只负责给你稳定的骨骼点剩下的工作一分都不能省。希望帮到你。本文还有配套的精品资源点击获取