多摄像头 API

注意:本页介绍的是 Camera2 软件包。除非您的应用需要 Camera2 的特定底层功能,否则我们建议您使用 CameraX。CameraX 和 Camera2 都支持 Android 5.0(API 级别 21)及更高版本。

多摄像头功能是在 Android 9(API 级别 28)中引入的。自该 API 发布以来,市场上已经出现了支持该 API 的设备。许多多摄像头用例都与特定的硬件配置紧密耦合。换句话说,并非所有用例都与每台设备兼容,因此多摄像头功能非常适合使用 Play Feature Delivery。

一些典型用例包括:

  • 变焦:根据剪裁区域或所选焦距在摄像头之间切换。
  • 深度:使用多个摄像头构建深度图。
  • 焦外成像:使用推断的深度信息来模拟类似单反相机的窄对焦范围。

逻辑摄像头和物理摄像头之间的区别

要了解多摄像头 API,需要先了解逻辑摄像头和物理摄像头之间的区别。例如,假设有一台设备配有三个后置摄像头。在此示例中,三个后置摄像头中的每一个都被视为一个物理摄像头。逻辑摄像头则是其中两个或更多实体摄像头的组合。逻辑摄像头的输出可以是来自其中一个底层物理摄像头的视频流,也可以是同时来自多个底层物理摄像头的融合视频流。无论哪种情况,数据流都由相机硬件抽象层 (HAL) 处理。

许多手机制造商都会开发第一方相机应用,这些应用通常会预安装在他们的设备上。为了使用硬件的所有功能,它们可能会使用私有或隐藏的 API,或者从驱动程序实现中获得其他应用无法获得的特殊处理。有些设备通过提供来自不同物理摄像头的融合帧流来实现逻辑摄像头的概念,但仅限于某些特权应用。通常,只有一个物理摄像头会向框架公开。下图说明了 Android 9 之前第三方开发者面临的情况:

通常仅适用于特权应用的相机功能
图 1. 通常仅适用于特权应用的相机功能

从 Android 9 开始,Android 应用中不再允许使用私有 API。 由于框架中包含多摄像头支持,Android 最佳实践强烈建议手机制造商为朝向同一方向的所有物理摄像头提供逻辑摄像头。以下是第三方开发者在搭载 Android 9 及更高版本的设备上应该看到的内容:

从 Android 9 开始,开发者可以完全访问所有相机设备
图 2. 从 Android 9 开始,开发者可以完全访问所有相机设备

逻辑摄像头提供的内容完全取决于相机 HAL 的 OEM 实现。例如,Pixel 3 等设备实现其逻辑摄像头的方式是,根据请求的焦距和剪裁区域选择其物理摄像头之一。

多摄像头 API

新 API 添加了以下新常量、类和方法:

由于 Android 兼容性定义文档 (CDD) 发生了变化,多摄像头 API 也需要满足开发者的一些预期。在 Android 9 之前,市场上就已经有双摄像头设备,但同时打开多个摄像头需要反复试错。在 Android 9 及更高版本中,多摄像头功能提供了一组规则,用于指定何时可以打开属于同一逻辑摄像头的一对物理摄像头。

在大多数情况下,搭载 Android 9 及更高版本的设备会公开所有物理摄像头(可能不包括红外摄像头等不太常见的传感器类型),以及一个更易于使用的逻辑摄像头。对于保证可以正常工作的每种信息流组合,属于逻辑摄像头的一个信息流可以替换为来自底层物理摄像头的两个信息流。

同时观看多个直播

同时使用多个摄像头画面介绍了在单个摄像头中同时使用多个画面的规则。除了一个值得注意的补充之外,相同的规则也适用于多个摄像头。CameraMetadata.REQUEST_AVAILABLE_CAPABILITIES_LOGICAL_MULTI_CAMERA 介绍了如何将一个逻辑 YUV_420_888 或 RAW 信息流替换为两个物理信息流。也就是说,每个 YUV 或 RAW 类型的信息流都可以替换为两个类型和大小相同的信息流。您可以从单摄像头设备的以下有保证配置的摄像头视频流开始:

  • 数据流 1:YUV 类型,来自逻辑相机 id = 0 的 MAXIMUM 大小

然后,在支持多摄像头的设备上,您可以创建一个会话,将该逻辑 YUV 流替换为两个物理流:

  • 信息流 1:YUV 类型,来自物理摄像头 id = 1 的 MAXIMUM 大小
  • 信息流 2:YUV 类型,来自物理摄像头 id = 2 的 MAXIMUM 大小

当且仅当这两个摄像头属于逻辑摄像头分组(列在 CameraCharacteristics.getPhysicalCameraIds() 下)时,您才能将一个 YUV 或 RAW 信息流替换为两个等效信息流。

