同時使用多個相機串流畫面

注意:本頁面所述是指 Camera2 套件。除非應用程式需要 Camera2 的特定低階功能,否則建議使用 CameraX。CameraX 和 Camera2 均支援 Android 5.0 (API 級別 21) 以上版本。

相機應用程式可以同時使用多個影格串流。在某些情況下,不同的串流甚至需要不同的影格解析度或像素格式。常見的用途包括:

  • 錄影:一個串流用於預覽,另一個串流則編碼並儲存至檔案。
  • 條碼掃描:一個串流用於預覽,另一個用於條碼偵測。
  • 計算攝影:一個串流用於預覽,另一個用於臉部或場景偵測。

處理影格時,會產生不小的效能成本,而執行平行串流或管道處理時,成本會倍增。

CPU、GPU 和 DSP 等資源或許能利用架構的重新處理功能,但記憶體等資源會線性成長。

每個要求有多個目標

多個攝影機串流影像可以合併為單一 CameraCaptureRequest。以下程式碼片段說明如何設定相機工作階段,其中一個串流用於相機預覽,另一個串流用於圖片處理:

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

// You will use the preview capture template for the combined streams
// because it is optimized for low latency; for high-quality images, use
// TEMPLATE_STILL_CAPTURE, and for a steady frame rate use 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)

// In this basic case, the SurfaceView gets updated automatically. ImageReader
// has its own callback that you have to listen to in order to retrieve the
// frames so there is no need to set up a callback for the capture request.
session.setRepeatingRequest(combinedRequest.build(), null, null)

如果正確設定目標途徑,這段程式碼只會產生符合 StreamComfigurationMap.GetOutputMinFrameDuration(int, Size) 和 StreamComfigurationMap.GetOutputStallDuration(int, Size) 決定的最低每秒影格數的串流。實際效能因裝置而異,但 Android 會根據三個變數,保證支援特定組合:輸出類型、輸出大小和硬體層級。

使用不支援的變數組合可能會導致影格率偏低;如果沒有,則會觸發其中一個失敗回呼。createCaptureSession 的說明文件會說明保證可運作的項目。

輸出類型

輸出類型是指影格的編碼格式。可能的值為 PRIV、YUV、JPEG 和 RAW。createCaptureSession 的說明文件會介紹這些方法。

選擇應用程式的輸出類型時,如果目標是盡可能提高相容性,請使用 ImageFormat.YUV_420_888 進行影格分析,並使用 ImageFormat.JPEG 處理靜態圖片。在預覽和錄製情境中,您可能會使用 SurfaceView、TextureView、MediaRecorder、MediaCodec 或 RenderScript.Allocation。在這種情況下,請勿指定圖片格式。為確保相容性,無論內部實際使用的格式為何,系統都會將其視為 ImageFormat.PRIVATE。如要查詢裝置支援的格式 (假設裝置具有 CameraCharacteristics),請使用下列程式碼:

val characteristics: CameraCharacteristics = ...
val supportedFormats = characteristics.get(
    CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP
)?.outputFormats

輸出大小

所有可用的輸出大小都會列在 StreamConfigurationMap.getOutputSizes() 中,但只有兩個與相容性有關:PREVIEW 和 MAXIMUM。大小會做為上限。如果大小為 PREVIEW 的項目可以運作,則任何小於 PREVIEW 的項目也可以運作。MAXIMUM 也是如此。CameraDevice 的說明文件會解釋這些大小。

可用的輸出大小取決於所選格式。在指定 CameraCharacteristics 和格式的情況下,您可以查詢可用的輸出大小,如下所示:

val characteristics: CameraCharacteristics = ...
val outputFormat: Int = ...  // such as ImageFormat.JPEG
val sizes = characteristics.get(
    CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP
)?.getOutputSizes(outputFormat)

在相機預覽和錄製用途中,請使用目標類別來判斷支援的大小。格式會由相機架構本身處理:

val characteristics: CameraCharacteristics = ...
val targetClass: Class<T> = ...  // such as 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 = checkNotNull(
        characteristics.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP)
    )

    // If image format is provided, use it to determine supported sizes; or else use target class.
    val allSizes = if (format == null)
        config.getOutputSizes(targetClass) else config.getOutputSizes(format)
    return allSizes.maxBy { it.height * it.width }
}

PREVIEW 是指符合裝置螢幕解析度或 1080p (1920x1080) 的最佳尺寸,以較小者為準。顯示比例可能與螢幕的顯示比例不完全相符,因此您可能需要對串流套用上下黑邊或裁剪,才能以全螢幕模式顯示。如要取得正確的預覽大小,請比較可用的輸出大小和螢幕大小,同時考量螢幕可能旋轉。

下列程式碼定義了輔助類別 SmartSize,可讓大小比較更輕鬆:

/** Helper class used to pre-compute shortest and longest sides of a [Size] */
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)
    override fun toString() = "SmartSize(${long}x${short})"
}

/** Standard High Definition size for pictures and video */
val SIZE_1080P: SmartSize = SmartSize(1920, 1080)

