拓冰建站拓冰建站
首页 / 资讯中心 / 正文

人体姿态识别与动作评分:基于ML Kit的Android实现

简介一份面向计算机相关专业课程设计与算法实战的人体姿态识别项目包聚焦指定动作的实时评分与分析场景可满足大作业、课程设计、初期项目演示等需求。压缩包共110个文件以Java核心源码、XML配置与Gradle构建脚本为主体同时包含TFLite推理模型、PNG结果可视化、MP4演示录屏、CSV关键点样例等辅助材料整体约48.2MB体积适中便于直接部署。源码模块涵盖相机图像采集、姿态关键点检测、图形叠加绘制与视觉处理基类层次清晰便于二次开发。项目说明文档对运行环境和评分逻辑做了交代代码已经过测试可正常运行既有完整工程骨架也有细节实现可作为课程设计完整方案参考。目前已有169人学习下载适合需要快速搭建人体姿态分析原型的初学者或开发者可直接参考工程结构实现动作捕捉、关键点输出与量化评分。1. 人体姿态识别不是只能画骨架还能给动作打分拿到这份人体姿态识别与动作评分源码时我第一反应是查它和普通姿态检测 Demo 的区别。文件列表里没有庞大的模型权重而是 gradlew、build.gradle、CameraSource.java、PoseGraphic.java、VisionProcessorBase.java 和 fitness_pose_samples.csv这基本说明它是一个可编译、可运行的 Android Gradle 工程姿态识别能力来自 ML Kit动作评分则依赖 CSV 中保存的模板样本。它解决的痛点是识别出“肩膀、手肘、膝盖在哪”之后怎么把坐标变成“这个动作到位了吗、给多少分”。对做课程设计、健身动作比对或康复评估演示的人而言这套资源的价值在于评分逻辑是开放的、可改的不是黑盒。2. 特征工程把人体关键点变成可比较的角度向量姿态识别模型输出的是 33 个人体关键点Android 端 ML Kit 通过PoseLandmark暴露每个点的getPosition()坐标和置信度。如果直接把坐标喂给评分器会遇到一个很尴尬的问题同一套动作人站在 0.5 米处和 2 米处关键点坐标完全不一样但角度几乎不变。所以评分前必须先完成特征转换也就是从“骨架坐标”走向“关节角度向量”。2.1 计算三点夹角关节角度的定义是取相邻三个关键点以中间点为顶点计算夹角。例如左肘角度由左肩、左肘、左腕三点构成。以下是标准的向量夹角计算函数public static double computeAngle(PointF a, PointF b, PointF c) { double abx a.x - b.x; double aby a.y - b.y; double cbx c.x - b.x; double cby c.y - b.y; double dot abx * cbx aby * cby; double abLen Math.sqrt(abx * abx aby * aby); double cbLen Math.sqrt(cbx * cbx cby * cby); double cosAngle dot / (abLen * cbLen); return Math.toDegrees(Math.acos(Math.max(-1.0, Math.min(1.0, cosAngle)))); }这段代码的输入是三个PointF其中b是关节所在点。返回值是 0 到 180 的浮点数Math.max和Math.min是为了防止acos因为浮点误差收到域外的值。注意这里的坐标单位是像素但角度是比例无关的量所以即使相机分辨率变化同一个动作角度值也会保持稳定。2.2 ML Kit 关键点索引对照BlazePose 模型定义了稳定的索引编号写特征提取器时需要把这些编号记牢。下面是在本项目中最常用的一组索引部位说明0鼻头部朝向参考11左肩上臂起点12右肩上臂起点13左肘左臂关节14右肘右臂关节15左腕左臂终点16右腕右臂终点23左髋躯干下缘24右髋躯干下缘25左膝左腿关节26右膝右腿关节27左踝左腿终点28右踝右腿终点拿深蹲举例评分时最看重的是膝角和髋角。左膝角用索引 23左髋、25左膝、27左踝计算右膝角用 24右髋、26右膝、28右踝计算。很多课程设计作业卡在这里原因是拿错了点位把肩当髋用算出来的角度会整体偏移十几度评分自然不准。2.3 构造特征向量与置信度过滤单帧的特征向量就是把若干个关节角度按固定顺序拼接起来。例如下面这个提取器输出一个 8 维向量适配深蹲、弯腰、抬臂这类动作private float[] extractFeatures(Pose pose) { Landmark ls pose.getPoseLandmark(PoseLandmark.LEFT_SHOULDER); Landmark le pose.getPoseLandmark(PoseLandmark.LEFT_ELBOW); Landmark lw pose.getPoseLandmark(PoseLandmark.LEFT_WRIST); Landmark rs pose.getPoseLandmark(PoseLandmark.RIGHT_SHOULDER); Landmark re pose.getPoseLandmark(PoseLandmark.RIGHT_ELBOW); Landmark rw pose.getPoseLandmark(PoseLandmark.RIGHT_WRIST); Landmark lh pose.getPoseLandmark(PoseLandmark.LEFT_HIP); Landmark lk pose.getPoseLandmark(PoseLandmark.LEFT_KNEE); Landmark la pose.getPoseLandmark(PoseLandmark.LEFT_ANKLE); float[] features new float[8]; features[0] (float) computeAngle(ls.getPosition(), le.getPosition(), lw.getPosition()); features[1] (float) computeAngle(rs.getPosition(), re.getPosition(), rw.getPosition()); features[2] (float) computeAngle(lh.getPosition(), lk.getPosition(), la.getPosition()); // 髋角、躯干倾角等其他维度按需求继续扩展 return features; }这里有个容易被忽视的坑getPosition()返回的是归一化坐标值在 0 到 1 之间computeAngle依然成立但如果你想同时用向量模长做尺度特征就需要先还原到像素坐标。本项目只做角度评分直接使用归一化坐标没问题。另外每帧提取前建议检查关键点的置信度if (ls.getInFrameLikelihood() 0.5f) { return null; // 该帧关键点不可信跳过 }getInFrameLikelihood()表示关键点落在画面内且被可靠检测到的概率阈值建议 0.30.6。设太高会导致频繁丢帧设太低会把错误的骨架送入评分器分数上下乱跳。这部分是评分准确性的第一道关卡。3. 动作评分用 fitness_pose_samples.csv 做模板匹配与相似度转换有了特征向量下一步就是回答“这个动作值多少分”。本资源里的 fitness_pose_samples.csv 就是评分算法的模板库。我打开类似项目的 CSV 时最常见到的结构是每行包含动作名称、多个关节角度、一个人工打分的质量分。这样写的好处是换一套标准动作只需要替换 csv不需要改动评分代码。3.1 静态姿势的余弦相似度评分对于单帧静态姿势比如站姿、抬手可以直接计算当前特征向量与模板向量的余弦相似度。Java 实现非常短public static double cosineSimilarity(float[] a, float[] b) { double dot 0, normA 0, normB 0; for (int i 0; i a.length; i) { dot a[i] * b[i]; normA a[i] * a[i]; normB b[i] * b[i]; } return dot / (Math.sqrt(normA) * Math.sqrt(normB)); }两个向量方向越接近相似度越接近 1。因为角度特征的每个分量都是非负的相似度不会出现负值。把相似度映射成 0100 分的公式我一般这样写public static int simToScore(double sim) { return (int) Math.round(sim * 100.0); }这个映射在使用时要注意如果两个向量的模为零人完全不在画面里相似度会变成NaN评分前要过滤掉置信度过低的帧。我在调试数据时发现多数误分为“满分”的帧其实都是人半出画面时的异常向量所以最好先判空。下面是一个简化的PoseSample类和匹配逻辑假设每行模板是“动作名 特征 分数”public static ScoreResult scorePose(float[] current, ListPoseSample templates) { double bestSim -1; PoseSample best null; for (PoseSample t : templates) { double sim cosineSimilarity(current, t.features); if (sim bestSim) { bestSim sim; best t; } } double score (best null) ? 0 : best.manualScore * bestSim; return new ScoreResult(best.actionName, score); }这里用manualScore表示该模板在录制时人工标注的满分基准用它乘以相似度就得到当前动作的评分。注意取最大值而不是加权平均是因为要找到“最接近的标准动作”而不是混合多个动作否则用户做半蹲会被错误地当成深蹲和站立各一半。3.2 时间序列的 DTW 对齐静态姿势评分只适用于“定住不动”的场景。健身动作的评分需要处理连续帧而不同人做同一个动作的快慢差异很大。如果只用单帧和模板比较动作快的人很容易在中途帧拿到低分即使动作轨迹完全正确。DTW动态时间规整是解决这个问题的常用算法它允许两个长度不同的序列按时间轴做非线性对齐。下面是一个适合嵌入本项目的简化版 DTW 距离计算函数public static double dtwDistance(Listfloat[] seqA, Listfloat[] seqB) { int n seqA.size(); int m seqB.size(); double[][] dp new double[n 1][m 1]; for (int i 0; i n; i) { Arrays.fill(dp[i], Double.POSITIVE_INFINITY); } dp[0][0] 0; for (int i 1; i n; i) { for (int j 1; j m; j) { double cost euclidean(seqA.get(i - 1), seqB.get(j - 1)); dp[i][j] cost Math.min(dp[i - 1][j], Math.min(dp[i][j - 1], dp[i - 1][j - 1])); } } return dp[n][m]; }其中euclidean()计算两个特征向量的欧氏距离。该算法的时间复杂度是 O(n×m)对课程设计的帧率一般是 1530 FPS来说序列长度在几十帧时完全可控。如果你想做成实时评分可以固定一个滑动窗口比如最近 32 帧作为历史序列每 5 帧与模板序列跑一次 DTW避免每帧都全量计算。DTW 距离转换成评分我用的经验公式是double score 100.0 * Math.exp(-lambda * dtwDistance / seqA.size());lambda控制衰减速度一般取 0.020.05。数值越大同样的距离会对应更低的分数适合要求严格的动作。这个参数可以直接写在一个ScoreConfig.java里方便课程设计报告里说明参数调节过程。3.3 模板 CSV 的加载与缓存Android 中把 CSV 放在assets目录用AssetManager读取。常见做法是应用启动时一次性加载到内存ListPoseSample samples loadSamples(context.getAssets().open(fitness_pose_samples.csv));加载时按行解析忽略空行和注释行。如果模板数量不大直接用列表遍历即可不用引入数据库。这里要记住CSV 中角度值的记录顺序必须与extractFeatures()中的特征顺序完全一致否则相似度计算就是错位比较。我在调试时踩过这个坑后来在加载处加了一个维度校验维度不匹配时直接抛异常比静默算错分数友好得多。4. 实时摄像头管线CameraSource、PoseGraphic 与 VisionProcessorBase 的协作前面两章解决了“怎么算分”的问题这部分解决“怎么把算分过程接到实时摄像头”上。文件中出现了 CameraSource.java、PoseGraphic.java、VisionProcessorBase.java这是 Google ML Kit 示例工程里常见的一套视觉处理管线。CameraSource 负责摄像头帧的获取与旋转校正VisionProcessorBase 负责把每帧交给检测器处理并同步图形叠加层PoseGraphic 则把检测结果画到屏幕上。4.1 在 VisionProcessorBase 中接入评分处理器VisionProcessorBase 的设计目标是省去开发者对 Camera2/CameraX 生命周期的手动管理。你只需要实现它的生命周期方法并在拿到帧时调用姿态检测和评分逻辑。以下是一个贴近项目结构的实现public class PoseDetectorProcessor extends VisionProcessorBaseListPose { private final PoseDetector poseDetector; private final GestureScorer gestureScorer; public PoseDetectorProcessor(Context context, GestureScorer scorer) { PoseDetectorOptions options new PoseDetectorOptions.Builder() .setDetectorMode(PoseDetectorOptions.STREAM_MODE) .setPerformanceMode(PoseDetectorOptions.PERFORMANCE_MODE_FAST) .build(); this.poseDetector PoseDetection.getClient(options); this.gestureScorer scorer; } Override protected void onFrame(Frame frame) { InputImage image InputImage.fromMediaImage( frame.getMediaImage(), frame.getRotation()); poseDetector.process(image) .addOnSuccessListener(poses - { for (Pose pose : poses) { float[] features FeatureExtractor.extractFeatures(pose); if (features ! null) { int score gestureScorer.score(features); // 通过回调把 score 送到 UI 或音效系统 onScoreUpdate.onScore(score); } } }) .addOnFailureListener(e - Log.e(PoseScore, e.getMessage(), e)); } }注意STREAM_MODE是给连续视频流使用的检测速度更快但精度稍低如果你想在演示时更准确可以改成IMAGE_MODE但帧率会明显下降。在实际设备上720p 分辨率下FAST模式大约能跑 2030 FPS对评分已经足够。4.2 PoseGraphic 绘制关键点与骨架PoseGraphic 的核心是在GraphicOverlay的画布上把关键点和连线画出来。因为 ML Kit 返回的坐标是归一化的绘制前要按画布的实际大小缩放Override public void draw(Canvas canvas) { for (PoseLandmark landmark : pose.getAllPoseLandmarks()) { PointF pos landmark.getPosition(); float x pos.x * canvas.getWidth(); float y pos.y * canvas.getHeight(); canvas.drawCircle(x, y, 8f, landmarkPaint); } // 按关键点索引连接骨架线段 canvas.drawLine(point(11), point(13), linePaint); canvas.drawLine(point(13), point(15), linePaint); }前置摄像头预览时画面会像照镜子一样左右相反所以实际绘制前需要对 x 坐标做一个镜像变换x canvas.getWidth() - x。否则用户抬左手屏幕上显示抬的是右手评分也容易混乱。这一点在课程设计的展示环节经常被忽略。4.3 性能优化与丢帧策略实时管线中影响评分稳定性的不是模型本身而是帧处理阻塞导致的关键点抖动。我一般会在VisionProcessorBase的帧回调里加一个帧率控制private int frameSkip 1; // 每 2 帧处理一次 private int frameCount 0; Override protected void onFrame(Frame frame) { if (frameCount % (frameSkip 1) ! 0) return; // 处理帧 }把frameSkip设为 1 就是隔帧处理对 60 FPS 的输入可以降到 30 FPS 参与计算视觉体验几乎不受影响但 CPU 占用率能明显下降。如果设备发热严重再调到 2。这个参数应放在配置类中方便现场展示时根据手机发热情况调节。此外评分器内部不要持有长期增长的队列。如果采用滑动窗口窗口大小要设置上限比如 32 帧。当窗口满时移除最旧的帧保证内存和计算量稳定。很多初版代码越跑越卡就是因为序列容器不断增长。5. 课程设计验收动作模板标定、评分校准与演示技巧把项目跑通只是第一步课程设计要想拿高分需要让评分结果能复现、能解释。这里给出我在调试此类源码时最常用的三个步骤。5.1 用脚本重新生成动作模板fitness_pose_samples.csv 作为模板库最好由你本人在固定环境下录制而不是直接用网上不匹配的数据。常见做法是写一个离线 Python 脚本从日志中读取特征帧并写入 CSVimport csv rows [] # features 来自 Android 端写入的日志每行为 [action, angle1, angle2, ...] with open(fitness_pose_samples.csv, w, newline) as f: writer csv.writer(f) writer.writerow([action, left_elbow, right_elbow, left_knee, right_knee, manual_score]) writer.writerows(rows)生成后通过 adb push 到应用的数据目录或者直接打进 assets 重新打包。录制时一个动作至少录 10 组包含不同身高的人这样可以增强模板的鲁棒性。5.2 校准 DTW 距离到分数的映射如果测试时发现评分普遍偏高或偏低优先调lambda而不是改特征维度。一组合适的参数是对 32 帧窗口的深蹲序列lambda0.03时标准动作 DTW 距离约为 58评分为 95100明显低于标准的动作距离在 20 以上评分会跌到 60 以下。校准方法是做一个满分动作记下距离 d 对应的分数再做一个明显错误的动作得到另一个距离。然后反推lambda -ln(targetScore/100) * n / d。实际操作中我用一个二分法脚本十分钟就能定下来。5.3 用录制视频替代实拍提高成功率在答辩现场摄像头受光线和背景影响很大。更稳妥的做法是提前录好一段固定动作视频在源码中提供一个“回放模式”读取视频帧并送入VisionProcessorBase相同的处理逻辑。这样评分曲线可复现、可截图。若动作序列在实时场景中因为抖动导致分数不稳可在特征序列上加入一阶低通滤波对更复杂的传感器融合场景才会考虑用扩展卡尔曼滤波EKF联合 IMU 与视觉数据纯视觉课程设计不建议引入 EKF 增加调试成本。最后再提醒一点所有评分阈值和 CSV 模板都应当与报告中的实验数据一一对应。把模板文件替换成你自己的样本评分逻辑不改一行整个系统就可以适配新的动作。本文还有配套的精品资源点击获取
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门