限时福利领取


MediaPipe架构图

最近在开发一个实时手势识别的健身APP时,发现MediaPipe在Android端的实际落地存在不少坑。经过两周的踩坑和优化,终于把推理速度从最初的200ms/帧提升到了68ms/帧,这里把完整经验分享给大家。

一、为什么选择MediaPipe?

在对比了TensorFlow Lite和MLKit后,发现MediaPipe有三个不可替代的优势:

  1. 流水线设计:内置的Graph机制可以组合多个模型(如手势+姿态估计)
  2. 跨平台一致性:相同的模型在iOS/Android/桌面端表现一致
  3. 预构建解决方案:官方提供现成的AR、手势、人脸mesh等方案

但实测在小米10(Android 12)上,MediaPipe的初始加载时间比TF Lite长3-5秒,这是我们需要优化的重点。

二、Gradle集成关键配置

在app/build.gradle里必须添加这些核心配置:

android {
    defaultConfig {
        ndk {
            abiFilters 'armeabi-v7a', 'arm64-v8a' // 必须指定ABI
        }
        externalNativeBuild {
            cmake {
                arguments "-DANDROID_STL=c++_shared"
                cppFlags "-std=c++17"
            }
        }
    }
}

dependencies {
    implementation 'com.google.mediapipe:solution-core:latest.release'
    implementation 'com.google.mediapipe:hands:latest.release'
}

注意:如果不加abiFilters,APK体积会增大30MB+,且可能引发Android 12的安装失败。

三、CameraX与MediaPipe联调

相机流水线

核心代码结构(Kotlin):

class CameraActivity : AppCompatActivity() {
    private lateinit var processor: FrameProcessor

    override fun onCreate(savedInstanceState: Bundle?) {
        // 初始化MediaPipe
        val hands = Hands.create(
            context = this,
            staticImageMode = false,
            maxNumHands = 2,
            runOnGpu = true // 必须启用GPU加速
        )

        processor = hands.toFrameProcessor().apply {
            setOnResultListener { result ->
                // 处理识别结果
                result.multiHandLandmarks()?.let { landmarks ->
                    updateUI(landmarks)
                }
            }
        }

        // 绑定CameraX
        val cameraProviderFuture = ProcessCameraProvider.getInstance(this)
        cameraProviderFuture.addListener({
            val cameraProvider = cameraProviderFuture.get()
            bindPreview(cameraProvider)
        }, ContextCompat.getMainExecutor(this))
    }

    private fun bindPreview(cameraProvider: ProcessCameraProvider) {
        val preview = Preview.Builder().build()
        val cameraSelector = CameraSelector.Builder()
            .requireLensFacing(CameraSelector.LENS_FACING_FRONT)
            .build()

        preview.setSurfaceProvider(binding.previewView.surfaceProvider)

        // 关键:将MediaPipe接入CameraX
        val imageAnalysis = ImageAnalysis.Builder()
            .setTargetResolution(Size(1280, 720))
            .setBackpressureStrategy(STRATEGY_KEEP_ONLY_LATEST)
            .build()
            .also {
                it.setAnalyzer(Executors.newSingleThreadExecutor()) { image ->
                    processor.process(
                        PacketCreator.createRgbaImage(
                            image.planes[0].buffer,
                            image.width,
                            image.height
                        )
                    )
                    image.close() // 必须手动释放
                }
            }

        cameraProvider.bindToLifecycle(
            this, cameraSelector, preview, imageAnalysis
        )
    }
}

四、性能优化实战

通过Android Benchmark测试发现两个瓶颈:

  1. 图像格式转换:YUV转RGB占用12ms
  2. 线程等待:默认单线程处理导致GPU空闲

优化方案:

// 改用RenderScript加速转换(需在build.gradle启用renderscript)
val rs = RenderScript.create(this)
val scriptYuvToRgb = ScriptIntrinsicYuvToRGB.create(rs, Element.U8_4(rs))

imageAnalysis.setAnalyzer(Executors.newFixedThreadPool(4)) { image ->
    val yuvBuffer = image.planes[0].buffer
    val rgbaBuffer = ByteBuffer.allocateDirect(image.width * image.height * 4)

    // RenderScript加速
    val yuvType = Type.Builder(rs, Element.U8(rs))
        .setX(yuvBuffer.remaining()).create()
    val inputAllocation = Allocation.createTyped(rs, yuvType)
    inputAllocation.copyFrom(yuvBuffer)

    val rgbaType = Type.Builder(rs, Element.RGBA_8888(rs))
        .setX(image.width).setY(image.height).create()
    val outputAllocation = Allocation.createTyped(rs, rgbaType)

    scriptYuvToRgb.setInput(inputAllocation)
    scriptYuvToRgb.forEach(outputAllocation)
    outputAllocation.copyTo(rgbaBuffer)

    processor.process(PacketCreator.createRgbaImage(
        rgbaBuffer, image.width, image.height
    ))
    image.close()
}

优化后数据对比(Pixel 6 Pro, Android 13):

| 优化项 | 原耗时 | 优化后 | |--------|--------|--------| | YUV转RGB | 12.4ms | 3.2ms | | 模型推理 | 58.7ms | 42.1ms | | 总延迟 | 82.3ms | 53.6ms |

五、避坑指南

  1. Android 12兼容性:在AndroidManifest.xml添加:

    <uses-permission android:name="android.permission.QUERY_ALL_PACKAGES" />
    <application
        android:extractNativeLibs="true"
        android:requestLegacyExternalStorage="true">
  2. 防止ANR:Graph配置错误会导致主线程阻塞,建议:

  3. 在子线程初始化MediaPipe
  4. 添加10秒超时机制

  5. 内存泄漏:每次处理完Image必须调用image.close()

六、延伸思考

现在我们的模型用的是MediaPipe内置的hand_landmark.tflite,如果要接入自定义模型: 1. 如何修改BUILD文件重新编译.so? 2. 不同模型间的数据流如何串联? 3. 怎样利用MediaPipe的Calculator机制实现业务逻辑?

这些问题留给大家实践,欢迎在评论区交流心得!

Logo

音视频技术社区,一个全球开发者共同探讨、分享、学习音视频技术的平台,加入我们,与全球开发者一起创造更加优秀的音视频产品!

更多推荐