[译] 充分利用多摄像头 API,30 分钟轻松入门 flutter
}
return dualCameras.toTypedArray()}
物理摄像头的状态处理由逻辑摄像头控制。因此,要打开我们的“双摄像头”,我们只需要打开与我们感兴趣的物理摄像头相对应的逻辑摄像头:
fun openDualCamera(cameraManager: CameraManager,dualCamera: DualCamera,executor: Executor = AsyncTask.SERIAL_EXECUTOR,callback: (CameraDevice) -> Unit) {
cameraManager.openCamera(dualCamera.logicalId, executor, object : CameraDevice.StateCallback() {override fun onOpened(device: CameraDevice) = callback(device)// 为了简便起见,我们省略...override fun onError(device: CameraDevice, error: Int) = onDisconnected(device)override fun onDisconnected(device: CameraDevice) = device.close()})}
在此之前,除了选择打开哪台摄像头之外,没有什么不同于我们过去打开任何其他摄像头所做的事情。现在是时候使用新的 会话参数 API 创建一个拍摄会话了,这样我们就可以告诉框架将某些目标与特定的物理摄像机 ID 关联起来:
/**
帮助类,封装了定义 3 组输出目标的类型:
逻辑摄像头
第一个物理摄像头
第二个物理摄像头*/typealias DualCameraOutputs =Triple<MutableList<Surface>?, MutableList<Surface>?, MutableList<Surface>?>
fun createDualCameraSession(cameraManager: CameraManager,dualCamera: DualCamera,targets: DualCameraOutputs,executor: Executor = AsyncTask.SERIAL_EXECUTOR,callback: (CameraCaptureSession) -> Unit) {
// 创建三组输出配置:一组用于逻辑摄像头,// 另一组用于逻辑摄像头。val outputConfigsLogical = targets.first?.map { OutputConfiguration(it) }val outputConfigsPhysical1 = targets.second?.map {OutputConfiguration(it).apply { setPhysicalCameraId(dualCamera.physicalId1) } }val outputConfigsPhysical2 = targets.third?.map {OutputConfiguration(it).apply { setPhysicalCameraId(dualCamera.physicalId2) } }
// 将所有输出配置放入单个数组中 val outputConfigsAll = arrayOf(outputConfigsLogical, outputConfigsPhysical1, outputConfigsPhysical2).filterNotNull().flatMap { it }
// 实例化可用于创建会话的会话配置 val sessionConfiguration = SessionConfiguration(SessionConfiguration.SESSION_REGULAR,outputConfigsAll, executor, object : CameraCaptureSession.StateCallback() {override fun onConfigured(session: CameraCaptureSession) = callback(session)// 省略...override fun onConfigureFailed(session: CameraCaptureSession) = session.device.close()})
// 使用前面定义的函数打开逻辑摄像头 openDualCamera(cameraManager, dualCamera, executor = executor) {
// 最后创建会话并通过回调返回 it.createCaptureSession(sessionConfiguration)}}
现在,我们可以参考 文档 或 以前的博客文章 来了解支持哪些流的融合。我们只需要记住这些是针对单个逻辑摄像头上的多个流的,并且兼容使用相同的配置的并将其中一个流替换为来自同一逻辑摄像头的两个物理摄像头的两个流。
在 摄像头会话 就绪后,剩下要做的就是发送我们想要的 拍摄请求。拍摄请求的每个目标将从相关的物理摄像头(如果有的话)接收数据,或者返回到逻辑摄像头。
缩放示例用例
为了将所有这一切与最初讨论的用例之一联系起来,让我们看看如何在我们的相机应用程序中实现一个功能,以便用户能够在不同的物理摄像头之间切换,体验到不同的视野——有效地拍摄不同的“缩放级别”。
将相机转换为缩放级别用例的示例(来自 Pixel 3 Ad)
首先,我们必须选择我们想允许用户在其中进行切换的一对物理摄像机。为了获得最大的效果,我们可以分别搜索提供最小焦距和最大焦距的一对摄像机。通过这种方式,我们选择一种可以在尽可能短的距离上对焦的摄像设备,另一种可以在尽可能远的点上对焦:
fun findShortLongCameraPair(manager: CameraManager, facing: Int? = null): DualCamera? {
return findDualCameras(manager, facing).map {val characteristics1 = manager.getCameraCharacteristics(it.physicalId1)val characteristics2 = manager.getCameraCharacteristics(it.physicalId2)
// 查询每个物理摄像头公布的焦距 val focalLengths1 = characteristics1.get(CameraCharacteristics.LENS_INFO_AVAILABLE_FOCAL_LENGTHS) ?: floatArrayOf(0F)val focalLengths2 = characteristics2.get(CameraCharacteristics.LENS_INFO_AVAILABLE_FOCAL_LENGTHS) ?: floatArrayOf(0F)
// 计算相机之间最小焦距和最大焦距之间的最大差异 val focalLengthsDiff1 = focalLengths2.max()!! - focalLengths1.min()!!val focalLengthsDiff2 = focalLengths1.max()!! - focalLengths2.min()!!
// 返回相机 ID 和最小焦距与最大焦距之间的差值 if (focalLengthsDiff1 < focalLengthsDiff2) {Pair(DualCamera(it.logicalId, it.physicalId1, it.physicalId2), focalLengthsDiff1)} else {Pair(DualCamera(it.logicalId, it.physicalId2, it.physicalId1), focalLengthsDiff2)}
// 只返回差异最大的对,如果没有找到对,则返回 null}.sortedBy { it.second }.reversed().lastOrNull()?.first}
一个合理的架构应该是有两个 SurfaceViews,每个流一个,在用户交互时交换,因此在任何给定的时间只有一个是可见的。在下面的代码片段中,我们将演示如何打开逻辑摄像头、配置摄像头输出、创建摄像头会话和启动两个预览流;利用前面定义的功能:
val cameraManager: CameraManager = ...
// 从 activity/fragment 中获取两个输出目标 val surface1 = ... // 来自 SurfaceViewval surface2 = ... // 来自 SurfaceView
val dualCamera = findShortLongCameraPair(manager)!!val outputTargets = DualCameraOutputs(null, mutableListOf(surface1), mutableListOf(surface2))
// 在这里,我们打开逻辑摄像头,配置输出并创建一个会话 createDualCameraSession(manager, dualCamera, targets = outputTargets) { session ->
// 为每个物理相头创建一个目标的单一请求// 注意:每个目标只会从它相关的物理相头接收帧 val requestTemplate = CameraDevice.TEMPLATE_PREVIEWval captureRequest = session.device.createCaptureRequest(requestTemplate).apply {arrayOf(surface1, surface2).forEach { addTarget(it) }}.build()
// 设置会话的粘性请求,就完成了 session.setRepeatingRequest(captureRequest, null, null)}
现在我们需要做的就是为用户提供一个在两个界面之间切换的 UI,比如一个按钮或者双击 “SurfaceView”;如果我们想变得更有趣,我们可以尝试执行某种形式的场景分析,并在两个流之间自动切换。
镜头失真
所有的镜头都会产生一定的失真。在 Android 中,我们可以使用 CameraCharacteristics.LENS_DISTORTION(它替换了现在已经废弃的 CameraCharacteristics.LENS_RADIAL_DISTORTION)查询镜头创建的失真。可以合理地预期,对于逻辑摄像头,失真将是最小的,我们的应用程序可以使用或多或少的框架,因为他们来自这个摄像头。然而,对于物理摄像头,我们应该期待潜在的非常不同的镜头配置——特别是在广角镜头上。
一些设备可以通过 CaptureRequest.DISTORTION_CORRECTION_MODE 实现自动失真校正。很高兴知道大多数设备的失真校正默认为开启。文档中有一些更详细的信息:
FAST/HIGH_QUALITY 均表示将应用相机设备确定的失真校正。HIGH_QUALITY 模式表示相机设备将使用最高质量的校正算法,即使它会降低捕获率。快速意味着相机设备在应用校正时不会降低捕获率。如果任何校正都会降低捕获速率,则 FAST 可能与 OFF 相同 [...] 校正仅适用于 YUV、JPEG 或 DEPTH16 等已处理的输出 [...] 默认情况下,此控件将在支持此功能的设备上启用控制。
如果我们想用最高质量的物理摄像头拍摄一张照片,那么我们应该尝试将校正模式设置为 HIGH_QUALITY(如果可用)。下面是我们应该如何设置拍摄请求:
val cameraSession: CameraCaptureSession = ...
// 使用静态拍摄模板来构建拍摄请求 val captureRequest = cameraSession.device.createCaptureRequest(CameraDevice.TEMPLATE_STILL_CAPTURE)
// 确定该设备是否支持失真校正 val characteristics: CameraCharacteristics = ...val supportsDistortionCorrection = characterist
ics.get(CameraCharacteristics.DISTORTION_CORRECTION_AVAILABLE_MODES)?.contains(CameraMetadata.DISTORTION_CORRECTION_MODE_HIGH_QUALITY) ?: false
if (supportsDistortionCorrection) {captureRequest.set(CaptureRequest.DISTORTION_CORRECTION_MODE,CameraMetadata.DISTORTION_CORRECTION_MODE_HIGH_QUALITY)}
// 添加输出目标,设置其他拍摄请求参数...
// 发送拍摄请求 cameraSession.capture(captureRequest.build(), ...)
请记住,在这种模式下设置拍摄请求将对相机可以产生的帧速率产生潜在的影响,这就是为什么我们只在静态图像拍摄中设置设置校正。
未完待续
唷!我们介绍了很多与新的多摄像头 API 相关的东西:
潜在的用例
逻辑摄像头 vs 物理摄像头
多摄像头 API 概述
评论