在上篇文章 一文理解Jetpack------CameraX中,我们介绍了 CameraX 的使用。
但是如果使用 CameraX 来实现拍照功能,你会发现 CameraX 拍照耗时在 405 ~ 475 ms 之间。对一些流畅度有要求的应用来说,这个耗时是不可接受的。
Camera框架对比
在优化 CameraX 拍照速度之前,我们需要确定该拍照耗时是否和 CameraX 框架有关。这里用 CameraX、CameraView、小米 Camera SDK) 三个框架的 demo 的拍照耗时作为对比。分别使用它们拍照10次的结果如下,测试机型为小米 13。
CameraX | CameraView | 小米 Camera SDK | |
---|---|---|---|
(10次拍照)耗时 | 405 ~ 475 ms | 312 ~ 378 ms | 443 - 480 ms |
从上面的表格中可以看到,三个框架中 CameraView 的拍照耗时最小,其次是 CameraX,最后是 小米 Camera SDK。本来我还以为小米手机上使用小米 Camera SDK 的效果应该是最好的,但是结果出乎意料,也可能是数据量太小导致偏差比较大。
不过,通过对比我们还是可以看出,相机拍照耗时应该在 300 ms 左右。CameraX 拍照有优化的空间,我们看看如何把 CameraX 拍照耗时优化到 300 ms 左右。
CameraX 拍照速度优化
CAPTURE_MODE_MAXIMIZE_QUALITY 和 CAPTURE_MODE_MINIMIZE_LATENCY
swift
/**
* 优化拍摄流程,将图像质量置于延迟之上。当拍摄模式设置为最高质量模式时,拍摄图像可能需要更长的时间。
*/
public static final int CAPTURE_MODE_MAXIMIZE_QUALITY = 0;
/**
* 优化拍摄流程,将延迟置于图像质量之上。当拍摄模式设置为最低延迟模式时,图像拍摄速度可能更快,但图像质量可能会下降。
*/
public static final int CAPTURE_MODE_MINIMIZE_LATENCY = 1;
// 设置 CaptureMode
imageCapture = ImageCapture.Builder()
.setCaptureMode(ImageCapture.CAPTURE_MODE_MINIMIZE_LATENCY)
.setTargetAspectRatio(screenAspectRatio)
.setTargetRotation(rotation)
.build()
在 CameraX 的接口中,我们可以设置 CAPTURE_MODE_MAXIMIZE_QUALITY
和 CAPTURE_MODE_MINIMIZE_LATENCY
来提高拍摄图像的速度。默认情况下,配置就是CAPTURE_MODE_MINIMIZE_LATENCY
。
但实际上,我们设置不同的 CaptureMode 效果差距不大。这是因为 CAPTURE_MODE_MAXIMIZE_QUALITY
和 CAPTURE_MODE_MINIMIZE_LATENCY
是提供设置 JpegQuality
来影响拍照速度的。其中 CAPTURE_MODE_MAXIMIZE_QUALITY
设置的 JpegQuality
值为 100,而CAPTURE_MODE_MINIMIZE_LATENCY
设置的值为 95。因此设置不同的 CaptureMode,对拍照速度的影响不太大。不过,我们可以主动设置 JpegQuality
的值来影响拍照速度,代码示例如下:
scss
imageCapture = ImageCapture.Builder()
.setJpegQuality(80)
.setTargetAspectRatio(screenAspectRatio)
.setTargetRotation(rotation)
.build()
通过 setJpegQuality
设置图片质量后,拍照耗时在 387 ~ 421ms 之间,提高了将近 40ms。
使用 setTargetResolution 设置目标分辨率
在 CameraX 中,我们可以通过 setTargetAspectRatio
来设置目标分辨比例。代码示例如下:
kotlin
val metrics = windowManager.getCurrentWindowMetrics().bounds
val screenAspectRatio = aspectRatio(metrics.width(), metrics.height())
imageCapture = ImageCapture.Builder()
.setJpegQuality(80)
.setTargetAspectRatio(screenAspectRatio)
.setTargetRotation(rotation)
.build()
/**
* [androidx.camera.core.ImageAnalysis.Builder] 需要 [androidx.camera.core.AspectRatio] 枚举值。
* 当前该枚举有 4:3 和 16:9 这两个值。
*
* 通过计算预览宽高比与给定值之一的绝对差值,来为 @params 中提供的尺寸检测最合适的宽高比。
*
* @param width - 预览宽度
* @param height - 预览高度
* @return 合适的宽高比
*/
private fun aspectRatio(width: Int, height: Int): Int {
// 计算预览的宽高比,将较大值除以较小值转换为 double 类型
val previewRatio = max(width, height).toDouble() / min(width, height)
// 比较预览宽高比与 4:3 和 16:9 宽高比的绝对差值
if (abs(previewRatio - RATIO_4_3_VALUE) <= abs(previewRatio - RATIO_16_9_VALUE)) {
// 如果预览宽高比更接近 4:3,则返回 4:3 的宽高比枚举值
return AspectRatio.RATIO_4_3
}
// 否则返回 16:9 的宽高比枚举值
return AspectRatio.RATIO_16_9
}
设置好 setTargetAspectRatio
后,CameraX 会根据你提供的比例来寻找当前摄像头最接近该比例的拍摄像素设置。此时选中的配置可能远比你手机显示图片的像素要大,这时我们可以通过 setTargetResolution
来设置所需要的分辨率,代码示例如下:
scss
imageCapture = ImageCapture.Builder()
.setJpegQuality(80)
.setTargetResolution(Size(1080, 2400))
.setTargetRotation(rotation)
.build()
通过 setTargetResolution
设置所需要的分辨率后,拍照耗时在 342 ~ 400ms 之间,提高了将近 20 ms。
使用两个参数的 takePicture 方法
在文档和官方示例中,CameraX 使用如下方法来拍照:
kotlin
val outputOptions = ImageCapture.OutputFileOptions
.Builder(requireContext().contentResolver,
MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
contentValues)
.build()
imageCapture.takePicture(
outputOptions,
cameraExecutor,
object : ImageCapture.OnImageSavedCallback {
override fun onError(exc: ImageCaptureException) {
Log.e(TAG, "Photo capture failed: ${exc.message}", exc)
}
override fun onImageSaved(output: ImageCapture.OutputFileResults) {
...
}
})
该方法会把拍照的图片转化为文件,如果你只想操作拍照的 Bitmap,就可以使用两个参数的 takePicture 重载方法。代码示例如下:
kotlin
imageCapture.takePicture(cameraExecutor, object : ImageCapture.OnImageCapturedCallback() {
override fun onError(exc: ImageCaptureException) {
Log.e(TAG, "Photo capture failed: ${exc.message}", exc)
}
@SuppressLint("UnsafeOptInUsageError")
override fun onCaptureSuccess(image: ImageProxy) {
val bitmap = image.image?.toBitmap()
}
})
fun Image.toBitmap(): Bitmap {
val buffer = planes[0].buffer
buffer.rewind()
val bytes = ByteArray(buffer.capacity())
buffer.get(bytes)
return BitmapFactory.decodeByteArray(bytes, 0, bytes.size)
}
修改后拍照耗时会在 312 ~ 360 ms 之间,提高了将近 20 ms。
直接从 PreviewView 获取 Bitmap
在 CameraX 中,拍照的核心逻辑在 takePictureInternal
方法中,源码如下所示。可以看到,在拍照之前,我们需要触发对焦、检查3A(自动对焦、自动曝光、自动白平衡)是否收敛;拍照完成之后,还需要取消对焦扫描等操作,这就是拍照耗时比较长的原因。
less
/**
* 拍照流程。
*
* <p>拍照分为三个步骤。
*
* <p>(1) 拍照前准备,此步骤会在必要时触发自动对焦(AF)/自动曝光(AE)扫描,或者打开闪光灯。然后在必要时检查 3A(自动对焦、自动曝光、自动白平衡)是否收敛。
*
* <p>(2) 发出单次拍照请求。
*
* <p>(3) 拍照后处理,此步骤会在必要时取消自动对焦(AF)/自动曝光(AE)扫描,或者关闭闪光灯。
*
* @param imageCaptureRequest 拍照请求对象,不能为 null
* @return 一个可监听的未来对象,包含拍摄到的图像代理
*/
private ListenableFuture<ImageProxy> takePictureInternal(
@NonNull ImageCaptureRequest imageCaptureRequest)
如果对照片的质量没有要求,那么就可以直接从 PreviewView
来获取预览的 Bitmap。唯一的问题就是,获取 Bitmap 时,可能没有对焦完成。解决方案也很简单,就是监听 CameraX 是否对焦完成,如果没有对焦完成,则拍照失败或者延时拍照,这个可以根据业务来定。
从 PreviewView
来获取预览的 Bitmap 的耗时在 47 ~ 58 ms 之间,是所有优化中最快的。
注意事项
less
private ListenableFuture<ImageProxy> takePictureInternal(
@NonNull ImageCaptureRequest imageCaptureRequest) {
return CallbackToFutureAdapter.getFuture(
completer -> {
mImageReader.setOnImageAvailableListener(
(imageReader) -> {
...
},
// 在主线程中执行
CameraXExecutors.mainThreadExecutor());
}
从 takePictureInternal
源码中,可以看到拍照流程是在主线程执行的,因此在执行拍照前,需要使用 handler.removeCallbacksAndMessages(null)
方法清除其他 Message,防止阻塞拍照的执行。