
简介本资源是一套基于人体姿态识别技术实现舞蹈动作自动评分的安卓应用完整开发包面向移动开发初学者、AI算法实践者及教育类App开发者解决舞蹈学习中缺乏实时动作反馈与量化评估的痛点。压缩包共72个文件含9个核心Java算法模块负责姿态估计与动作比对、13个XML布局与配置文件、10个PNG/JPG界面资源、4个.so动态库集成底层推理引擎以及3个.pb模型文件含预训练姿态识别模型整体大小为38.49MB。已有41人下载学习适合希望理解端侧AI落地流程的开发者。读者可直接运行APK体验评分功能通过源码深入学习Android平台下OpenCV/TFLite模型调用、关键点坐标映射、动作相似度计算等关键技术并参考已划分清晰的app/src/main/与recscore/模块结构快速掌握算法与前端协同开发的工程组织方式。 去年年底我女儿报了个街舞班每次练完新动作都要我拿手机录下来再对着视频一帧一帧地抠动作。录了几次我就发现一个痛点孩子想进步但没人告诉她哪一拍手的高度不够、哪一步膝盖没打直。老师一节课就四十五分钟不可能盯到每个细节。我想能不能做一个安卓应用用摄像头实时追踪人体骨架再跟标准舞蹈动作逐帧比对自动打分并指出偏差部位这个念头一直压在心里直到我花了一个多月的时间把人体姿态识别、安卓应用开发和评分算法这条链路整个跑通最后产出了一份完整可运行的源码和APK包。这篇文章就是这份源码从零到一的全过程复盘。我会讲清楚为什么选MediaPipe而不是OpenPose安卓端实时姿态提取链路怎么搭舞蹈评分核心算法怎么设计才能相对公平以及从源代码到APK打包交付这中间踩过的所有坑。如果你正打算做人姿态识别相关的安卓项目或者单纯想给运动训练类应用加一个自动评分功能这篇内容应该能帮你省掉不少弯路。1. 方案选型为什么是MediaPipe而不是OpenPose或MoveNet1.1 移动端姿态识别的现实约束人体姿态识别Human Pose Estimation本身不是新东西学术界做得已经很成熟了但真正落地到安卓手机上问题就变得非常现实模型太大跑不动推理太慢卡画面还有耗电、发热、内存占用等一系列麻烦。我第一次看OpenPose的时候差点劝退自己。OpenPose的模型几百MB起步桌面端GPU跑起来都得考虑一下帧率放进安卓那就是灾难。后来又看了Posenet、MoveNet这些轻量方案但真正决定用哪套方案还需要一套清晰的评估标准。我当时的核心需求主要有四个实时性要在手机上实时跑每帧推理不能超过50毫秒否则用户体验就是幻灯片。关键点精度舞蹈动作评分需要比较准确的关节位置至少能区分手肘弯曲是30度还是60度。集成成本安卓端要有现成的SDK或推理代码不用自己从零写TFLite runtime。开源与兼容性项目要能稳定运行在主流安卓手机上最好有活跃的社区支持。1.2 主流方案横向对比为了搞清楚哪套方案最合适我直接拉了一张表做对比这也是我每次做技术选型都会做的一步——不急着写代码先把候选方案都摆到明面上。方案关键点数量模型体积安卓端集成方式实时性推荐指数OpenPose18/25数百MB无官方SDK需自己转换差不推荐Posenet17约10MBTFLite中一般MoveNet17约5-10MBTFLite中一般MediaPipe Pose33Lite约5MB官方安卓SDK很好强烈推荐从表里能明显看出来MediaPipe Pose在集成便利性和实时性上优势最明显。它是Google开源的跨平台方案底层用TFLite做推理但比直接调TFLite更进一步的是它已经帮你封装好了完整的处理链路——输入图像、检测人体、输出33个关键点甚至自带追踪逻辑能在视频流里保持关键点的时序稳定。很多人容易忽略一个细节人体姿态识别分两套逻辑一个是单帧检测一个是时序追踪。MediaPipe内部把这两者结合得很好既能对单帧图像提取关键点又能在连续视频流中基于上一帧的位置做跟踪这防止了动作快速变化时检测结果跳来跳去。这个特性对舞蹈评分至关重要因为舞蹈动作本身就有大量快速运动。1.3 我最终确定的处理链路选定了MediaPipe之后整体的技术架构也变得清晰了。整个应用的骨架可以拆成五个环节摄像头采集视频帧使用CameraX框架每帧送入MediaPipe Pose Landmarker做姿态估计从33个关键点中提取关节角度特征这部分后面详细说为什么要用角度而不是坐标把用户动作与标准舞蹈模板做动态时间规整DTW对齐基于对齐后的特征相似度计算实时得分和最终评分并在UI上叠加骨架可视化这个分层架构并不是我一开始就设计好的而是开发到中间被迫重构出来的——一开始我把评分逻辑直接写在了姿态识别的回调里结果调一个参数就要重新编译一次。后来才把特征提取、对齐、评分拆成三个独立模块用接口对接。事实证明这个重构非常值得后面调参的时候只需要改评分模块完全不影响底层。2. 安卓端的实时姿态提取链路搭建2.1 CameraX摄像头画面获取的基础设施安卓上做摄像头开发大多数开发者第一反应是Camera2但跟我说实话Camera2的API设计实在是太绕了。每次写会话、配置、回调都像在做脑筋急转弯而且还要自己去适配各种设备上的兼容性问题。所以我直接用了Jetpack的CameraX它是在Camera2之上封装的一套生命周期友好型API。我的需求很简单拿到摄像头画面喂给MediaPipe做推理同时在界面上实时显示画面和骨架覆盖层。CameraX的配置代码分三块// 预览用例 val previewView findViewByIdPreviewView(R.id.preview_view) val preview Preview.Builder() .setTargetResolution(Size(768, 1024)) .build() preview.surfaceProvider previewView.surfaceProvider // 分析用例 val analysis ImageAnalysis.Builder() .setTargetResolution(Size(480, 640)) .setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST) .setOutputImageFormat(ImageAnalysis.OUTPUT_IMAGE_FORMAT_YUV_420_888) .build() analysis.setAnalyzer(executor) { imageProxy - detectPose(imageProxy) } // 绑定到生命周期 cameraProvider.unbindAll() cameraProvider.bindToLifecycle( this, CameraSelector.DEFAULT_BACK_CAMERA, preview, analysis )有一个非常关键的细节我相信很多新手都踩过ImageAnalysis.Analyzer里处理完ImageProxy之后一定要调用imageProxy.close()。我之前就因为漏了这一步导致摄像头进入系统只有开屏画面明明调用了reopen还是黑屏。这其实就是底层图像缓冲区没有被释放垃圾堆积以后摄像头就罢工了。这段代码看似无害但漏掉它会让你排查半天。另外一个细节是输出图像格式。MediaPipe官方推荐输入RGB图像而CameraX默认输出YUV_420_888格式。我在initialize的时候直接用了OUTPUT_IMAGE_FORMAT_YUV_420_888然后在分析回调里把YUV转换成RGBA交给MediaPipe。如果你用的API版本不同可能需要打开convertToBitmap等转换逻辑尽量在里面用复用Bitmap不要每次new否则GC频繁触发会造成卡顿。2.2 MediaPipe Pose Landmarker的核心配置MediaPipe的集成方式经历了几个版本的迭代。老版本用的是PoseLandmarker或PoseLandmark之类的API最近的新SDK0.10.x开始统一为PoseLandmarker模型文件也从palm_detection.tflite这类裸TFLite换成了.task格式。如果你看到网上老教程让你在assets里放一个.tflite文件并手动创建Interpreter那基本是旧方案了新SDK已经不这样玩了。我的项目用了pose_landmarker_lite.task这个模型文件位于assets目录。初始化代码大致是这样val baseOptions BaseOptions.builder() .setModelAssetPath(pose_landmarker_lite.task) .build() val options PoseLandmarkerOptions.builder() .setBaseOptions(baseOptions) .setRunningMode(RunningMode.LIVE_STREAM) .setNumPoses(1) // 舞蹈评分只关注一个人 .setMinPoseDetectionConfidence(0.5f) .setMinPosePresenceConfidence(0.5f) .setMinTrackingConfidence(0.5f) .setResultListener(this::handlePoseResult) // 回调在主线程 .build() poseLandmarker PoseLandmarker.createFromOptions(context, options)这里有几个参数必须解释清楚因为它们直接影响检测效果和性能RunningMode.LIVE_STREAM实时流模式MediaPipe会持续处理输入帧并通过结果回调推送结果。还有VIDEO和IMAGE两种模式分别用于离线视频处理和一帧图像。NumPoses设置为1舞蹈评分只需要关注画面里最显著的一个人。如果设为多人模式性能和准确性都会受到影响。MinPoseDetectionConfidence人体检测置信度的阈值。低于阈值的帧会被判定为没检测到人不返回关键点。这个值设得过高会导致动作剧烈时频繁丢帧设得过低会导致误检。经过反复测试0.5是比较均衡的值。ResultListener回调线程默认在主线程。这个坑我踩得很疼——如果直接在回调里做耗时计算主线程会被卡住导致掉帧。正确做法是把评分计算放在后台线程。我把检测调用写成了一个极简方法fun detectPose(imageProxy: ImageProxy) { val bitmap imageProxy.toBitmap() // 自行实现注意复用 val mpImage BitmapImageBuilder(bitmap).build() poseLandmarker.detectAsync(mpImage, imageProxy.imageInfo.timestamp) bitmap.recycle() imageProxy.close() }注意detectAsync的第二个参数是时间戳这个时间戳会被原样传递给结果回调我用来做时间对齐和滑动窗口统计。2.3 关键点数据与骨架可视化MediaPipe Pose返回的关键点有33个比OpenPose的18个和Posenet的17个都要丰富。33个关键点覆盖了面部轮廓、躯干、左右手臂、左右腿的完整结构这对舞蹈评分来说帮助非常大——特别是手指尖和脚踝这些精细部位很多舞种恰恰是看手脚末端是否到位。回调里拿到的是PoseLandmarkerResult对象里面包含一个landmarks列表其中每个元素是一个ListPoseLandmark。PoseLandmark的关键属性如下x、y归一化的坐标0~1基于图像宽高归一化。z关键点在相机坐标系中的深度值以髋部中心为原点负值表示离相机更近。visibility关键点可见性置信度0~1遮挡严重或出画面时这个值会很低。为了在界面上画骨架覆盖层我自定义了一个SkeletonView继承自View把关键点坐标映射到View的实际尺寸上再画点和线。override fun onDraw(canvas: Canvas) { super.onDraw(canvas) if (landmarks null) return val width this.width.toFloat() val height this.height.toFloat() // 画关键点 for ((index, lm) in landmarks.withIndex()) { val px lm.x * width val py lm.y * height canvas.drawCircle(px, py, 8f, pointPaint) // 超过置信度阈值才画否则画半透明点 if (lm.visibility 0.5f) { pointPaint.alpha 80 } else { pointPaint.alpha 255 } } // 画骨架连接线 for (connection in POSE_CONNECTIONS) { val start landmarks[connection.first] val end landmarks[connection.second] canvas.drawLine( start.x * width, start.y * height, end.x * width, end.y * height, linePaint ) } }骨架连接线的连接关系我直接参考了MediaPipe官方给出的POSE_CONNECTIONS列表比如左肩11连左肘13左肘13连左手腕15左髋23连左膝25等等。这个列表是固定的不需要自己发明直接在代码里定义成数组即可。这里要特别提醒一个可视化的问题MediaPipe返回的关键点坐标是图像坐标系也就是摄像头采集到的原始画面的坐标系。而你在界面上看到的预览画面由于摄像头传感器方向、镜像模式、ScaleType等影响并不一定跟图像坐标系一一对应。如果直接把关键点坐标画在Canvas上会出现骨架和画面里的人物错位的问题。这个坑我见过太多人踩后面专门开一节讲。3. 舞蹈评分的核心算法从关键点到分数3.1 为什么不能直接用坐标算距离这是整个项目里最容易被低估、也最坑人的一个环节。不少做动作识别的人第一反应是提取关键点坐标然后跟标准动作的关键点坐标做欧氏距离或者余弦相似度。你说完全没有道理吧也能跑出一点分数来但稍微想深一层就会发现这个思路有一个致命缺陷两个人身高不一样站的距离摄像头远近不一样体型也不一样。同一个右手抬起90度的动作在高个子身上和矮个子身上关键点坐标的差异可能非常大但角度几乎是一样的。坐标直接比较等于把人体的绝对尺寸和位置都当成了评判依据这显然不公平。所以核心思路必须转移到几何角度上去。角度是尺度无关、位移无关、旋转相关的特征。比如评判手臂是否举平就看肩关节、肘关节、腕关节三点构成的角度是否接近180度。评判深蹲是否到位就看髋关节、膝关节、踝关节之间的膝关节角度是否小于某个阈值。用角度做特征身高不同、离摄像头远近不同的两个人也能公平比较。有了这个认知之后整个评分算法的设计方向就对了。3.2 姿态特征向量的构造从33个点到12个角度从33个关键点里我最终选取了12个关键角度作为姿态特征向量。为什么是12个不是33个或者更多因为舞蹈动作评价从实操角度来看关注的是大关节的运动趋势而不是每个骨骼点的绝对位置。角度太少表现力不足角度太多又容易过拟合。我的12个角度选取如下左肘角度左肩-左肘-左手腕右肘角度右肩-右肘-右手腕左肩侧平举角度左髋-左肩-左肘右肩侧平举角度右髋-右肩-右肘左髋前屈角度右肩-左髋-左膝右髋前屈角度左肩-右髋-右膝左膝角度左髋-左膝-左踝右膝角度右髋-右膝-右踝躯干前倾角度左髋-左肩-竖直方向分量颈部倾斜角度左耳-左肩-左髋左手腕高度角度左肩-左手腕与水平面夹角右手腕高度角度右肩-右手腕与水平面夹角角度计算的函数其实很基础就是向量点积求余弦再转反余弦fun calculateAngle( first: PoseLandmark, mid: PoseLandmark, last: PoseLandmark ): Float { // 从mid出发到first和last的两个向量 val v1x first.x - mid.x val v1y first.y - mid.y val v2x last.x - mid.x val v2y last.y - mid.y val dotProduct v1x * v2x v1y * v2y val normV1 sqrt(v1x * v1x v1y * v1y) val normV2 sqrt(v2x * v2x v2y * v2y) if (normV1 0f || normV2 0f) return 0f val cosAngle (dotProduct / (normV1 * normV2)).coerceIn(-1f, 1f) return Math.toDegrees(acos(cosAngle.toDouble())).toFloat() }摄像头是二维平面的所以我们先只用x、y计算角度。z轴深度信息我也采集了但在评分时只是辅助不直接参与角度计算因为z值受相机距离和人体朝向影响太大误差高。关键点的筛选是另一个要点。实时流模式下MediaPipe已经帮我跟踪了人体但仍然存在某些帧的关键点置信度极低的情况比如手抬出画面、转身时被身体挡住之类的。我设置了visibility 0.5的关键点不参与角度计算这个角度直接标记为无效评分时跳过这一项而不是记0分。如果一帧里有效角度小于8个这一帧就整帧丢弃不参与评分。3.3 DTW对齐解决跳得快的人和跳得慢的人公平打分特征向量有了接下来就是怎么跟标准动作序列比较。最简单的思路是逐帧算出角度差异用户第1帧对应标准模板第1帧用户第2帧对应标准模板第2帧以此类推。但舞蹈不是广播体操节奏的细微变化太常见了——同样一个八拍动作有的人第一拍提前0.2秒有的人最后收尾慢了0.3秒。逐帧硬对齐的结果是动作明明跳得没错但分数被时间错位拉得很低。解决这个问题的经典方法是动态时间规整DTWDynamic Time Warping。它允许两个时间序列在时间轴上做非线性对齐用户的某一帧可以对应标准模板的前一帧、当前帧或后一帧只要路径代价最小。你可以把它想成一条橡皮筋标准动作和用户动作各自代表一串珠子DTW找一种方式把两串珠子拉伸或压缩到长度一致并且使得对应珠子之间的差异总和最小。DTW的核心代码不复杂但写的时候有几个优化点要注意。下面是我精简过的一个实现fun dtwDistance( user: ListFloatArray, template: ListFloatArray ): Double { val n user.size val m template.size // DP表避免O(n*m)空间不足的问题 val dtw Array(2) { DoubleArray(m 1) { Double.MAX_VALUE } } dtw[0][0] 0.0 for (i in 1..n) { dtw[i % 2][0] Double.MAX_VALUE for (j in 1..m) { val cost euclideanDistance(user[i - 1], template[j - 1]) dtw[i % 2][j] cost minOf( dtw[(i - 1) % 2][j], // 删除/插入 dtw[i % 2][j - 1], // 插入/删除 dtw[(i - 1) % 2][j - 1] // 匹配 ) } } return dtw[n % 2][m] / (n m) // 归一化 }空间优化这里用了滚动数组把O(n×m)的空间压缩成O(m)在手机上跑几十秒的舞蹈序列基本没有内存问题。时间归一化也很重要。如果标准动作15秒、用户动作17秒DTW距离比较大直接比较不公平。我在算完原始DTW距离后除以了(n m)得到平均每帧的匹配代价再映射到分数。但DTW也并非万能它有一个前提条件标准序列和用户序列的内容大致是同构的只是时间拉伸不同。如果用户跳的动作跟标准动作完全不符那段视频DTW还是会找出一条路径强行对齐只是距离值会非常大这本身就可以作为评分很低的一个信号。所以我在设计扣分项时把这个也能用上。3.4 评分体系实时得分、最终得分与部位反馈DTW距离算出的是一个相似度的反指标。在公布分数之前还需要把距离映射到用户容易理解的百分制。经过实测我发现不同动作段的DTW平均距离和主观体验大致对应如下DTW平均距离主观评价映射分数 0.15非常标准90 ~ 1000.15 ~ 0.3动作基本到位75 ~ 900.3 ~ 0.5有明显偏差60 ~ 75 0.5动作不符或没跟上 60所以映射函数我用了分段线性映射fun mapDistanceToScore(distance: Double): Int { return when { distance 0.15 - (100 - (distance / 0.15) * 10).toInt() distance 0.3 - (90 - ((distance - 0.15) / 0.15) * 15).toInt() distance 0.5 - (75 - ((distance - 0.3) / 0.2) * 15).toInt() else - (60 - ((distance - 0.5) / 0.5) * 40).coerceAtLeast(0) } }最终的评分数值是取整段动作所有帧的加权平均而不是简单平均。我做了权重设计因为舞蹈里有些拍子是重心转移的关键帧有些拍子是过渡帧。评分反馈做了分部位输出。在计算得分的同时我会单独累计手臂部分左右肘、左右肩相关角度的平均偏差、腿部部分左右膝、髋的平均偏差和躯干部分的平均偏差。最终展示分数的时候同时显示三行小字提示手臂到位度略低腿部弯曲幅度偏小等。这个提示虽然一句话但对用户调整动作特别有帮助因为它把抽象的得分变成了可执行的修正方向。4. 实机调试中的坑与优化手段4.1 镜像翻转问题前置摄像头的坐标陷阱我在模拟器上测试的时候一切都正常骨架和人物完美重叠评分也很稳定。但一放到真机上做前置摄像头测试整个系统就乱了明明用户举的是右手系统却判定左手高度不够画面里骨架方向跟真人完全相反。查了半天最后发现是镜像没处理。前置摄像头获取的画面Android系统会默认做一次水平翻转所以从PreviewView里看到的画面是镜像效果——就像你照镜子一样。但MediaPipe分析的是原始图像帧的像素返回的关键点坐标是基于未翻转的原始图像。这两个坐标系不一致导致骨架位置和实际预览画面左右颠倒。解决方案是在SkeletonView绘制的时候对方位做一个镜像转换。因为CameraX已经帮我把预览画面做了镜像处理我只需要在画骨架时把x坐标也做对应的镜像即可val mirroredX width - lm.x * width val py lm.y * height canvas.drawCircle(mirroredX, py, 8f, pointPaint)更稳妥的做法是判断摄像头方向前置摄像头做镜像后置摄像头不处理。我当初图省事直接全部镜像结果后置摄像头测试时又反了。4.2 置信度过滤与骨架抖动姿态识别模型在单帧上的输出不是绝对稳定的。尤其是快速舞动时同一个关键点相邻两帧之间可能会有一两个像素的跳动。这个抖动单独看影响不大但累积到角度计算上可能造成得分小幅波动让用户感觉分数飘忽不定。我做了双保险第一道保险是置信度过滤也就是前面提到的visibility 0.5的关键点不参与角度计算。比如当用户手臂挥舞到身侧肩膀关键点被身体挡住时肩关节的visibility会明显下降这时候如果硬算角度就会引入巨大噪声。第二道保险是平滑。我用了指数移动平均EMA对每个角度特征做了平滑处理val smoothedAngle alpha * currentAngle (1 - alpha) * previousSmoothedAnglealpha取0.3左右让新数据占三成权重过去平滑值占七成。这样角度序列会变得很顺滑又不会滞后到让评分反应迟钝。这里有一个平衡平滑系数太大反应慢得分会有明显惯性平滑系数太小噪声大得分会抖。我倾向于alpha0.3到0.4之间具体数值可以做成一个可调参数方便在真机上根据体验微调。4.3 性能实测与帧率优化我拿两台设备做了测试一台是骁龙870的旗舰一台是骁龙680的千元机。结果如下设备推理分辨率单帧推理耗时实际能稳定帧率骁龙870480x640约25ms25fps左右骁龙680480x640约60ms15fps左右骁龙680320x240约35ms20fps左右对于舞蹈评分场景15fps完全够用因为人跳舞的动作再快关节角度的变化频率也很少超过10Hz。所以我最终把ImageAnalysis的目标分辨率固定在480x640客户端层面主动降帧到20fps。还有几个能明显提升性能的点都是我在调试过程中验证过的复用位图对象不要把每一帧的YUV转成新Bitmap那个分配开销和GC压力非常大。我改成了一开始就创建可复用的Bitmap转换时只更新像素。避免主线程耗时操作评分计算全部放到单线程Executor里MediaPipe回调在workers线程里处理。这一点我调了很久才明白mediapipe有个回调线程参数setResultListener默认是主线程那是指结果通知不代表推理本身在UI线程。你需要在自己的代码里把分线程逻辑跟UI更新剥离开。控制UI刷新频率骨架View不是每帧都要重绘我限制了它最多每100ms刷新一次这样在低端机上也能保住基本流畅度。5. 从源码到APK打包过程中的关键细节5.1 ABI配置与APK体积控制MediaPipe的底层是C计算库通过JNI和OpenGL ES做GPU加速所以不同CPU架构需要对应编译好的so库。如果不做任何配置生成的APK会把所有ABI全都打包进去体积直接爆炸。我在build.gradle里做了如下限制android { defaultConfig { ndk { abiFilters listOf(arm64-v8a, armeabi-v7a) } } }arm64-v8a覆盖了99%以上的现代安卓手机armeabi-v7a照顾老设备。x86和x86_64只在模拟器上用到真机不需要。加了过滤器之后APK体积从大约85MB直接降到52MB左右。MediaPipe的模型文件默认放在assets目录不会被打进so库所以也要注意assets文件本身的大小。pose_landmarker_lite.task约为5MB这个是必经体积但我用shrinkResources压缩资源后整体体积还能再瘦一些。5.2 ProGuard混淆规则与so库保护开启混淆时我遇到了一个比较典型的坑MediaPipe的JNI接口在混淆后被调不到了运行时报UnsatisfiedLinkError或找不到Native方法。常规破法是在ProGuard规则里把mediapipe相关的类全部keep住-keep class com.google.mediapipe.** { *; } -keep class com.google.protobuf.** { *; }MediaPipe官方其实提供了一个consumer规则文件但不同版本差异较大我直接手动加了上述规则最稳妥。自定义的PoseLandmarker相关封装类也需要keep住因为JNI层要通过类名反射回调结果。还有一点assets里的.task模型文件不会被混淆但如果你想防止别人直接解包APK拿到模型再做二次分发可以自己再包一层加密运行时解密到应用私有目录再加载。不过说实话这个防不住刻意的逆向更多是提高门槛我觉得对个人项目来说不必过度投入。5.3 系统版本适配与权限变更打包完APK在Android 12上运行正常但在Android 13上崩溃了一次排查发现是通知权限的锅。Android 13引入了POST_NOTIFICATIONS运行时权限如果应用要在评分结束时弹出通知或打分结果页必须先申请这个权限。另外如果你的应用要读取视频文件或保存评分记录到公共存储目录Android 11API 30之后还需要适配分区存储。我在项目里选择把评分记录保存到应用私有目录getExternalFilesDir这样既绕开了分区存储的权限问题又不影响用户主动访问文件。权限清单最终只有以下三项CAMERA摄像头取流POST_NOTIFICATIONSAndroid 13开始需要运行时申请INTERNET预留后续做排行榜或云同步本次未实际使用targetSdkVersion我最终设为了33Android 13compileSdkVersion用34。targetSdkVersion设得太低虽然能暂时躲过某些权限限制但在新系统上会收到系统警告而且明年新版本应用市场大概率也会强制要求提高target版本。6. 我从这个项目里学到的三个判断整个项目复盘下来我觉得最值得沉淀的并不是某一串代码而是三个判断层面的东西。第一个判断是做技术选型时能在手机上跑和能跑得很好是两码事。OpenPose理论精度更高但在真实手机上推理效率太差直接用体验翻车。而MediaPipe虽然在学术指标上不是最强的却是安卓端实时性、准确性和集成成本三者平衡得最好的方案。做工程选型要选的是在具体场景下综合最优的方案不是学术指标最高的方案。第二个判断是评分类应用的公平性远比想象中重要。用户如果觉得自己努力跳了却因为身高、站位等原因得分比别别人低他会立刻失去对应用的信任。所以角度特征、DTW对齐、关键点置信度过滤这些细节全部都在为公平服务。说白了评分系统的价值不是把数字算出来而是让用户相信这个数字是合理的。第三个判断是这个应用的扩展空间其实很大。底层姿态识别和特征提取是通用的舞蹈可以切换成健身动作评分、太极姿势评分DTW对齐和评分模块稍加改动还能直接迁移到体育训练的姿态纠正场景。如果你有类似的想法完全可以基于这套架构把评分标准换成你的业务规则不需要重新搭建整个底层链路。最后再分享一个小技巧开发过程中我用MediaPipe离线处理了一小段标准舞蹈视频把提取出来的关键点序列保存成了JSON文件作为评分的标准模板。这个做法成本极低不需要额外动捕设备就能拿到一套相对稳定的参考数据。以后你有条件了也可以在动捕棚里采集更高质量的标准动作数据效果肯定会更好。本文还有配套的精品资源点击获取