[譯] 同時使用多的相機流

這篇文章是當前關於 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前端後端區塊鏈產品設計人工智能等領域,想要查看更多優質譯文請持續關注 掘金翻譯計劃官方微博知乎專欄

相關文章
相關標籤/搜索