Android AI 实战:手把手教你用 Compose + CameraX 跑通 YOLOv8
一句话记住它Android 端侧推理 = CameraX 喂图 ➕ TFLite Support 预处理 ➕ YOLO 解析。核心三要点:每一帧图片的入口,要在这里做线程切换和数据处理。:Google 提供的神器,一行代码搞定缩放和归一化,别再手写 Bitmap 操作了。后处理 (NMS):模型输出的只是候选框,必须通过 NMS 算法去重,才能得到最终结果。

关键词: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) 📦
- 在
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")
}
- 在
AndroidManifest.xml中申请相机权限:
<uses-feature android:name="android.hardware.camera.any" />
<uses-permission android:name="android.permission.CAMERA" />
⚠️ 注意:Android 6.0 以上需要动态申请权限!在 Compose 中可以使用
Accompanist库或者Activity的requestPermissions来搞定。如果不申请,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)
)
// 画文字...
}
}
}
}
效果演示:
⚠️ 飞哥避坑指南:
- 坐标转换:模型输出的坐标是基于 640x640 的,画到屏幕上时,一定要根据屏幕实际宽高进行等比例映射,否则框会歪到姥姥家去。
- 线程问题:
analyze方法运行在子线程,更新 UI (detections = results) 一定要切回主线程!- YUV 转 Bitmap:CameraX 给的是 YUV_420_888 格式,直接转 Bitmap 很慢。推荐用 Google 的
YuvToRgbConverter或者 TFLite Support 库里的ImageProcessor。
4. 总结 📝
一句话记住它:
Android 端侧推理 = CameraX 喂图 ➕ TFLite Support 预处理 ➕ YOLO 解析。
核心三要点:
- CameraX Analyzer:每一帧图片的入口,要在这里做线程切换和数据处理。
- TFLite Support:Google 提供的神器,一行代码搞定缩放和归一化,别再手写 Bitmap 操作了。
- 后处理 (NMS):模型输出的只是候选框,必须通过 NMS 算法去重,才能得到最终结果。
🎁 示例源码
源码包内含:
- ✅ 完整的 Android Studio 工程
- ✅ 已封装好的
YoloV8Detector.kt(含 NMS 算法实现) - ✅ 预训练好的 TFLite 模型
创作不易,记得👇关注飞哥👇 ,点赞、收藏哦~~,下篇见👋
更多推荐

所有评论(0)