摄像头捕获会话和请求

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

单个 Android 设备可以有多个摄像头。每个摄像头都是一个 CameraDevice,并且一个 CameraDevice 可以同时输出多个流。

这样做的一个原因是为了使一个流(来自 CameraDevice 的连续摄像头帧)针对特定任务(例如显示取景器)进行优化,而其他流则可用于拍照或录制视频。这些流充当并行管道,每次处理一个从摄像头输出的原始帧。

图 1. 来自“构建通用摄像头应用”(Google I/O ‘18) 的插图

并行处理表明,性能可能会受到 CPU、GPU 或其他处理器可用处理能力的限制。如果某个管道无法跟上传入的帧,它就会开始丢弃帧。

每个管道都有自己的输出格式。传入的原始数据通过与每个管道相关的隐式逻辑自动转换为适当的输出格式。本页面代码示例中使用的 CameraDevice 是非特定的,因此在继续之前,您需要先枚举所有可用的摄像头。

您可以使用 CameraDevice 创建一个 CameraCaptureSession,该会话专用于该 CameraDevice。一个 CameraDevice 必须使用 CameraCaptureSession 为每个原始帧接收一个帧配置。该配置指定了摄像头属性,例如自动对焦、光圈、效果和曝光。由于硬件限制,摄像头传感器在任何给定时间只能有一个活动配置,这称为活动配置。

然而,流用例(Stream Use Cases)增强并扩展了以前使用 CameraDevice 进行流式捕获会话的方式,这使您可以针对特定用例优化摄像头流。例如,在优化视频通话时,它可以改善电池续航。

一个 CameraCaptureSession 描述了绑定到 CameraDevice 的所有可能管道。创建会话后,您无法添加或移除管道。CameraCaptureSession 维护一个 CaptureRequest 队列,这些请求将成为活动配置。

一个 CaptureRequest 会向队列添加配置,并选择一个、多个或所有可用管道以从 CameraDevice 接收帧。在捕获会话的生命周期中,您可以发送许多捕获请求。每个请求都可以更改活动配置和接收原始图像的输出管道集。

使用流用例以获得更好的性能

流用例是一种提高 Camera2 捕获会话性能的方法。它们为硬件设备提供了更多信息来调整参数,从而为您的特定任务提供更好的摄像头体验。

这允许摄像头设备根据每个流的用户场景优化摄像头硬件和软件管道。有关流用例的更多信息,请参阅 setStreamUseCase

除了在 CameraDevice.createCaptureRequest() 中设置模板外,流用例还允许您更详细地指定特定摄像头流的用途。这使得摄像头硬件能够根据适合特定用例的质量或延迟权衡来优化参数,例如调优、传感器模式或摄像头传感器设置。

流用例包括

  • DEFAULT:涵盖所有现有应用行为。它相当于未设置任何流用例。

  • PREVIEW:建议用于取景器或应用内图像分析。

  • STILL_CAPTURE:针对高质量高分辨率捕获进行了优化,预计不会保持类似预览的帧速率。

  • VIDEO_RECORD:针对高质量视频捕获进行了优化,包括高质量图像稳定功能(如果设备支持且应用已启用)。此选项可能会生成与实时存在显著滞后的输出帧,以实现最高质量的稳定或其他处理。

  • VIDEO_CALL:建议用于长时间运行且关注电量消耗的摄像头用例。

  • PREVIEW_VIDEO_STILL:建议用于社交媒体应用或单流用例。它是一个多用途流。

  • VENDOR_START:用于原始设备制造商定义的用例。

创建 CameraCaptureSession

要创建摄像头会话,请为其提供一个或多个可供您的应用写入输出帧的输出缓冲区。每个缓冲区代表一个管道。您必须在使用摄像头之前完成此操作,以便框架可以配置设备的内部管道并分配内存缓冲区,用于将帧发送到所需的输出目标。

以下代码片段演示了如何使用两个输出缓冲区准备摄像头会话,一个属于 SurfaceView,另一个属于 ImageReader。将 PREVIEW 流用例添加到 previewSurface,并将 STILL_CAPTURE 流用例添加到 imReaderSurface,可以进一步优化这些流的设备硬件。

Kotlin

// Retrieve the target surfaces, which might be coming from a number of places:
// 1. SurfaceView, if you want to display the image directly to the user
// 2. ImageReader, if you want to read each frame or perform frame-by-frame
// analysis
// 3. OpenGL Texture or TextureView, although discouraged for maintainability
      reasons
// 4. RenderScript.Allocation, if you want to do parallel processing
val surfaceView = findViewById<SurfaceView>(...)
val imageReader = ImageReader.newInstance(...)

// Remember to call this only *after* SurfaceHolder.Callback.surfaceCreated()
val previewSurface = surfaceView.holder.surface
val imReaderSurface = imageReader.surface
val targets = listOf(previewSurface, imReaderSurface)

