多鏡頭 API

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

多鏡頭功能是在 Android 9 (API 級別 28) 推出。自發布以來,市面上已出現支援該 API 的裝置。許多多鏡頭用途與特定硬體設定緊密結合。換句話說,並非所有用途都與每部裝置相容,因此多鏡頭功能很適合使用 Play Feature Delivery。

常見的用途包括:

  • 變焦:根據裁剪區域或所選焦距切換鏡頭。
  • 深度:使用多個相機建立深度圖。
  • 散景:使用推論的深度資訊,模擬類似數位單眼相機的窄焦距範圍。

邏輯相機和實體相機的差異

如要瞭解多鏡頭 API,必須先瞭解邏輯相機和實體相機之間的差異。舉例來說,假設裝置有三個後置鏡頭。在這個範例中,三個後置鏡頭都視為實體相機。邏輯相機則是將兩個或更多實體相機分組。邏輯相機的輸出內容可以是來自其中一個基礎實體相機的串流,也可以是同時來自多個基礎實體相機的融合串流。無論是哪一種方式,串流都會由相機硬體抽象層 (HAL) 處理。

許多手機製造商會開發第一方相機應用程式,通常會預先安裝在裝置上。為了使用所有硬體功能,他們可能會使用私人或隱藏的 API,或從驅動程式實作中獲得其他應用程式無法存取的特殊處理。部分裝置會提供來自不同實體相機的融合影格串流,藉此實作邏輯相機的概念,但僅限於特定權限應用程式。通常只有一個實體相機會向架構公開。下圖說明 Android 9 之前第三方開發人員的情況:

相機功能通常僅適用於具特殊權限的應用程式
圖 1 通常只有特殊權限應用程式才能使用的相機功能

自 Android 9 起,Android 應用程式不得再使用私人 API。由於架構中包含多相機拍攝支援,Android 最佳做法強烈建議手機製造商為所有朝向相同方向的實體相機提供邏輯相機。以下是第三方開發人員在搭載 Android 9 以上版本的裝置上,應該會看到的內容:

開發人員可完整存取所有相機裝置 (Android 9 以上版本)
圖 2. 自 Android 9 起,開發人員可完整存取所有相機裝置

邏輯相機提供的功能完全取決於原始設備製造商 (OEM) 實作的相機硬體抽象層 (HAL)。舉例來說,Pixel 3 等裝置會實作邏輯相機,根據要求的焦距和裁剪區域選擇其中一個實體相機。

多鏡頭 API

新 API 新增了下列常數、類別和方法:

由於 Android 相容性定義說明文件 (CDD) 有所變更,多鏡頭 API 也對開發人員提出某些要求。Android 9 推出前,市面上已有雙鏡頭裝置,但要同時開啟多個鏡頭,需要反覆測試。在 Android 9 以上版本中,多鏡頭功能提供一組規則,可指定何時能開啟屬於同一邏輯相機的一對實體相機。

在多數情況下,搭載 Android 9 以上版本的裝置會公開所有實體相機 (可能不包括紅外線等較不常見的感應器類型),以及更易於使用的邏輯相機。對於保證可運作的每個串流組合,屬於邏輯相機的一個串流可以替換為來自基礎實體相機的兩個串流。

同時串流多部影片

同時使用多個攝影機串流影像:說明在單一攝影機中同時使用多個串流影像的規則。多部攝影機也適用相同規則,但有一項重要差異。CameraMetadata.REQUEST_AVAILABLE_CAPABILITIES_LOGICAL_MULTI_CAMERA 說明如何將邏輯 YUV_420_888 或原始串流替換為兩個實體串流。也就是說,每個 YUV 或 RAW 類型的串流都可以替換為兩個類型和大小相同的串流。您可以先使用下列保證設定的攝影機串流影像,適用於單一攝影機裝置:

  • 串流 1:YUV 類型,MAXIMUM 大小,來自邏輯相機 id = 0