框架提供的保证只是同时从多个物理摄像头获取帧所需的最低要求。大多数设备都支持其他数据流,有时甚至允许独立打开多个物理摄像头设备。由于框架无法提供硬性保证,因此需要通过反复试验来执行每设备测试和调优。

创建包含多个实体摄像头的会话

在支持多摄像头的设备上使用物理摄像头时,请打开单个 CameraDevice(逻辑摄像头),并在单个会话中与其互动。使用 API 级别 28 中添加的 API CameraDevice.createCaptureSession(SessionConfiguration config) 创建单个会话。会话配置具有多个输出配置,每个输出配置都有一组输出目标,还可以选择一个物理摄像头 ID。

SessionConfiguration 和 OutputConfiguration 模型
图 3. SessionConfiguration 和 OutputConfiguration 模型

捕获请求具有与其关联的输出目标。框架会根据附加的输出目标确定将请求发送到哪个物理(或逻辑)摄像头。如果输出目标与作为输出配置连同物理摄像头 ID 一起发送的输出目标之一相对应,则该物理摄像头会接收并处理请求。

使用一对物理摄像头

多摄像头 API 的另一项新增功能是能够识别逻辑摄像头并找出其背后的物理摄像头。您可以定义一个函数,以帮助识别可用于替换其中一个逻辑摄像头视频流的潜在实体摄像头对:

/**
 * Helper class used to encapsulate a logical camera and two underlying
 * physical cameras
 */
data class DualCamera(val logicalId: String, val physicalId1: String, val physicalId2: String)

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

    // Iterate over all the available camera characteristics.
    manager.cameraIdList.map {
        Pair(manager.getCameraCharacteristics(it), it)
    }.filter {
        // Filter by cameras facing the requested direction.
        facing == null || it.first.get(CameraCharacteristics.LENS_FACING) == facing
    }.filter {
        // Filter by logical cameras.
        // CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES_LOGICAL_MULTI_CAMERA requires API >= 28.
        it.first.get(CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES)?.contains(
            CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES_LOGICAL_MULTI_CAMERA
        ) ?: false
    }.forEach {
        // All possible pairs from the list of physical cameras are valid results.
        // NOTE: There could be N physical cameras as part of a logical camera grouping.
        // getPhysicalCameraIds() requires API >= 28.
        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
}

物理摄像头的状态处理由逻辑摄像头控制。如需打开“双摄像头”,请打开与物理摄像头对应的逻辑摄像头:

fun openDualCamera(
    cameraManager: CameraManager,
    dualCamera: DualCamera,
    executor: Executor,
    callback: (CameraDevice) -> Unit
) {

    // openCamera() requires API >= 28.
    cameraManager.openCamera(
        dualCamera.logicalId, executor,
        object : CameraDevice.StateCallback() {
            override fun onOpened(device: CameraDevice) = callback(device)
            // Omitting for brevity...
            override fun onError(device: CameraDevice, error: Int) = onDisconnected(device)
            override fun onDisconnected(device: CameraDevice) = device.close()
        }
    )
}

除了选择要打开的摄像头之外,该过程与在过去的 Android 版本中打开摄像头相同。使用新的会话配置 API 创建拍摄会话,以告知框架将特定目标与特定物理摄像头 ID 相关联:

/**
 * Helper type definition that encapsulates 3 sets of output targets:
 *
 * 1. Logical camera
 * 2. First physical camera
 * 3. Second physical camera
 */
typealias DualCameraOutputs =
    Triple<MutableList<Surface>?, MutableList<Surface>?, MutableList<Surface>?>

fun createDualCameraSession(
    cameraManager: CameraManager,
    dualCamera: DualCamera,
    targets: DualCameraOutputs,
    executor: Executor,
    callback: (CameraCaptureSession) -> Unit
) {

    // Create 3 sets of output configurations: one for the logical camera, and
    // one for each of the physical cameras.
    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) }
    }

    // Put all the output configurations into a single flat array.
    val outputConfigsAll = arrayOf(
        outputConfigsLogical, outputConfigsPhysical1, outputConfigsPhysical2
    )
        .filterNotNull().flatMap { it }

    // Instantiate a session configuration that can be used to create a session.
    val sessionConfiguration = SessionConfiguration(
        SessionConfiguration.SESSION_REGULAR,
        outputConfigsAll, executor,
        object : CameraCaptureSession.StateCallback() {
            override fun onConfigured(session: CameraCaptureSession) = callback(session)
            // Omitting for brevity...
            override fun onConfigureFailed(session: CameraCaptureSession) = session.device.close()
        }
    )

    // Open the logical camera using the previously defined function.
    openDualCamera(cameraManager, dualCamera, executor = executor) {

        // Finally create the session and return using the callback.
        it.createCaptureSession(sessionConfiguration)
    }
}

如需了解支持哪些数据流组合,请参阅 createCaptureSession。组合数据流适用于单个逻辑摄像头上的多个数据流。兼容性还扩展到使用相同的配置,并将其中一个信息流替换为来自同一逻辑摄像头中两个物理摄像头的两个信息流。

