API multi-camera

Nota: questa pagina fa riferimento al pacchetto Camera2. A meno che la tua app non richieda funzionalità specifiche di basso livello di Camera2, ti consigliamo di utilizzare CameraX. Sia CameraX che Camera2 supportano Android 5.0 (livello API 21) e versioni successive.

La funzionalità multicamera è stata introdotta con Android 9 (livello API 28). Dalla sua uscita, sono stati introdotti sul mercato dispositivi che supportano l'API. Molti casi d'uso con più fotocamere sono strettamente accoppiati a una configurazione hardware specifica. In altre parole, non tutti i casi d'uso sono compatibili con ogni dispositivo, il che rende le funzionalità multi-fotocamera un buon candidato per Play Feature Delivery.

Alcuni casi d'uso tipici includono:

  • Zoom: passaggio da una fotocamera all'altra in base alla regione di ritaglio o alla lunghezza focale scelta.
  • Profondità: utilizza più fotocamere per creare una mappa di profondità.
  • Bokeh: utilizza le informazioni sulla profondità dedotte per simulare un intervallo di messa a fuoco ridotto simile a quello di una fotocamera reflex digitale.

La differenza tra fotocamere logiche e fisiche

Per comprendere l'API multi-camera, è necessario comprendere la differenza tra fotocamere logiche e fisiche. Ad esempio, considera un dispositivo con tre fotocamere posteriori. In questo esempio, ciascuna delle tre fotocamere posteriori è considerata una fotocamera fisica. Una fotocamera logica è quindi un raggruppamento di due o più di queste fotocamere fisiche. L'output della fotocamera logica può essere uno stream proveniente da una delle fotocamere fisiche sottostanti o uno stream combinato proveniente da più fotocamere fisiche sottostanti contemporaneamente. In entrambi i casi, lo stream viene gestito dall'Hardware Abstraction Layer (HAL) della fotocamera.

Molti produttori di smartphone sviluppano applicazioni per fotocamere proprietarie, che di solito sono preinstallate sui loro dispositivi. Per utilizzare tutte le funzionalità dell'hardware, potrebbero utilizzare API private o nascoste o ricevere un trattamento speciale dall'implementazione del driver a cui altre applicazioni non hanno accesso. Alcuni dispositivi implementano il concetto di fotocamere logiche fornendo uno stream di frame combinato proveniente dalle diverse fotocamere fisiche, ma solo ad alcune applicazioni con privilegi. Spesso, solo una delle fotocamere fisiche è esposta al framework. La situazione per gli sviluppatori di terze parti prima di Android 9 è illustrata nel seguente diagramma:

Funzionalità della fotocamera in genere disponibili solo per le applicazioni con privilegi
Figura 1. Funzionalità della fotocamera in genere disponibili solo per le applicazioni con privilegi

A partire da Android 9, le API private non sono più consentite nelle app per Android. Con l'inclusione del supporto della modalità multicamera nel framework, le best practice di Android consigliano vivamente ai produttori di smartphone di esporre una fotocamera logica per tutte le fotocamere fisiche rivolte nella stessa direzione. Di seguito è riportato ciò che gli sviluppatori di terze parti dovrebbero aspettarsi di vedere sui dispositivi con Android 9 e versioni successive:

Accesso completo degli sviluppatori a tutti i dispositivi con fotocamera a partire da Android 9
Figura 2. Accesso completo degli sviluppatori a tutti i dispositivi con fotocamera a partire da Android 9

Ciò che fornisce la fotocamera logica dipende interamente dall'implementazione OEM dell'HAL della fotocamera. Ad esempio, un dispositivo come Pixel 3 implementa la sua fotocamera logica in modo da scegliere una delle sue fotocamere fisiche in base alla lunghezza focale e alla regione di ritaglio richieste.

L'API multi-camera

La nuova API aggiunge le seguenti nuove costanti, classi e metodi:

A causa delle modifiche al documento di definizione della compatibilità (CDD) di Android, l'API Multi-Camera prevede anche alcune aspettative da parte degli sviluppatori. I dispositivi con doppia fotocamera esistevano prima di Android 9, ma l'apertura di più fotocamere contemporaneamente comportava tentativi ed errori. Su Android 9 e versioni successive, la funzionalità multicamera fornisce un insieme di regole per specificare quando è possibile aprire una coppia di fotocamere fisiche che fanno parte della stessa fotocamera logica.

Nella maggior parte dei casi, i dispositivi con Android 9 e versioni successive espongono tutte le fotocamere fisiche (ad eccezione, forse, dei tipi di sensori meno comuni come gli infrarossi) insieme a una fotocamera logica più facile da usare. Per ogni combinazione di stream che funzionano sicuramente, uno stream appartenente a una fotocamera logica può essere sostituito da due stream delle fotocamere fisiche sottostanti.

Più stream contemporaneamente

Utilizzo di più stream della videocamera contemporaneamente illustra le regole per l'utilizzo di più stream contemporaneamente in una singola videocamera. Con un'aggiunta importante, le stesse regole si applicano a più videocamere. CameraMetadata.REQUEST_AVAILABLE_CAPABILITIES_LOGICAL_MULTI_CAMERA spiega come sostituire uno stream YUV_420_888 logico o non elaborato con due stream fisici. In altre parole, ogni stream di tipo YUV o RAW può essere sostituito con due stream di tipo e dimensioni identici. Puoi iniziare con uno stream della videocamera con la seguente configurazione garantita per i dispositivi con una sola videocamera:

  • Stream 1: tipo YUV, dimensioni MAXIMUM dalla fotocamera logica id = 0

Un dispositivo con supporto della modalità multicamera ti consente di creare una sessione sostituendo lo stream YUV logico con due stream fisici:

  • Stream 1: tipo YUV, dimensioni MAXIMUM dalla fotocamera fisica id = 1
  • Stream 2: tipo YUV, dimensioni MAXIMUM dalla fotocamera fisica id = 2

Puoi sostituire uno stream YUV o RAW con due stream equivalenti se e solo se queste due fotocamere fanno parte di un raggruppamento di fotocamere logiche,  elencato in CameraCharacteristics.getPhysicalCameraIds().

Le garanzie fornite dal framework sono solo il minimo indispensabile per ottenere frame da più di una fotocamera fisica contemporaneamente. La maggior parte dei dispositivi supporta stream aggiuntivi, a volte consentendo anche l'apertura indipendente di più dispositivi con fotocamera fisica. Poiché non si tratta di una garanzia assoluta del framework, per farlo è necessario eseguire test e ottimizzazioni per ogni dispositivo utilizzando il metodo per tentativi ed errori.

Creazione di una sessione con più fotocamere fisiche

Quando utilizzi fotocamere fisiche su un dispositivo con più fotocamere, apri una singola CameraDevice (la fotocamera logica) e interagisci con questa in un'unica sessione. Crea la singola sessione utilizzando l'API CameraDevice.createCaptureSession(SessionConfiguration config), che è stata aggiunta nel livello API 28. La configurazione della sessione ha una serie di configurazioni di output, ognuna delle quali ha un insieme di target di output e, facoltativamente, un ID fotocamera fisica scelto.

Modello SessionConfiguration e OutputConfiguration
Figura 3. Modello SessionConfiguration e OutputConfiguration

Alle richieste di acquisizione è associata una destinazione di output. Il framework determina a quale fotocamera fisica (o logica) vengono inviate le richieste in base alla destinazione di output collegata. Se la destinazione di output corrisponde a una delle destinazioni di output inviate come configurazione di output insieme a un ID fotocamera fisica, la fotocamera fisica riceve ed elabora la richiesta.

Utilizzo di un paio di videocamere fisiche