接著,支援多相機拍攝的裝置可讓您建立工作階段,將邏輯 YUV 串流替換為兩個實體串流:

  • 串流 1:YUV 類型,MAXIMUM 大小,來自實體相機 id = 1
  • 串流 2:YUV 類型,MAXIMUM 大小,來自實體相機 id = 2

只有在兩個相機屬於邏輯相機分組 (列於 CameraCharacteristics.getPhysicalCameraIds() 下) 時,才能將 YUV 或 RAW 串流替換為兩個等效串流。

架構提供的保證只是從多個實體相機同時取得影格所需的最低要求。大多數裝置都支援額外的串流,有時甚至可以獨立開啟多個實體相機裝置。由於這並非架構的硬性保證,因此必須透過反覆試驗,在每部裝置上進行測試和調整。

建立包含多個實體相機的工作階段

在支援多鏡頭的裝置上使用實體鏡頭時,請開啟單一 CameraDevice (邏輯鏡頭),並在單一工作階段中與其互動。使用 API CameraDevice.createCaptureSession(SessionConfiguration config) 建立單一工作階段,該 API 是在 API 級別 28 中新增。工作階段設定有多個輸出設定,每個設定都有一組輸出目標,以及選用的實體相機 ID。

SessionConfiguration 和 OutputConfiguration 模型
圖 3 SessionConfiguration 和 OutputConfiguration 模型

擷取要求具有相關聯的輸出目標。架構會根據附加的輸出目標,決定要將要求傳送至哪個實體 (或邏輯) 攝影機。如果輸出目標與做為輸出設定傳送的其中一個輸出目標 (以及實體相機 ID) 相符,則該實體相機會接收並處理要求。

使用一對實體攝影機

多鏡頭相機 API 的另一項新增功能是識別邏輯相機,並找出其背後的實體相機。您可以定義函式,協助找出可用的潛在實體相機組合,用來取代其中一個邏輯攝影機串流影像:

/**
 * Helper class used to encapsulate a logical camera and two underlying
 * physical cameras
 */
data class DualCamera(val logicalId: String, val physicalId1: String, val physicalId2: String)

fun findDualCameras(manager: CameraManager, facing: Int? = null): List<DualCamera> {
    val dualCameras = mutableListOf<DualCamera>()

    // Iterate over all the available camera characteristics.
    manager.cameraIdList.map {
        Pair(manager.getCameraCharacteristics(it), it)
    }.filter {
        // Filter by cameras facing the requested direction.
        facing == null || it.first.get(CameraCharacteristics.LENS_FACING) == facing
    }.filter {
        // Filter by logical cameras.
        // CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES_LOGICAL_MULTI_CAMERA requires API >= 28.
        it.first.get(CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES)?.contains(
            CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES_LOGICAL_MULTI_CAMERA
        ) ?: false
    }.forEach {
        // All possible pairs from the list of physical cameras are valid results.
        // NOTE: There could be N physical cameras as part of a logical camera grouping.
        // getPhysicalCameraIds() requires API >= 28.
        val physicalCameras = it.first.physicalCameraIds.toTypedArray()
        for (idx1 in 0 until physicalCameras.size) {
            for (idx2 in (idx1 + 1) until physicalCameras.size) {
                dualCameras.add(
                    DualCamera(
                        it.second, physicalCameras[idx1], physicalCameras[idx2]
                    )
                )
            }
        }
    }

    return dualCameras
}

實體相機的狀態處理是由邏輯相機控制。如要開啟「雙鏡頭」,請開啟與實體鏡頭對應的邏輯鏡頭:

fun openDualCamera(
    cameraManager: CameraManager,
    dualCamera: DualCamera,
    executor: Executor,
    callback: (CameraDevice) -> Unit
) {

    // openCamera() requires API >= 28.
    cameraManager.openCamera(
        dualCamera.logicalId, executor,
        object : CameraDevice.StateCallback() {
            override fun onOpened(device: CameraDevice) = callback(device)
            // Omitting for brevity...
            override fun onError(device: CameraDevice, error: Int) = onDisconnected(device)
            override fun onDisconnected(device: CameraDevice) = device.close()
        }
    )
}

