专栏名称: Android_开发者
目录
相关文章推荐
51好读  ›  专栏  ›  Android_开发者

[译] 如何在 Android 开发中充分利用多摄像头 API

Android_开发者  · 掘金  · android  · 2019-03-22 01:21

正文

阅读 8

[译] 如何在 Android 开发中充分利用多摄像头 API

这篇博客是对我们的 Android 开发者峰会 2018 演讲 的补充,是与来自合作伙伴开发者团队中的 Vinit Modi、Android Camera PM 和 Emilie Roberts 合作完成的。查看我们之前在该系列中的文章,包括 相机枚举 相机拍摄会话和请求 同时使用多个摄像机流

多摄像头用例

多摄像头是在 Android Pie 中引入的,自几个月前发布以来,现现在已有多个支持该 API 的设备进入了市场,比如谷歌 Pixel 3 和华为 Mate 20 系列。许多多摄像头用例与特定的硬件配置紧密结合;换句话说,并非所有的用例都适配每台设备 — 这使得多摄像头功能成为模块 动态传输 的一个理想选择。一些典型的用例包括:

  • 缩放:根据裁剪区域或所需焦距在相机之间切换
  • 深度:使用多个摄像头构建深度图
  • 背景虚化:使用推论的深度信息来模拟类似 DSLR(digital single-lens reflex camera)的窄焦距范围

逻辑和物理摄像头

要了解多摄像头 API,我们必须首先了解逻辑摄像头和物理摄像头之间的区别;这个概念最好用一个例子来说明。例如,我我们可以想像一个有三个后置摄像头而没有前置摄像头的设备。在本例中,三个后置摄像头中的每一个都被认为是一个物理摄像头。然后逻辑摄像头就是两个或更多这些物理摄像头的分组。逻辑摄像头的输出可以是来自其中一个底层物理摄像机的一个流,也可以是同时来自多个底层物理摄像机的融合流;这两种方式都是由相机的 HAL(Hardware Abstraction Layer)来处理的。

许多手机制造商也开发了他们自身的相机应用程序(通常预先安装在他们的设备上)。为了利用所有硬件的功能,他们有时会使用私有或隐藏的 API,或者从驱动程序实现中获得其他应用程序没有特权访问的特殊处理。有些设备甚至通过提供来自不同物理双摄像头的融合流来实现逻辑摄像头的概念,但同样,这只对某些特权应用程序可用。通常,框架只会暴露一个物理摄像头。Android Pie 之前第三方开发者的情况如下图所示:

相机功能通常只对特权应用程序可用

从 Android Pie 开始,一些事情发生了变化。首先,在 Android 应用程序中使用 私有 API 不再可行 。其次,Android 框架中包含了 多摄像头支持 ,Android 已经 强烈推荐 手机厂商为面向同一方向的所有物理摄像头提供逻辑摄像头。因此,这是第三方开发人员应该在运行 Android Pie 及以上版本的设备上看到的内容:

开发人员可完全访问从 Android P 开始的所有摄像头设备

值得注意的是,逻辑摄像头提供的功能完全依赖于相机 HAL 的 OEM 实现。例如,像 Pixel 3 是根据请求的焦距和裁剪区域选择其中一个物理摄像头,用于实现其逻辑相机。

多摄像头 API

新 API 包含了以下新的常量、类和方法:

  • CameraMetadata.REQUEST_AVAILABLE_CAPABILITIES_LOGICAL_MULTI_CAMERA
  • CameraCharacteristics.getPhysicalCameraIds()
  • CameraCharacteristics.getAvailablePhysicalCameraRequestKeys()
  • CameraDevice.createCaptureSession(SessionConfiguration config)
  • CameraCharactersitics.LOGICAL_MULTI_CAMERA_SENSOR_SYNC_TYPE
  • OutputConfiguration & SessionConfiguration

由于 Android CDD 的更改,多摄像头 API 也满足了开发人员的某些期望。双摄像头设备在 Android Pie 之前就已经存在,但同时打开多个摄像头需要反复试验;Android 上的多摄像头 API 现在给了我们一组规则,告诉我们什么时候可以打开一对物理摄像头,只要它们是同一逻辑摄像头的一部分。

