Cómo hacer

Crea un efecto de foco con CameraX y Jetpack Compose

Lectura de 8 min
Jolanda Verhoef
Ingeniera de Relaciones con Desarrolladores

¡Hola! Te damos la bienvenida nuevamente a nuestra serie sobre CameraX y Jetpack Compose. En las publicaciones anteriores, explicamos los conceptos básicos para configurar una vista previa de la cámara y agregamos la función de enfoque con un toque.

🧱 Parte 1: Cómo compilar una vista previa básica de la cámara con el nuevo artefacto camera-compose. Explicamos el control de permisos y la integración básica.

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

🔦 Parte 3 (esta publicación): Cómo superponer elementos de la IU de Compose sobre la vista previa de la cámara para brindar una experiencia del usuario más enriquecida.

📂 Parte 4: Cómo usar las APIs adaptativas y el framework de animación de Compose para animar de forma fluida el cambio al modo de mesa y desde este en teléfonos plegables.

En esta publicación, analizaremos algo un poco más atractivo visualmente: la implementación de un efecto de foco sobre la vista previa de la cámara, usando la detección de rostros como base para el efecto. ¿Por qué?, te preguntarás. No lo sé. Pero se ve genial 🙂. Y, lo que es más importante, demuestra cómo podemos traducir fácilmente las coordenadas del sensor en coordenadas de la IU, lo que nos permite usarlas en Compose.

face-detection.gif

Habilita la detección de rostros

Primero, modifiquemos CameraPreviewViewModel para habilitar la detección de rostros. Usaremos la Camera2Interop API, que nos permite interactuar con la API de Camera2 subyacente desde CameraX. Esto nos da la oportunidad de usar funciones de la cámara que CameraX no expone directamente. Debemos realizar los siguientes cambios:

  • Crea un StateFlow que contenga los límites de los rostros como una lista de Rects.
  • Configura la opción de solicitud de captura STATISTICS_FACE_DETECT_MODE en FULL, lo que habilita la detección de rostros.
  • Configura un CaptureCallback para obtener la información del rostro 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 Rect objetos que representan los cuadros delimitadores de los rostros detectados en las coordenadas del sensor.

Traduce las coordenadas del sensor a las coordenadas de la IU

Los cuadros delimitadores de los rostros detectados que almacenamos en la sección anterior usan coordenadas en el sistema de coordenadas del sensor. Para dibujar los cuadros delimitadores en nuestra IU, debemos transformar estas coordenadas para que sean correctas en el sistema de coordenadas de Compose. Debemos hacer lo siguiente:

  • Transformar las coordenadas del sensor en coordenadas del búfer de vista previa
  • Transformar las coordenadas del búfer de vista previa en coordenadas de la IU de Compose

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

Podemos crear un método auxiliar que pueda realizar 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 por la lista de rostros detectados y, para cada rostro, ejecutamos la transformación.
  • El CoordinateTransformer.transformMatrix que obtenemos de nuestro CameraXViewfinder transforma las coordenadas de la IU en coordenadas del búfer de forma predeterminada. En nuestro caso, queremos que la matriz funcione al revés, transformando las coordenadas del búfer en coordenadas de la IU. Por lo tanto, usamos el método invert() para invertir la matriz.
  • Primero, transformamos el rostro de las coordenadas del sensor a las coordenadas del búfer con sensorToBufferTransformMatrix y, luego, transformamos esas coordenadas del búfer en coordenadas de la IU con bufferToUiTransformMatrix.

Implementa el efecto de foco

Ahora, actualicemos el elemento componible CameraPreviewContent para dibujar el efecto de foco. Usaremos un elemento componible Canvas para dibujar una máscara de gradiente sobre la vista previa, lo que hará que los rostros detectados 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
                    )
                }
            }
        }
    }
}

Aquí te mostramos cómo funciona:

  • Recopilamos la lista de rostros del modelo de vista.
  • Para asegurarnos de no recomponer toda la pantalla cada vez que cambia la lista de rostros detectados, usamos derivedStateOf para hacer un seguimiento de si se detecta algún rostro. Luego, se puede usar con AnimatedVisibility para animar la superposición de color dentro y fuera.
  • surfaceRequest contiene la información que necesitamos para transformar las coordenadas del sensor en coordenadas del búfer en SurfaceRequest.TransformationInfo. Usamos la función produceState para configurar un objeto de escucha 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 cubre toda la pantalla.
  • Diferimos la lectura de la variable sensorFaceRects hasta que estemos dentro del bloque de dibujo Canvas. Luego, transformamos las coordenadas en coordenadas de la IU.
  • Iteramos sobre los rostros detectados y, para cada rostro, dibujamos un gradiente radial que hará que el interior del rectángulo del rostro sea transparente.
  • Usamos BlendMode.DstOut para asegurarnos de que estamos recortando el gradiente del rectángulo rosa, lo que crea el efecto de foco.

Nota: Cuando cambies la cámara a DEFAULT_FRONT_CAMERA notarás que el foco se refleja. Este es un problema conocido, que se registra en el Issue Tracker de Google.

Resultado

Con este código, tenemos un efecto de foco completamente funcional que destaca los rostros detectados. Puedes encontrar el fragmento de código completo aquí.

Este efecto es solo el comienzo. Con el poder de Compose, puedes crear una gran cantidad de experiencias de cámara visualmente impresionantes. Poder transformar las coordenadas del sensor y del búfer en coordenadas de la IU de Compose y viceversa significa que podemos utilizar todas las funciones de la IU de Compose y las integramos sin problemas con el sistema de cámara subyacente. Con animaciones, gráficos avanzados de la IU, administración simple del estado de la IU y control total de gestos, tu imaginación es el límite.

En la última publicación de la serie, analizaremos cómo usar las APIs adaptativas y el framework de animación de Compose para realizar una transición sin problemas entre diferentes IUs de cámara en dispositivos plegables. ¡No te lo 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 y enviar comentarios. Esto fue posible gracias al arduo trabajo de Yasith Vidanaarachch.

 

Escrito por:

Seguir leyendo