[译] 同时使用多的相机流

这篇文章是当前关于 Android 相机介绍中最新的一篇,咱们以前介绍过相机阵列相机会话和请求html

多个相机流的使用场景

一个相机应用可能但愿同时使用多个帧流,在某些状况下不一样的流甚至须要不一样的帧分辨率或像素格式;如下是一些典型使用场景:前端

  • 录像:一个流用于预览,另外一个用于并编码保存成文件
  • 扫描条形码:一个流用于预览,另外一个用于条形码检测
  • 计算摄影学:一个流用于预览,另外一个用于人脸或场景的检测

正如咱们在以前的文章中讨论的那样,当咱们处理帧时,存在较大的性能成本,而且这些成本在并行流 / 流水线处理中还会成倍增加。java

CPU、GPU 和 DSP 这样的资源能够利用框架的从新处理能力,可是像内存这样的资源需求将线性增加。android

每次请求对应多个目标

经过执行某种官方程序,多相机流能够整合成一个 CaptureRequest,此代码段代表了如何使用一个流开启相机会话进行相机预览并使用另外一个流进行图像处理:ios

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

// 咱们将使用预览捕获模板来组合流,由于
// 它针对低延迟进行了优化; 用于高质量的图像时使用
// TEMPLATE_STILL_CAPTURE,用于高速和稳定的帧速率时使用
// TEMPLATE_RECORD
val requestTemplate = CameraDevice.TEMPLATE_PREVIEW
val combinedRequest = session.device.createCaptureRequest(requestTemplate)

// Link the Surface targets with the combined request
combinedRequest.addTarget(previewSurface)
combinedRequest.addTarget(imReaderSurface)

// 在咱们的样例中,SurfaceView 会自动更新。
// ImageReader 有本身的回调,咱们必须监听,以检索帧
// 因此不须要为捕获请求设置回调
session.setRepeatingRequest(combinedRequest.build(), null, null)
复制代码

若是你正确配置了目标 surfaces,则此代码将仅生成知足 StreamComfigurationMap.GetOutputMinFrameDuration(int, Size)StreamComfigurationMap.GetOutputStallDuration(int, Size) 肯定的最小 FPS 流。实际表现还会因机型而异,Android 给了咱们一些保证,能够根据输出类型输出大小硬件级别三个变量来支持特定组合。使用不支持的参数组合可能会以低帧率工做,甚至不能工做,触发其中一个故障回调。文档很是详细地描述了保证工做的内容,强烈推荐完整阅读,咱们在此将介绍基础知识。git

输出类型

输出类型指的是帧编码格式,文档描述中支持的类型有 PRIV、YUV、JEPG 和 RAW。文档很好的解释了它们:github

PRIV 指的是使用了 StreamConfigurationMap.getOutputSizes(Class) 获取可用尺寸的任何目标,没有直接的应用程序可见格式后端

YUV 指的是目标 surface 使用了 ImageFormat.YUV_420_888 编码格式数组

JPEG 指的是 ImageFormat.JPEG 格式bash

RAW 指的是 ImageFormat.RAW_SENSOR 格式

当选择应用程序的输出类型时,若是目标是使兼容性最大化,推荐使用 ImageFormat.YUV_420_888 作帧分析并使用 ImageFormat.JPEG 保存图像。对于预览和录像传感器来讲,你可能会用一个 SurfaceViewTextureViewMediaRecorderMediaCodec 或者 RenderScript.Allocation。在这些状况下,不指定图像格式,出于兼容性目的,它将被计为 ImageFormat.PRIVATE(无论它的实际格式是什么)。去查看设备支持的格式可使用以下代码:

val characteristics: CameraCharacteristics = ...
val supportedFormats = characteristics.get(
            CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP).outputFormats
复制代码

输出大小

咱们调用 StreamConfigurationMap.getOutputSizes() 可列出全部可用的输出大小,但随着兼容性的发展,咱们只须要关心两种:PREVIEW 和 MAXIMUM。咱们能够将这种大小视为上限;若是文档中说的 PREVIEW 的大小有效,那么任何比 PREVIEW 尺寸小的均可以,MAXIMUM 同理。这有一个文档的相关摘录:

对于尺寸最大的列,PREVIEW 意味着适配屏幕的最佳尺寸,或 1080p(1920x1080),以较小者为准。RECORD 指的是相机支持的最大分辨率由 CamcorderProfile 肯定。MAXIMUM 还指 StreamConfigurationMap.getOutputSizes(int)中相机设备对该格式或目标的最大输出分辨率。

