相机捕获会话和请求

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

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

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

图 1.来自构建通用相机应用(Google I/O '18)的插图

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

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

您可以使用CameraDevice来创建特定于该CameraDeviceCameraCaptureSession。每个原始帧都需要使用CameraCaptureSessionCameraDevice提供帧配置。配置指定了摄像头的属性,例如自动对焦、光圈、效果和曝光。由于硬件限制,在任何给定时间,摄像机传感器中只有一个配置处于活动状态,称为活动配置。

但是,流使用案例增强并扩展了以前使用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<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激活之前的最后一个重复请求AA再次使用之间,延迟没有保证,因此您可能会遇到一些跳帧的情况。您可以采取一些措施来缓解此问题

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

  • 使用旨在用于此特定场景的模板组合,例如零快门延迟。