关键词:Android, Jetpack Compose, CameraX, TFLite, Object Detection

大家好,我是飞哥!👋

拒绝云端依赖!3MB 模型跑在手机上,TFLite 转换保姆级教程我们成功把 YOLOv8 模型“压缩”成了 tflite 格式。今天,我们要把它装进 Android 手机,开发一个酷炫的实时物体检测 App

想象一下,打开摄像头,对着家里的猫、杯子、电脑,手机屏幕上立马画出框框,并显示“Cat 99%”——是不是很有科技感?😎


1. 为什么要用 Jetpack Compose + CameraX?(Why)

锚定已知 ⚓️

以前做 Android 相机预览,要写 SurfaceView, TextureView,还要处理各种旋转、拉伸问题,代码又臭又长。
就像用古老的胶卷相机,拍张照片要调半天光圈快门。

生动类比 📷

Jetpack Compose + CameraX 就是 Android 界的“傻瓜数码相机”!

  • Compose:就像 Vue/React,你告诉它“我要一个相机预览窗口”,它就给你画出来。不用管底层的 XML 布局。
  • CameraX:Google 帮你把底层的脏活累活(设备兼容性、生命周期)全干了。你只需要关注两件事:“给我画面” (Preview)“给我数据” (ImageAnalysis)

提炼骨架 🦴

核心数据流如下:
摄像头 (CameraX) ➡️ 每一帧图片 (ImageAnalysis) ➡️ AI 大脑 (TFLite) ➡️ 结果坐标 ➡️ 绘制 (Compose Canvas)


2. 核心代码拆解 (How)

第一步:添加依赖与权限 (build.gradle.kts & Manifest) 📦

  1. app/build.gradle.kts (Kotlin DSL) 里加上这些:
dependencies {
    // 1. CameraX (相机库)
    val camerax_version = "1.3.0"
    implementation("androidx.camera:camera-core:$camerax_version")
    implementation("androidx.camera:camera-camera2:$camerax_version")
    implementation("androidx.camera:camera-lifecycle:$camerax_version")
    implementation("androidx.camera:camera-view:$camerax_version")

    // 2. TensorFlow Lite (AI 推理库)
    implementation("org.tensorflow:tensorflow-lite:2.14.0")
    implementation("org.tensorflow:tensorflow-lite-support:0.4.4")
}
  1. AndroidManifest.xml 中申请相机权限:
<uses-feature android:name="android.hardware.camera.any" />
<uses-permission android:name="android.permission.CAMERA" />

⚠️ 注意:Android 6.0 以上需要动态申请权限!在 Compose 中可以使用 Accompanist 库或者 ActivityrequestPermissions 来搞定。如果不申请,App 会直接闪退。

第二步:核心分析器 (Analyzer) 🧠

这是最关键的一步。我们需要自定义一个 ImageAnalysis.Analyzer,它会不断收到摄像头的图片帧。

为了简化繁琐的图片处理(YUV转RGB、缩放、归一化),强烈推荐使用 TFLite Support Library

class ObjectDetectorAnalyzer(
    private val context: Context,
    private val onResult: (List<DetectionResult>, Int, Int) -> Unit // 回调:结果, 宽, 高
) : ImageAnalysis.Analyzer {

    // 1. 加载模型
    private val interpreter: Interpreter by lazy {
        val model = FileUtil.loadMappedFile(context, "yolov8n.tflite")
        val options = Interpreter.Options()
        Interpreter(model, options)
    }

    // 图像预处理:调整大小 + 归一化
    private val imageProcessor = ImageProcessor.Builder()
        .add(ResizeOp(640, 640, ResizeOp.ResizeMethod.BILINEAR))
        .add(NormalizeOp(0f, 255f))
        .build()

    @androidx.annotation.OptIn(ExperimentalGetImage::class)
    override fun analyze(imageProxy: ImageProxy) {
        val mediaImage = imageProxy.image
        if (mediaImage != null) {
            // 2. 转换为 Bitmap
            val bitmap = imageProxy.toBitmap()

            // 3. 核心步骤:裁剪为正方形 (Center Crop) ✂️
            // 必须裁剪!因为 YOLO 模型输入是正方形,而我们为了 UI 显示不拉伸,
            // 也在 UI 上强制显示为正方形。这里必须保持一致。
            val size = minOf(bitmap.width, bitmap.height)
            val left = (bitmap.width - size) / 2
            val top = (bitmap.height - size) / 2
            val squareBitmap = Bitmap.createBitmap(bitmap, left, top, size, size)

            // 4. 预处理 (TensorImage)
            var tensorImage = TensorImage.fromBitmap(squareBitmap)
            tensorImage = imageProcessor.process(tensorImage)

            // 5. 准备输出容器 [1, 84, 8400]
            val outputBuffer = TensorBuffer.createFixedSize(intArrayOf(1, 84, 8400), DataType.FLOAT32)
            
            // 6. 推理
            interpreter.run(tensorImage.buffer, outputBuffer.buffer.rewind())

            // 7. 后处理 (NMS)
            val results = postProcess(outputBuffer.floatArray)
            
            // 8. 传回 UI (附带裁剪后的尺寸,用于坐标映射)
            onResult(results, size, size)
        }
        imageProxy.close()
    }
    
    // 💡 飞哥小贴士:
    // postProcess 里面要做三件事:
    // 1. 维度转置:把 [84, 8400] 转成 [8400, 84] 方便遍历。
    // 2. 阈值过滤:置信度 < 0.5 的框直接扔掉。
    // 3. NMS (非极大值抑制):同一个物体可能有很多重叠框,只保留分最高的那个。
}

