Nota: Esta página hace referencia al paquete Camera2. A menos que tu app requiera funciones específicas de bajo nivel de Camera2, te recomendamos que uses CameraX. CameraX y Camera2 admiten Android 5.0 (nivel de API 21) y versiones posteriores.
La función de varias cámaras se introdujo en Android 9 (nivel de API 28). Desde su lanzamiento, se lanzaron al mercado dispositivos que admiten la API. Muchos casos de uso de varias cámaras están estrechamente vinculados a una configuración de hardware específica. En otras palabras, no todos los casos de uso son compatibles con todos los dispositivos, lo que hace que las funciones de varias cámaras sean una buena opción para Play Feature Delivery.
Estos son algunos casos prácticos típicos:
- Zoom: Cambia entre cámaras según la región de recorte o la longitud focal elegida.
- Profundidad: Se usan varias cámaras para crear un mapa de profundidad.
- Bokeh: Usa información de profundidad inferida para simular una profundidad de campo estrecha similar a la de una DSLR.
La diferencia entre las cámaras lógicas y físicas
Para comprender la API de varias cámaras, es necesario entender la diferencia entre las cámaras lógicas y las físicas. Como referencia, considera un dispositivo con tres cámaras posteriores. En este ejemplo, cada una de las tres cámaras posteriores se considera una cámara física. Una cámara lógica es una agrupación de dos o más de esas cámaras físicas. El resultado de la cámara lógica puede ser una transmisión que provenga de una de las cámaras físicas subyacentes o una transmisión fusionada que provenga de más de una cámara física subyacente de forma simultánea. De cualquier manera, la capa de abstracción de hardware (HAL) de la cámara controla la transmisión.
Muchos fabricantes de teléfonos desarrollan aplicaciones de cámara propias, que suelen venir preinstaladas en sus dispositivos. Para usar todas las capacidades del hardware, pueden usar APIs privadas o escondidas, o recibir un tratamiento especial de la implementación del controlador al que otras aplicaciones no tienen acceso. Algunos dispositivos implementan el concepto de cámaras lógicas proporcionando una transmisión fusionada de fotogramas de las diferentes cámaras físicas, pero solo para ciertas aplicaciones privilegiadas. A menudo, solo una de las cámaras físicas se expone al framework. En el siguiente diagrama, se ilustra la situación de los desarrolladores externos antes de Android 9:
A partir de Android 9, ya no se permiten las APIs privadas en las apps para Android. Con la inclusión de la compatibilidad con varias cámaras en el framework, las prácticas recomendadas de Android sugieren que los fabricantes de teléfonos expongan una cámara lógica para todas las cámaras físicas que apunten en la misma dirección. A continuación, se muestra lo que los desarrolladores externos deberían ver en los dispositivos que ejecutan Android 9 y versiones posteriores:
Lo que proporciona la cámara lógica depende por completo de la implementación de la HAL de la cámara que realice el OEM. Por ejemplo, un dispositivo como Pixel 3 implementa su cámara lógica de tal manera que elige una de sus cámaras físicas según la longitud focal y la región de recorte solicitadas.
La API de varias cámaras
La API nueva agrega las siguientes constantes, clases y métodos nuevos:
CameraMetadata.REQUEST_AVAILABLE_CAPABILITIES_LOGICAL_MULTI_CAMERACameraCharacteristics.getPhysicalCameraIds()CameraCharacteristics.getAvailablePhysicalCameraRequestKeys()CameraDevice.createCaptureSession(SessionConfiguration config)CameraCharacteritics.LOGICAL_MULTI_CAMERA_SENSOR_SYNC_TYPEOutputConfigurationySessionConfiguration
Debido a los cambios en el Documento de definición de compatibilidad (CDD) de Android, la API de varias cámaras también incluye ciertas expectativas de los desarrolladores. Los dispositivos con cámaras duales existían antes de Android 9, pero abrir más de una cámara de forma simultánea implicaba un proceso de prueba y error. En Android 9 y versiones posteriores, la función de varias cámaras proporciona un conjunto de reglas para especificar cuándo es posible abrir un par de cámaras físicas que forman parte de la misma cámara lógica.
En la mayoría de los casos, los dispositivos que ejecutan Android 9 y versiones posteriores exponen todas las cámaras físicas (excepto, posiblemente, los tipos de sensores menos comunes, como el infrarrojo) junto con una cámara lógica más fácil de usar. Para cada combinación de transmisiones que se garantiza que funcionarán, una transmisión que pertenezca a una cámara lógica se puede reemplazar por dos transmisiones de las cámaras físicas subyacentes.
Múltiples transmisiones simultáneas
En Cómo usar varias transmisiones de cámara a la vez, se explican las reglas para usar varias transmisiones de forma simultánea en una sola cámara. Con una
adición notable, se aplican las mismas reglas para varias cámaras.
CameraMetadata.REQUEST_AVAILABLE_CAPABILITIES_LOGICAL_MULTI_CAMERA
explica cómo reemplazar un flujo lógico YUV_420_888 o sin procesar por dos flujos
físicos. Es decir, cada transmisión de tipo YUV o RAW se puede reemplazar por dos transmisiones de tipo y tamaño idénticos. Puedes comenzar con una transmisión de la cámara de la siguiente configuración garantizada para dispositivos de una sola cámara:
- Transmisión 1: Tipo YUV, tamaño
MAXIMUMde la cámara lógicaid = 0
Luego, un dispositivo compatible con varias cámaras te permite crear una sesión en la que se reemplace esa transmisión lógica de YUV por dos transmisiones físicas:
- Transmisión 1: Tipo YUV, tamaño
MAXIMUMde la cámara físicaid = 1 - Transmisión 2: Tipo YUV, tamaño
MAXIMUMde la cámara físicaid = 2
Puedes reemplazar un flujo de YUV o RAW por dos flujos equivalentes solo si esas dos cámaras forman parte de una agrupación de cámaras lógicas, que se indica en CameraCharacteristics.getPhysicalCameraIds().
Las garantías que proporciona el framework son solo el mínimo necesario para obtener fotogramas de más de una cámara física de forma simultánea. En la mayoría de los dispositivos, se admiten transmisiones adicionales, lo que, a veces, incluso permite abrir varios dispositivos de cámara físicos de forma independiente. Como no es una garantía estricta del framework, para hacerlo, se deben realizar pruebas y ajustes por dispositivo mediante prueba y error.
Cómo crear una sesión con varias cámaras físicas
Cuando uses cámaras físicas en un dispositivo con varias cámaras, abre una sola CameraDevice (la cámara lógica) y, luego, interactúa con ella en una sola sesión. Crea la sesión única con la API CameraDevice.createCaptureSession(SessionConfiguration config), que se agregó en el nivel de API 28. La configuración de la sesión tiene una serie de parámetros de configuración de salida, cada uno de los cuales tiene un conjunto de destinos de salida y, de manera opcional, un ID de cámara física elegido.
Las solicitudes de captura tienen un destino de salida asociado. El framework determina a qué cámara física (o lógica) se envían las solicitudes según el destino de salida que se adjunte. Si el destino de salida corresponde a uno de los destinos de salida que se enviaron como una configuración de salida junto con un ID de cámara física, esa cámara física recibe y procesa la solicitud.
Usa un par de cámaras físicas
Otra incorporación a las APIs de cámara para varias cámaras es la capacidad de identificar cámaras lógicas y encontrar las cámaras físicas detrás de ellas. Puedes definir una función que te ayude a identificar posibles pares de cámaras físicas que puedes usar para reemplazar una de las transmisiones de la cámara lógica:
/** * 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 cámara lógica controla el manejo de estado de las cámaras físicas. Para abrir una "cámara dual", abre la cámara lógica correspondiente a las cámaras físicas:
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() } ) }
Además de seleccionar qué cámara abrir, el proceso es el mismo que para abrir una cámara en versiones anteriores de Android. Cuando creas una sesión de captura con la nueva API de configuración de sesión, le indicas al framework que asocie ciertos destinos con IDs de cámaras físicas específicos:
/** * 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) } }
Consulta createCaptureSession para obtener información sobre qué
combinación de transmisiones se admite. La combinación de transmisiones es para varias transmisiones en una sola cámara lógica. La compatibilidad se extiende al uso de la misma configuración y al reemplazo de uno de esos flujos por dos flujos de dos cámaras físicas que forman parte de la misma cámara lógica.
Con la sesión de la cámara lista, envía las solicitudes de captura elegidas. Cada destino de la solicitud de captura recibe sus datos de la cámara física asociada, si hay alguna en uso, o recurre a la cámara lógica.
Ejemplo de caso de uso de Zoom
Es posible fusionar las cámaras físicas en una sola transmisión para que los usuarios puedan cambiar entre las diferentes cámaras físicas y experimentar un campo visual diferente, lo que captura de manera efectiva un "nivel de zoom" diferente.
Para comenzar, selecciona el par de cámaras físicas entre las que los usuarios podrán alternar. Para obtener el máximo efecto, puedes elegir el par de cámaras que proporcionan la longitud focal mínima y máxima disponible.
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 }
Una arquitectura razonable para esto sería tener dos
SurfaceViews, uno para cada transmisión. Estos SurfaceViews se intercambian según la interacción del usuario, de modo que solo uno esté visible en un momento determinado.
En el siguiente código, se muestra cómo abrir la cámara lógica, configurar las salidas de la cámara, crear una sesión de cámara y, luego, iniciar dos transmisiones de vista previa:
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) }
Solo queda proporcionar una IU para que el usuario cambie entre las dos superficies, como un botón o presionar dos veces el SurfaceView. Incluso podrías realizar algún tipo de análisis de escena y cambiar entre las dos transmisiones automáticamente.
Distorsión de la lente
Todas las lentes producen una cierta cantidad de distorsión. En Android, puedes consultar la distorsión que crean las lentes con CameraCharacteristics.LENS_DISTORTION, que reemplaza a CameraCharacteristics.LENS_RADIAL_DISTORTION, que ahora está obsoleto. En el caso de las cámaras lógicas, la distorsión es mínima, y tu aplicación puede usar los fotogramas más o menos como provienen de la cámara. En el caso de las cámaras físicas, puede haber configuraciones de lentes muy diferentes, en especial en las lentes gran angular.
Algunos dispositivos pueden implementar la corrección automática de distorsión con CaptureRequest.DISTORTION_CORRECTION_MODE.
La corrección de distorsión está activada de forma predeterminada en la mayoría de los dispositivos.
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(), ...)
Establecer una solicitud de captura en este modo puede afectar la velocidad de fotogramas que puede producir la cámara. Puedes optar por configurar la corrección de distorsión solo en las capturas de imágenes fijas.