Android NDK开发详解相机之相机捕获会话和请求
相机捕获会话和请求是我们日常生活中必不可少的操作,也是我们Android操作必须要操作的必要步骤。特别是在Android开发过程中,更是我们需要掌握的必备技能之一,所以小伙伴们需要对其了解并熟练,才能更好的进行开发工作。
注意:本页介绍的是 Camera2 软件包。除非您的应用需要 Camera2 的特定低层级功能,否则我们建议您使用 CameraX。CameraX 和 Camera2 都支持 Android 5.0(API 级别 21)及更高版本。
单个 Android 设备可以具有多个摄像头。每个摄像头都是一个 CameraDevice,而 CameraDevice 可以同时输出多个信息流。
这样做的原因之一是,一个数据流(来自 CameraDevice 的依序相机帧)针对特定任务(例如显示取景器)进行了优化,而其他数据流可用于拍摄照片或录制视频。这些数据流充当并行流水线,一次处理传出相机的原始帧:
图 1. 插图:构建通用相机应用(2018 年 Google I/O 大会)
并行处理表明存在性能限制,具体取决于 CPU、GPU 或其他处理器的可用处理能力。如果流水线无法跟上传入的帧,则开始丢弃这些帧。
每个流水线都有自己的输出格式。传入的原始数据会通过与每个流水线关联的隐式逻辑,自动转换为相应的输出格式。本页面的代码示例中使用的 CameraDevice 并不具体,因此,您需要先枚举所有可用的摄像头,然后再继续。
您可以使用 CameraDevice 创建特定于该 CameraDevice 的 CameraCaptureSession。CameraDevice 必须使用 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
用于每一帧的配置在 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 还允许用户通过发送重复 CaptureRequests 从相机捕获视频,如 GitHub 上的此 Camera2 示例代码库所示。此外,它还可以通过使用重复连拍 CaptureRequests 捕获高速(慢动作)视频来渲染慢动作视频,如 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. 正在进行的相机会话的请求队列图示
无法保证从 A 发出的上一个重复请求(在请求 B 激活之前到下次使用 A)之间的延迟时间,因此您可能会遇到一些跳过帧。您可以通过以下方法缓解此问题:
将请求 A 中的输出目标添加到请求 B 中。这样,当 B 的帧准备就绪后,系统会将其复制到 A 的输出目标中。例如,在生成视频快照以保持稳定的帧速率时,这非常重要。在前面的代码中,您应在构建请求之前添加 singleRequest.addTarget(previewSurface)。
组合使用适合此特定场景的模板,例如零快门延迟。
本页面上的内容和代码示例受内容许可部分所述许可的限制。Java 和 OpenJDK 是 Oracle 和/或其关联公司的注册商标。
最后更新时间 (UTC):2023-11-07。