// Create a capture session using the predefined targets; this also involves
// defining the session state callback to be notified of when the session is
// ready
// Setup Stream Use Case while setting up your Output Configuration.
@RequiresApi(Build.VERSION_CODES.TIRAMISU)
fun configureSession(device: CameraDevice, targets: List<Surface>){
    val configs = mutableListOf<OutputConfiguration>()
    val streamUseCase = CameraMetadata
        .SCALER_AVAILABLE_STREAM_USE_CASES_PREVIEW_VIDEO_STILL

    targets.forEach {
        val config = OutputConfiguration(it)
        config.streamUseCase = streamUseCase.toLong()
        configs.add(config)
    }
    ...
    device.createCaptureSession(session)
}

Java

// Retrieve the target surfaces, which might be coming from a number of places:
// 1. SurfaceView, if you want to display the image directly to the user
// 2. ImageReader, if you want to read each frame or perform frame-by-frame
      analysis
// 3. RenderScript.Allocation, if you want to do parallel processing
// 4. OpenGL Texture or TextureView, although discouraged for maintainability
      reasons
Surface surfaceView = findViewById<SurfaceView>(...);
ImageReader imageReader = ImageReader.newInstance(...);

// Remember to call this only *after* SurfaceHolder.Callback.surfaceCreated()
Surface previewSurface = surfaceView.getHolder().getSurface();
Surface imageSurface = imageReader.getSurface();
List<Surface> targets = Arrays.asList(previewSurface, imageSurface);

// Create a capture session using the predefined targets; this also involves defining the
// session state callback to be notified of when the session is ready
private void configureSession(CameraDevice device, List<Surface> targets){
    ArrayList<OutputConfiguration> configs= new ArrayList()
    String streamUseCase=  CameraMetadata
        .SCALER_AVAILABLE_STREAM_USE_CASES_PREVIEW_VIDEO_STILL

    for(Surface s : targets){
        OutputConfiguration config = new OutputConfiguration(s)
        config.setStreamUseCase(String.toLong(streamUseCase))
        configs.add(config)
}

device.createCaptureSession(session)
}

此时,您尚未定义摄像头的活动配置。配置会话后,您可以创建并分派捕获请求来执行此操作。

输入写入其缓冲区时所应用的转换由每个目标的类型决定,该类型必须是 Surface。Android 框架知道如何将活动配置中的原始图像转换为适合每个目标的格式。转换由特定 Surface 的像素格式和大小控制。

框架会尽力而为,但某些 Surface 配置组合可能无法正常工作,从而导致会话无法创建、分派请求时抛出运行时错误或性能下降等问题。框架对设备、表面和请求参数的特定组合提供了保证。createCaptureSession() 的文档提供了更多信息。

单次 CaptureRequest

用于每个帧的配置编码在发送到摄像头的 CaptureRequest 中。要创建捕获请求,您可以使用预定义的模板之一,或者使用 TEMPLATE_MANUAL 进行完全控制。选择模板时,您需要提供一个或多个要与请求一起使用的输出缓冲区。您只能使用已在您打算使用的捕获会话上定义的缓冲区。

捕获请求使用构建器模式,并为开发者提供了设置许多不同选项的机会,包括自动曝光自动对焦镜头光圈。在设置字段之前,请确保通过调用 CameraCharacteristics.getAvailableCaptureRequestKeys() 来检查特定选项是否可用于设备,并通过检查相应的摄像头特性(例如可用的自动曝光模式)来确保所需值受支持。

要使用专为预览设计且未经任何修改的模板为 SurfaceView 创建捕获请求,请使用 CameraDevice.TEMPLATE_PREVIEW

Kotlin

val session: CameraCaptureSession = ...  // from CameraCaptureSession.StateCallback
val captureRequest = session.device.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW)
captureRequest.addTarget(previewSurface)

Java

CameraCaptureSession session = ...;  // from CameraCaptureSession.StateCallback
CaptureRequest.Builder captureRequest =
    session.getDevice().createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW);
captureRequest.addTarget(previewSurface);

定义捕获请求后,您现在可以将其分派到摄像头会话。

Kotlin

val session: CameraCaptureSession = ...  // from CameraCaptureSession.StateCallback
val captureRequest: CaptureRequest = ...  // from CameraDevice.createCaptureRequest()

// The first null argument corresponds to the capture callback, which you
// provide if you want to retrieve frame metadata or keep track of failed capture
// requests that can indicate dropped frames; the second null argument
// corresponds to the Handler used by the asynchronous callback, which falls
// back to the current thread's looper if null
session.capture(captureRequest.build(), null, null)

Java

