前言
ML Kit 那一篇讲了开箱即用的能力,这一篇聊更专业的:CameraX 与自训练视觉模型的集成。从一个"智能裁剪"案例出发,覆盖 CameraX 高级配置、ONNX Runtime / TFLite 推理、性能与功耗平衡。
一、CameraX vs Camera2:为什么选 CameraX
CameraX 把 Camera2 的复杂度藏到 Lifecycle 后面:
工程上 95% 的场景选 CameraX 即可。
二、CameraX + 视觉模型的整体链路
Surface ──▶ PreviewView (预览)
│
▼
ImageAnalysis ──▶ 抽帧 ──▶ 预处理 (resize/normalize) ──▶ 模型推理 ──▶ 后处理
│ │
▼ ▼
ImageCapture (拍照) 业务回调 / UI
其中"模型推理"可以用 MediaPipe、ML Kit、ONNX Runtime 或 TFLite。
三、一个完整示例:实时智能裁剪
需求:拍照时,根据画面主体(人脸或物体)自动裁剪成 1:1 头像。
3.1 添加依赖
// build.gradle.kts
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("com.google.mlkit:face-detection:16.1.7")
implementation("ai.onnxruntime:onnxruntime-android:1.18.0")
3.2 CameraX 初始化
class SmartCameraActivity : AppCompatActivity() {
private val analyzer by lazy { FaceAnalyzer() }
private lateinit var previewView: PreviewView
private lateinit var cropOverlay: CropOverlayView
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_smart_camera)
previewView = findViewById(R.id.preview)
cropOverlay = findViewById(R.id.crop_overlay)
val providerFuture = ProcessCameraProvider.getInstance(this)
providerFuture.addListener({
val provider = providerFuture.get()
bindUseCases(provider)
}, ContextCompat.getMainExecutor(this))
}
private fun bindUseCases(provider: ProcessCameraProvider) {
val preview = Preview.Builder().build().also {
it.setSurfaceProvider(previewView.surfaceProvider)
}
val analysis = ImageAnalysis.Builder()
.setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
.setOutputImageFormat(ImageAnalysis.OUTPUT_IMAGE_FORMAT_RGBA_8888)
.build().apply {
setAnalyzer(Executors.newSingleThreadExecutor()) { proxy ->
analyzer.detect(proxy) { faces ->
runOnUiThread { cropOverlay.updateFaces(faces) }
proxy.close()
}
}
}
provider.bindToLifecycle(this, CameraSelector.DEFAULT_FRONT_CAMERA, preview, analysis)
}
}
3.3 人脸检测 Analyzer
class FaceAnalyzer {
private val detector = FaceDetection.getClient(
FaceDetectorOptions.Builder()
.setPerformanceMode(FaceDetectorOptions.PERFORMANCE_MODE_FAST)
.setLandmarkMode(FaceDetectorOptions.LANDMARK_MODE_NONE)
.setContourMode(FaceDetectorOptions.CONTOUR_MODE_NONE)
.build()
)
fun detect(imageProxy: ImageProxy, onResult: (List<RectF>) -> Unit) {
val bitmap = imageProxy.toBitmap()
val image = InputImage.fromBitmap(bitmap, imageProxy.imageInfo.rotationDegrees)
detector.process(image)
.addOnSuccessListener { faces ->
val rects = faces.map {
val b = it.boundingBox
// 把像素 rect 映射到预览视图坐标
RectF(
b.exactCenterX() - b.width() / 2f,
b.exactCenterY() - b.height() / 2f,
b.exactCenterX() + b.width() / 2f,
b.exactCenterY() + b.height() / 2f
)
}
onResult(rects)
}
.addOnFailureListener { onResult(emptyList()) }
}
}
3.4 智能裁剪的"主区域 + 留白"逻辑
fun calculateCropRect(faces: List<RectF>, previewSize: Size): RectF {
if (faces.isEmpty()) {
// 默认居中
val cx = previewSize.width / 2f
val cy = previewSize.height / 2f
val half = min(previewSize.width, previewSize.height) / 2f
return RectF(cx - half, cy - half, cx + half, cy + half)
}
// 找到最大人脸
val main = faces.maxBy { it.width() * it.height() }
val cx = main.centerX()
val cy = main.centerY()
val half = maxOf(main.width(), main.height()) * 1.4f
// 限制在预览范围内
val left = (cx - half).coerceAtLeast(0f)
val right = (cx + half).coerceAtMost(previewSize.width.toFloat())
val top = (cy - half).coerceAtLeast(0f)
val bottom = (cy + half).coerceAtMost(previewSize.height.toFloat())
return RectF(left, top, right, bottom)
}
实际拍照时,按 previewView 的裁剪坐标 + ImageCapture 的 takePicture 回调用 Bitmap 替换。
四、低光场景:用 ONNX 自训练模型
通用 ML Kit 人脸检测在低光下会丢失。专业 App 会接入自训练模型。
4.1 模型转换(Python 端)
# export_yolov8_face.py
from ultralytics import YOLO
m = YOLO("yolov8n-face.pt")
m.export(format="onnx", imgsz=320, opset=12)
部署到 Android 的 .onnx 文件约 6MB。
4.2 Android 端推理
class FaceNetDetector(context: Context) {
private val env = OrtEnvironment.getEnvironment()
private val session = env.createSession(
File(context.filesDir, "yolov8n-face.onnx").absolutePath,
OrtSession.SessionOptions().apply {
addCPU(OrtEpProvider.CPU)
setIntraOpNumThreads(2)
}
)
fun detect(bitmap: Bitmap): List<RectF> {
val resized = Bitmap.createScaledBitmap(bitmap, 320, 320, false)
val buf = ByteBuffer.allocateDirect(320 * 320 * 3 * 4)
.order(ByteOrder.nativeOrder())
val pixels = IntArray(320 * 320)
resized.getPixels(pixels, 0, 320, 0, 0, 320, 320)
pixels.forEach { px ->
val r = ((px shr 16) and 0xFF) / 255f
val g = ((px shr 8) and 0xFF) / 255f
val b = (px and 0xFF) / 255f
buf.putFloat(r)
buf.putFloat(g)
buf.putFloat(b)
}
buf.rewind()
val shape = longArrayOf(1, 3, 320, 320)
val tensor = OnnxTensor.createTensor(env, buf, shape)
val output = session.run(mapOf("images" to tensor))
// 后处理:解析 YOLO 输出,NMS
return postProcess(output)
}
}
4.3 性能调优
推理设备加速:
高通 8 Gen 3 用 QNN EP(QNN Execution Provider)
联发科用 Neuron EP
苹果端用 Core ML EP(iOS)
各厂商 SDK 都在赶,2026 年下半年会看到更多端侧 NPU 跑 YOLO。
五、性能与功耗:踩过的坑
5.1 主线程爆炸
// 错误:直接传 ImageProxy 到模型
setAnalyzer(executor) { proxy ->
val bitmap = proxy.toBitmap() // CPU 解码
detector.detect(bitmap) // 模型推理
// 上面的代码大概率卡在这
}
修法:限流 + 异步 + OutputImageFormat。
5.2 后台功耗
持续分析是耗电大头。建议:
class SmartCameraController {
fun pauseAnalysis() {
cameraProvider.unbind(imageAnalysis)
}
fun resumeAnalysis() {
cameraProvider.bindToLifecycle(this, selector, preview, imageAnalysis)
}
}
进入后台或不可见时停止分析。
5.3 内存上限
高分辨率分析需要更多 RAM,可以限制:
ImageAnalysis.Builder()
.setTargetResolution(Size(720, 1280))
.build()
六、商业落地的实际建议
离线 First:把模型打 APK 一起发布,放弃"动态下载"。商业 App 不希望 OTA 才能用
降级策略:模型推理失败或耗时过长,回退到 ML Kit 或默认策略
缓存命中:如果用户短时间内连续拍照,模型推理结果可以缓存 1-2 秒
GPU/NPU 检查:在低端机型用 CPU 推理,旗舰机型用 NPU 加速
隐私:人脸数据高度敏感,建议明确告知用户
七、CameraX Extensions:超广角、夜景
CameraX Extensions 是 Google 给厂商留的扩展入口——夜景模式、HDR、超广角、人像虚化。
implementation("androidx.camera:camera-extensions:1.3.4")
val nightEffect = ImageCapture.Builder()
.setCameraSelector(CameraSelector.DEFAULT_BACK_CAMERA)
.setCaptureMode(ImageCapture.CAPTURE_MODE_MAXIMIZE_QUALITY)
.build()
val ext = ExtensionsManager.getExtensionsAsync(context).get()
if (ext.isExtensionAvailable(CameraSelector.DEFAULT_BACK_CAMERA, ExtensionCharacteristic.Night)) {
val nightSelector = ext.getExtensionEnabledCameraSelector(
CameraSelector.DEFAULT_BACK_CAMERA, ExtensionCharacteristic.Night
)
cameraProvider.bindToLifecycle(this, nightSelector, nightEffect)
}
配合 AI 推理,能做出"夜间拍照增强 + 智能补光"的效果。
小结
CameraX + AI 不是简单把模型堆进去,而是全链路调优:抽帧策略、预处理、模型选择、后处理、缓存、省电策略。哪个环节掉链子都会影响最终体验。建议先用一个简单版本(ML Kit 人脸检测)跑通,再升级到自训练模型。
下一篇聊 Android XR 与扩展现实——手机形态之外的新互动方式。
参考阅读
CameraX 官方文档
ONNX Runtime Mobile Android
YOLOv8 模型转换教程