- 原文地址:Using multiple camera streams simultaneously
- 原文作者:Oscar Wahltinez
- 譯文出自:掘金翻譯計劃
- 本文永久連結:github.com/xitu/gold-m…
- 譯者:zx-Zhu
- 校對者:nanjingboy, gs666
這篇文章是當前關於 Android 相機介紹中最新的一篇,我們之前介紹過相機陣列和相機會話和請求。
多個相機流的使用場景
一個相機應用可能希望同時使用多個幀流,在某些情況下不同的流甚至需要不同的幀解析度或畫素格式;以下是一些典型使用場景:
- 錄影:一個流用於預覽,另一個用於並編碼儲存成檔案
- 掃描條形碼:一個流用於預覽,另一個用於條形碼檢測
- 計算攝影學:一個流用於預覽,另一個用於人臉或場景的檢測
正如我們在之前的文章中討論的那樣,當我們處理幀時,存在較大的效能成本,並且這些成本在並行流 / 流水線處理中還會成倍增長。
CPU、GPU 和 DSP 這樣的資源可以利用框架的重新處理能力,但是像記憶體這樣的資源需求將線性增長。
每次請求對應多個目標
通過執行某種官方程式,多相機流可以整合成一個 CaptureRequest,此程式碼段表明瞭如何使用一個流開啟相機會話進行相機預覽並使用另一個流進行影像處理:
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 給了我們一些保證,可以根據輸出型別,輸出大小和硬體級別三個變數來支援特定組合。使用不支援的引數組合可能會以低幀率工作,甚至不能工作,觸發其中一個故障回撥。文件非常詳細地描述了保證工作的內容,強烈推薦完整閱讀,我們在此將介紹基礎知識。
輸出型別
輸出型別指的是幀編碼格式,文件描述中支援的型別有 PRIV、YUV、JEPG 和 RAW。文件很好的解釋了它們:
PRIV 指的是使用了 StreamConfigurationMap.getOutputSizes(Class) 獲取可用尺寸的任何目標,沒有直接的應用程式可見格式
YUV 指的是目標 surface 使用了 ImageFormat.YUV_420_888 編碼格式
JPEG 指的是 ImageFormat.JPEG 格式
RAW 指的是 ImageFormat.RAW_SENSOR 格式
當選擇應用程式的輸出型別時,如果目標是使相容性最大化,推薦使用 ImageFormat.YUV_420_888 做幀分析並使用 ImageFormat.JPEG 儲存影像。對於預覽和錄影感測器來說,你可能會用一個 SurfaceView
、TextureView
、MediaRecorder
、MediaCodec
或者 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 的大小。
總結
這篇文章中,我們介紹了:
- 用單鏡頭的裝置同時輸出多個流
- 在單次拍照中組合不同的目標規則
- 查詢並選擇合適的輸出格式,輸出尺寸和硬體等級
- 設定並使用
SurfaceView
和ImageReader
提供的Surface
有了這些知識,現在我們可以創作一個相機 APP,可以顯示和預覽流,同時在單獨的流中對傳入幀進行非同步分析。
如果發現譯文存在錯誤或其他需要改進的地方,歡迎到 掘金翻譯計劃 對譯文進行修改並 PR,也可獲得相應獎勵積分。文章開頭的 本文永久連結 即為本文在 GitHub 上的 MarkDown 連結。
掘金翻譯計劃 是一個翻譯優質網際網路技術文章的社群,文章來源為 掘金 上的英文分享文章。內容覆蓋 Android、iOS、前端、後端、區塊鏈、產品、設計、人工智慧等領域,想要檢視更多優質譯文請持續關注 掘金翻譯計劃、官方微博、知乎專欄。