Solicitudes y sesiones de capturas de la cámara

Nota: En esta página, se hace referencia al paquete Camera2. A menos que tu app requiera funciones específicas y de bajo nivel de Camera2, te recomendamos usar CameraX. CameraX y Camera2 admiten Android 5.0 (nivel de API 21) y versiones posteriores.

Un solo dispositivo Android puede tener varias cámaras. Cada cámara es un CameraDevice, y un CameraDevice puede generar más de una transmisión a la vez.

Un motivo para hacerlo es que una transmisión, los fotogramas de cámara secuenciales que provienen de una CameraDevice, se optimice para una tarea específica, como mostrar un visor, mientras que otras se pueden usar para tomar una foto o grabar un video.Las transmisiones actúan como canalizaciones paralelas que procesan fotogramas sin procesar que salen de la cámara,un fotograma a la vez:

Figura 1: Ilustración de Cómo compilar una app de cámara universal (Google I/O 2018)

El procesamiento paralelo sugiere que puede haber límites de rendimiento según la capacidad de procesamiento disponible de la CPU, la GPU o algún otro procesador. Si una canalización no puede seguir el ritmo de los marcos entrantes, comienza a descartarlos.

Cada canalización tiene su propio formato de salida. Los datos sin procesar que entran se transforman automáticamente en el formato de salida apropiado mediante la lógica implícita asociada con cada canalización. El CameraDevice que se usa en las muestras de código de esta página no es específico, por lo que primero debes enumerar todas las cámaras disponibles antes de continuar.

Puedes usar el CameraDevice para crear un CameraCaptureSession, que es específico de ese CameraDevice. Un CameraDevice debe recibir una configuración de marcos para cada fotograma sin procesar mediante CameraCaptureSession. La configuración especifica los atributos de la cámara, como el enfoque automático, la apertura, los efectos y la exposición. Debido a restricciones del hardware, solo hay una configuración activa en el sensor de la cámara en un momento dado, lo que se denomina configuración activa.

Sin embargo, los casos de uso de transmisión mejoran y extienden las formas anteriores de usar CameraDevice para transmitir sesiones de captura, lo que te permite optimizar la transmisión de la cámara para tu caso de uso particular. Por ejemplo, puede mejorar la duración de batería cuando se optimizan las videollamadas.

Una CameraCaptureSession describe todas las canalizaciones posibles vinculadas a CameraDevice. Cuando se crea una sesión, no puedes agregar ni quitar canalizaciones. CameraCaptureSession mantiene una cola de CaptureRequest, que se convierten en la configuración activa.

Un CaptureRequest agrega una configuración a la cola y selecciona una, más de una o todas las canalizaciones disponibles para recibir un marco del CameraDevice. Puedes enviar muchas solicitudes de captura durante el ciclo de vida de una sesión de captura. Cada solicitud puede cambiar la configuración activa y el conjunto de canalizaciones de salida que reciben la imagen sin procesar.

Usa casos de uso de transmisión para obtener un mejor rendimiento

Los casos de uso de transmisión son una forma de mejorar el rendimiento de las sesiones de captura de Camera2. Le proporcionan más información al dispositivo de hardware para ajustar los parámetros, lo que brinda una mejor experiencia de cámara para tu tarea específica.

Esto permite que el dispositivo de cámara optimice las canalizaciones de hardware y software de la cámara en función de las situaciones del usuario para cada transmisión. Para obtener más información sobre los casos de uso de transmisión, consulta setStreamUseCase.

Los casos de uso de transmisión te permiten especificar con mayor detalle cómo se usa una transmisión de cámara en particular, además de configurar una plantilla en CameraDevice.createCaptureRequest(). Esto permite que el hardware de la cámara optimice parámetros, como el ajuste, el modo o la configuración del sensor de la cámara, en función de las compensaciones de calidad o latencia adecuadas para casos de uso específicos.

