相机捕获会话和请求

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

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

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

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

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

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

您可以使用 CameraDevice 创建一个 CameraCaptureSession,它特定于该 CameraDeviceCameraDevice 必须使用 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<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)

  • 使用专为这种情况设计的模板组合,例如零快门延迟。