如上所述,我们可以预期,在大多数情况下,使用 Android Pie 发布的新设备将公开所有物理摄像头(除了更奇特的传感器类型,如红外线),以及更容易使用的逻辑摄像头。此外,非常关键的是,我们可以预期,对于每个保证有效的融合流,属于逻辑摄像头的一个流可以被来自底层物理摄像头的 两个 流替换。让我们通过一个例子更详细地介绍它。

同时使用多个流

在上一篇博文中,我们详细介绍了在单个摄像头中 同时使用多个流 的规则。同样的规则也适用于多个摄像头,但在 这个文档 中有一个值得注意的补充说明:

对于每个有保证的融合流,逻辑摄像头都支持将一个逻辑 YUV_420_888 或原始流替换为两个相同大小和格式的物理流,每个物理流都来自一个单独的物理摄像头,前提是两个物理摄像头都支持给定的大小和格式。

换句话说,YUV 或 RAW 类型的每个流可以用相同类型和大小的两个流替换。例如,我们可以从单摄像头设备的摄像头视频流开始,配置如下:

  • 流 1:YUV 类型, id = 0 的逻辑摄像机的最大尺寸

然后,一个支持多摄像头的设备将允许我们创建一个会话,用两个物理流替换逻辑 YUV 流:

  • 流 1:YUV 类型, id = 1 的物理摄像头的最大尺寸
  • 流 2:YUV 类型, id = 2 的物理摄像头的最大尺寸

诀窍是,当且仅当这两个摄像头是一个逻辑摄像头分组的一部分时,我们可以用两个等效的流替换 YUV 或原始流 — 即被列在 CameraCharacteristics.getPhysicalCameraIds() 中的。

另一件需要考虑的事情是,框架提供的保证仅仅是同时从多个物理摄像头获取帧的最低要求。我们可以期望在大多数设备中支持额外的流,有时甚至允许我们独立地打开多个物理摄像头设备。不幸的是,由于这不是框架的硬性保证,因此需要我们通过反复试验来执行每个设备的测试和调优。

使用多个物理摄像头创建会话

当我们在一个支持多摄像头的设备中与物理摄像头交互时,我们应该打开一个 CameraDevice (逻辑相机),并在一个会话中与它交互,这个会话必须使用 API CameraDevice.createCaptureSession(SessionConfiguration config) 创建,这个 API 自 SDK 级别 28 起可用。然后,这个 会话参数 将有很多 输出配置 ,其中每个输出配置将具有一组输出目标,以及(可选的)所需的物理摄像头 ID。

会话参数和输出配置模型

稍后,当我们分派拍摄请求时,该请求将具有与其关联的输出目标。框架将根据附加到请求的输出目标来决定将请求发送到哪个物理(或逻辑)摄像头。如果输出目标对应于作为 输出配置 的输出目标之一和物理摄像头 ID 一起发送,那么该物理摄像头将接收并处理该请求。

使用一对物理摄像头

面向开发人员的多摄像头 API 中最重要的一个新增功能是识别逻辑摄像头并找到它们背后的物理摄像头。现在我们明白,我们可以同时打开多个物理摄像头(再次,通过打开逻辑摄像头和作为同一会话的一部分),并且有明确的融合流的规则,我们可以定义一个函数来帮助我们识别潜在的可以用来替换一个逻辑摄像机视频流的一对物理摄像头:

/**
* 帮助类,用于封装逻辑摄像头和两个底层
* 物理摄像头
*/
data class DualCamera(val logicalId: String, val physicalId1: String, val physicalId2: String)

fun findDualCameras(manager: CameraManager, facing: Int? = null): Array<DualCamera> {
    val dualCameras = ArrayList<DualCamera>()

    // 遍历所有可用的摄像头特征
    manager.cameraIdList.map {
        Pair(manager.getCameraCharacteristics(it), it)
    }.filter {
        // 通过摄像头的方向这个请求参数进行过滤
        facing == null || it.first.get(CameraCharacteristics.LENS_FACING) == facing
    }.filter {
        // 逻辑摄像头过滤
        it.first.get(CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES)!!.contains(
                CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES_LOGICAL_MULTI_CAMERA)
    }.forEach {
        // 物理摄像头列表中的所有可能对都是有效结果
        // 注意:可能有 N 个物理摄像头作为逻辑摄像头分组的一部分
        val physicalCameras = it.first.physicalCameraIds.toTypedArray()
        for (idx1 in 0 until physicalCameras.size) {
            for






请到「今天看啥」查看全文