姿态估计驱动运动计数:从关键点角度到状态机实现

发布时间:2026/10/17 18:05:53

姿态估计驱动运动计数:从关键点角度到状态机实现 说到运动计数APP开发上一篇我们把摄像头预览、媒体管线和基础工程走通了框架搭起来之后其实真正决定项目能不能落地的是后面这两块硬骨头一块是姿态估计模型给出的原始关键点数据怎么变成有意义的动作信息另一块是计数逻辑在各种真实动作下怎么做到不误计、不漏计、不重计。这篇就把这两个核心环节完整拆开从坐标数据讲到角度计算从状态机讲到阈值调参全是实测跑的流程。这篇文章适合两类朋友一是已经在用姿态估计做点东西但卡在“关键点拿到了却不会用”的阶段二是想做个健身计数类APP正在纠结计数逻辑怎么实现。不管你是刚接触姿态估计还是已经跑通了Demo这篇文章都能让你少走不少弯路。1. 姿态估计选型与数据基础1.1 模型怎么选从实时性和精度看移动端方案做运动计数APP模型选型基本就决定了你后续所有工作的难度。姿态估计方案常见的无非这么几个方向一种是传统的多人姿态估计模型精度不错但模型体积大、推理速度慢在手机CPU上跑尤其是实时处理视频流时帧率低得没法看另一种是轻量级方案专门为移动端和嵌入式场景设计模型体积小、推理速度快精度虽然比不上大模型但对动作计数这种场景来说已经完全够用。我最后选的是轻量级方案核心决策因素有三个第一实时性。运动计数APP要处理的是连续视频帧模型推理速度直接决定计数响应是跟手还是滞后。实测中轻量级模型在主流手机上单帧推理能控制在10到30毫秒而重量级模型随便一跑就是上百毫秒体验差距天壤之别。第二跨平台支持。项目如果用跨平台框架开发模型方案最好能同时覆盖双端省去在多个平台各做一套模型集成的成本。第三关键点输出格式。运动计数需要稳定的关节点坐标目标模型直接输出33个关键点的归一化坐标、可见性和深度信息这就够了不依赖额外的目标检测网络。对比一下几个方案的差异更直观方案模型体积移动端推理速度关键点数量适合场景轻量级方案A3-5MB10-30ms33个实时移动端应用中量级方案B10-20MB50-100ms17-25个离线分析、PC端重量级方案C200MB500ms以上多人体多个点位实验室、服务器所以选型的结论很明确移动端做实时计数优先考虑轻量级方案把模型精度放在第二位把实时性和集成便利性放在第一位。1.2 33个关键点怎么读别把坐标搞反了拿到模型之后第一步就是把关键点的数据结构吃透。以轻量级模型输出的33个关键点为例每个关键点包含三个核心字段x归一化坐标范围0到1表示关键点相对输入图像宽度的横向位置y归一化坐标范围0到1表示关键点相对输入图像高度的纵向位置z深度信息表示关键点相对人体中心的深度数值越大通常表示离镜头越远其中x和y是归一化坐标这个很好理解但我在实际开发中遇到不少朋友在这里踩坑拿到坐标后直接当成像素坐标用了结果画叠加层的时候点位全部偏移。这里的关键是如果你想在画面上的正确位置画点画线必须先把归一化坐标乘上画面的实际宽高double pixelX landmark.x * imageWidth; double pixelY landmark.y * imageHeight;另外还有一个很重要的点关键点的索引编号是固定的这是后续角度计算的基石。运动计数常用的关键点索引对应关系大致如下索引部位运动计数用途11 / 12左肩 / 右肩俯卧撑角度计算13 / 14左肘 / 右肘俯卧撑、弯举角度计算15 / 16左腕 / 右腕俯卧撑末端判定23 / 24左髋 / 右髋深蹲角度计算起点25 / 26左膝 / 右膝深蹲、箭步蹲角度计算中枢27 / 28左踝 / 右踝深蹲角度计算终点这个索引表建议你记在便利贴上因为后续所有角度计算都要反复用到。如果记串了角度算出来就是错的但看数据又很难立刻发现排查起来特别痛苦。2. 关节角度计算与动作特征提取2.1 三点法求关节角度点积公式与NaN防护拿到关键点坐标之后最核心的任务就是把“坐标系里的点”变成“有物理意义的关节角度”。这里用一个非常经典的三点法以关节为顶点取相邻两个关键点构成两条向量求夹角的余弦再转换成角度值。比如计算膝关节角度取髋点索引23/24、膝点索引25/26、踝点索引27/28以膝点为顶点分别判断两侧叉开的角度。数学公式是这样的cos(θ) (BA · BC) / (|BA| * |BC|) θ arccos(cos(θ))翻译成代码就是O(1)的向量运算import dart:math; double calculateAngle(Offset a, Offset b, Offset c) { // a、b、c分别是三个关键点的坐标 // 顶点是ba和c是两侧点 double baX a.dx - b.dx; double baY a.dy - b.dy; double bcX c.dx - b.dx; double bcY c.dy - b.dy; double dotProduct baX * bcX baY * bcY; double magBA sqrt(baX * baX baY * baY); double magBC sqrt(bcX * bcX bcY * bcY); if (magBA 1e-6 || magBC 1e-6) { return _lastAngle; // 关键点重合时返回上一帧角度 } double cosTheta dotProduct / (magBA * magBC); cosTheta cosTheta.clamp(-1.0, 1.0).toDouble(); double angleInDegrees acos(cosTheta) * 180 / pi; _lastAngle angleInDegrees; return angleInDegrees; }这里有两个特别容易被忽略的坑第一个是向量模长可能为零。当两个关键点完全重合比如手遮挡在肘关节前面模型把两个点估计到同一位置时除法会导致数值异常。必须加一个最小模长判断否则你的角度值会出现抖动的怪异数据计数逻辑直接乱掉。第二个是acos的入参范围。浮点计算下dotProduct / (magBA * magBC)理论上应该在-1到1之间但由于浮点误差可能算出1.0000001之类的值导致acos返回NaN。加上clamp(-1.0, 1.0)这行代码能一次性把这个坑堵死。2.2 用角度曲线刻画动作深蹲和俯卧撑的判定特征关节角度是一种连续变化的曲线动作本质上就体现为特定关节角度的“下降-保持-回升”过程。以深蹲为例我们关注的是膝关节角度站立时膝盖基本伸直角度在170度左右下蹲到最低点时膝关节角度降到90度以下然后再回升到170度左右。一次完整的深蹲角度曲线就是一个清晰的V字形。俯卧撑则稍有不同关注的是肘关节角度手臂伸直撑起时为180度左右身体下沉到最低点时肘关节角度降到60度以下撑起时再回到180度。同样是一个U字形曲线。这里有个很重要的实操经验深蹲和俯卧撑的计数采集方向建议让用户侧对镜头。正对镜头时关节的深度信息不可靠而且人体容易遮挡关键点角度计算的误差会明显增大侧对镜头时关键点的二维投影能比较好地反映真实的关节弯曲状态。另外原始角度数据往往非常抖动主要是因为模型输出本身存在噪声加上用户动作不是完全平滑的。直接拿原始角度做计数判断会产生大量误触发。我这里用的是指数移动平均EMA做平滑double _smoothAngle 0; bool _isFirstFrame true; double smoothAngle(double rawAngle) { if (_isFirstFrame) { _smoothAngle rawAngle; _isFirstFrame false; } else { _smoothAngle _smoothAngle * 0.7 rawAngle * 0.3; } return _smoothAngle; }0.7和0.3这两个权重是实测比较合适的值平滑效果够好同时滞后不会太大。如果想让曲线更平滑可以调到0.8/0.2但滞后会明显增加计数响应会显得迟钝反过来调到0.6/0.4响应快了但噪声也更明显。具体权重可以根据你目标动作的节奏微调。3. 运动计数逻辑的核心实现3.1 状态机思路把连续动作切成离散状态角度曲线拿到之后接下来就是计数逻辑的设计。很多初学者最容易犯的错误是只用简单的阈值判断角度小于某个值就加一。这样做的后果很惨用户在最低点微调一下、或者角度在阈值附近来回抖动计数就会连续加好几次。正确的做法是用状态机来管理动作过程。核心思路是把一次完整动作抽象成几个离散状态只有在状态之间完成特定的转换序列才算作一次有效计数。以深蹲为例我用的状态序列是等待状态WAITING初始状态角度在站立区间等待动作开始下降状态DESCENDING角度持续下降跨越半程阈值进入下蹲过程回升状态ASCENDING角度开始回升跨越半程阈值说明正在站起只有在“下降状态”进入“回升状态”这个转换发生时计数加一。这样设计的原因是一次完整的深蹲必然包含“角度先降后升”的过程只在下降沿或只在上升沿加一都会导致重复计数而把“下降上升”作为一个整体单元来统计天然就避免了这个问题。代码层面状态机的骨架大致是这样的enum RepState { waiting, descending, ascending } class RepCounter { RepState _state RepState.waiting; int _count 0; void update(double angle, double threshold, double minAngle) { switch (_state) { case RepState.waiting: if (angle threshold) { _state RepState.descending; } break; case RepState.descending: if (angle minAngle) { _state RepState.ascending; } break; case RepState.ascending: if (angle threshold) { _count; _state RepState.waiting; } break; } } }这个骨架是通用框架深蹲、俯卧撑、卷腹等动作都能套用只要替换对应的角度计算关键点和阈值参数就行。3.2 自适应阈值的取舍与代码落地刚才的代码里有一个关键参数threshold半程阈值。最简单的方案是写死一个值比如深蹲用125度。但实际用下来这个方案问题很大不同用户的蹲姿深度差异很大有人蹲得很低角度能到70度有人只蹲到90度就起来了。写死125度对前者没问题对后者可能在下降过程中就触发了或者在回升时因为姿势变化导致漏计。解决思路是改成自适应阈值记录上一次完整动作的峰值和谷值然后取两者的中值作为下一次动作的触发阈值。这样动作幅度大的用户阈值自动放低动作幅度小的用户阈值自动抬高适配性大大增强。class AdaptiveRepCounter { double _peak 170; // 站立角度初始值 double _valley 80; // 最小角度初始值 double get _threshold (_peak _valley) / 2; int _count 0; RepState _state RepState.waiting; double _currentMin 180; void update(double angle) { // 实时更新峰值 if (angle _peak) _peak angle; // 跟踪本次动作的最小角度 if (angle _currentMin) _currentMin angle; switch (_state) { case RepState.waiting: if (angle _threshold) { _state RepState.descending; _currentMin angle; } break; case RepState.descending: if (angle _threshold) { // 直接回升跨过阈值说明下降不够彻底 if (_currentMin _valley 15) { _valley _currentMin; } _count; _state RepState.waiting; } break; } } }顺着这个逻辑你就能理解为什么状态机里对“最低点到位”的判断那么重要。如果用户只蹲下去一点点就回升了角度根本没跨过有效深度那这次动作不应该被计数。因此需要设置一个minAngle来强制要求动作深度。这个值可以取上次谷值加15到20度的容差区间既要放掉那些半途而废的动作又要防止真正的大幅度动作被漏掉。这里还有一个细节用户动作的节奏快慢会影响判断。如果用户做了个慢蹲快起角度下降沿和上升沿的时间间隔可能很长如果做了快速深蹲间隔又很短。状态机天然能适应这个变化因为它只关心底角度的变化方向不关心速度。但要防止“用户只蹲到一半就站起”这种无效动作必须加上最低有效深度约束。我在实际项目里还把“单个动作最短时长”加了进去——一次动作最少要持续0.3秒小于这个时长的角度变化大概率是抖动或者肢体晃动不是有效动作。这个参数在用户做快速波比跳之类的动作时特别管用。4. 性能优化与真机调参实录4.1 推理线程与UI分离别让模型拖垮界面姿态估计模型在手机上跑最大的问题就是推理耗时会占据主线程导致界面卡顿、帧率骤降。我在项目里的做法是把整条链路分成两条线程UI线程负责摄像头预览、自定义叠加层绘制关键点连线、计数信息展示推理线程负责帧数据转换、模型推理、关键点后处理两条线程之间用异步通道传递数据UI线程只拿推理结果不做任何耗时计算。实测下来这种架构下界面始终保持流畅即使模型单帧推理耗时较长也不会卡住预览画面。具体操作上有几个参数直接影响性能表现配置项高配方案中配方案低配方案输入分辨率1280x720640x480320x240模型复杂度高中低处理帧率30fps20fps15fps适用机型旗舰机型中端机型入门机型我在中端机型上测试时一开始用1280x720输入加高模型复杂度帧率只有12fps角度曲线跳变明显。后来降成640x480加中模型复杂度帧率直接提升到25fps角度数据也稳定了很多。这里的关键认知是姿态估计模型输出的关键点坐标都是相对输入图像的归一化坐标输入分辨率只要满足基本的人体尺寸比例就能保证识别效果。分辨率降一半推理速度能翻倍而精度损失远没有想象中那么严重。4.2 实测帧率与发热控制实测定帧率这件事建议放在功能完成之后立刻做不要拖到最后。我用三条不同档位的测试机型跑过整理出来的参考数据大致是这样的机型档位输入分辨率实时帧率连续运行10分钟后的温度变化旗舰机640x48028-30fps小幅温热中端机640x48020-25fps温热可接受入门机480x36015-18fps明显发热需降频发热控制是个很现实的问题。运动类APP一拍就是十几二十分钟手机发热后系统会主动降频推理速度跟着变慢计数精度就崩了。我的处理方案有三板斧第一启用相机的帧率限制不要无脑跑满帧。运动计数的有效帧率上限其实不需要太高25fps完全够用更高帧率只增加发热不会提升计数精度。第二推理线程采用“丢帧处理”策略如果模型推理跟不上帧到达的速率就自动丢弃待处理队列里的旧帧而不是让它无限堆积。堆积只会增加延迟而且堆积到一定程度后算出来的计数结果对应的已经不是当前画面了。第三考虑到运动场景中用户主要是看画面提示来完成动作不是长期盯着屏幕可以在检测到持续运动中主动降低屏幕亮度或者提示用户锁定屏幕减少屏幕耗电和发热。有一组数据可以直观说明问题不做任何优化时入门机连续跑10分钟后帧率暴跌到8fps左右做了上述三板斧之后同一台机器能稳定在15fps左右温度也从“烫手”变成“温热”。这个差距对计数体验是决定性的。5. 常见问题与排查技巧实录5.1 关键点跳变、角度NaN等典型问题开发过程中踩过的坑我整理成了一张排查速查表遇到了直接对着查现象可能原因处理建议关键点偶尔跳到画面外人体部分被遮挡或置信度低过滤置信度低于0.5的关键点跳变时不更新计数状态角度出现明显异常跳变向量模长接近零或浮点误差越界增加最小模长判断acos入参必须clamp(-1,1)计数重复加一状态机缺少回升判断或在阈值附近抖动改用“下降回升”完整状态序列增加阈值回差带计数不增加最低动作深度约束太严格或用户动作幅度不足放宽minAngle阈值或改用自适应阈值跟踪谷值深蹲时角度算反了用错了关键点索引或角度顶点取错核对髋、膝、踝索引画线调试输出检测到的关键点偏移归一化坐标没有换算成像素坐标绘制叠加层前乘上画面实际宽高正面识别效果差深度信息受二维投影影响关键点在身体前方遮挡引导用户侧对镜头这里有一个非常值得注意的现象关键点置信度过滤。模型会为每个关键点输出一个置信度分数但在运动场景里手指出汗挡镜头、衣服和背景颜色相近、动作幅度过大导致关键点暂时出画这些都会让置信度骤降。硬过滤置信度低于阈值直接不用会导致角度数据断档影响计数连续性我后来用的是软过滤置信度低于阈值时保留上一帧的值不更新角度保持曲线平滑过渡。这个思路用在跳变问题上非常有效。5.2 计数不准的调参方向和调试方法计数不准的调试本质上就是对着角度曲线找异常。我强烈建议在开发阶段保留一个调试模式把实时的关键点连线、角度数值、状态机状态直接绘制到预览画面上同时把角度数据以日志形式输出。这样每出现一次误计或者漏计你都能精确地回放当时发生了什么。具体调参的方向分几种情况如果出现“多计”大概率是阈值回差不够。给状态机增加回差带是解决这个问题的标准手段比如下降沿跨过阈值触发下降状态回升时要超过阈值加上一个偏移量例如15度才算回升完成。这个偏移量把阈值附近的无意义抖动全部滤掉了。如果出现“漏计”优先检查最低动作深度的约束条件。很多用户深蹲时蹲得不够低如果你把最低角度设定在70度他蹲到85度就起来了那永远不会触发计数。这种情况下要么放宽minAngle要么使用自适应阈值去循环更新谷值让系统自己适应用户的实际动作幅度。如果出现“延迟计数”就是用户已经做完动作一秒后才加一这大概率是平滑权重太偏平滑了或者处理帧率太低导致状态转换反应迟钝。把EMA的平滑系数从0.7/0.3调整到0.6/0.4同时检查推理线程是否有帧堆积。如果出现“逻辑错乱”比如没做动作也计数看状态机的初始状态是否被正确复位。用户站在画面里切换动作类型时状态机还保留着上一个动作的状态会直接把当前动作的起始角度当成回升过程导致误计。每次切换动作模式时必须重置状态机。最后分享一个调试心得运动计数的调参过程非常依赖录制回放。如果是实时调试动作重现的随机性太强你根本没法对比参数调整前后的差异。我的做法是先用调试模式录制一段包含多次标准动作的视频然后把这段视频反复回放喂给计数逻辑改一次参数跑一遍回放用同一段数据对比不同参数下的计数结果。这样调参才有依据否则就是瞎试。我实测下来用这个方法把深蹲计数从最初的一做动作就乱跳调到能稳定识别连续20次深蹲大概花了两个晚上。相比之下如果靠实时测试来调参可能要花一到两周而且调完还不能确定是参数生效了还是动作刚好做得更标准了。所以先录视频再调参这条路建议所有做类似项目的人优先选择。这个项目后期还可以继续扩展的方向我目前正在做的是加入动作质量评估模块利用角度曲线的平滑度和最低点深度给每次动作打一个质量分提醒用户动作是否做得足够标准。从单一计数到质量反馈姿态估计类应用的想象空间其实很大但底层的角度计算和状态机逻辑永远是最值得打磨的地基。
延伸阅读

更多相关文章

2026/10/17 18:00:53

Kimi K2 + Claude Code 接入 3 个神级开源 MCP,相当丝滑

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/17 18:00:53

增量学习中的结构增扩方法:从灾难性遗忘到硬隔离

1. 增量学习的核心痛点与结构增扩的定位1.1 灾难性遗忘是绕不开的坎做实际项目的同学都有体会:模型在旧任务上明明已经收敛得挺好,用新数据一微调,之前的准确率就哗哗往下掉。这个现象有个很形象的名字,叫灾难性遗忘(C…

2026/10/17 18:00:53

YOLOv5结合BiFPN:特征融合改造与实战调参指南

做目标检测调过模型的人,对YOLOv5都不会太陌生。但真正决定它在小目标、遮挡场景下表现上限的,往往是Neck那一段特征融合结构,而不是前面Backbone有多深。我最近一直在折腾一个改造方向:把YOLOv5的PANet换掉,接上BiFPN…

2026/10/17 19:10:59

WIN7下查看CPU核心数:cmd、wmic与任务管理器三种方式实测

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/17 19:10:59

影评分类实战:朴素贝叶斯、逻辑回归与SVM模型对比

1. 影评分类任务到底在解决什么问题第一次接触影评分类这个任务时,很多人会把它简单理解成"给一段文字贴个好评或差评的标签"。但真正动手跑过几个模型之后就会发现,这个任务远没有想象中那么直白。影评文本里充满了反讽、隐喻、口语化表达和上…

2026/10/17 19:10:59

Kafka接入AI实战:从架构设计到消息可靠性与排障全解析

先说结论:Kafka接入AI这事,我在生产环境里已经跑了大半年,踩过的坑比写过的代码还多。当初团队决定把Kafka作为AI服务的数据通道时,我一度觉得是杀鸡用牛刀——毕竟我们只是想把日志和用户行为喂给大模型做分析。但实际跑下来才发…

2026/10/17 19:05:59

从工作组到活动目录:域环境搭建与安全攻击面全解析

第七天的笔记主题是“域”。做安全这行,尤其内网方向,域这个字基本决定了你能走多远。一套网络安全视频课程学到第七天,正好卡在从单机渗透转向域环境的关键节点上。如果你只是想打靶机、挖点Web漏洞,域可能暂时用不上&#xff1b…

2026/10/16 5:33:43

Python调用Gemini Structured Outputs实现工单路由门禁

客服工单最怕的不是模型“答错一句话”,而是它给出一段看起来合理的说明,程序却从中猜错优先级。通俗做法是:要求模型只交 JSON(JavaScript Object Notation,轻量数据格式),再让代码验证它。Gem…

2026/10/16 4:03:48

Spring Boot超市进销存系统毕设实战:从需求拆解到答辩通关

最近带的一个学生项目组里,有A同学跑来问我:选什么毕设题目最稳妥,既能让评审老师觉得工作量够,又不会在答辩时被问到语无伦次。我第一反应就是推荐基于Spring Boot的超市仓库管理系统——也就是超市进销存系统。这个题目乍一看平…

2026/10/17 14:20:58

Flutter StatefulWidget 生命周期核心解析

很多刚开始接触 Flutter 的朋友,在看完一堆“Hello World”和基础组件之后,大概率都会撞上同一堵墙:StatefulWidget 里那堆 initState、build、dispose 方法,到底什么时候被调用?为什么顺序是那样?在里面到…

2026/10/17 0:04:32

PlanarSLAM:面向曼哈顿世界的点线面联合SLAM系统

简介:本资源是面向机器人视觉与SLAM研究者的C开源RGB-D SLAM系统实现,专为结构化室内场景(如办公室、走廊)设计,通过融合点、线、平面三类几何特征并引入“曼哈顿世界”假设,显著提升位姿跟踪鲁棒性与稠密地…

2026/10/17 0:04:32

TDH Workflow调度实战:从DAG依赖驱动到任务编排与故障排查

1. 从一个调度脚本说起:TDH Workflow到底在解决什么问题第一次接触TDH的Workflow,是在一个数据清洗项目里。当时团队每天要跑几十个ETL任务,任务之间有严格的先后依赖——A表清洗完才能跑B表的关联,B表跑完才能触发C表的聚合。最开…

还想了解更多?直接咨询顾问

免费诊断 + 免费方案 + 透明报价。

全国咨询热线400-8866-253
免费获取方案
☎咨询二维码 ☎ ↑