Estos son algunos casos de uso de las transmisiones:

  • DEFAULT: Abarca todo el comportamiento de la aplicación existente. Equivale a no establecer ningún caso de uso de transmisión.

  • PREVIEW: Se recomienda para el visor o el análisis de imágenes en la app.

  • STILL_CAPTURE: Optimizado para la captura de alta resolución de alta calidad y no se espera que mantenga velocidades de fotogramas similares a las de la vista previa.

  • VIDEO_RECORD: Optimizado para la captura de video de alta calidad, incluida la estabilización de imagen de alta calidad, si es compatible con el dispositivo y la aplicación la habilita. Esta opción puede producir fotogramas de salida con un retraso considerable en tiempo real para permitir una estabilización de la más alta calidad o algún otro tipo de procesamiento.

  • VIDEO_CALL: Se recomienda para usos de cámara de larga duración en los que el consumo de energía es un problema.

  • PREVIEW_VIDEO_STILL: Se recomienda para apps de redes sociales o casos de uso de transmisión única. Es una transmisión multipropósito.

  • VENDOR_START: Se usa para casos de uso definidos por el OEM.

Cómo crear una CameraCaptureSession

Para crear una sesión de cámara, proporciónale uno o más búferes de salida en los que tu app pueda escribir fotogramas de salida. Cada búfer representa una canalización. Debes hacer esto antes de comenzar a usar la cámara para que el framework pueda configurar las canalizaciones internas del dispositivo y asignar búferes de memoria a fin de enviar marcos a los destinos de salida necesarios.

En el siguiente fragmento de código, se muestra cómo puedes preparar una sesión de cámara con dos búferes de salida, uno que pertenezca a un SurfaceView y otro a un ImageReader. Agregar el caso de uso de transmisión PREVIEW a previewSurface y STILL_CAPTURE el caso de uso de transmisión a imReaderSurface permite que el hardware del dispositivo optimice aún más estas transmisiones.

Kotlin


// Retrieve the target surfaces, which might be coming from a number of places:
// 1. SurfaceView, if you want to display the image directly to the user
// 2. ImageReader, if you want to read each frame or perform frame-by-frame
// analysis
// 3. OpenGL Texture or TextureView, although discouraged for maintainability
      reasons
// 4. RenderScript.Allocation, if you want to do parallel processing
val surfaceView = findViewById<SurfaceView>(...)
val imageReader = ImageReader.newInstance(...)

// Remember to call this only *after* SurfaceHolder.Callback.surfaceCreated()
val previewSurface = surfaceView.holder.surface
val imReaderSurface = imageReader.surface
val targets = listOf(previewSurface, imReaderSurface)

// Create a capture session using the predefined targets; this also involves
// defining the session state callback to be notified of when the session is
// ready
// Setup Stream Use Case while setting up your Output Configuration.
@RequiresApi(Build.VERSION_CODES.TIRAMISU)
fun configureSession(device: CameraDevice, targets: List<Surface>){
    val configs = mutableListOf<OutputConfiguration>()
    val streamUseCase = CameraMetadata
        .SCALER_AVAILABLE_STREAM_USE_CASES_PREVIEW_VIDEO_STILL

    targets.forEach {
        val config = OutputConfiguration(it)
        config.streamUseCase = streamUseCase.toLong()
        configs.add(config)
    }
    ...
    device.createCaptureSession(session)
}

Java


// Retrieve the target surfaces, which might be coming from a number of places:
// 1. SurfaceView, if you want to display the image directly to the user
// 2. ImageReader, if you want to read each frame or perform frame-by-frame
      analysis
// 3. RenderScript.Allocation, if you want to do parallel processing
// 4. OpenGL Texture or TextureView, although discouraged for maintainability
      reasons
Surface surfaceView = findViewById<SurfaceView>(...);
ImageReader imageReader = ImageReader.newInstance(...);

// Remember to call this only *after* SurfaceHolder.Callback.surfaceCreated()
Surface previewSurface = surfaceView.getHolder().getSurface();
Surface imageSurface = imageReader.getSurface();
List<Surface> targets = Arrays.asList(previewSurface, imageSurface);

// Create a capture session using the predefined targets; this also involves defining the
// session state callback to be notified of when the session is ready
private void configureSession(CameraDevice device, List<Surface> targets){
    ArrayList<OutputConfiguration> configs= new ArrayList()
    String streamUseCase=  CameraMetadata
        .SCALER_AVAILABLE_STREAM_USE_CASES_PREVIEW_VIDEO_STILL

    for(Surface s : targets){
        OutputConfiguration config = new OutputConfiguration(s)
        config.setStreamUseCase(String.toLong(streamUseCase))
        configs.add(config)
}

device.createCaptureSession(session)
}