相机拍摄会话准备就绪后,分派所选的拍摄请求。拍摄请求的每个目标都会从其关联的物理摄像头(如果有正在使用的物理摄像头)接收数据,或者回退到逻辑摄像头。

Zoom 示例用例

可以将多个物理摄像头合并为一个数据流,以便用户在不同的物理摄像头之间切换,从而体验不同的视野范围,有效捕捉不同的“缩放级别”。

缩放级别用例的摄像头切换示例
图 4. 切换摄像头以实现缩放级别用例的示例(来自 Pixel 3 广告)

首先,选择一对可供用户切换的物理摄像头。为达到最佳效果,您可以选择提供最小和最大可用焦距的一对摄像头。

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)

        // Query the focal lengths advertised by each physical camera.
        val focalLengths1 = characteristics1.get(
            CameraCharacteristics.LENS_INFO_AVAILABLE_FOCAL_LENGTHS
        ) ?: floatArrayOf(0F)
        val focalLengths2 = characteristics2.get(
            CameraCharacteristics.LENS_INFO_AVAILABLE_FOCAL_LENGTHS
        ) ?: floatArrayOf(0F)

        // Compute the largest difference between min and max focal lengths between cameras.
        val focalLengthsDiff1 = (focalLengths2.maxOrNull() ?: 0f) - (focalLengths1.minOrNull() ?: 0f)
        val focalLengthsDiff2 = (focalLengths1.maxOrNull() ?: 0f) - (focalLengths2.minOrNull() ?: 0f)

        // Return the pair of camera IDs and the difference between min and max focal lengths.
        if (focalLengthsDiff1 < focalLengthsDiff2) {
            Pair(DualCamera(it.logicalId, it.physicalId1, it.physicalId2), focalLengthsDiff1)
        } else {
            Pair(DualCamera(it.logicalId, it.physicalId2, it.physicalId1), focalLengthsDiff2)
        }

        // Return only the pair with the largest difference, or null if no pairs are found.
    }.maxByOrNull { it.second }?.first
}

对此,一种合理的架构是使用两个 SurfaceViews,每个数据流一个。这些 SurfaceViews 会根据用户互动进行切换,因此在任何给定时间,只有一个可见。

以下代码展示了如何打开逻辑摄像头、配置摄像头输出、创建摄像头会话以及启动两个预览流:

val cameraManager: CameraManager = ...
val cameraExecutor: Executor = ...  // owned by your activity / fragment

// Get the two output targets from the activity / fragment.
val surface1 = ...  // from SurfaceView
val surface2 = ...  // from SurfaceView

val dualCamera = findShortLongCameraPair(cameraManager) ?: return
val outputTargets = DualCameraOutputs(
    null, mutableListOf(surface1), mutableListOf(surface2)
)

// Here you open the logical camera, configure the outputs and create a session.
createDualCameraSession(
    cameraManager, dualCamera, targets = outputTargets, executor = cameraExecutor
) { session ->

    // Create a single request which has one target for each physical camera.
    // NOTE: Each target receive frames from only its associated physical camera.
    val requestTemplate = CameraDevice.TEMPLATE_PREVIEW
    val captureRequest = session.device.createCaptureRequest(requestTemplate).apply {
        arrayOf(surface1, surface2).forEach { addTarget(it) }
    }.build()

    // Set the sticky request for the session and you are done.
    session.setRepeatingRequest(captureRequest, null, null)
}

剩下的工作就是为用户提供一个界面,以便在两个 Surface 之间切换,例如按钮或点按两次 SurfaceView。您甚至可以执行某种形式的场景分析,并在两个视频流之间自动切换。

镜头畸变

所有镜头都会产生一定程度的失真。在 Android 中,您可以使用 CameraCharacteristics.LENS_DISTORTION 查询镜头造成的失真,该方法取代了现已弃用的 CameraCharacteristics.LENS_RADIAL_DISTORTION。对于逻辑摄像头,失真极小,您的应用可以或多或少地使用来自摄像头的帧。对于实体摄像头,镜头配置可能大不相同,尤其是广角镜头。

某些设备可能会使用 CaptureRequest.DISTORTION_CORRECTION_MODE 实现自动失真校正。对于大多数设备,失真校正功能默认处于开启状态。

val cameraSession: CameraCaptureSession = ...

// Use still capture template to build the capture request.
val captureRequest = cameraSession.device.createCaptureRequest(
    CameraDevice.TEMPLATE_STILL_CAPTURE
)

// Determine if this device supports distortion correction.
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
    )
}

// Add output target, set other capture request parameters...

// Dispatch the capture request.
cameraSession.capture(captureRequest.build(), ...)

在此模式下设置拍摄请求可能会影响相机可产生的帧速率。您可以选择仅对静态图片拍摄设置失真校正。