Guías prácticas

Crear un efecto de foco con CameraX y Jetpack Compose

Lectura de 8 minutos
Jolanda Verhoef
Ingeniero de relaciones con desarrolladores

¡Hola! Te damos la bienvenida de nuevo a nuestra serie sobre CameraX y Jetpack Compose. En las publicaciones anteriores, hemos explicado los conceptos básicos para configurar una vista previa de la cámara y hemos añadido la función de enfoque al tocar.

🧱 Parte 1: crear una vista previa básica de la cámara con el nuevo artefacto camera-compose. Hemos tratado la gestión de permisos y la integración básica.

👆 Parte 2: usar el sistema de gestos, los gráficos y las corrutinas de Compose para implementar un toque para enfocar visual.

🔦 Parte 3 (esta publicación): descubre cómo superponer elementos de la interfaz de usuario de Compose en la vista previa de la cámara para ofrecer una experiencia de usuario más completa.

📂 Parte 4: usar APIs adaptativas y el framework de animación de Compose para animar de forma fluida el cambio al modo Mesa y viceversa en teléfonos plegables.

En esta entrada, vamos a centrarnos en algo más atractivo visualmente: implementar un efecto de foco sobre la vista previa de la cámara usando la detección de caras como base para el efecto. ¿Por qué?, te preguntarás. No lo sé. Pero tiene un aspecto genial 🙂. Y, lo que es más importante, demuestra cómo podemos traducir fácilmente las coordenadas de los sensores a coordenadas de la interfaz de usuario, lo que nos permite usarlas en Compose.

face-detection.gif

Habilitar la detección de caras