注意,可用的输出尺寸取决于选择的格式。给定 CameraCharacteristics,咱们能够像这样查询可用的输出尺寸:

val characteristics: CameraCharacteristics = ...
val outputFormat: Int = ...  // 好比 ImageFormat.JPEG
val sizes = characteristics.get(
        CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP)
        .getOutputSizes(outputFormat)
复制代码

在相机预览和录像的使用场景中,咱们应该使用目标类来肯定支持的大小,由于文件格式将由相机框架自身处理:

val characteristics: CameraCharacteristics = ...
val targetClass: Class<T> = ...  // 好比 SurfaceView::class.java
val sizes = characteristics.get(
        CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP)
        .getOutputSizes(targetClass)
复制代码

获取到 MAXIMUM 的尺寸很简单——只须要将输出尺寸排序而后返回最大的:

fun <T>getMaximumOutputSize(
        characteristics: CameraCharacteristics, targetClass: Class<T>, format: Int? = null):
        Size {
    val config = characteristics.get(
            CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP)

    // 若是提供图像格式,请使用它来肯定支持的大小;不然使用目标类
    val allSizes = if (format == null)
        config.getOutputSizes(targetClass) else config.getOutputSizes(format)
    return allSizes.sortedWith(compareBy { it.height * it.width }).reversed()[0]
}
复制代码

获取 PREVIEW 的尺寸就须要动下脑子了。回想一下,PREVIEW 指的是适配屏幕的最佳尺寸,或者 1080p (1920x1080),取较小者。请记住,长宽比可能与屏幕的不匹配,若是咱们打算全屏显示,咱们须要显示黑边或者裁剪。为了获取到正确的预览尺寸,咱们须要对比可用的输出尺寸和显示尺寸,同时考虑到能够旋转显示。在这段代码里,咱们还封装了一个辅助类 SmartSize 用来横简单的比较尺寸大小:

class SmartSize(width: Int, height: Int) {
    var size = Size(width, height)
    var long = max(size.width, size.height)
    var short = min(size.width, size.height)
}

fun getDisplaySmartSize(context: Context): SmartSize {
    val windowManager = context.getSystemService(
            Context.WINDOW_SERVICE) as WindowManager
    val outPoint = Point()
    windowManager.defaultDisplay.getRealSize(outPoint)
    return SmartSize(outPoint.x, outPoint.y)
}

fun <T>getPreviewOutputSize(
        context: Context, characteristics: CameraCharacteristics, targetClass: Class<T>,
        format: Int? = null): Size {

    // 比较哪一个更小:屏幕尺寸仍是 1080p
    val hdSize = SmartSize(1080, 720)
    val screenSize = getDisplaySmartSize(context)
    val hdScreen = screenSize.long >= hdSize.long || screenSize.short >= hdSize.short
    val maxSize = if (hdScreen) screenSize else hdSize

    // 若是提供图像格式,请使用它来肯定支持的大小;不然使用目标类
    val config = characteristics.get(
            CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP)
    val allSizes = if (format == null)
        config.getOutputSizes(targetClass) else config.getOutputSizes(format)

    // 获取可用尺寸并按面积从最大到最小排序
    val validSizes = allSizes
            .sortedWith(compareBy { it.height * it.width })
            .map { SmartSize(it.width, it.height) }.reversed()

    // 而后,得到小于或等于最大尺寸的最大输出尺寸
    return validSizes.filter {
        it.long <= maxSize.long && it.short <= maxSize.short }[0].size
}
复制代码

硬件层次

要决定运行时可用能力,相机应用须要的最重要的信息是支持的硬件级别。再一次,咱们能够今后文档学习:

支持的硬件级别是摄像机设备功能的上层描述,汇总出多种功能到一个字段中。每一等级相比前一等级都新增了一些功能,而且始终是上一级别的超集。等级的顺序是 LEGACY < LIMITED < FULL < LEVEL_3。

使用 CameraCharacteristics 对象,咱们可使用单个语句检索硬件级别:

val characteristics: CameraCharacteristics = ...

// 硬件级别将是其中之一:
// - CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LEGACY,
// - CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_EXTERNAL,
// - CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED,
// - CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_FULL,
// - CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_3
val hardwareLevel = characteristics.get(
        CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL)