En este punto, no definiste la configuración activa de la cámara. Cuando la sesión está configurada, puedes crear y enviar solicitudes de captura para hacerlo.

La transformación que se aplica a las entradas a medida que se escriben en su búfer se determina mediante el tipo de cada destino, que debe ser un Surface. El framework de Android sabe cómo convertir una imagen sin procesar en la configuración activa en un formato apropiado para cada destino. La conversión se controla mediante el formato de píxeles y el tamaño del Surface en particular.

El framework intenta dar lo mejor de sí, pero es posible que algunas combinaciones de configuración de Surface no funcionen, lo que provoca problemas, como que no se cree la sesión, que arroje un error de tiempo de ejecución cuando despaches una solicitud o una degradación del rendimiento. El framework proporciona garantías para combinaciones específicas de parámetros de dispositivo, superficie y solicitud. En la documentación de createCaptureSession(), se proporciona más información.

Solicitudes de captura única

La configuración que se utiliza para cada fotograma está codificada en un CaptureRequest, que se envía a la cámara. Para crear una solicitud de captura, puedes usar una de las plantillas predefinidas o puedes usar TEMPLATE_MANUAL a fin de tener un control total. Cuando eliges una plantilla, debes proporcionar uno o más búferes de salida para usar con la solicitud. Solo puedes usar búferes que ya estén definidos en la sesión de captura que quieras usar.

Las solicitudes de captura usan un patrón de compilador y les brindan a los desarrolladores la oportunidad de configurar muchas opciones diferentes, como la exposición automática, el enfoque automático y la apertura del lente. Antes de configurar un campo, asegúrate de que la opción específica esté disponible para el dispositivo llamando a CameraCharacteristics.getAvailableCaptureRequestKeys() y de que se admita el valor deseado. Para ello, verifica las características adecuadas de la cámara, como los modos de exposición automática disponibles.

Si deseas crear una solicitud de captura para un SurfaceView con la plantilla diseñada para la vista previa sin modificaciones, usa CameraDevice.TEMPLATE_PREVIEW:

Kotlin

val session: CameraCaptureSession = ...  // from CameraCaptureSession.StateCallback
val captureRequest = session.device.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW)
captureRequest.addTarget(previewSurface)

Java

CameraCaptureSession session = ...;  // from CameraCaptureSession.StateCallback
CaptureRequest.Builder captureRequest =
    session.getDevice().createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW);
captureRequest.addTarget(previewSurface);

Con una solicitud de captura definida, ahora puedes enviarla a la sesión de la cámara:

Kotlin

val session: CameraCaptureSession = ...  // from CameraCaptureSession.StateCallback
val captureRequest: CaptureRequest = ...  // from CameraDevice.createCaptureRequest()

// The first null argument corresponds to the capture callback, which you
// provide if you want to retrieve frame metadata or keep track of failed capture
// requests that can indicate dropped frames; the second null argument
// corresponds to the Handler used by the asynchronous callback, which falls
// back to the current thread's looper if null
session.capture(captureRequest.build(), null, null)

Java

CameraCaptureSession session = ...;  // from CameraCaptureSession.StateCallback
CaptureRequest captureRequest = ...;  // from CameraDevice.createCaptureRequest()

// The first null argument corresponds to the capture callback, which you
// provide if you want to retrieve frame metadata or keep track of failed
// capture
// requests that can indicate dropped frames; the second null argument
// corresponds to the Handler used by the asynchronous callback, which falls
// back to the current thread's looper if null
session.capture(captureRequest.build(), null, null);

Cuando se coloca un fotograma de salida en el búfer específico, se activa una devolución de llamada de captura. En muchos casos, se activan devoluciones de llamada adicionales, como ImageReader.OnImageAvailableListener, cuando se procesa el marco que contiene. En este punto, puedes recuperar datos de imágenes del búfer especificado.

Repite solicitudes de captura

Las solicitudes de una sola cámara son fáciles de realizar, pero no son muy útiles para mostrar una vista previa en vivo o un video. En ese caso, debes recibir una transmisión continua de fotogramas, no solo uno. En el siguiente fragmento de código, se muestra cómo agregar una solicitud recurrente a la sesión:

Kotlin