补充:关于 toBitmap

虽然 CameraX 提供了 toBitmap(),但性能一般。生产环境建议使用 YuvToRgbConverter (Google Sample 中有提供) 实现 GPU 加速转换。

第三步:Compose UI 界面 🎨

用 Compose 写界面简直是享受。我们把相机预览和画框图层叠在一起。

@Composable
fun ObjectDetectionScreen() {
    // 权限请求逻辑 (略) ...
    // 如果有权限,显示 CameraContent()
    CameraContent() 
}

@Composable
fun CameraContent() {
    val context = LocalContext.current
    val lifecycleOwner = LocalLifecycleOwner.current
    var detections by remember { mutableStateOf(emptyList<DetectionResult>()) }
    var imageWidth by remember { mutableIntStateOf(640) } // 裁剪后的正方形边长
    var imageHeight by remember { mutableIntStateOf(640) }

    // 重点:强制 UI 显示为正方形 (1:1),与模型输入保持一致
    Box(modifier = Modifier.fillMaxWidth().aspectRatio(1f)) {
        // 1. 相机预览层 (AndroidView + CameraX)
        AndroidView(
            factory = { ctx ->
                val previewView = PreviewView(ctx)
                val cameraProviderFuture = ProcessCameraProvider.getInstance(ctx)

                cameraProviderFuture.addListener({
                    val cameraProvider = cameraProviderFuture.get()

                    // 配置 Preview
                    val preview = Preview.Builder().setTargetAspectRatio(AspectRatio.RATIO_4_3).build()
                    preview.setSurfaceProvider(previewView.surfaceProvider)

                    // 配置 Analyzer
                    val imageAnalyzer = ImageAnalysis.Builder()
                        .setTargetAspectRatio(AspectRatio.RATIO_4_3)
                        .setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
                        .build()
                        .also {
                            it.setAnalyzer(Executors.newSingleThreadExecutor(), 
                                ObjectDetectorAnalyzer(ctx) { results, w, h ->
                                    detections = results
                                    imageWidth = w
                                    imageHeight = h
                                }
                            )
                        }

                    // 关键点:配置 ViewPort (1:1 视口)
                    // 告诉 CameraX 只输出正方形画面,自动裁剪掉多余部分
                    val viewPort = ViewPort.Builder(Rational(1, 1), Surface.ROTATION_0).build()
                    
                    val useCaseGroup = UseCaseGroup.Builder()
                        .addUseCase(preview)
                        .addUseCase(imageAnalyzer)
                        .setViewPort(viewPort) // 绑定视口
                        .build()

                    try {
                        cameraProvider.unbindAll()
                        cameraProvider.bindToLifecycle(
                            lifecycleOwner, 
                            CameraSelector.DEFAULT_BACK_CAMERA, 
                            useCaseGroup // 绑定 UseCaseGroup 而不是单独的 use cases
                        )
                    } catch (e: Exception) { Log.e("Camera", "Binding failed", e) }
                }, ContextCompat.getMainExecutor(ctx))
                
                previewView
            },
            modifier = Modifier.fillMaxSize()
        )

        // 2. 绘图层 (Canvas)
        Canvas(modifier = Modifier.fillMaxSize()) {
            // 坐标映射:屏幕显示尺寸 / 图片实际尺寸
            val scale = size.width / imageWidth 

            detections.forEach { result ->
                // 将模型输出坐标映射到屏幕坐标
                val left = result.x * scale
                val top = result.y * scale
                val width = result.w * scale
                val height = result.h * scale

                // 画框
                drawRect(
                    color = Color.Red,
                    topLeft = Offset(left, top),
                    size = Size(width, height),
                    style = Stroke(width = 5f)
                )
                // 画文字...
            }
        }
    }
}

效果演示:
在这里插入图片描述

⚠️ 飞哥避坑指南

  1. 坐标转换:模型输出的坐标是基于 640x640 的,画到屏幕上时,一定要根据屏幕实际宽高进行等比例映射,否则框会歪到姥姥家去。
  2. 线程问题analyze 方法运行在子线程,更新 UI (detections = results) 一定要切回主线程
  3. YUV 转 Bitmap:CameraX 给的是 YUV_420_888 格式,直接转 Bitmap 很慢。推荐用 Google 的 YuvToRgbConverter 或者 TFLite Support 库里的 ImageProcessor

4. 总结 📝

一句话记住它
Android 端侧推理 = CameraX 喂图 ➕ TFLite Support 预处理 ➕ YOLO 解析。

核心三要点

  1. CameraX Analyzer:每一帧图片的入口,要在这里做线程切换和数据处理。
  2. TFLite Support:Google 提供的神器,一行代码搞定缩放和归一化,别再手写 Bitmap 操作了。
  3. 后处理 (NMS):模型输出的只是候选框,必须通过 NMS 算法去重,才能得到最终结果。

🎁 示例源码

实战代码

源码包内含:

  • ✅ 完整的 Android Studio 工程
  • ✅ 已封装好的 YoloV8Detector.kt (含 NMS 算法实现)
  • ✅ 预训练好的 TFLite 模型

创作不易,记得👇关注飞哥👇 ,点赞、收藏哦~~,下篇见👋

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