除了選取要開啟的相機外,其他程序與在舊版 Android 中開啟相機相同。使用新的工作階段設定 API 建立擷取工作階段,指示架構將特定目標與特定實體相機 ID 建立關聯:

/**
 * Helper type definition that encapsulates 3 sets of output targets:
 *
 * 1. Logical camera
 * 2. First physical camera
 * 3. Second physical camera
 */
typealias DualCameraOutputs =
    Triple<MutableList<Surface>?, MutableList<Surface>?, MutableList<Surface>?>

fun createDualCameraSession(
    cameraManager: CameraManager,
    dualCamera: DualCamera,
    targets: DualCameraOutputs,
    executor: Executor,
    callback: (CameraCaptureSession) -> Unit
) {

    // Create 3 sets of output configurations: one for the logical camera, and
    // one for each of the physical cameras.
    val outputConfigsLogical = targets.first?.map { OutputConfiguration(it) }
    val outputConfigsPhysical1 = targets.second?.map {
        OutputConfiguration(it).apply { setPhysicalCameraId(dualCamera.physicalId1) }
    }
    val outputConfigsPhysical2 = targets.third?.map {
        OutputConfiguration(it).apply { setPhysicalCameraId(dualCamera.physicalId2) }
    }

    // Put all the output configurations into a single flat array.
    val outputConfigsAll = arrayOf(
        outputConfigsLogical, outputConfigsPhysical1, outputConfigsPhysical2
    )
        .filterNotNull().flatMap { it }

    // Instantiate a session configuration that can be used to create a session.
    val sessionConfiguration = SessionConfiguration(
        SessionConfiguration.SESSION_REGULAR,
        outputConfigsAll, executor,
        object : CameraCaptureSession.StateCallback() {
            override fun onConfigured(session: CameraCaptureSession) = callback(session)
            // Omitting for brevity...
            override fun onConfigureFailed(session: CameraCaptureSession) = session.device.close()
        }
    )

    // Open the logical camera using the previously defined function.
    openDualCamera(cameraManager, dualCamera, executor = executor) {

        // Finally create the session and return using the callback.
        it.createCaptureSession(sessionConfiguration)
    }
}

如要瞭解支援哪些串流組合,請參閱 createCaptureSession。合併串流適用於單一邏輯相機上的多個串流。相容性也延伸至使用相同設定,並將其中一個串流替換為兩個實體相機的兩個串流,這些相機屬於同一個邏輯相機。

相機工作階段準備就緒後,請調度所選擷取要求。擷取要求的每個目標都會從相關聯的實體相機 (如有使用) 接收資料,或改用邏輯相機。

Zoom 應用實例

您可以將實體相機合併為單一串流,讓使用者在不同的實體相機之間切換,體驗不同的視野,有效擷取不同的「縮放等級」。

切換相機以調整縮放等級的範例
圖 4:切換相機以調整縮放等級的用途範例 (來自 Pixel 3 廣告)

首先,請選取一組實體相機,讓使用者切換。為達到最佳效果,您可以選擇提供最小和最大焦距的攝影機組合。

fun findShortLongCameraPair(manager: CameraManager, facing: Int? = null): DualCamera? {

    return findDualCameras(manager, facing).map {
        val characteristics1 = manager.getCameraCharacteristics(it.physicalId1)
        val characteristics2 = manager.getCameraCharacteristics(it.physicalId2)

        // Query the focal lengths advertised by each physical camera.
        val focalLengths1 = characteristics1.get(
            CameraCharacteristics.LENS_INFO_AVAILABLE_FOCAL_LENGTHS
        ) ?: floatArrayOf(0F)
        val focalLengths2 = characteristics2.get(
            CameraCharacteristics.LENS_INFO_AVAILABLE_FOCAL_LENGTHS
        ) ?: floatArrayOf(0F)

        // Compute the largest difference between min and max focal lengths between cameras.
        val focalLengthsDiff1 = (focalLengths2.maxOrNull() ?: 0f) - (focalLengths1.minOrNull() ?: 0f)
        val focalLengthsDiff2 = (focalLengths1.maxOrNull() ?: 0f) - (focalLengths2.minOrNull() ?: 0f)

        // Return the pair of camera IDs and the difference between min and max focal lengths.
        if (focalLengthsDiff1 < focalLengthsDiff2) {
            Pair(DualCamera(it.logicalId, it.physicalId1, it.physicalId2), focalLengthsDiff1)
        } else {
            Pair(DualCamera(it.logicalId, it.physicalId2, it.physicalId1), focalLengthsDiff2)
        }

        // Return only the pair with the largest difference, or null if no pairs are found.
    }.maxByOrNull { it.second }?.first
}