复制代码

把全部部分拼合起来

一旦咱们了解了输出类型、输出尺寸和硬件级别,咱们就能够肯定哪些视频流组合是有效的。举个例子,有一个具备 LEGACY 硬件级别的 CameraDevice 支持的配置的快照.照来自 createCaptureSession 方法的文档:

由于 LEGACY 是可能性最低的硬件等级,咱们能够从一个表中推断出每个支持 Camera2 的设备(API 21 及以上)可使用正确的配置输出最多三个并发流——这很是酷!然而,可能在不少机器上没法实现最大可用吞吐量,由于你的代码可能会产生很大性能开销,引起性能约束,例如内存、CPU 甚至是发热。

如今咱们已经掌握了在框架的支持下使用两个并发流的所需知识,咱们能够更深刻了解目标输出缓冲区的配置。例如,若是咱们的目标是具备 LEGACY 硬件级别的设备,咱们能够设置两个目标输出表面:一个使用 ImageFormat.PRIVATE 另外一个使用 ImageFormat.YUV_420_888。只要咱们使用 PREVIEW 的尺寸,这应该是上表所支持的组合。使用上面定义的方法,获取相机 ID 所需的预览尺寸很是简单:

val characteristics: CameraCharacteristics = ...
val context = this as Context  // 假设咱们在一个 Activity 中

val surfaceViewSize = getPreviewOutputSize(
        context, characteristics, SurfaceView::class.java)
val imageReaderSize = getPreviewOutputSize(
        context, characteristics, ImageReader::class.java, format = ImageFormat.YUV_420_888)
复制代码

We must wait until SurfaceView is ready using the provided callbacks, like this:

val surfaceView = findViewById<SurfaceView>(...)
surfaceView.holder.addCallback(object : SurfaceHolder.Callback {
    override fun surfaceCreated(holder: SurfaceHolder) {
        // 咱们不须要具体的图片格式,他会被视为 RRIV
        // 如今 Surface 已经就绪,咱们能够用它做为 CameraSession 的输出目标
    }
    ...
})
复制代码

咱们甚至能够调用 SurfaceHolder.setFixedSize() 强制 SurfaceView 适配输出流的大小,但在 UI 方面更好的作法是采起相似于 GitHub 上 HDR 取景器FixedAspectSurfaceView 的方法,这样能够同时在宽高比和可用空间上使用绝对大小,同时可在 Activity 改变时自动调整。

使用所需格式从 ImageReader 中设置另外一个表面更加容易,由于无需等待回调:

val frameBufferCount = 3  // 只是一个例子,取决于你对 ImageReade 的使用
val imageReader = ImageReader.newInstance(
        imageReaderSize.width, imageReaderSize.height, ImageFormat.YUV_420_888
        frameBufferCount)
复制代码

当使用 ImageReader 这样的阻塞目标缓冲区时,咱们须要在使用后丢弃这些帧:

imageReader.setOnImageAvailableListener({
        val frame =  it.acquireNextImage()
        // 在这用 frame 作些什么
        it.close()
}, null)
复制代码

要记住,咱们的目标是最低的共同标准——使用 LEGACY 硬件级别的设备。咱们能够添加条件分支,为 LIMITED 硬件等级的设备中的一个输出表面使用 RECORD 尺寸,或者甚至为具备 FULL 硬件级别的设备提供高达 MAXIMUM 的大小。

总结

这篇文章中,咱们介绍了:

  1. 用单镜头的设备同时输出多个流
  2. 在单次拍照中组合不一样的目标规则
  3. 查询并选择合适的输出格式,输出尺寸和硬件等级
  4. 设置并使用 SurfaceViewImageReader 提供的 Surface

有了这些知识,如今咱们能够创做一个相机 APP,能够显示和预览流,同时在单独的流中对传入帧进行异步分析。

若是发现译文存在错误或其余须要改进的地方,欢迎到 掘金翻译计划 对译文进行修改并 PR,也可得到相应奖励积分。文章开头的 本文永久连接 即为本文在 GitHub 上的 MarkDown 连接。


掘金翻译计划 是一个翻译优质互联网技术文章的社区,文章来源为 掘金 上的英文分享文章。内容覆盖 AndroidiOS前端后端区块链产品设计人工智能等领域,想要查看更多优质译文请持续关注 掘金翻译计划官方微博知乎专栏

相关文章
相关标签/搜索