[译] 充分利用多摄像头 API

栏目: IOS · Android · 发布时间: 5年前

内容简介:这篇博客是对我们的多摄像头是在Android Pie 中引入的,自几个月前发布以来,现现在已有多个支持该 API 的设备进入了市场,比如谷歌 Pixel 3 和华为 Mate 20 系列。许多多摄像头用例与特定的硬件配置紧密结合;换句话说,并非所有的用例都适配每台设备 — 这使得多摄像头功能成为模块动态传输 的一个理想选择。一些典型的用例包括:要了解多摄像头 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 之前第三方开发者的情况如下图所示:

[译] 充分利用多摄像头 API

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

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

[译] 充分利用多摄像头 API

开发人员可完全访问从 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 流:

id = 1
id = 2

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

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

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

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

[译] 充分利用多摄像头 API

会话参数和输出配置模型

稍后,当我们分派拍摄请求时,该请求将具有与其关联的输出目标。框架将根据附加到请求的输出目标来决定将请求发送到哪个物理(或逻辑)摄像头。如果输出目标对应于作为输出配置 的输出目标之一和物理摄像头 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 (idx2 in (idx1 + 1) until physicalCameras.size) {
                dualCameras.add(DualCamera(
                        it.second, physicalCameras[idx1], physicalCameras[idx2]))
            }
        }
    }

    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 组输出目标的类型:
 *
 *   1. 逻辑摄像头
 *   2. 第一个物理摄像头
 *   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)
    }
}
复制代码

现在,我们可以参考文档 或以前的博客文章 来了解支持哪些流的融合。我们只需要记住这些是针对单个逻辑摄像头上的多个流的,并且兼容使用相同的配置的并将其中一个流替换为来自同一逻辑摄像头的两个物理摄像头的两个流。

在摄像头会话 就绪后,剩下要做的就是发送我们想要的拍摄请求。拍摄请求的每个目标将从相关的物理摄像头(如果有的话)接收数据,或者返回到逻辑摄像头。

缩放示例用例

为了将所有这一切与最初讨论的用例之一联系起来,让我们看看如何在我们的相机应用程序中实现一个功能,以便用户能够在不同的物理摄像头之间切换,体验到不同的视野——有效地拍摄不同的“缩放级别”。

[译] 充分利用多摄像头 API

将相机转换为缩放级别用例的示例(来自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 = ...  // 来自 SurfaceView
val surface2 = ...  // 来自 SurfaceView

val dualCamera = findShortLongCameraPair(manager)!!
val outputTargets = DualCameraOutputs(
        null, mutableListOf(surface1), mutableListOf(surface2))

// 在这里,我们打开逻辑摄像头,配置输出并创建一个会话
createDualCameraSession(manager, dualCamera, targets = outputTargets) { session ->

    // 为每个物理相头创建一个目标的单一请求
    // 注意:每个目标只会从它相关的物理相头接收帧
    val requestTemplate = CameraDevice.TEMPLATE_PREVIEW
    val 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 = characteristics.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(), ...)
复制代码

请记住,在这种模式下设置拍摄请求将对相机可以产生的帧速率产生潜在的影响,这就是为什么我们只在静态图像拍摄中设置设置校正。


以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

轻量级Django

轻量级Django

茱莉亚·埃尔曼 (Julia Elman)、马克·拉温 (Mark Lavin) / 侯荣涛、吴磊 / 中国电力出版社; 第1版 / 2016-11-1 / 35.6

自Django 创建以来,各种各样的开源社区已经构建了很多Web 框架,比如JavaScript 社区创建的Angular.js 、Ember.js 和Backbone.js 之类面向前端的Web 框架,它们是现代Web 开发中的先驱。Django 从哪里入手来适应这些框架呢?我们如何将客户端MVC 框架整合成为当前的Django 基础架构? 本书讲述如何利用Django 强大的“自支持”功......一起来看看 《轻量级Django》 这本书的介绍吧!

图片转BASE64编码
图片转BASE64编码

在线图片转Base64编码工具

Markdown 在线编辑器
Markdown 在线编辑器

Markdown 在线编辑器

RGB CMYK 转换工具
RGB CMYK 转换工具

RGB CMYK 互转工具