Un'altra aggiunta alle API della fotocamera per la modalità multicamera è la possibilità di identificare le fotocamere logiche e trovare le fotocamere fisiche dietro di esse. Puoi definire una funzione per identificare le potenziali coppie di fotocamere fisiche che puoi utilizzare per sostituire uno degli stream della videocamera logica:

/**
 * 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
}

La gestione dello stato delle fotocamere fisiche è controllata dalla fotocamera logica. Per aprire una "doppia fotocamera", apri la fotocamera logica corrispondente alle fotocamere fisiche:

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()
        }
    )
}

Oltre a selezionare la fotocamera da aprire, la procedura è la stessa di quella per aprire una fotocamera nelle versioni precedenti di Android. La creazione di una sessione di acquisizione utilizzando la nuova API di configurazione della sessione indica al framework di associare determinati target a ID di fotocamere fisiche specifici:

/**
 * 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)
    }
}

Per informazioni sulla combinazione di stream supportata, consulta createCaptureSession. La combinazione di stream è destinata a più stream su una singola videocamera logica. La compatibilità si estende all'utilizzo della stessa configurazione e alla sostituzione di uno di questi stream con due stream provenienti da due fotocamere fisiche che fanno parte della stessa fotocamera logica.

Con la sessione della fotocamera pronta, invia le richieste di acquisizione scelte. Ogni target della richiesta di acquisizione riceve i dati dalla fotocamera fisica associata, se in uso, o torna alla fotocamera logica.

Esempio di caso d'uso di Zoom

È possibile unire le fotocamere fisiche in un unico stream in modo che gli utenti possano passare da una fotocamera fisica all'altra per sperimentare un campo visivo diverso, acquisendo di fatto un "livello di zoom" diverso.

Esempio di scambio di fotocamere per il caso d'uso del livello di zoom
Figura 4. Esempio di scambio di fotocamere per il caso d'uso del livello di zoom (dall'annuncio di Pixel 3)

Per prima cosa, seleziona la coppia di fotocamere fisiche tra cui gli utenti possono passare. Per ottenere il massimo effetto, puoi scegliere la coppia di fotocamere che fornisce la lunghezza focale minima e massima disponibile.

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
}

Un'architettura sensata per questo caso sarebbe avere due SurfaceViews, uno per ogni stream. Questi SurfaceViews vengono scambiati in base all'interazione dell'utente, in modo che ne sia visibile solo uno alla volta.

Il seguente codice mostra come aprire la fotocamera logica, configurare le uscite della fotocamera, creare una sessione della fotocamera e avviare due stream di anteprima:

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)
}

Non resta che fornire un'UI che consenta all'utente di passare da una superficie all'altra, ad esempio un pulsante o un doppio tocco su SurfaceView. Potresti persino eseguire una sorta di analisi della scena e passare automaticamente da uno stream all'altro.

Distorsione dell'obiettivo

Tutti gli obiettivi producono una certa quantità di distorsione. In Android, puoi eseguire query sulla distorsione creata dagli obiettivi utilizzando CameraCharacteristics.LENS_DISTORTION, che sostituisce CameraCharacteristics.LENS_RADIAL_DISTORTION, ora ritirato. Per le fotocamere logiche, la distorsione è minima e l'applicazione può utilizzare i frame più o meno così come provengono dalla fotocamera. Per le fotocamere fisiche, esistono configurazioni degli obiettivi potenzialmente molto diverse, in particolare per gli obiettivi grandangolari.

Alcuni dispositivi potrebbero implementare la correzione automatica delle distorsioni utilizzando CaptureRequest.DISTORTION_CORRECTION_MODE. La correzione delle distorsioni è attiva per impostazione predefinita sulla maggior parte dei dispositivi.

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(), ...)

L'impostazione di una richiesta di acquisizione in questa modalità può influire sulla frequenza fotogrammi che può essere prodotta dalla fotocamera. Puoi scegliere di impostare la correzione delle distorsioni solo per l'acquisizione di immagini fisse.