Tutoriels

Créer un effet de surbrillance avec CameraX et Jetpack Compose

Temps de lecture : 8 min
Voir le profil de Jolanda Verhoef
Jolanda Verhoef Ingénieure en relations avec les développeurs

Bonjour ! Bienvenue dans notre série d'articles sur CameraX et Jetpack Compose. Dans les articles précédents, nous avons abordé les principes de base de la configuration d'un aperçu de la caméra et ajouté une fonctionnalité de mise au point par appui.

🧱 Partie 1: Créer un aperçu de caméra de base à l'aide du nouvel artefact camera-compose. Nous avons abordé la gestion des autorisations et l'intégration de base.

👆 Partie 2: Utiliser le système de gestes, les graphiques et les coroutines Compose pour implémenter une mise au point par appui visuelle.

🔦 Partie 3 (cet article) : Découvrir comment superposer des éléments d'interface utilisateur Compose sur l'aperçu de votre caméra pour une expérience utilisateur plus riche.

📂 Partie 4 : Utiliser des API adaptatives et le framework d'animation Compose pour animer en douceur le passage au mode sur table et inversement sur les téléphones pliables.

Dans cet article, nous allons aborder un sujet un peu plus attrayant visuellement : l'implémentation d'un effet de surbrillance sur l'aperçu de notre caméra, en utilisant la détection de visages comme base de l'effet. Pourquoi, me direz-vous ? Je ne sais pas. Mais c'est plutôt cool 🙂. Et, plus important encore, cela montre comment nous pouvons facilement traduire les coordonnées du capteur en coordonnées d'interface utilisateur, ce qui nous permet de les utiliser dans Compose !

face-detection.gif

Activer la détection de visages

Commençons par modifier CameraPreviewViewModel pour activer la détection de visages. Nous allons utiliser l'API Camera2Interop, qui nous permet d'interagir avec l'API Camera2 sous-jacente à partir de CameraX. Cela nous donne la possibilité d'utiliser des fonctionnalités de caméra qui ne sont pas exposées directement par CameraX. Nous devons apporter les modifications suivantes :

  • Créer un StateFlow qui contient les limites du visage sous forme de liste de Rect.
  • Définir l'option de requête de capture STATISTICS_FACE_DETECT_MODE sur FULL, ce qui active la détection de visages.
  • Définir un CaptureCallback pour obtenir les informations sur le visage à partir du résultat de la capture.
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 {
    ...
}

Une fois ces modifications apportées, notre modèle de vue émet une liste d'objets Rect représentant les cadres de délimitation des visages détectés dans les coordonnées du capteur.

Traduire les coordonnées du capteur en coordonnées d'interface utilisateur

Les cadres de délimitation des visages détectés que nous avons stockés dans la section précédente utilisent des coordonnées dans le système de coordonnées du capteur. Pour dessiner les cadres de délimitation dans notre interface utilisateur, nous devons transformer ces coordonnées afin qu'elles soient correctes dans le système de coordonnées Compose. Nous devons procéder comme suit :

  • Transformer les coordonnées du capteur en coordonnées du tampon d'aperçu
  • Transformer les coordonnées du tampon d'aperçu en coordonnées de l'interface utilisateur Compose

Ces transformations sont effectuées à l'aide de matrices de transformation. Chacune des transformations possède sa propre matrice :

Nous pouvons créer une méthode d'assistance qui peut effectuer la transformation pour nous :

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
}
  • Nous parcourons la liste des visages détectés et, pour chaque visage, nous exécutons la transformation.
  • Le CoordinateTransformer.transformMatrix que nous obtenons de notre CameraXViewfinder transforme par défaut les coordonnées de l'interface utilisateur en coordonnées du tampon. Dans notre cas, nous voulons que la matrice fonctionne dans l'autre sens, en transformant les coordonnées du tampon en coordonnées de l'interface utilisateur. Par conséquent, nous utilisons la méthode invert() pour inverser la matrice.
  • Nous transformons d'abord le visage des coordonnées du capteur en coordonnées du tampon à l'aide de sensorToBufferTransformMatrix, puis nous transformons ces coordonnées du tampon en coordonnées de l'interface utilisateur à l'aide de bufferToUiTransformMatrix.

Implémenter l'effet de surbrillance

Nous allons maintenant mettre à jour le composable CameraPreviewContent pour dessiner l'effet de surbrillance. Nous allons utiliser un Canvas composable pour dessiner un masque de dégradé sur l'aperçu, ce qui rendra les visages détectés 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
                    )
                }
            }
        }
    }
}

Voici comment cela fonctionne :

  • Nous collectons la liste des visages à partir du modèle de vue.
  • Pour nous assurer que nous ne recomposons pas l'intégralité de l'écran chaque fois que la liste des visages détectés change, nous utilisons derivedStateOf pour savoir si des visages sont détectés. Nous pouvons ensuite utiliser AnimatedVisibility pour animer la superposition colorée.
  • Le surfaceRequest contient les informations dont nous avons besoin pour transformer les coordonnées du capteur en coordonnées du tampon dans le SurfaceRequest.TransformationInfo. Nous utilisons la fonction produceState pour configurer un écouteur dans la requête de surface et effacer cet écouteur lorsque le composable quitte l'arborescence de composition.
  • Nous utilisons un Canvas pour dessiner un rectangle rose translucide qui couvre l'intégralité de l'écran.
  • Nous différons la lecture de la variable sensorFaceRects jusqu'à ce que nous nous trouvions dans le bloc de dessin Canvas. Nous transformons ensuite les coordonnées en coordonnées de l'interface utilisateur.
  • Nous parcourons les visages détectés et, pour chaque visage, nous dessinons un dégradé radial qui rendra l'intérieur du rectangle du visage transparent.
  • Nous utilisons BlendMode.DstOut pour nous assurer que nous découpons le dégradé du rectangle rose, ce qui crée l'effet de surbrillance.

Remarque : Lorsque vous remplacez la caméra par DEFAULT_FRONT_CAMERA vous remarquerez que le projecteur est mis en miroir ! Il s'agit d'un problème connu, suivi dans l' outil de suivi des problèmes de Google.

Résultat

Avec ce code, nous disposons d'un effet de surbrillance entièrement fonctionnel qui met en évidence les visages détectés. Vous trouverez l'extrait de code complet ici.

Cet effet n'est que le début. En utilisant la puissance de Compose, vous pouvez créer une myriade d'expériences de caméra visuellement époustouflantes. La possibilité de transformer les coordonnées du capteur et du tampon en coordonnées de l'interface utilisateur Compose et inversement signifie que nous pouvons utiliser toutes les fonctionnalités de l'interface utilisateur Compose et les intégrer de manière transparente au système de caméra sous-jacent. Avec les animations, les graphiques d'interface utilisateur avancés, la gestion simple de l'état de l'interface utilisateur et le contrôle total des gestes, votre imagination est la seule limite !

Dans le dernier article de la série, nous verrons comment utiliser des API adaptatives et le framework d'animation Compose pour passer de manière transparente d'une interface utilisateur de caméra à une autre sur les appareils pliables. Tenez-vous informé !


Les extraits de code de ce blog sont soumis à la licence suivante :

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

Merci beaucoup à Nick Butcher, Alex Vanyo, Trevor McGuire, Don Turner et Lauren Ward pour leur examen et leurs commentaires. Merci à Yasith Vidanaarachch pour son travail acharné.

 

Written by:
Lire la suite