简介:这套基于Java的人体姿态识别与动作评分系统,面向计算机、人工智能、电子信息等专业学生及开发者,可用于课程设计、毕业设计或姿态分析相关课题的二次开发。系统可实时捕捉画面中人体关键点,在双侧肩、肘、髋、膝八个关节生成角度数据,并集成姿态评估、实时语音提示与训练后多维分析,为运动康复、体态矫正提供量化支持。资源共132个文件,压缩包约48.54MB,主要包含Java源码、Android工程配置、TensorFlow Lite模型、演示视频、图片与文档等,源码结构与构建脚本齐全,便于直接导入和按模块扩展。已有53人浏览学习。项目代码经过验证,各模块功能可用,使用者可参照说明文档理解技术细节,也可基于现有架构增加新的姿态评分策略或交互功能,适合作为教学实践和功能扩展的起点。
1. 基于Java的人体姿态识别与动作评分系统:Java侧跑通整条链路
没有接触《基于Java的人体姿态识别与动作评分系统实现》这份源码包之前,我一直觉得姿态识别是Python团队的活,Java后端只能调接口。直到一个健身App项目丢过来:用户上传一段深蹲视频,服务端要在三秒内返回动作得分和膝关节角度反馈,还要求私有化部署、不开Python服务。拆完这份代码才看清楚,Java侧完全能独立跑完整链路。它用DJL加载MoveNet模型做单目摄像头姿态估计,OpenCV负责取帧和预处理,推理结果解析成17个骨骼关键点之后,再通过关节角度、余弦相似度、DTW动态时间弯曲三个算法合并成最终评分,对外用Spring Boot接口输出。适合想把AI视觉能力接进现有Java后端的人,做体育考试自动评分、健身动作纠正、康复训练反馈或者Java方向毕设,都能直接复用这套骨架。对做Java后端的人来说,这套东西比面试八股文里那些理论题实在多了。
2. 技术选型:为什么是Java + DJL + MoveNet,而不是给Python打工
2.1 Java生态做姿态识别的三条路线
先说选型理由,不然很多人一上来就纠结“Java到底能不能做姿态识别”。答案是可以,但有讲究。Java生态里落地姿态识别常见三条路:JNI包C++推理引擎、JavaCV调OpenCV、DJL接入PyTorch/TensorFlow模型。
第一条路JNI包C++,性能确实最强,但需要本地编译OpenCV、OpenPose等依赖,Windows和Linux环境各编一遍,团队里没人会C++基本是灾难。第二条路JavaCV是OpenCV的Java封装,图像处理、视频流读取很方便,但OpenCV自带的DNN模块对MoveNet这类新模型的算子兼容性不完整,做个边缘检测没问题,做姿态识别就差口气。第三条路DJL(Deep Java Library)接入PyTorch引擎,这是我现在默认的选型。DJL是AWS开源的Java深度学习框架,能用Java代码直接加载PyTorch导出的模型,底层native库由官方托管,不需要本地编译,Spring Boot内嵌的Java容器顺手就把服务跑起来了。
三者对比如下:
| 路线 | 性能 | 部署成本 | 模型生态 | 坑位 |
|---|---|---|---|---|
| JNI + C++ | 高 | 极高 | 依赖C++轮子 | 编译依赖地狱 |
| JavaCV + OpenCV DNN | 中 | 中 | 算子兼容性差 | 模型导入失败率高 |
| DJL + PyTorch | 中高 | 低 | 覆盖Torch/ONNX/TF | 注意native库版本 |
我最后选了DJL。整套系统就是Spring Boot + MyBatis + DJL + OpenCV的组合:Spring Boot管REST接口和定时任务调度,MyBatis落评分记录,DJL只干模型推理一件事,OpenCV管视频解码和图像缩放,各层职责分得很清爽。MoveNet是Google 2021年开源的轻量级单目姿态估计模型,输入普通RGB视频帧,输出17个COCO骨骼关键点坐标加置信度,192x192输入在普通i5 CPU上单帧推理约15毫秒,正好压住“三秒出评分”的需求。
2.2 评分不是模型直接给的:关键点之后的第二套算法
很多人有个误区,以为姿态模型能直接输出“深蹲80分”。实际上MoveNet这类姿态估计模型只输出“人身上17个点的位置在哪”,它不知道动作好不好,从骨骼点变成评分是另外一套算法在做的事。这套系统把评分拆成了两段。
第一段是特征提取:把每一帧的17个关键点转换成关节角度。比如左肘关节由关键点5(左肩)、7(左肘)、9(左腕)三点构成,深蹲主要看膝关节角度(11左髋、13左膝、15左踝)和髋关节角度(5左肩、11左髋、13左膝)。用角度而不是原始坐标,直接好处是不同身高、臂长的人站在不同距离,角度特征天然尺度无关,不用先做身高标定。第二段是模板比对:先录一段标准动作,提取角度时间序列作为模板;用户做同一动作,提取相同特征序列;两条序列比对余弦相似度和DTW距离,映射成0到100分。
这个设计还有个额外收益:换动作类型不需要重新训练模型,只要录一段新模板、调一组关节权重,代码一行不用动。常见误用是试图让模型直接学习“什么是对的动作”,既缺训练数据又缺标签,模型在现实环境里泛化一塌糊涂;模板比对是在固定尺度下做相对判断,稳定得多。
2.3 系统模块划分与完整数据流
整个系统按数据流拆成七个模块,边界非常清晰:
| 模块 | 职责 | 关键依赖 |
|---|---|---|
| 视频输入模块 | 上传/本地读取、逐帧解码 | OpenCV VideoCapture |
| 帧预处理模块 | 缩放、颜色通道转换、归一化 | OpenCV Imgproc |
| 模型推理模块 | 加载MoveNet、执行推理 | DJL Predictor |
| 关键点后处理 | 解析54维输出、逆归一化 | 自研 |
| 特征提取模块 | 计算关节角度序列 | 自研 |
| 评分引擎 | 余弦相似度 + DTW | 自研 |
| 结果落库 | 评分记录写入MySQL | MyBatis |
完整链路是:上传视频后,OpenCV逐帧解码,每帧缩放成192x192的RGB张量并归一化到[-1,1],喂给DJL加载的MoveNet模型;模型输出[1,6,54]的张量,后处理模块从里面解析出置信度最高的那个人,还原17个关键点坐标;再按动作类型计算关节角度时间序列;评分引擎拿这条序列跟模板序列做DTW对齐和余弦相似度加权,产出总分和分项反馈;最终MyBatis写入评分表,前端能查到每一次的分数和角度曲线。如果要做批量视频打分,还能挂一个Spring的@Scheduled定时任务框架,放在凌晨统一处理。
这条链路里最难的不是模型加载,而是后处理和评分对齐。模型输出的[1,6,54]张量格式、y和x的存储顺序、坐标逆归一化,任何一个细节错了,后面的评分全是错的。第3章和第4章把这两块展开讲。
3. 关键点提取实战:把MoveNet模型部署进Java服务并解析骨骼点
3.1 用DJL加载模型并完成单帧推理
先看模型加载这块。整个系统里模型只加载一次,放在Spring的@Configuration里作为单例Bean,避免每次请求都重新load一次模型。模型加载是个重量级操作,加载一次要几百毫秒,放Bean里能省掉绝大多数重复开销:
@Configuration public class MoveNetConfig { @Bean public ZooModel<NDList, NDList> movenetModel() throws ModelException, IOException { Criteria<NDList, NDList> criteria = Criteria.builder() .optApplication(Application.CV.POSE_ESTIMATION) .optModelPath(Paths.get("models/movenet_thunder.pt")) .optEngine("PyTorch") .optProgress(new ProgressBar()) .build(); return criteria.loadModel(); } }逻辑说明:Criteria是DJL的模型加载描述器,optModelPath指向模型文件路径,optEngine指定推理引擎,loadModel执行实际加载。模型文件用的是PyTorch格式的MoveNet Thunder权重,Thunder版本在精度和速度之间比较均衡,192x192输入单帧推理大约15ms。参数说明:模型路径不要写死ClassPath相对路径,部署时用配置文件里的绝对路径覆盖,因为打成Jar包后模型文件放在resources里虽然能读,但解压到临时目录再加载容易踩一堆权限坑。
提示:模型加载务必做成单例,别在每个请求里loadModel,否则并发一上来内存和耗时都会翻车。
推理部分的代码如下:
public List<KeyPoint> infer(float[] inputBuffer, int width, int height) { try (NDManager manager = NDManager.newBaseManager()) { NDArray array = manager.create(inputBuffer, new Shape(1, 3, 192, 192)); NDList input = new NDList(array); NDList output = predictor.predict(input); NDArray result = output.get(0); return parseKeyPoints(result, width, height); } }逻辑说明:NDManager是DJL管理张量生命周期的核心类,用try-with-resources确保底层显存和内存被及时释放,这是Java侧最容易忽略的资源泄漏点。NDList是DJL的统一输入输出容器,predictor.predict返回的NDList里get(0)就是MoveNet的输出张量。参数说明:inputBuffer是192x192x3的RGB float数组,归一化到[-1,1],shape必须是[1,3,192,192]而不是[1,192,192,3],PyTorch模型默认通道在前(CHW),顺序错了输出结果会完全乱掉。
这里有个隐藏难点是inputBuffer怎么来。用OpenCV做完缩放后,Mat的像素数据是BGR顺序,必须转RGB再手动归一化。Java基础里float的数据类型在这里直接决定了精度,用double反而更慢:
Mat resized = new Mat(); Imgproc.resize(frame, resized, new Size(192, 192)); Imgproc.cvtColor(resized, resized, Imgproc.COLOR_BGR2RGB); float[] buffer = new float[3 * 192 * 192]; int idx = 0; for (int c = 0; c < 3; c++) { for (int i = 0; i < 192; i++) { for (int j = 0; j < 192; j++) { double[] pixel = resized.get(i, j); buffer[idx++] = (float) ((pixel[c] / 127.5) - 1.0); } } }逻辑说明:三层循环按通道优先顺序填充buffer,归一化公式是像素值除以127.5再减1,把[0,255]映射到[-1,1],这是MoveNet训练时的标准预处理。参数说明:Mat.get(i,j)拿到的是double[3]数组,代表该像素的R、G、B三个通道,c是通道下标。这段代码在调试时没问题,性能优化时建议用resized.get(0, 0, byteData)一次性拿到全部字节,再批量转float,能省掉大量单像素调用开销——第5章避坑部分还会细说。
3.2 从[1,6,54]输出张量里解析17个骨骼关键点
MoveNet的输出不是直接的17个点,而是6个候选人的检测头,每个人是一个54维向量。54 = 17个关键点乘以3(y, x, score),再加上3个整框的y、x、score。实际应用里取置信度最高的那个候选框,解析逻辑如下:
private List<KeyPoint> parseKeyPoints(NDArray result, int width, int height) { float[] data = result.toFloatArray(); int bestIdx = 0; float bestScore = -1f; for (int k = 0; k < 6; k++) { float boxScore = data[k * 54 + 53]; if (boxScore > bestScore) { bestScore = boxScore; bestIdx = k; } } float[] person = Arrays.copyOfRange(data, bestIdx * 54, bestIdx * 54 + 54); List<KeyPoint> points = new ArrayList<>(17); for (int i = 0; i < 17; i++) { float y = person[i * 3]; float x = person[i * 3 + 1]; float score = person[i * 3 + 2]; points.add(new KeyPoint(i, x * width, y * height, score)); } return points; }逻辑说明:先遍历6个候选人的整框score,找到置信度最高的那个索引,再从这个索引的起始位置截取54个元素。person数组前51个元素是17个关键点,最后3个是整框的y、x、score,所以整框score的下标是k*54+53。参数说明:x和y都是相对192x192输入图的归一化坐标,乘上原始帧的宽高才能回到原图坐标系。这里最坑的是MoveNet把每个关键点的y放在x前面,跟常规的(x, y)习惯相反,不处理的话整个骨架会在垂直方向错位。
解析出来的KeyPoint对象我会顺手封装成不可变类,只暴露getter和构造器,后面评分模块大量使用这个对象,不可变能让并发场景省很多心。
3.3 推理前后两次归一化:最容易翻车的坐标变换
3.1节说了输入归一化,这只是第一步。输出侧还有个容易翻车的点:坐标系方向。MoveNet的y方向是图像从上往下增大,跟OpenCV一致,但如果你用摄像头采集,很多摄像头默认是镜面成像,此时关键点的左右会互换。建议在预处理时统一做一次水平翻转,或者干脆关闭摄像头镜像,测试时用一张正脸照片验证左右手是否对得上。
另一个细节是帧率。视频解码帧率参差不齐,评分前需要按时间戳采样,把任意长度的帧序列线性插值成固定长度,我习惯用30帧。插值公式就是相邻两帧关键点的线性插值,代码很简单但作用很大,它让后续DTW和余弦相似度不用面对长度不一致的序列。这一步放在评分模块之前做,属于预处理管线的一部分,不要在评分算法里临时处理,不然每个算法都要重复处理一遍长度问题。
4. 动作评分核心算法:关节角度、余弦相似度与DTW的Java实现
4.1 关节角度计算:评分的最小特征单元
评分特征的核心是关节角度。以深蹲为例,观察膝关节角度就够了:动作开始时膝关节接近伸直,角度约170度,下蹲到最低点时降到110到120度,起身时又回到170度以上。整条角度曲线就是动作的签名。
Java实现用余弦定理,输入是三个关键点:
public double angleOf(KeyPoint a, KeyPoint b, KeyPoint c) { double ab = distance(a, b); double bc = distance(b, c); double ac = distance(a, c); double cos = (ab * ab + bc * bc - ac * ac) / (2 * ab * bc); cos = Math.max(-1.0, Math.min(1.0, cos)); return Math.toDegrees(Math.acos(cos)); } private double distance(KeyPoint a, KeyPoint b) { double dx = a.x() - b.x(); double dy = a.y() - b.y(); return Math.sqrt(dx * dx + dy * dy); }逻辑说明:a是关节上方的点,b是关节中心点,c是关节下方的点。以左膝关节为例,传入顺序是关键点11(左髋)、13(左膝)、15(左踝),顺序反了算出来的角度就是补角,评分直接失真。余弦定理在三条边长度已知时计算夹角,核心优势是只用距离,不涉及向量方向,代码简洁。参数说明:cos值理论上在[-1,1],但浮点除法可能产生1.0000002这样的越界值,Math.acos对超出定义域的输入直接返回NaN,所以先做一次裁剪,这个细节能救命。
实际使用中,我会把17个关键点一次性转成该动作需要的角度数组。比如深蹲关注左膝、右膝、左髋、右髋、左踝、右踝六个角度,每一帧就得到一个6维特征向量。整帧特征从34个坐标值压缩成6个角度值,数据量小且抗抖动,后续评分速度也快。
4.2 模板比对:余弦相似度与关节加权
有了角度序列,评分的第一步是跟标准模板做余弦相似度。先定义动作模板的数据结构:
public class ActionTemplate { private String actionName; // squat / pushup / lunge private List<Integer> jointIndex; // 关注哪些关节角度 private double[] jointWeights; // 每个关节角度的权重 private double[][] angleSeries; // 模板角度序列:帧数 x 角度数 }逻辑说明:jointIndex记录这个动作关注哪些关节角度,和特征提取模块对应。jointWeights表示每个角度在总分里的重要性,深蹲时膝关节角度权重最高。angleSeries是标准动作的角度序列,录制时对标准动作跑一遍特征提取就能得到。
相似度计算用余弦相似度实现:
public double cosineSimilarity(double[] template, double[] actual) { double dot = 0, normT = 0, normA = 0; for (int i = 0; i < template.length; i++) { dot += template[i] * actual[i]; normT += template[i] * template[i]; normA += actual[i] * actual[i]; } if (normT == 0 || normA == 0) { return 0; } return dot / (Math.sqrt(normT) * Math.sqrt(normA) + 1e-6); }逻辑说明:两个角度序列看成两个高维向量,余弦值越接近1,方向越一致,也就是动作形态越像;余弦值接近0说明两个序列几乎不相关;负值说明方向完全背离,基本可以判断动作做反了。参数说明:分母加上1e-6是为了防止除零,虽然前面已经判断了norm为0的情况,但加上这行能让数值更稳定。实际评分时我不会只算一个相似度,而是按关节权重加权:
double totalWeight = 0; double weightedScore = 0; for (int j = 0; j < jointCount; j++) { double sim = cosineSimilarity(templateColumn(j), actualColumn(j)); weightedScore += sim * jointWeights[j]; totalWeight += jointWeights[j]; } double finalScore = weightedScore / totalWeight * 100;参数说明:jointWeights数组的值由动作类型决定,深蹲我给膝关节0.6、髋关节0.3、踝关节0.1,引体向上则是肩关节0.5、肘关节0.5。权重不同,评分灵敏度会差很多,这个是调参重点,第6章再细讲。
4.3 不同节奏动作的救星:动态时间弯曲DTW
余弦相似度有个前提:两条序列长度必须一样。但同一个深蹲,有人2秒做完,有人4秒做完,直接逐帧对齐会把快慢差异放大成评分差异,这是没必要的惩罚。解法是DTW(Dynamic Time Warping),它允许两条序列在时间轴上做局部伸缩再比对。
public double dtwDistance(double[][] templateSeries, double[][] actualSeries) { int n = templateSeries.length; int m = actualSeries.length; double[][] dp = new double[n + 1][m + 1]; for (double[] row : dp) { Arrays.fill(row, Double.POSITIVE_INFINITY); } dp[0][0] = 0; for (int i = 1; i <= n; i++) { for (int j = 1; j <= m; j++) { double cost = euclidean(templateSeries[i - 1], actualSeries[j - 1]); double minPrev = Math.min(dp[i - 1][j - 1], Math.min(dp[i - 1][j], dp[i][j - 1])); dp[i][j] = cost + minPrev; } } return dp[n][m] / Math.max(n, m); } private double euclidean(double[] a, double[] b) { double sum = 0; for (int i = 0; i < a.length; i++) { double d = a[i] - b[i]; sum += d * d; } return Math.sqrt(sum); }逻辑说明:dp[i][j]表示模板前i帧与实际前j帧之间的最小累积距离。三种转移方向对应三种时间对齐方式:dp[i-1][j-1]表示两帧同时前进,dp[i-1][j]表示模板帧停顿一帧,dp[i][j-1]表示实际帧停顿一帧。以对角转移为主,所以同节奏动作得分最高,快慢差异大的动作通过停顿帧消除时间偏差,这就解决了“同样做对的动作为什么得分忽高忽低”的经典问题。参数说明:最后除以max(n, m)是平均每帧距离,不然动作越长累积值越大,长短动作没法比。二重循环的时间复杂度是O(n*m),n和m都在几十帧量级,实际跑起来微秒级,不用担心性能。
评分最终映射统一用这个公式:
double score = Math.max(0, 100 - dtwDistance * penaltyFactor);penaltyFactor是惩罚系数,默认25。理想情况下模板和实测完全一致,DTW距离为0,得分100;每偏离一个单位扣一分,扣完即止。这个映射关系简单直观,也方便调参,第6章的调参实验会用到。
5. 避坑与参数调优:Java侧部署姿态识别系统的五个真实问题
5.1 现象:DJL加载模型报错Engine not found
第一次跑项目,控制台直接抛异常,大概意思是找不到PyTorch Engine,或者提示“No Deep Learning Engine, please add djl.pytorch”。
原因:DJL的engine是按artifact分开打包的,只引入djl核心包不引入PyTorch引擎包,运行时自然找不到native库。而且PyTorch的native分CPU和CUDA两个版本,服务器没有GPU时如果引入了CUDA版本,加载阶段会输出一堆跟CUDA相关的异常日志,看起来很吓人。
解决:在pom.xml里显式引入PyTorch引擎依赖,并让DJL按当前环境自动选择native版本:
<dependency> <groupId>ai.djl.pytorch</groupId> <artifactId>pytorch-native-auto</artifactId> </dependency>逻辑说明:pytorch-native-auto会根据运行平台的系统属性和是否支持CUDA,自动downloading对应的native库,常规部署环境够用。如果公司服务器内网隔离、不能访问外网,就得预先下载好对应平台的native jar包,手动加入本地仓库或lib目录。
5.2 现象:骨骼点在画面上左右颠倒,或者x/y轴错位
把关键点画回原图调试时,发现左右手反了,或者整个骨架旋转了90度。这个坑我踩过两次,一次是BGR通道问题,一次是y/x顺序问题。
原因:OpenCV读出来的视频帧默认是BGR顺序,直接喂给按RGB训练的MoveNet模型,颜色通道错位会让模型的特征提取完全乱掉;摄像头采集的原始画面是镜像的,左右互换;还有一个是我自己写解析时把person[i*3]当成了x,实际上那是y。
解决:推理前统一在FramePreprocessor里做三件事:cvtColor转RGB、必要的水平翻转、通道顺序校验。解析时严格按“第0维y、第1维x、第2维score”的顺序读,测试时用一张左右手特征明显的照片验证:举起左手,画面里必须是左肩、左肘、左腕这一条链的坐标值偏右。把这三件事都封装在一个类里,测一次后面不再碰。
5.3 现象:同一个标准动作,不同身高的人评分差异很大
同一个深蹲模板,身高180的人和身高160的人各自做标准动作,打分一个85一个67,明显不合理。
原因:虽然特征用的是关节角度,但关键点坐标受检测抖动影响,身高矮的人骨架整体尺寸小,同样的像素级抖动换算成角度误差更大。另外,不同人骨骼比例不同,关节角度的绝对范围也不完全一致。
解决:对关键点做滑动平均滤波,窗口设5帧,能压掉大部分抖动噪声;同时在关节权重里降低小关节的权重,重点关注髋、膝、肩三个大关节。如果要求更严格,可以在特征里加入骨长比辅助向量,等于把骨架做一次尺度归一化,让不同体格的人站在同一套度量体系里评分。
5.4 现象:视频处理速度极慢,CPU直接被打满,一帧要几百毫秒
模型推理本身只要15ms,但整个流程跑下来一帧要400ms,怎么优化都上不去。
原因:问题出在Mat转float数组那一步。我最早是用双层循环遍历像素,逐个调用Mat.get(i, j)拿像素值,192x192x3有11万次调用,每次都分配一个double[3]数组,Java侧对象分配和JNI回调开销直接拖垮性能。
解决:改成Mat.get(0, 0, byteData)一次性把整块像素数据取到byte[],再在Java侧用位运算把无符号byte转成float,最后调用NDArray.fromArray批量创建张量。优化后单帧总耗时从400ms降到60ms左右,CPU占用也降了大半。这里的原则是减少JNI边界调用次数,能用批量接口绝不用单像素接口。
5.5 现象:评分对动作节奏太敏感,同样做对的动作为什么忽高忽低
同一个视频跑三次评分,分数从65到90乱跳;换一个人做同样动作,分数又不一样了。表面看是评分算法不稳定,实际上是时间对齐根本没做。
原因:直接用原始帧序列做余弦相似度,没做长度归一化。2秒做完的深蹲和4秒做完的深蹲,帧数不同、相位不同,逐帧比对把快慢差异当成动作差异惩罚了,分数自然乱跳。
解决:所有动作评分管线强制走“长度归一化 + 插值 + DTW”三段式预处理。先把原始帧序列线性插值到固定30帧,再用DTW对齐模板序列和实测序列,最后才进评分器。从那以后我在任何项目里都不再直接拿原始帧序列算相似度,这个教训很深刻。
6. 验证与进阶:用自定义动作模板把评分调到靠谱
6.1 端到端验证:上传视频拿到评分
源码包自带三个测试视频和一套深蹲模板。工程是标准Maven结构,JDK 11加Maven 3.6以上环境变量配好就能跑。先启动服务:
mvn spring-boot:run -Dspring-boot.run.profiles=dev评分接口是POST /api/score/action,参数是actionName和video文件,返回JSON里包含总分、各关节角度均值和建议文案。先用自带视频验证链路通不通,再换自己的视频测泛化:
curl -X POST http://localhost:8080/api/score/action \ -F "video=@test/squat_standard.mp4" \ -F "actionName=squat"预期返回score大于85,同一个视频连跑三次标准差应该在3分以内。波动太大先别急着调算法,把关键点可视化画到视频帧上,看是检测抖动还是角度序列本身没对齐。可视化这一步能排除掉一半的“算法玄学”问题。
6.2 调参实验:权重、帧数与DTW惩罚系数
评分质量主要由三组参数决定,默认值在Resource配置文件里,改完重启即可生效:
| 参数 | 默认值 | 调整建议 |
|---|---|---|
| jointWeights | 膝0.6 髋0.3 踝0.1 | 分数拉不开时,先加大主要关节权重 |
| frameCount | 30帧 | 动作越复杂越大,引体向上建议40 |
| dtwPenalty | 25 | 高分太松就调到30,标准动作拿不到80就降到20 |
调参验证方法分两步:先跑标准模板视频三次,确认评分稳定;再跑一个明知做错的视频,得分必须低于70。如果正确动作和错误动作分数拉不开,先调关节权重、再动惩罚系数,一般两个来回就能找到合适的组合。整套系统调到这里,基本就能上一个真实的健身或者体育考试场景了。
我自己以前在另一个项目里就是没做帧率归一化,直接拿原始序列算相似度,结果同一个标准视频跑三次分数从65到90乱跳,被甲方当众演示翻车。从那以后我每次做动作评分都强制走一遍“长度归一化 + 插值 + DTW”的预处理管线,并且先跑重复性测试再谈准确性。希望帮到你。
本文还有配套的精品资源,点击获取