ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Android实时人体检测Demo:MediaPipe姿态估计与骨骼绘制实战

Android实时人体检测Demo:MediaPipe姿态估计与骨骼绘制实战 简介这是一份可直接运行的Android人体检测APP Demo面向希望在移动端集成行人检测功能的Android开发者解决实时人体识别需求。Demo基于主流目标检测算法能在手机摄像头画面中实时框出人体位置适用于安防监控、智能零售、人流量统计等场景也适合作为学习与二次开发的基础。资源包共2个文件包含一个可安装的APK安装包和一个JSON配置文件压缩包整体大小约50.6MBAPK可直接安装运行体验JSON文件可用于配置模型路径、检测阈值等参数方便开发者按需调整。目前已有2908人学习下载良好的学习量体现了该应用的实用性和参考价值。结合配套技术博客开发者可进一步了解Android端人体检测的完整实现思路包括摄像头画面预览、目标检测框实时绘制、模型加载与推理线程管理、APK打包等关键环节从而快速将人体感知能力集成到自己的应用中。 我在电脑上跑人体检测的时候随手就是一个几个GB的权重、1080p的输入帧率照样漂亮。可当我想把同样的事搬到Android手机上第一版崩溃得非常彻底模型塞进去安装包直接胖了三倍手机上推理一帧要几百毫秒画面跟幻灯片似的。后来我才想明白手机上做人体检测真正的难点从来不是“检测”本身而是“实时”这两个字。这篇文章从一个可以直接跑到真机上的人体检测Demo出发把端侧实时姿态识别这条链路完整过一遍先讲为什么选MediaPipe而不是YOLO然后是Android Studio环境配置、CameraX取流、MediaPipe推理、关键点骨架绘制最后是实测有效的性能优化手段。整个方案不依赖云服务器摄像头画面全部在本地处理拿到代码就能跑。适合准备做运动健身、体感交互或者刚接触端侧AI想找个完整项目参考的朋友。1. 先解决选型问题为什么最终选了MediaPipe而不是YOLO1.1 “人体检测”的两种实现范式在正式写代码之前必须先想清楚“人体检测”这四个字到底要做什么。我在做第一版的时候走了不少弯路一开始下意识就上了YOLO系列结果模型文件十几MB在低端机上跑一帧要80ms以上还没算NMS后处理的时间手机上做实时基本没戏。实际在移动端做人体检测业界基本分成两条路线一条是目标检测范式输出人体的bounding box告诉你“人在这里”另一条是姿态估计范式输出33个骨骼关键点不但告诉你在哪还地图出四肢的姿态。标题里的“人体检测APP Demo”如果想做出演示效果强烈建议用姿态估计范式因为实时画骨架的画面感很强同时我们也可以从关键点坐标反推出人体矩形框等于一份输出拿到两种结果。1.2 BlazePose为什么快两阶段核心思路MediaPipe用的Pose Landmarker背后是BlazePose模型它的设计思路很适合手机端。它在检测人体时不是一上来就跑全图而是先用一个轻量级的检测网络在整帧里确定人体ROI区域然后再用姿态回归网络在ROI内精确预测33个关键点。这种两阶段思路的好处是第一个阶段负责找到“人大概在哪”第二个阶段只需要处理比全图小得多的区域计算量大幅下降。BlazePose在关键点预测上用了热图和回归混合的方式先用热图给出关键点的粗略位置再用回归网络做精细对齐所以既保证了定位精度又控制了模型体量。它的lite版本只有几MB适合实时视频流full/ heavy版本精度更高但推理时间也成倍增加。实时Demo项目直接选lite就够了这是最稳妥的选择。1.3 SDK集成 vs 自己写TFLite推理技术路线上还有一层选择。一种是直接集成MediaPipe Tasks SDK官方封装好了模型加载、预处理、推理和结果回调开发量很小另一种是下载.tflite模型文件自己写Interpreter推理代码手动处理输入Tensor和输出Tensor。我把两条路线的对比列出来对比维度MediaPipe Tasks SDK手动TFLite推理开发量小几十行代码大需要自己处理Tensor模型更换支持.task格式换模型要重新导出自由任何tflite都能跑预处理内置旋转、缩放自己写YUV转RGB、缩放实时性能官方针对性优化取决于自己写的好坏上手难度低高如果你之后想跑自己训练的人体检测模型TFLite路线更灵活但如果你只是想快速验证“手机实时人体检测”这件事我个人建议先走SDK路线把链路跑通后再考虑替换成自己的模型。这个Demo我最终选了SDK路线后面的所有步骤都基于MediaPipe Tasks Vision库展开。2. Android Studio环境与依赖配置第一道坎2.1 一套可以直接跑通Demo的版本清单很多人在这一步就把时间耗掉了实际99%的构建失败都和版本不匹配有关。我这里给出当时实测过的一套稳定组合照抄即可Android Studio用Hedgehog2023.1.1或更新的版本AGP用8.2以上Gradle用8.2以上JDK直接用Android Studio自带的JDK 17不用另外装。compileSdk和targetSdk设为34minSdk至少21因为MediaPipe Tasks要求Android 5.0以上。两个新手常见的事先说在前面其一Android Studio第一次启动会下载很多SDK组件如果卡在下载界面很久是网络原因可以配置国内的镜像源这个在网上一搜就有很多教程其二界面想换成中文的话在Settings的Plugins里搜索“Chinese (Simplified) Language Pack”语言包安装后重启就是中文界面对不熟悉英文菜单的朋友来说体验好很多。人体检测Demo涉及摄像头预览模拟器基本没法好好跑必须准备一台真机这个后面会讲。2.2 引入tasks-vision与模型文件的坑在模块的build.gradle.kts里加上dependencies { implementation(com.google.mediapipe:tasks-vision:0.10.14) implementation(androidx.camera:camera-core:1.3.4) implementation(androidx.camera:camera-camera2:1.3.4) implementation(androidx.camera:camera-lifecycle:1.3.4) implementation(androidx.camera:camera-view:1.3.4) implementation(org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3) }这里有个非常容易踩的坑.task模型文件如果放在assets目录Gradle打包时默认会压缩资源文件导致MediaPipe在运行时无法直接读取。必须在android{}块里关闭对task文件的压缩android { aaptOptions { noCompress task } }我第一版跑起来的时候报了一堆找不到模型的错排查了半天才发现是这个原因。模型文件本身也很容易找去MediaPipe官方模型库里搜pose_landmarker_lite.task下载后放到app/src/main/assets目录即可。2.3 真机权限与manifest配置Demo用到了摄像头manifest里需要声明权限uses-permission android:nameandroid.permission.CAMERA /Android 6.0以上还需要运行时申请权限我用的是简单粗暴的写法进入页面时检查checkSelfPermission没授权就requestPermissions。另外Android 12后targetSdk大于31时manifest里所有带intent-filter的组件必须显式声明android:exported属性否则直接编译报错。这是一个纯构建期问题但排查起来很容易让人一头雾水写在这里提醒一下。3. CameraX取流到模型输入把画面喂给AI3.1 预览和分析用例的绑定策略CameraX是我在Android上做视频流处理的首选它把相机设备管理、生命周期绑定和旋转处理都封装好了。Demo里需要同时绑定PreviewUseCase和ImageAnalysisUseCasePreview负责把画面显示在屏幕上ImageAnalysis负责把每一帧图像交给模型做推理。实际代码大致是val cameraProviderFuture ProcessCameraProvider.getInstance(this) cameraProviderFuture.addListener({ val cameraProvider cameraProviderFuture.get() val preview Preview.Builder().build().also { it.setSurfaceProvider(binding.previewView.surfaceProvider) } val analysis ImageAnalysis.Builder() .setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST) .setTargetResolution(Size(640, 480)) .build() analysis.setAnalyzer(executor) { imageProxy - runHumanDetection(imageProxy) } cameraProvider.bindToLifecycle(this, CameraSelector.DEFAULT_BACK_CAMERA, preview, analysis) }, ContextCompat.getMainExecutor(this))这里的setBackpressureStrategy(STRATEGY_KEEP_ONLY_LATEST)极其关键。它表示当上一帧还没处理完时新帧来了直接覆盖掉旧帧保证分析器拿到的永远是最新的一帧。这样做的结果是如果模型推理慢画面帧率会下降但不会出现帧堆积导致的内存爆炸和延迟越来越大的问题。3.2 YUV_420_888转Bitmap顺序别搞反ImageAnalysis返回的ImageProxy里的图像格式是YUV_420_888而MediaPipe的BitmapImageBuilder接收的是Bitmap所以需要自己做一次格式转换。我用的方法是把YUV数据转成NV21再通过YuvImage压缩成JPEG最后解码成Bitmapfun ImageProxy.toBitmap(): Bitmap { val yBuffer planes[0].buffer val uBuffer planes[1].buffer val vBuffer planes[2].buffer val ySize yBuffer.remaining() val uSize uBuffer.remaining() val vSize vBuffer.remaining() val nv21 ByteArray(ySize uSize vSize) // Y数据 yBuffer.get(nv21, 0, ySize) // 注意这里V在前U在后NV21格式是Y VU交错 vBuffer.get(nv21, ySize, vSize) uBuffer.get(nv21, ySize vSize, uSize) val yuvImage YuvImage(nv21, ImageFormat.NV21, width, height, null) val out ByteArrayOutputStream() yuvImage.compressToJpeg(Rect(0, 0, width, height), 90, out) val imageBytes out.toByteArray() return BitmapFactory.decodeByteArray(imageBytes, 0, imageBytes.size) }注意NV21的排列是Y分量后跟VU交替不是UV。很多从YUV_420_888直接搬代码的人容易把U和V的顺序搞反结果画面出现严重的偏色和变形检测结果自然也是乱的。一旦发现图像偏绿偏紫先检查这里。3.3 创建PoseLandmarker并跑起实时检测PoseLandmarker实例创建一次后要一直复用不要在每帧里new否则性能会崩掉。创建方式和实时流模式绑定的代码val baseOptions BaseOptions.builder() .setModelAssetPath(pose_landmarker_lite.task) .setDelegate(BaseOptions.Delegate.GPU) .build() val options PoseLandmarker.PoseLandmarkerOptions.builder() .setBaseOptions(baseOptions) .setRunningMode(PoseLandmarker.RunningMode.LIVE_STREAM) .setMinPoseDetectionConfidence(0.5f) .setMinTrackingConfidence(0.5f) .setResultListener { result, _ - result ?: returnsetResultListener runOnUiThread { overlayView.setLandmarks(result.landmarks()) overlayView.invalidate() } } .setErrorListener { error - Log.e(PoseLandmarker, detect error, error) } .build() poseLandmarker PoseLandmarker.createFromOptions(this, options)在LIVE_STREAM模式下调用detectAsync时除了传入图像还必须传入时间戳单位是毫秒val bitmap imageProxy.toBitmap() val rotation imageProxy.imageInfo.rotationDegrees val mpImage BitmapImageBuilder(bitmap, rotation).build() poseLandmarker.detectAsync(mpImage, SystemClock.uptimeMillis()) imageProxy.close()这个时间戳是用来做多帧跟踪关联的如果一直传固定值会出现跟踪断掉、关键点抖动的问题。图像旋转方向也在这里面顺手处理了竖屏手机取到的原始帧是传感器方向通过rotationDegrees告诉MediaPipe即可否则模型看到的是一张横着的画面。4. 输出解析与骨架绘制让检测结果可见4.1 33个关键点到底长什么样PoseLandmarker返回的PoseLandmarkerResult里包含ListList 每个列表对应画面中的一个人。NormalizedLandmark里的坐标是归一化的0~1浮点数表示相对图像宽高的比例。33个关键点中常用的是0鼻子、5左肩、6右肩、7左肘、8右肘、9左腕、10右腕、11左胯、12右胯、13左膝、14右膝、15左踝、16右踝。再加上17~32对应面部、手指等更细的点位。4.2 Overlay绘制与坐标变换在自定义View的onDraw里把归一化坐标乘以View的实际宽高override fun onDraw(canvas: Canvas) { super.onDraw(canvas) val landmarks currentLandmarks ?: return for (landmark in landmarks) { for (connection in POSE_CONNECTIONS) { val start landmark[connection.first] val end landmark[connection.second] canvas.drawLine( start.x() * width, start.y() * height, end.x() * width, end.y() * height, linePaint ) } for (lm in landmark) { canvas.drawCircle(lm.x() * width, lm.y() * height, 6f, pointPaint) } } }POSE_CONNECTIONS是骨架的连线关系比如0到1、5到7、5到11这样的索引对网上和官方仓库都有现成的常量。如果只是想画个框可以用所有关键点的最小和最大x、y坐标生成RectF再在画布上画出来。我把每个关键点画成绿色圆点连线用亮蓝色这样在黑色背景上对比很强烈演示效果非常醒目。4.3 裁切导致的错位问题一个容易被忽略的细节坐标映射这里藏着一个大坑我差点被它整崩溃PreviewView的对齐方式默认是FILL_CENTER也就是说预览画面会根据屏幕尺寸做裁切保证画面填满屏幕但不变形。而ImageAnalysis拿到的帧是没有经过裁切的完整帧。于是在模型检测出的坐标正常映射到Overlay上后会发现骨架和画面里的人始终存在偏移。解决思路有两种一种是把PreviewView的scaleType改成FIT_CENTER让预览完整显示不裁切这时Overlay和预览的宽高比一致另一种是计算出PreviewView显示区域的rect把检测坐标裁剪到可视范围内再映射。最简单可控的还是第一种把预览改成完整展示哪怕上下会出现黑边也能保证检测结果对齐。这个细节不处理好Demo演示给别人的时候就会显得很“业余”。5. 性能调优从“能跑”到“实时流畅”5.1 先量化瓶颈在哪拿到第一版能跑起来的Demo后先别急着优化先把每一段的耗时量化出来。我在关键路径上记了三段时间YUV转Bitmap的耗时、detectAsync的耗时、UI绘制耗时。实际看下来YUV转Bitmap在480p下大约要10~20msdetectAsync在GPU delegate下大约要20~40ms绘制只有几毫秒。所以主要瓶颈都在图像转换和模型推理上尤其是模型推理直接决定了能不能达到“实时”。5.2 四个立竿见影的优化手段第一个是降低ImageAnalysis的分辨率从1280x720降到640x480推理时间能下降20%以上而且对关键点精度的影响非常小。第二个是跳帧设置一个计数器隔一帧再处理一次比如只处理偶数帧这样图像转换和UI回调的压力直接减半。第三个是复用Bitmap和每次close ImageProxy不要每次分配新对象减少GC对帧率的影响。第四个是GPU delegate优先个别机型不兼容时回退到CPU。跳帧代码很简单private var frameSkip 0 override fun analyze(imageProxy: ImageProxy) { frameSkip if (frameSkip % 2 ! 0) { imageProxy.close() return } // 继续处理 }5.3 实测数据参考我自己在不同设备上测试过一组数据关闭跳帧、开启GPU delegate、输入分辨率480x640结果大致如下设备芯片推理耗时(ms)画面帧率(fps)中高端机A骁龙87025~3525~30中端机B天玑81035~5015~20低端机C骁龙66260~9010~15低端机上如果再把分辨率降到320x240并开启跳帧帧率就能稳定在20fps以上。注意GPU delegate下设置numThreads是无效的只有在CPU delegate下才需要手动设置线程数通常设置4个线程。另外如果发现某些设备启动后结果一直不回调优先怀疑GPU兼容性问题把delegate切到CPU再试。6. 打包发布与继续延伸的方向6.1 release签名与防混淆配置Demo做得差不多了要安装到别人手机上演示最好打一个release包。第一步用keytool生成签名文件keytool -genkeypair -v -keystore demo-release.keystore -alias demo -keyalg RSA -keysize 2048 -validity 10000然后在build.gradle.kts里配置signingConfigs正常填上storeFile、storePassword、keyAlias、keyPassword就行。这里要特别提醒开启minifyEnabled做代码混淆时MediaPipe的native库很容易被误删或重命名导致运行时UnsatisfiedLinkError。在proguard-rules.pro里加上-keep class com.google.mediapipe.** { *; } -keepattributes *Annotation*我第一版打release包后闪退抓日志发现就是native方法找不到加上keep规则后问题立刻消失。6.2 从关键点坐标到业务场景到了这一步你已经拥有了每帧画面里每个人的33个关键点坐标能做的事情一下就多起来了。最典型的扩展是动作计数比如深蹲可以通过左胯、左膝、左踝三点构成的角度变化来判断是否完成一次下蹲肩部和胯部的相对位置可以判断是否站直。类似地跌倒检测可以根据关键点中心点的瞬时速度和躯干倾斜角度来判断比单纯看加速度计更直观。如果想做虚拟形象驱动把这个坐标映射到3D模型关节上就能做一个AR试穿或者数字人Demo。我个人在实际开发中的体会是端侧视觉项目的前期成本主要不在算法而在“从摄像头到模型再到绘制”这条基础链路上。这篇Demo把最折腾的这部分都拆开讲清楚了后面你想加什么场景本质上就是针对关键点坐标写业务判断逻辑而已。本文还有配套的精品资源点击获取
返回列表