Instructivos

Crea un efecto de foco con CameraX y Jetpack Compose

Lectura de 8 min
Ver el perfil de Jolanda Verhoef
Jolanda Verhoef Ingeniero 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 Ya vimos el manejo 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 toque para enfocar visual.

🔦 Parte 3 (esta publicación): Exploramos 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 modo de mesa en teléfonos plegables.

En esta entrada, profundizaremos en 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 el CameraPreviewViewModel para habilitar la detección de rostros. Usaremos la API de Camera2Interop, que nos permite interactuar con la API de Camera2 subyacente desde CameraX. Esto nos brinda 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 del rostro como una lista de Rects.
  • Establece la opción de solicitud de captura STATISTICS_FACE_DETECT_MODE en FULL, lo que habilita la detección de rostros.
  • Establece 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 implementados, nuestro modelo de vista ahora emite una lista de objetos Rect que representan los cuadros delimitadores de los rostros detectados en las coordenadas del sensor.

Cómo traducir las coordenadas del sensor a 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:

  • Transforma 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 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 realice 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 en 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 de 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 coordenadas del sensor a coordenadas del búfer con sensorToBufferTransformMatrix y, luego, transformamos esas coordenadas del búfer a coordenadas de la IU con bufferToUiTransformMatrix.

Implementa el efecto de foco

Ahora, actualicemos el elemento CameraPreviewContent componible para dibujar el efecto de foco. Usaremos un elemento Canvas componible para dibujar una máscara de degradado 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
                    )
                }
            }
        }
    }
}

Funciona de la siguiente manera:

  • Recopilamos la lista de rostros del modelo de vistas.
  • Para asegurarnos de que no recomponemos 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 entrada y salida de la superposición coloreada.
  • El surfaceRequest contiene la información que necesitamos para transformar las coordenadas del sensor en coordenadas de búfer en el 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 traslúcido que cubre toda la pantalla.
  • Diferimos la lectura de la variable sensorFaceRects hasta que estamos dentro del bloque de dibujo Canvas. Luego, transformamos las coordenadas en coordenadas de la IU.
  • Iteramos sobre los rostros detectados y, para cada uno, dibujamos un gradiente radial que hará que el interior del rectángulo del rostro sea transparente.
  • Usamos BlendMode.DstOut para asegurarnos de que cortamos el gradiente del rectángulo rosa y creamos el efecto de foco.

Nota: Cuando cambies la cámara a DEFAULT_FRONT_CAMERA, notarás que la luz se refleja. Este es un problema conocido, del que se realiza un seguimiento en Issue Tracker de Google.

Resultado

Con este código, tenemos un efecto de foco totalmente 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 visualmente impresionantes con la cámara. 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, luego, integrarlas 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, ¡el único límite es tu imaginación!

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 transiciones sin problemas entre diferentes IU de la cámara en dispositivos plegables. ¡Estate atento!


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 brindar comentarios. Esto es posible gracias al arduo trabajo de Yasith Vidanaarachchi.

 

Escrito por:
Continuar leyendo