/** Returns a [SmartSize] object for the given [Display] */
fun getDisplaySmartSize(display: Display): SmartSize {
    val outPoint = Point()
    display.getRealSize(outPoint)
    return SmartSize(outPoint.x, outPoint.y)
}

/**
 * Returns the largest available PREVIEW size. For more information, see:
 * https://d.android.com/reference/android/hardware/camera2/CameraDevice
 */
fun<T> getPreviewOutputSize(
        display: Display,
        characteristics: CameraCharacteristics,
        targetClass: Class<T>,
        format: Int? = null
): Size {

    // Find which is smaller: screen or 1080p
    val screenSize = getDisplaySmartSize(display)
    val hdScreen = screenSize.long >= SIZE_1080P.long || screenSize.short >= SIZE_1080P.short
    val maxSize = if (hdScreen) SIZE_1080P else screenSize

    // If image format is provided, use it to determine supported sizes; else use target class
    val config = characteristics.get(
            CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP)!!
    if (format == null)
        assert(StreamConfigurationMap.isOutputSupportedFor(targetClass))
    else
        assert(config.isOutputSupportedFor(format))
    val allSizes = if (format == null)
        config.getOutputSizes(targetClass) else config.getOutputSizes(format)

    // Get available sizes and sort them by area from largest to smallest
    val validSizes = allSizes
            .sortedWith(compareBy { it.height * it.width })
            .map { SmartSize(it.width, it.height) }.reversed()

    // Then, get the largest output size that is smaller or equal than our max size
    return validSizes.first { it.long <= maxSize.long && it.short <= maxSize.short }.size
}

查看支援的硬體級別

如要判斷執行階段可用的功能,請使用 CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL 檢查支援的硬體層級。

使用 CameraCharacteristics 物件,您可以使用單一陳述式擷取硬體層級:

val characteristics: CameraCharacteristics = ...

// Hardware level will be one of:
// - 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 支援的設定。

目標 1 目標 2 目標 3 用途範例
類型 大小上限 類型 大小上限 類型 大小上限
PRIV MAXIMUM 簡單預覽、GPU 影片處理,或無預覽錄影。
JPEG MAXIMUM 不使用觀景窗圖片拍攝。
YUV MAXIMUM 應用程式內影片/圖片處理。
PRIV PREVIEW JPEG MAXIMUM 標準靜態影像。
YUV PREVIEW JPEG MAXIMUM 應用程式內處理和靜態影像擷取。
PRIV PREVIEW PRIV PREVIEW 標準錄製。
PRIV PREVIEW YUV PREVIEW 預覽畫面和應用程式內處理。
PRIV PREVIEW YUV PREVIEW JPEG MAXIMUM 靜態影像擷取和應用程式內處理。

LEGACY 是最低的硬體等級。這份表格顯示,只要設定正確,且沒有過多限制效能的負擔 (例如記憶體、CPU 或散熱限制),每部支援 Camera2 (API 級別 21 以上) 的裝置最多可同時輸出三個串流。

應用程式也需要設定符合設定的目標輸出緩衝區。舉例來說,如要指定硬體層級為 LEGACY 的裝置,您可以設定兩個目標輸出途徑,一個使用 ImageFormat.PRIVATE,另一個使用 ImageFormat.YUV_420_888。使用 PREVIEW 大小時,系統支援這種組合。使用本文稍早定義的函式,取得相機 ID 所需的預覽大小需要下列程式碼:

val characteristics: CameraCharacteristics = ...
val context = this as Context // assuming you are inside of an activity

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

您必須使用提供的回呼,等待 SurfaceView 準備就緒:

val surfaceView = findViewById<SurfaceView>(...)
surfaceView.holder.addCallback(object : SurfaceHolder.Callback {
    override fun surfaceCreated(holder: SurfaceHolder) {
        // You do not need to specify image format, and it will be considered of type PRIV.
        // Surface is now ready and you could use it as an output target for CameraSession.
    }
    // ...
})

您可以呼叫 SurfaceHolder.setFixedSize(),強制 SurfaceView 符合相機輸出大小,也可以採用類似 GitHub 上相機範例通用模組中 AutoFitSurfaceView 的方法,設定絕對大小,同時考量顯示比例和可用空間,並在觸發活動變更時自動調整。

使用所選格式從 ImageReader 設定其他介面會比較容易,因為不需要等待回呼:

val frameBufferCount = 3 // just an example, depends on your usage of ImageReader
val imageReader = ImageReader.newInstance(
    imageReaderSize.width, imageReaderSize.height, ImageFormat.YUV_420_888,
    frameBufferCount
)

使用 ImageReader 等封鎖目標緩衝區時,請在使用後捨棄影格:

imageReader.setOnImageAvailableListener({
    val frame = it.acquireNextImage()
    // Do something with "frame" here.
    it.close()
}, null)

LEGACY 硬體層級指定最低公分母裝置。您可以新增條件式分支,並在硬體層級為 LIMITED 的裝置上,將其中一個輸出目標介面設為 RECORD 大小,甚至在硬體層級為 FULL 的裝置上,將其增加至 MAXIMUM 大小。