简介
- CameraX 是一个 Jetpack 支持库,旨在帮助您简化相机应用的开发工作。
- 它提供一致且易于使用的 API Surface,适用于大多数 Android 设备,并可向后兼容至 Android 5.0
- 基于Camera2,即对Camera2的封装
简单的设计思想
将整个拍照过程分为三块,每块对应一个用例
- 预览(Preview)
- 图片分析(ImageAnalysis)
不使用预览的数据流,使用比较轻的ImageReader读出的yuv数据,进行计算,常见的应用场景是拍照取词
- 图片拍摄 (ImageCapture)
另外需要谈到一个session的概念,这其实来自Camera2
- Session
CameraX通过lifecycle实现默认的生命周期回调,即onStart的时候重新打开session即
openSession(), onStop的时候关闭session
下面我们看一下这三个用例如何使用的
实现步骤
- 写xml
<PreviewView
android:id="@+id/camera_view"
android:focusable="true"
android:clickable="true"
android:layout_width="match_parent"
android:layout_height="match_parent" />
PreviewView是用于显示预览界面的view,内部用surface实现的
- 创建用例,配置用例
private fun createCameraXUseCases() {
MyLog.d("yyyyyy", "createCameraUseCases start")
val metrics = DisplayMetrics().also { mBinding.cameraView.display.getRealMetrics(it) }
val ration = 1f
val applyWidthPixels = (metrics.widthPixels * ration).toInt()
val applyHeightPixel = (metrics.heightPixels * ration).toInt()
val screenAspectRatio = aspectRatio(applyWidthPixels, applyHeightPixel)
val resolution = Size(applyWidthPixels, applyHeightPixel)
val rotation = (context?.getSystemService(Context.WINDOW_SERVICE) as WindowManager).defaultDisplay.rotation
// Preview用例配置,这里经过了大量的测试得到的最兼容的配置
mPreview = Preview.Builder().setMaxResolution(resolution)
// .setTargetAspectRatio(screenAspectRatio)
.setTargetResolution(resolution)
.setTargetRotation(rotation)
.build()
mImageCapture = ImageCapture.Builder()
.setMaxResolution(resolution)
// 尽量用screen AspectRation,fix SM-J600G 预览拉伸
.setTargetAspectRatio(screenAspectRatio)
.setTargetRotation(rotation)
.setCaptureMode(ImageCapture.CAPTURE_MODE_MINIMIZE_LATENCY)
.build()
// 配置此处的原因是 预览界面与相机捕获界面一致
mImageCapture?.setCropAspectRatio(Rational(applyWidthPixels, applyHeightPixel))
mImageAnalysis = ImageAnalysis.Builder().setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST).setMaxResolution(resolution)
// .setTargetAspectRatio(screenAspectRatio)
.setTargetResolution(resolution)
.setTargetRotation(rotation)
.build()
}
- 绑定相机
private fun bindCameraXUseCases(owner: LifecycleOwner, selector: CameraSelector, vararg useCases: UseCase?) {
try {
val processCameraProvider = mCameraProviderFuture.get() as ProcessCameraProvider
processCameraProvider.unbindAll()
mCamera = processCameraProvider.bindToLifecycle(this, selector, *useCases)
// 此处是实例化PreviewViewImplementation
mPreview?.setSurfaceProvider(mBinding.cameraView.surfaceProvider)
} catch (e: Exception) {
mCameraSelector = null
e.printStackTrace()
}
}
- 调用
private fun startLaunchCamera() {
ProcessCameraProvider.getInstance(requireContext()).also { mCameraProviderFuture = it }.addListener(Runnable {
// 创建用例必须在回调里面做
createCameraXUseCases()
launchCamera()
}, ContextCompat.getMainExecutor(requireContext()))
}
下面聊聊上述几个步骤的原理
原理
写xml即PreviewView
PreviewView是只负责显示相机画面的自定义View。内部有两种实现方式SurfaceView与TextureView,默认是性能更好的surfaceView,当然如果你的手机不支持SurfaceView就选择TextureView,当然你可以通过PreviewView.setPreferredImplementationMode(ImplementationMode)指定实现方式。逻辑如下图:
从上图可以得出一个结论:当首选模式设置为 SURFACE_VIEW 时,PreviewView 会尽可能遵循您的设置 (使用 SurfaceView);而当首选模式设置为 TEXTURE_VIEW 时,PreviewView 会确保一直使用 TEXTURE_VIEW 模式。 这个具体的实现类PreviewViewImplementation的实例mImplementation的赋值需要调用Preview.setSurfaceProvider(PreviewView.getSurfaceProvider())。
除了上述要点,PreviewView还有如下两个功能,在此就不多做介绍了
- 相机对焦:直接调用API即可
- 预览界面拖动
用例配置及原理
三个用例都需要配置,通过构建者模式且三个用例的配置几乎是一套方法,常用的方法如下图所示
你可以给三种用例设置最大分辨率,目标长宽比,目标分辨率,目标旋转角度,CameraX需要这些信息计算出各个用例的输出的最终分辨率,最终旋转角度,源码逻辑大体如下图所示
预览(Preview)
预览的配置非常重要,不当的配置往往会导致如下几种情况发生
- 预览变形,比如拉伸等
- 拍照的图和预览图不一致
至于如何配置需要看你的需求,预览框的比例是多少等,我们产品的需求是全屏的预览框,所以我使用了
setTargetResolution这个函数,参数是屏幕的分辨率, 具体的计算逻辑在SupportedSurfaceCombination的getSupportedOutputSizes中,这里给出具体的计算逻辑图
通过上图我们给出几个对实践有指导意义的说明和结论
- 如果你不设置目标宽高比AspectRatio,CameraX会自己根据你给的目标分辨率计算出目标宽高比,最后依然是按照目标宽高比作为轴进行排序,原理如下图所示
- CameraX在计算合适的分辨率的过程中,会将每个用例输出的
SupportedOutputSizes做笛卡尔积,如果用例支持的outputSizes过多,会导致笛卡尔积后的数据非常庞大。所以我们可以设置maxsize去尽量减少这样的空间占用 - 在输出的最后一步即筛选笛卡尔积后的数据,这个数据是用例可能的各种配置的组合,如下图
在筛选过程中就是以这个组合作为基本单位,所以他们其实是耦合的,进一步讲,假设预览,图片分析配置不变,只改变图片拍摄的目标宽高比或者目标分辨率,可能会导致预览和图片分析的输出分辨率有变化。
分析图片(ImageAnalysis)
分析图片是实时获取图片的字节流,用于分析,常见的场景有AR,识别花草,屏幕取词等,这个用例的参数配置可以沿用Preview用例,主要想说明的有以下几点
- 生产者消费者问题:
既然是实时获取图片,就必然有生产者与消费者问题,这里每张图片的字节流产生是生产者,消费者则是分析这些图片,都是耗时操作,如何平衡生产与消费的速度?google提供了生产者与消费者模型中常见的两种方式,阻塞模式和非阻塞,前者取最老的帧,后者取最新的帧,这需要看你的需求,这个配置是通过setBackpressureStrategy,如下代码可以说明:
val imageAnalysis = ImageAnalysis.Builder()
.setTargetResolution(Size(1280, 720))
// 通过该函数配置模式STRATEGY_BLOCK_PRODUCER或者STRATEGY_KEEP_ONLY_LATEST
.setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
.build()
- CameraX图片分析返回的图像格式是YUV_420_888, 不是返回JPEG,而且不允许你配置,当然会有讨巧的方法(hook)解决,因为YUV_420_888对5.0.1的有些手机不太兼容,我们先来看看它是怎么返回的?使用第一点配置成功的
imageAnalysis
imageAnalysis.setAnalyzer(mCameraExecutor, image -> {
// 得到类型为ImageProxy的image
image.close();
});
跟进源码,很容易看到如下类
final class ImageAnalysisBlockingAnalyzer extends ImageAnalysisAbstractAnalyzer {
@Override
public void onImageAvailable(@NonNull ImageReaderProxy imageReaderProxy) {
// 1. 默认是AndroidImageReaderProxy
ImageProxy image = imageReaderProxy.acquireNextImage();
if (image == null) {
return;
}
// 2. 回调image
ListenableFuture<Void> analyzeFuture = analyzeImage(image);
// Callback to close the image only after analysis complete regardless of success
Futures.addCallback(analyzeFuture, new FutureCallback<Void>() {
@Override
public void onSuccess(Void result) {
// No-op. Keep blocking the image reader until user closes the current one.
}
@Override
public void onFailure(Throwable t) {
image.close();
}
}, CameraXExecutors.directExecutor());
}
}
- 注释1处的
ImageReaderProxy是一个接口,在CameraX中有四个实现,如下图
实现类的确定是在ImageAnalysis.java里的createPipeline(),里面有如下关键代码
SessionConfig.Builder createPipeline(@NonNull String cameraId,
@NonNull ImageAnalysisConfig config, @NonNull Size resolution) {
// ...
// 1. 确定imageReader队列的缓存的最大个数
int imageQueueDepth =
getBackpressureStrategy() == STRATEGY_BLOCK_PRODUCER ? getImageQueueDepth()
: NON_BLOCKING_IMAGE_DEPTH;
SafeCloseImageReaderProxy imageReaderProxy;
// 2. 确定使用哪个ImageReaderProxy
if (config.getImageReaderProxyProvider() != null) {
imageReaderProxy = new SafeCloseImageReaderProxy(
config.getImageReaderProxyProvider().newInstance(
resolution.getWidth(), resolution.getHeight(), getImageFormat(),
imageQueueDepth, 0));
} else {
//3
imageReaderProxy =
new SafeCloseImageReaderProxy(ImageReaderProxys.createIsolatedReader(
resolution.getWidth(),
resolution.getHeight(),
getImageFormat(),
imageQueueDepth));
}
// ...
return sessionConfigBuilder;
}
1处注释是确定ImageRader的maxImages参数,这个可以通过用例配置setImageQueueDepth(n: Int)进行配置,2处注释是确定使用哪个ImageReadProxy,这里可以自己实现一个ImageReadProxy,我之前说的hook处理就是在这里实现的。所以配置可以是这样
val imageAnalysis = ImageAnalysis.Builder()
.setTargetResolution(Size(1280, 720))
// 通过该函数配置模式STRATEGY_BLOCK_PRODUCER或者STRATEGY_KEEP_ONLY_LATEST
.setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
.setImageQueueDepth(4)
.setImageReaderProxyProvider(object : ImageReaderProxyProvider {
override fun newInstance(width: Int, height: Int, format: Int, queueDepth: Int, usage: Long): ImageReaderProxy {
}
}).build()
当然如果没有自定义的ImageReaderProxy,跟进注释3中的代码则默认使用是AndroidImageReaderProxy,这个类其实就是利用最简单的代理模式重新实现了ImageReader的所有功能。其他几个代理会用到图片捕获(caputure)上
现在让我们回到ImageAnalysisBlockingAnalyzer这段代码,
final class ImageAnalysisBlockingAnalyzer extends ImageAnalysisAbstractAnalyzer {
@Override
public void onImageAvailable(@NonNull ImageReaderProxy imageReaderProxy) {
// 1. 默认是AndroidImageReaderProxy
ImageProxy image = imageReaderProxy.acquireNextImage();
if (image == null) {
return;
}
// 2. 回调image
ListenableFuture<Void> analyzeFuture = analyzeImage(image);
// Callback to close the image only after analysis complete regardless of success
Futures.addCallback(analyzeFuture, new FutureCallback<Void>() {
@Override
public void onSuccess(Void result) {
// No-op. Keep blocking the image reader until user closes the current one.
}
@Override
public void onFailure(Throwable t) {
image.close();
}
}, CameraXExecutors.directExecutor());
}
}
注释2处表示获取了Image之后的回调,这里的image类型是ImageProxy,它的实现自然是与之对应AndroidImageProxy, 这里使用的装饰者模式。分析analyzeImage,最后回调给开发者收到的image的实现类型是SettableImageProxy,如下代码所示
ListenableFuture<Void> analyzeImage(ImageProxy imageProxy) {
// ...
future = CallbackToFutureAdapter.getFuture(
completer -> {
executor.execute(() -> {
if (!isClosed()) {
// 得到image的相关信息
ImageInfo imageInfo = ImmutableImageInfo.create(
imageProxy.getImageInfo().getTagBundle(),
imageProxy.getImageInfo().getTimestamp(),
mRelativeRotation);
// 对AndroidImageProxy包装一层
analyzer.analyze(new SettableImageProxy(imageProxy, imageInfo));
completer.set(null);
} else {
completer.setException(new OperationCanceledException("Closed "
+ "before analysis"));
}
});
return "analyzeImage";
});
// ...
return future;
}
SettableImageProxy比AndroidImageProxy多了一些ImageInfo的信息,开发者到时需要使用到
- 返回的
Image的使用- 裁剪:使用
ImageProxy.setCropRect(Rect rect)即可, 其实内部也是使用的Image.setCropRect - 转成bitmap字节流: CameraX源码提供了这样的工具类
androidx.camera.core.internal.utils.ImageUtil
- 裁剪:使用
图片拍摄
使用
// 使用之前构造的用例
mImageCapture?.takePicture(mCameraExecutor, object : OnImageCapturedCallback() {
override fun onCaptureSuccess(image: ImageProxy) {
// 和分析图片用例一样,也是返回image
}
override fun onError(exception: ImageCaptureException) {
super.onError(exception)
}
})
两点说明:
- 可以看到,一样是返回imageProxy,格式默认是JPEG, 如需使用YUV_420_888,这个配置对拍照的速度替身还是比较明显的,原因是转字节流比较快。可以使用
setBufferFormat配置,但是有一定风险. - 这里面还有一个mCameraExecutor参数,这个是线程池实例,takePicture内部的耗时操作运行在这个线程池里。
裁剪
另外,为了保证预览界面和拍照界面的一致性,需要设置裁剪,如下
// 配置此处的原因是 预览界面与相机捕获界面一致
mImageCapture.setCropAspectRatio(Rational(applyWidthPixels, applyHeightPixel))
就是说如果picture捕获到的分辨率和你设置的分辨率不一致,需要裁剪成一致的比例。
旋转
由于相机硬件的不一致性,可能有些手机拍出来的图片会和你想要的角度(你在用例配置中的目标角度)的图片不一致,但没有关系,CameraX返回的image里含有你应该旋转多少度才能实现你的目标角度,比如下面的代码:
val bitmap = BitmapUtils
.decodeByteArrayInBitmap(ImageUtil.imageToJpegByteArray(image), sCameraBitmap, inSampleSize).rotateBitmap(image.imageInfo.rotationDegrees.toFloat())
图片的旋转信息通过这个image.imageInfo.rotationDegrees获得