合理的架構是使用兩個 SurfaceViews,每個串流各一個。這些 SurfaceViews 會根據使用者互動進行切換,因此在任何指定時間只會顯示一個。

以下程式碼顯示如何開啟邏輯相機、設定相機輸出內容、建立相機工作階段,以及啟動兩個預覽串流:

val cameraManager: CameraManager = ...
val cameraExecutor: Executor = ...  // owned by your activity / fragment

// Get the two output targets from the activity / fragment.
val surface1 = ...  // from SurfaceView
val surface2 = ...  // from SurfaceView

val dualCamera = findShortLongCameraPair(cameraManager) ?: return
val outputTargets = DualCameraOutputs(
    null, mutableListOf(surface1), mutableListOf(surface2)
)

// Here you open the logical camera, configure the outputs and create a session.
createDualCameraSession(
    cameraManager, dualCamera, targets = outputTargets, executor = cameraExecutor
) { session ->

    // Create a single request which has one target for each physical camera.
    // NOTE: Each target receive frames from only its associated physical camera.
    val requestTemplate = CameraDevice.TEMPLATE_PREVIEW
    val captureRequest = session.device.createCaptureRequest(requestTemplate).apply {
        arrayOf(surface1, surface2).forEach { addTarget(it) }
    }.build()

    // Set the sticky request for the session and you are done.
    session.setRepeatingRequest(captureRequest, null, null)
}

接下來,只要提供 UI 讓使用者在兩個介面之間切換即可,例如按鈕或輕觸兩下 SurfaceView。您甚至可以執行某種形式的場景分析,並在兩個串流之間自動切換。

鏡頭扭曲

所有鏡頭都會產生一定程度的失真。在 Android 中,您可以使用 CameraCharacteristics.LENS_DISTORTION 查詢鏡頭造成的失真,這會取代現已淘汰的 CameraCharacteristics.LENS_RADIAL_DISTORTION。如果是邏輯相機,失真程度極低,應用程式可以大致使用相機提供的影格。實體相機的鏡頭配置可能大不相同,尤其是廣角鏡頭。

部分裝置可能會使用 CaptureRequest.DISTORTION_CORRECTION_MODE 實作自動變形校正。大多數裝置預設會開啟變形校正功能。

val cameraSession: CameraCaptureSession = ...

// Use still capture template to build the capture request.
val captureRequest = cameraSession.device.createCaptureRequest(
    CameraDevice.TEMPLATE_STILL_CAPTURE
)

// Determine if this device supports distortion correction.
val characteristics: CameraCharacteristics = ...
val supportsDistortionCorrection = characteristics.get(
    CameraCharacteristics.DISTORTION_CORRECTION_AVAILABLE_MODES
)?.contains(
    CameraMetadata.DISTORTION_CORRECTION_MODE_HIGH_QUALITY
) ?: false

if (supportsDistortionCorrection) {
    captureRequest.set(
        CaptureRequest.DISTORTION_CORRECTION_MODE,
        CameraMetadata.DISTORTION_CORRECTION_MODE_HIGH_QUALITY
    )
}

// Add output target, set other capture request parameters...

// Dispatch the capture request.
cameraSession.capture(captureRequest.build(), ...)

在此模式下設定擷取要求,可能會影響相機產生的影格率。您可以選擇只對靜態圖片拍攝設定變形校正。