Primero, vamos a modificar CameraPreviewViewModel para habilitar la detección de caras. Usaremos la API Camera2Interop, que nos permite interactuar con la API Camera2 subyacente desde CameraX. Esto nos da la oportunidad de usar funciones de la cámara que no expone CameraX directamente. Tenemos que hacer los siguientes cambios:

  • Crea un StateFlow que contenga los límites de la cara como una lista de Rects.
  • Define la opción de solicitud de captura STATISTICS_FACE_DETECT_MODE en FULL para habilitar la detección de caras.
  • Define un CaptureCallback para obtener la información de la cara del resultado de la captura.
  class CameraPreviewViewModel : ViewModel() {
    ...
    private val _sensorFaceRects = MutableStateFlow(listOf<Rect>())
    val sensorFaceRects: StateFlow<List<Rect>> = _sensorFaceRects.asStateFlow()

    private val cameraPreviewUseCase = Preview.Builder()
        .apply {
            Camera2Interop.Extender(this)
                .setCaptureRequestOption(
                    CaptureRequest.STATISTICS_FACE_DETECT_MODE,
                    CaptureRequest.STATISTICS_FACE_DETECT_MODE_FULL
                )
                .setSessionCaptureCallback(object : CameraCaptureSession.CaptureCallback() {
                    override fun onCaptureCompleted(
                        session: CameraCaptureSession,
                        request: CaptureRequest,
                        result: TotalCaptureResult
                    ) {
                        super.onCaptureCompleted(session, request, result)
                        result.get(CaptureResult.STATISTICS_FACES)
                            ?.map { face -> face.bounds.toComposeRect() }
                            ?.toList()
                            ?.let { faces -> _sensorFaceRects.update { faces } }
                    }
                })
        }
        .build().apply {
    ...
}

Con estos cambios, nuestro modelo de vista ahora emite una lista de objetos Rect que representan los cuadros delimitadores de las caras detectadas en las coordenadas del sensor.

Traducir las coordenadas del sensor a las coordenadas de la interfaz de usuario

Los recuadros delimitadores de las caras detectadas que hemos almacenado en la sección anterior usan coordenadas del sistema de coordenadas del sensor. Para dibujar los cuadros delimitadores en nuestra interfaz de usuario, debemos transformar estas coordenadas para que sean correctas en el sistema de coordenadas de Compose. Tenemos que hacer lo siguiente:

  • Transformar las coordenadas del sensor en coordenadas del búfer de vista previa
  • Transforma las coordenadas del búfer de vista previa en coordenadas de la interfaz de Compose.

Estas transformaciones se realizan mediante matrices de transformación. Cada una de las transformaciones tiene su propia matriz:

Podemos crear un método auxiliar que haga la transformación por nosotros:

  private fun List<Rect>.transformToUiCoords(
    transformationInfo: SurfaceRequest.TransformationInfo?,
    uiToBufferCoordinateTransformer: MutableCoordinateTransformer
): List<Rect> = this.map { sensorRect ->
    val bufferToUiTransformMatrix = Matrix().apply {
        setFrom(uiToBufferCoordinateTransformer.transformMatrix)
        invert()
    }

    val sensorToBufferTransformMatrix = Matrix().apply {
        transformationInfo?.let {
            setFrom(it.sensorToBufferTransform)
        }
    }

    val bufferRect = sensorToBufferTransformMatrix.map(sensorRect)
    val uiRect = bufferToUiTransformMatrix.map(bufferRect)

    uiRect
}
  • Iteramos la lista de caras detectadas y, por cada cara, ejecutamos la transformación.
  • El CoordinateTransformer.transformMatrix que obtenemos de nuestro CameraXViewfinder transforma las coordenadas de la interfaz de usuario en coordenadas de búfer de forma predeterminada. En nuestro caso, queremos que la matriz funcione al revés, es decir, que transforme las coordenadas del búfer en coordenadas de la interfaz de usuario. Por lo tanto, usamos el método invert() para invertir la matriz.
  • Primero, transformamos la cara de las coordenadas del sensor a las coordenadas del búfer mediante sensorToBufferTransformMatrix y, a continuación, transformamos esas coordenadas del búfer a las coordenadas de la interfaz de usuario mediante bufferToUiTransformMatrix.

Implementar el efecto de foco

Ahora, vamos a actualizar el elemento componible CameraPreviewContent para dibujar el efecto de foco. Usaremos un elemento componible Canvas para dibujar una máscara de degradado sobre la vista previa y hacer que las caras detectadas sean visibles:

  @Composable
fun CameraPreviewContent(
    viewModel: CameraPreviewViewModel,
    modifier: Modifier = Modifier,
    lifecycleOwner: LifecycleOwner = LocalLifecycleOwner.current
) {
    val surfaceRequest by viewModel.surfaceRequest.collectAsStateWithLifecycle()
    val sensorFaceRects by viewModel.sensorFaceRects.collectAsStateWithLifecycle()
    val transformationInfo by
        produceState<SurfaceRequest.TransformationInfo?>(null, surfaceRequest) {
            try {
                surfaceRequest?.setTransformationInfoListener(Runnable::run) { transformationInfo ->
                    value = transformationInfo
                }
                awaitCancellation()
            } finally {
                surfaceRequest?.clearTransformationInfoListener()
            }
        }
    val shouldSpotlightFaces by remember {
        derivedStateOf { sensorFaceRects.isNotEmpty() && transformationInfo != null} 
    }
    val spotlightColor = Color(0xDDE60991)
    ..

    surfaceRequest?.let { request ->
        val coordinateTransformer = remember { MutableCoordinateTransformer() }
        CameraXViewfinder(
            surfaceRequest = request,
            coordinateTransformer = coordinateTransformer,
            modifier = ..
        )

        AnimatedVisibility(shouldSpotlightFaces, enter = fadeIn(), exit = fadeOut()) {
            Canvas(Modifier.fillMaxSize()) {
                val uiFaceRects = sensorFaceRects.transformToUiCoords(
                    transformationInfo = transformationInfo,
                    uiToBufferCoordinateTransformer = coordinateTransformer
                )

                // Fill the whole space with the color
                drawRect(spotlightColor)
                // Then extract each face and make it transparent

                uiFaceRects.forEach { faceRect ->
                    drawRect(
                        Brush.radialGradient(
                            0.4f to Color.Black, 1f to Color.Transparent,
                            center = faceRect.center,
                            radius = faceRect.minDimension * 2f,
                        ),
                        blendMode = BlendMode.DstOut
                    )
                }
            }
        }
    }
}

Funciona así:

  • Recopilamos la lista de caras del modelo de vista.
  • Para asegurarnos de que no recomponemos toda la pantalla cada vez que cambia la lista de caras detectadas, usamos derivedStateOf para registrar si se detecta alguna cara. Después, se puede usar con AnimatedVisibility para animar la superposición de color de entrada y salida.
  • El surfaceRequest contiene la información que necesitamos para transformar las coordenadas del sensor en coordenadas del búfer en el SurfaceRequest.TransformationInfo. Usamos la función produceState para configurar un listener en la solicitud de superficie y borrarlo cuando el elemento componible abandona el árbol de composición.
  • Usamos un Canvas para dibujar un rectángulo rosa translúcido que cubra toda la pantalla.
  • Diferimos la lectura de la variable sensorFaceRects hasta que estemos dentro del bloque de dibujo Canvas. Después, transformamos las coordenadas en coordenadas de la interfaz de usuario.
  • Iteramos sobre las caras detectadas y, por cada cara, dibujamos un degradado radial que hará que el interior del rectángulo de la cara sea transparente.
  • Usamos BlendMode.DstOut para asegurarnos de que recortamos el gradiente del rectángulo rosa y creamos el efecto de foco.

Nota: Cuando cambies la cámara a DEFAULT_FRONT_CAMERA, verás que el foco está reflejado. Se trata de un problema conocido que se está monitorizando en la herramienta de seguimiento de incidencias de Google.

Resultado

Con este código, tenemos un efecto de foco totalmente funcional que destaca las caras detectadas. Puedes consultar el fragmento de código completo aquí.

Este efecto es solo el principio. Con la potencia de Compose, puedes crear una gran variedad de experiencias de cámara visualmente impresionantes. Al poder transformar las coordenadas de los sensores y los búferes en coordenadas de la interfaz de Compose y viceversa, podemos utilizar todas las funciones de la interfaz de Compose e integrarlas sin problemas con el sistema de cámara subyacente. Con animaciones, gráficos avanzados de la interfaz de usuario, una gestión sencilla del estado de la interfaz de usuario y un control total por gestos, el límite es tu imaginación.

En la última publicación de la serie, veremos cómo usar las APIs adaptativas y el framework de animación de Compose para cambiar de forma fluida entre diferentes interfaces de cámara en dispositivos plegables. ¡No te las pierdas!


Los fragmentos de código de este blog tienen la siguiente licencia:

// Copyright 2024 Google LLC. SPDX-License-Identifier: Apache-2.0

Muchas gracias a Nick Butcher, Alex Vanyo, Trevor McGuire, Don Turner y Lauren Ward por revisar el artículo y enviarnos sus comentarios. Posibilitado por el arduo trabajo de Yasith Vidanaarachch.

 

Escrito por:

Seguir leyendo