Android 相机 App 开发指南:从 CameraX 到实时图像处理
面向有开发经验的 Android 开发者,从 CameraX 会话、图像与颜色、实时性能、照片与视频、AI 到产品交付,梳理相机 App 的六层架构和渐进开发路线。
文章目录
对有 Android 开发经验的人来说,把相机预览显示在页面上并不难。真正的工作通常从这之后开始:切换摄像头会不会黑屏,照片为什么与预览构图不同,滤镜为什么只出现在屏幕上,录制几分钟后帧率为什么下降,以及返回页面后相机为什么打不开。
相机 App 同时连接硬件、系统相机管线、图像算法和交互界面。它既有普通应用的生命周期与状态管理问题,又有持续运行的数据流和设备能力差异。开发时,最好先做出一条可靠的采集与保存链路,再逐步加控制、视频、滤镜和 AI。
本文默认你熟悉 Kotlin、Android 生命周期、异步任务和基础应用架构。目标是设计一个可以持续迭代的 Android 相机应用,而不是重新讲解如何创建 Activity。文中代码是关键接入片段,省略了依赖声明和界面实现;没有把这些片段作为完整 Android 项目编译或进行真机验证,接入后仍需按项目版本与目标设备测试。
第一层:先把相机会话做成可管理的资源
CameraX 为主,Camera2 按需求进入
新项目可以先选 CameraX。它提供预览、照片、分析和视频用例,并将相机会话与生命周期绑定;Camera2 则适合确实需要更底层控制的场景。想做专业手动相机,不代表必须从第一天就自己管理全部底层会话。
CameraX 有两种常见接入方式:LifecycleCameraController 适合较简单的相机界面;ProcessCameraProvider 让用例组合和输出配置更明确。本文用后者作为主线。其结构及生命周期约定可参考 CameraX 架构文档。
依赖先选择适合项目的稳定版本,并在版本目录里集中锁定。camera-camera2、camera-lifecycle、camera-view 与后续加入的视频、扩展模块保持匹配;不要从不同年代的示例拼出一组版本。
分清四种用例的职责
| 用例 | 核心用途 | 工程上关心什么 |
|---|---|---|
Preview | 把画面送到显示 Surface | 首帧、裁剪、旋转与镜像 |
ImageCapture | 生成照片 | 清晰度、延迟、方向与保存结果 |
ImageAnalysis | 提供可分析的图像缓冲区 | 背压、处理时延与资源释放 |
VideoCapture | 录制视频 | 质量、音频、状态与文件完成 |
不要从屏幕截图得到“照片”,也不要默认分析流具有照片输出的分辨率。用例同时绑定时,设备会协商流配置,组合、分辨率和扩展模式并非任意兼容。绑定失败要记录配置,并准备减少用例或降低质量的回退方案。
相机模块可分成会话、处理和存储三个边界:会话负责选择摄像头与绑定用例;处理模块接收帧和参数;存储模块负责媒体输出。ViewModel 保存拍摄模式、滤镜参数与界面状态,避免长期持有已经销毁的 PreviewView 或 Fragment。
下面是绑定预览与拍照用例的片段。provider 需先异步获取,调用发生在主线程,且相机权限已经获准、previewView 已可用:
import android.view.Surface
import androidx.camera.core.Camera
import androidx.camera.core.CameraSelector
import androidx.camera.core.ImageCapture
import androidx.camera.core.Preview
import androidx.camera.lifecycle.ProcessCameraProvider
import androidx.camera.view.PreviewView
import androidx.lifecycle.LifecycleOwner
fun bindPhotoCamera(
provider: ProcessCameraProvider,
owner: LifecycleOwner,
previewView: PreviewView,
imageCapture: ImageCapture,
selector: CameraSelector
): Camera {
val rotation = previewView.display?.rotation ?: Surface.ROTATION_0
imageCapture.targetRotation = rotation
val preview = Preview.Builder()
.setTargetRotation(rotation)
.build()
preview.setSurfaceProvider(previewView.surfaceProvider)
return provider.bindToLifecycle(owner, selector, preview, imageCapture)
}
Fragment 中通常绑定 viewLifecycleOwner。切前后摄时,串行执行“停止当前操作、解绑本模块持有的用例、绑定新配置”;不要在每次 Compose 重组时重新建立会话,也不要无条件解绑其他模块的用例。还要约定页面退出时如何处理进行中的拍照或录像,而不是让回调继续修改旧界面。
第二层:先搞懂图像的布局,再讨论滤镜
图像不只是一块 RGB 数组
分析流常见格式是 YUV_420_888。读取平面时必须考虑 rowStride、pixelStride 和裁剪范围,不能假设所有设备都给你连续排列的 NV21。最容易暴露问题的往往不是主力测试机,而是另一款平面布局不同的手机。
相机帧还携带旋转和裁剪信息。预览通常经过缩放、裁剪以及前摄镜像处理;检测结果若直接按图像宽高缩放到界面,人脸框和对焦点就可能错位。把图像到预览的变换作为独立模块维护,并用横竖屏、前后摄与不同屏幕比例验证。输出变换文档说明了这类映射的处理方式。
前摄还需要一个明确的产品决定:预览镜像,保存照片是否也镜像?如果两者不同,应让用户理解这个行为。旋转同样要贯穿捕获和导出,不能只让预览看起来正常,却把错误方向写进照片。
颜色处理需要统一约定
RGB、YUV 是数据表示方式;sRGB、Display P3 是色域;Gamma 与传递函数影响数值怎样对应亮度。把它们混成一个“颜色格式”,会让滤镜在预览和导出之间出现难以解释的差异。
首版可以先统一采用 SDR、明确的 sRGB 输出流程,再逐步考虑广色域或 HDR。LUT 输入范围、颜色空间、滤镜顺序和插值方法要固定下来;不要让预览使用一套假设,而高分辨率照片处理使用另一套。
曝光、白平衡和后期调色也要分开。曝光补偿是在采集阶段影响相机自动曝光,滤镜里的“亮度”则是处理已得到的像素。后期提亮不能恢复传感器没有记录到的细节,模糊与锐化也不能替代正确对焦。
第三层:让实时处理始终有界
背压比多开几个线程更重要
30fps 对应每帧约 33ms,但这并不意味着你的算法可以独占这段时间。相机、调度、显示和其他处理都在共享资源。性能目标应包含端到端延迟,而不仅是一次推理耗时。
对人脸框、扫码和实时识别这类任务,通常优先保留最新帧,而不是把旧帧全部排队。CameraX 的 STRATEGY_KEEP_ONLY_LATEST 可以丢弃过时数据;阻塞生产者则可能影响绑定的其他用例。完成处理必须调用 ImageProxy.close(),而不是关闭内部的 Media.Image。这些约定见图像分析文档。
下面用 ML Kit 展示异步任务的释放位置。analysis 需绑定到相机会话,executor 由相机模块管理,detector 在会话间复用,reportError 是项目自己的错误回调:
import androidx.camera.core.ExperimentalGetImage
import androidx.camera.core.ImageAnalysis
import com.google.mlkit.vision.common.InputImage
import com.google.mlkit.vision.face.FaceDetector
import java.util.concurrent.Executor
@androidx.annotation.OptIn(ExperimentalGetImage::class)
fun attachFaceAnalyzer(
analysis: ImageAnalysis,
executor: Executor,
detector: FaceDetector,
reportError: (Exception) -> Unit
) {
analysis.setAnalyzer(executor) { proxy ->
val image = proxy.image
if (image == null) {
proxy.close()
return@setAnalyzer
}
try {
val input = InputImage.fromMediaImage(
image, proxy.imageInfo.rotationDegrees
)
detector.process(input)
.addOnFailureListener { reportError(it) }
.addOnCompleteListener { proxy.close() }
} catch (error: Exception) {
proxy.close()
reportError(error)
}
}
}
同步算法可以在 finally 中释放;异步算法必须等消费者使用完缓冲区。不能在启动异步任务后立即执行 finally 关闭图像。检测结果回到 UI 前,还要检查页面或会话是否已切换,丢弃旧会话的结果;停用分析后,让进行中的任务收尾,再释放检测器。ML Kit 人脸检测指南提供了图像输入及实时处理建议。
GPU 是一条管线,不是一个开关
实时滤镜可以使用 OpenGL ES 纹理与着色器,复杂需求再评估 Vulkan。避免每帧经过“YUV 转 Bitmap、CPU 处理、上传纹理、读回内存”的往返。缓冲复用、减少转换与限制分析分辨率,常常比换一种图形 API 更先见效。
但 AI 不必全部放到 GPU 上。CPU、GPU 或其他加速路径的收益取决于算子支持、模型规模、数据转换和设备。测量完整链路再选后端,不能只比较一段推理时间。
更关键的是输出一致:自定义预览 Surface 上的滤镜,不会自动进入 ImageCapture 保存的照片,也不会自动进入录像。需要为照片导出复用同一套参数与算法,或设计可覆盖目标输出的处理链路,并确认所用 CameraX 版本支持的效果接口与目标组合。
第四层:功能背后都有能力协商
对焦、曝光和镜头切换
点击对焦应把触摸位置转换到相机测光坐标,优先利用 PreviewView 的测光点工厂,再通过 CameraControl 发起对焦测光。对焦动画不能只播放固定时长,要区分请求、完成、失败和取消。
曝光滑条根据设备支持的补偿范围显示,不能在所有手机上硬编码相同档位。手动 ISO、快门和焦距控制则需要核查能力;若使用 Camera2 互操作,还要理解参数与自动控制的关系,避免与 CameraX 管理的状态冲突。
前后摄切换不等于任意物理镜头切换,超广角与长焦也不一定作为独立摄像头开放。基于实际可用摄像头与变焦范围生成控件,不要照着系统相机的按钮数量制作界面。
HDR、夜景、虚化与防抖
Camera Extensions 可调用厂商提供的部分照片能力,但必须按设备、摄像头与模式检查可用性。当前官方说明将这些扩展用于预览与照片捕获,不用于视频录制。不能因为支持夜景拍照,就显示“夜景录像”开关。参见相机扩展说明。
照片 HDR、多帧降噪、HDR 视频与 HDR 图片格式也不是同一项功能。先写清楚你需要的是提升暗部细节、厂商照片扩展,还是特定动态范围输出,再选择实现路线。
OIS 属于硬件与相机控制能力,EIS 涉及电子稳定与可能的画面裁切;可用模式及组合需要查询。连拍、深度图、慢动作同样有设备边界。功能不可用时隐藏或说明,比提供一个不起作用的按钮更可信。
视频是另一个完整状态流程
先用 Recorder 与 VideoCapture 做常规录像,再考虑自定义编码。质量选项按设备支持情况生成,并设置回退策略;不要把 4K、60fps、HEVC 当作所有机型的默认承诺。MP4 是容器,H.264、HEVC 是编码格式,配置与导出时要分清。
有声音的录像需要麦克风权限。没获准时,可以按产品设计提供静音录像,而不是阻止所有拍摄。录制结束要等待最终事件,检查错误与输出 URI,再更新相册缩略图或允许分享;调用停止不等于文件已经可用。录像文档介绍了 Recorder、Recording 与事件流程。
自定义音视频链路还要管理时间戳、结束信号和封装完成。裁剪、拼接属于后处理;慢动作涉及高帧率采集及播放时间轴;延时摄影涉及采样间隔。不要把它们理解成修改一个播放速度参数就完成的功能。
第五层:算法与 AI 要接入明确的任务
如果产品是扫描工具,先做文档边界、透视校正和 OCR;如果是人像相机,先做检测、分割和参数可控的美化;如果是摄影工具,曝光交互与导出质量可能更重要。先选一个能验证价值的任务,不必同时接入所有模型。
Android 上可评估 ML Kit、LiteRT 等推理工具,以及 OpenCV 的传统图像处理能力。模型不是相机架构的替代品:推理分辨率、预处理、旋转、输出坐标和结果平滑,都需要与你的采集链路配合。
人脸检测只回答位置等信息,并不自动提供可靠的人像分割、磨皮或瘦脸。背景替换需要分割掩码、边缘处理与时间稳定性;否则头发边缘闪烁,比静态截图上的效果更容易被用户发现。
计算摄影的难点还包括多帧对齐、运动处理、曝光差异与内存峰值。把多张照片平均一下不能等同于成熟夜景算法,超分辨率也不是无条件恢复真实细节。首版优先使用经过设备能力检查的系统输出,再决定是否投入自研。
第六层:把拍到的内容可靠地交给用户
保存、权限与错误恢复
打开相机按需申请 CAMERA,启用有声录像时申请 RECORD_AUDIO。仅保存自己拍摄的媒体,不等于必须获得读取整个相册的权限:Android 10 及以上访问应用自身创建的媒体通常不需要存储权限;读取其他媒体则是另一种需求。具体规则见共享媒体存储文档。
使用 CameraX 的 MediaStore 输出选项时,按保存回调判断结果;自建导出流程时,则要处理写入期间的可见性、失败清理和最终提交。目标是避免用户在相册里看到半个文件,也避免失败后留下无用条目。若支持旧系统,单独处理相应存储规则。
应用状态可以包含 Initializing、Ready、Capturing、Recording 和 Error,并为每条操作定义前置条件。权限被撤回、相机被占用、磁盘写入失败时,都应能结束当前操作并提供可执行的恢复路径。
UI 和性能一起验收
快门反馈要对应实际事件:接受拍摄请求、捕获发生与文件保存完成是不同节点。不要在保存成功之前就提示“已保存”。曝光滑条、变焦手势和模式切换,也应跟随能力与状态启用,防止一次操作触发多个重绑。
性能测试至少记录首帧耗时、拍照到保存时延、分析耗时分布、长时间录像帧率、内存峰值和温度变化。用中低端真机连续运行,再测试退后台、快速切摄、旋转、拒绝权限和低空间等路径。
出现持续负载问题时,要有具体降级顺序,例如先降低分析频率,再降低分析分辨率,必要时关闭昂贵效果。预览流畅与拍摄成功通常比同时开启最多效果更值得优先保护。
一个人或小团队,怎样安排开发路线?
可以借用三个阶段,但按验收结果推进,不把周数当作交付保证:
| 阶段 | 交付内容 | 通过标准 |
|---|---|---|
| 基础闭环 | 预览、拍照、保存、前后摄 | 拒绝权限与返回页面可恢复,方向和文件正确 |
| 日常可用 | 对焦、曝光、变焦、基础录像 | 控件基于能力,录像能可靠结束并分享 |
| 差异化 | 一种滤镜或 AI 任务 | 预览与导出一致,长时间运行可控 |
已有经验的人,可以用一两周作为基础原型的规划起点,随后投入数周完善控制与录像;真正需要多少时间,取决于设备覆盖、质量目标和异常路径。自定义 GPU 管线、多帧算法与美颜,不适合仅按“再接一个库”估时。
第一版值得完成的,是一条有边界、可观测、能恢复的拍摄链路。等它在目标设备上稳定运行,再增加一项用户能感知的能力,并重新检查画质、延迟和资源开销。这样扩展出来的相机 App,才不会每增加一个功能,就让整条管线变得更难维护。