val session: CameraCaptureSession = ...  // from CameraCaptureSession.StateCallback
val captureRequest: CaptureRequest = ...  // from CameraDevice.createCaptureRequest()

// This keeps sending the capture request as frequently as possible until
// the
// session is torn down or session.stopRepeating() is called
// session.setRepeatingRequest(captureRequest.build(), null, null)

Java

CameraCaptureSession session = ...;  // from CameraCaptureSession.StateCallback
CaptureRequest captureRequest = ...;  // from CameraDevice.createCaptureRequest()

// This keeps sending the capture request as frequently as possible until the
// session is torn down or session.stopRepeating() is called
// session.setRepeatingRequest(captureRequest.build(), null, null);

Una solicitud de captura recurrente hace que el dispositivo de cámara capture imágenes de forma continua mediante la configuración del CaptureRequest proporcionado. La API de Camera2 también permite a los usuarios capturar videos de la cámara mediante el envío de CaptureRequests repetidos, como se ve en este repositorio de muestra de Camera2 en GitHub. También puede renderizar un video en cámara lenta capturando un video de alta velocidad (cámara lenta) con una ráfaga recurrente CaptureRequests, como se muestra en la app de ejemplo de video en cámara lenta de Camera2 en GitHub.

Intercala CaptureRequests

Para enviar una segunda solicitud de captura mientras la solicitud de captura recurrente está activa, por ejemplo, para mostrar un visor y permitir que los usuarios tomen una foto, no es necesario que detengas la solicitud recurrente en curso. En cambio, emites una solicitud de captura no repetitiva mientras se sigue ejecutando la solicitud recurrente.

Cualquier búfer de salida utilizado debe configurarse como parte de la sesión de la cámara cuando se crea la sesión por primera vez. Las solicitudes repetidas tienen menor prioridad que las solicitudes de un solo fotograma o de ráfaga, lo que permite que funcione el siguiente ejemplo:

Kotlin

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

// Create the repeating request and dispatch it
val repeatingRequest = session.device.createCaptureRequest(
CameraDevice.TEMPLATE_PREVIEW)
repeatingRequest.addTarget(previewSurface)
session.setRepeatingRequest(repeatingRequest.build(), null, null)

// Some time later...

// Create the single request and dispatch it
// NOTE: This can disrupt the ongoing repeating request momentarily
val singleRequest = session.device.createCaptureRequest(
CameraDevice.TEMPLATE_STILL_CAPTURE)
singleRequest.addTarget(imReaderSurface)
session.capture(singleRequest.build(), null, null)

Java

CameraCaptureSession session = ...;  // from CameraCaptureSession.StateCallback

// Create the repeating request and dispatch it
CaptureRequest.Builder repeatingRequest =
session.getDevice().createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW);
repeatingRequest.addTarget(previewSurface);
session.setRepeatingRequest(repeatingRequest.build(), null, null);

// Some time later...

// Create the single request and dispatch it
// NOTE: This can disrupt the ongoing repeating request momentarily
CaptureRequest.Builder singleRequest =
session.getDevice().createCaptureRequest(CameraDevice.TEMPLATE_STILL_CAPTURE);
singleRequest.addTarget(imReaderSurface);
session.capture(singleRequest.build(), null, null);

Sin embargo, este enfoque tiene una desventaja: no se sabe con exactitud cuándo se produce una solicitud única. En la siguiente figura, si A es la solicitud de captura recurrente y B es la solicitud de captura de un solo fotograma, la sesión procesa la cola de solicitudes de la siguiente manera:

Figura 2: Ilustración de una cola de solicitudes para la sesión de la cámara en curso

No hay garantías para la latencia entre la última solicitud recurrente de A antes de que se active la solicitud B y la próxima vez que se use A de nuevo, por lo que es posible que experimentes algunos fotogramas omitidos. Puedes realizar algunas acciones para mitigar este problema:

  • Agrega los objetivos de salida de la solicitud A a la solicitud B. De esa manera, cuando el marco de B esté listo, se copia en los destinos de salida de A. Por ejemplo, esto es fundamental cuando se crean instantáneas de video para mantener una velocidad de fotogramas constante. En el código anterior, se agrega singleRequest.addTarget(previewSurface) antes de compilar la solicitud.

  • Usa una combinación de plantillas diseñadas para funcionar en esta situación en particular, como el retraso sin obturador.