相机捕获会话和请求

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

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

这样做的原因之一是,为了让一个数据流(即来自 CameraDevice 的连续相机帧)针对特定任务进行优化,例如显示取景器,而其他数据流可用于拍照或录制视频。这些数据流充当并行流水线,一次处理来自相机的原始帧,一次处理一帧:

图 1. 构建通用相机应用的图示(2018 年 Google I/O 大会)

并行处理表明存在性能限制,具体取决于 CPU、GPU 或其他处理器的可用处理能力。如果流水线跟不上传入的帧,它就会开始丢弃这些帧。

每个流水线都有自己的输出格式。传入的原始数据会通过与每个流水线关联的隐式逻辑自动转换为相应的输出格式。本页面的代码示例中使用的 CameraDevice 并不具体,因此请先枚举所有可用的摄像头,然后再继续。

您可以使用 CameraDevice 创建一个特定于该 CameraDeviceCameraCaptureSessionCameraDevice 必须使用 CameraCaptureSession 接收每个原始帧的帧配置。该配置用于指定相机属性,如自动对焦、光圈、效果和曝光。由于硬件限制,在任何给定时间,摄像头传感器中只有一个配置处于活跃状态,这称为“活跃”配置。

不过,数据流用例增强并扩展了之前使用 CameraDevice 流式传输捕获会话的方法,让您可以针对特定用例优化相机数据流。例如,在优化视频通话时,它可以延长电池续航时间。

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

CaptureRequest 会将配置添加到队列中,并选择一条、多个或所有可用流水线以从 CameraDevice 接收帧。您可以在拍摄会话的生命周期内发送多个拍摄请求。每个请求都可以更改活跃配置以及接收原始图片的一组输出流水线。

使用数据流用例提高性能

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

这样,摄像头设备就可以根据每个数据流的用户场景来优化相机硬件和软件流水线。如需详细了解数据流用例,请参阅 setStreamUseCase

除了在 CameraDevice.createCaptureRequest() 中设置模板之外,在数据流用例中,您还可以更详细地指定如何使用特定相机数据流。这样,相机硬件就可以根据适合特定用例的质量或延迟权衡来优化参数,例如微调、传感器模式或相机传感器设置。

数据流用例包括:

  • DEFAULT:涵盖所有现有应用行为。这相当于不设置任何数据流用例。

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

  • STILL_CAPTURE:针对高品质高分辨率拍摄进行了优化,不需要保持类似预览的帧速率。

  • VIDEO_RECORD:已针对高品质视频拍摄(包括高品质图像防抖)进行了优化(如果设备支持并由应用启用)。此选项可能会生成与实时相差极大的输出帧,以实现最高质量的防抖或其他处理。

  • VIDEO_CALL:建议用于容易耗电的长时间运行摄像头。

  • PREVIEW_VIDEO_STILL:建议用于社交媒体应用或单流用例。这是一种多用途的视频流

  • VENDOR_START:用于 OEM 定义的用例。

创建 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){
    val configs = mutableListOf()
    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 targets){
    ArrayList 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 配置组合可能不起作用,从而导致出现诸如以下问题:无法创建会话、在您发送请求时抛出运行时错误,或性能下降。该框架为设备、Surface 和请求参数的特定组合提供保证。如需了解详情,请参阅 createCaptureSession() 的文档。

单个捕获请求

每一帧所用的配置会在发送到相机的 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)。此时,您可以从指定的缓冲区检索图像数据。

重复捕获请求

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

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 还允许用户通过发送重复 CaptureRequests 从相机拍摄视频,如 GitHub 上的此 Camera2 示例代码库所示。它还可以通过使用重复连拍 CaptureRequests 捕获高速(慢动作)视频来渲染慢动作视频,如 GitHub 上的 Camera2 慢动作视频示例应用中所示。

交错捕获请求

如需在重复拍摄请求处于活跃状态时发送第二个拍摄请求(例如,为了显示取景器并允许用户拍摄照片),您无需停止正在进行的重复请求。您可以改为发出非重复捕获请求,同时重复请求会继续运行。

首次创建会话时,使用的任何输出缓冲区都需要配置为相机会话的一部分。重复请求的优先级低于单帧请求或脉冲串请求,这使得以下示例能够正常运行:

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. 进行中的相机会话的请求队列图示

无法保证从 A 发出的最后一个重复请求在激活请求 B 之前与下次再次使用 A 之间的延迟时间,因此可能会出现一些跳过的帧。您可以通过以下方法缓解此问题:

  • 从请求 A 向请求 B 添加输出目标。这样,当 B 的帧准备就绪时,它会复制到 A 的输出目标中。例如,在生成视频快照以保持稳定的帧速率时,这样做至关重要。在前面的代码中,您在构建请求之前添加了 singleRequest.addTarget(previewSurface)

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