CameraCaptureSession session = ...;  // from CameraCaptureSession.StateCallback
CaptureRequest captureRequest = ...;  // from CameraDevice.createCaptureRequest()

// The first null argument corresponds to the capture callback, which you
// provide if you want to retrieve frame metadata or keep track of failed
// capture
// requests that can indicate dropped frames; the second null argument
// corresponds to the Handler used by the asynchronous callback, which falls
// back to the current thread's looper if null
session.capture(captureRequest.build(), null, null);

当输出帧放入特定缓冲区时,会触发捕获回调。在许多情况下,当包含帧被处理时,会触发额外的回调,例如 ImageReader.OnImageAvailableListener。此时,您可以从指定的缓冲区中检索图像数据。

重复 CaptureRequest

单次摄像头请求操作起来很简单,但对于显示实时预览或视频来说,它们并不是很有用。在这种情况下,您需要接收连续的帧流,而不仅仅是单个帧。以下代码片段展示了如何向会话添加重复请求

Kotlin

val session: CameraCaptureSession = ...  // from CameraCaptureSession.StateCallback
val captureRequest: CaptureRequest = ...  // from CameraDevice.createCaptureRequest()

// This keeps sending the capture request as frequently as possible until
// the
// session is torn down or session.stopRepeating() is called
// session.setRepeatingRequest(captureRequest.build(), null, null)

Java

CameraCaptureSession session = ...;  // from CameraCaptureSession.StateCallback
CaptureRequest captureRequest = ...;  // from CameraDevice.createCaptureRequest()

// This keeps sending the capture request as frequently as possible until the
// session is torn down or session.stopRepeating() is called
// session.setRepeatingRequest(captureRequest.build(), null, null);

重复捕获请求使摄像头设备使用提供的 CaptureRequest 中的设置持续捕获图像。Camera2 API 还允许用户通过发送重复的 CaptureRequest 从摄像头捕获视频,如 GitHub 上的此 Camera2 示例代码库所示。它还可以通过使用重复的突发 CaptureRequest 捕获高速(慢动作)视频来渲染慢动作视频,如 GitHub 上的 Camera2 慢动作视频示例应用所示。

交错 CaptureRequest

在重复捕获请求处于活动状态时发送第二个捕获请求(例如显示取景器并让用户拍照),您不需要停止正在进行的重复请求。相反,您可以在重复请求继续运行时发出一个非重复捕获请求。

任何使用的输出缓冲区都需要在会话首次创建时作为摄像头会话的一部分进行配置。重复请求的优先级低于单帧或突发请求,这使得以下示例能够正常工作。

Kotlin

val session: CameraCaptureSession = ...  // from CameraCaptureSession.StateCallback

// Create the repeating request and dispatch it
val repeatingRequest = session.device.createCaptureRequest(
CameraDevice.TEMPLATE_PREVIEW)
repeatingRequest.addTarget(previewSurface)
session.setRepeatingRequest(repeatingRequest.build(), null, null)

// Some time later...

// Create the single request and dispatch it
// NOTE: This can disrupt the ongoing repeating request momentarily
val singleRequest = session.device.createCaptureRequest(
CameraDevice.TEMPLATE_STILL_CAPTURE)
singleRequest.addTarget(imReaderSurface)
session.capture(singleRequest.build(), null, null)

Java

CameraCaptureSession session = ...;  // from CameraCaptureSession.StateCallback

// Create the repeating request and dispatch it
CaptureRequest.Builder repeatingRequest =
session.getDevice().createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW);
repeatingRequest.addTarget(previewSurface);
session.setRepeatingRequest(repeatingRequest.build(), null, null);

// Some time later...

// Create the single request and dispatch it
// NOTE: This can disrupt the ongoing repeating request momentarily
CaptureRequest.Builder singleRequest =
session.getDevice().createCaptureRequest(CameraDevice.TEMPLATE_STILL_CAPTURE);
singleRequest.addTarget(imReaderSurface);
session.capture(singleRequest.build(), null, null);

然而,这种方法有一个缺点:您不知道单个请求何时准确发生。在下图中,如果 A 是重复捕获请求,B 是单帧捕获请求,则会话按如下方式处理请求队列:

图 2. 正在进行的摄像头会话的请求队列插图

无法保证在请求 B 激活之前来自 A 的最后一个重复请求与 A 再次被使用之间存在多少延迟,因此您可能会遇到一些跳过帧的情况。您可以采取一些措施来缓解此问题:

  • 将请求 A 的输出目标添加到请求 B。这样,当 B 的帧准备好时,它会被复制到 A 的输出目标中。例如,在进行视频快照以保持稳定帧速率时,这一点至关重要。在上述代码中,您在构建请求之前添加了 singleRequest.addTarget(previewSurface)

  • 使用专为这种特定场景设计的模板组合,例如零快门延迟。