Guides pratiques

Créer un effet de mise en avant avec CameraX et Jetpack Compose

Temps de lecture : 8 min
Jolanda Verhoef
Ingénieur chargé des relations avec les développeurs

Bonjour ! Bienvenue dans notre série 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é la fonctionnalité de mise au point par pression.

🧱 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 fonctionnalité de mise au point visuelle par appui.

🔦 Partie 3 (cet article) : découvrez 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 les API adaptatives et le framework d'animation Compose pour passer en douceur du mode sur table au mode plié sur les téléphones pliables.

Dans cet article, nous allons nous intéresser à quelque chose d'un peu plus visuel : l'implémentation d'un effet de mise en avant sur l'aperçu de notre caméra, en utilisant la détection des visages comme base de l'effet. Pourquoi ? Je ne sais pas. Mais il a l'air cool 🙂. Et, plus important encore, il montre comment nous pouvons facilement traduire les coordonnées du capteur en coordonnées d'UI, 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 des visages. Nous allons utiliser l'API Camera2Interop, qui nous permet d'interagir avec l'API Camera2 sous-jacente à partir de CameraX. Cela nous permet d'utiliser des fonctionnalités de l'appareil photo qui ne sont pas directement exposées par CameraX. Nous devons apporter les modifications suivantes :

  • Créez un StateFlow contenant les limites du visage sous forme de liste de Rect.
  • Définissez l'option de demande de capture STATISTICS_FACE_DETECT_MODE sur FULL (COMPLET), ce qui active la détection des visages.
  • Définissez 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 {
    ...
}

Avec ces modifications, notre modèle de vue émet désormais 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 de l'UI

Les cadres de sélection 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 sélection dans notre UI, nous devons transformer ces coordonnées afin qu'elles soient correctes dans le système de coordonnées Compose. Nous devons :

  • 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'UI 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 chacun d'eux, nous exécutons la transformation.
  • Le CoordinateTransformer.transformMatrix que nous obtenons à partir de notre CameraXViewfinder transforme par défaut les coordonnées de l'UI 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'UI. 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'UI à l'aide de bufferToUiTransformMatrix.

Implémenter l'effet de surbrillance

Maintenant, mettons à jour le composable CameraPreviewContent pour dessiner l'effet de mise en avant. Nous allons utiliser un composable Canvas 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 de ne pas recomposer 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. Il peut ensuite être utilisé avec 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 Canvas pour dessiner un rectangle rose translucide qui recouvre l'intégralité de l'écran.
  • Nous différons la lecture de la variable sensorFaceRects jusqu'à ce que nous soyons à l'intérieur du bloc de dessin Canvas. Nous transformons ensuite les coordonnées en coordonnées d'interface utilisateur.
  • Nous parcourons les visages détectés et, pour chacun d'eux, nous dessinons un dégradé radial qui rendra l'intérieur du rectangle du visage transparent.
  • Nous utilisons BlendMode.DstOut pour nous assurer de découper le dégradé du rectangle rose, créant ainsi l'effet de projecteur.

Remarque : Lorsque vous passez à la caméra DEFAULT_FRONT_CAMERA, vous remarquerez que le projecteur est inversé. Il s'agit d'un problème connu, qui est suivi dans l' outil de suivi des problèmes Google.

Résultat

Avec ce code, nous avons un effet de projecteur entièrement fonctionnel qui met en évidence les visages détectés. Pour consulter l'extrait de code complet, cliquez ici.

Cet effet n'est qu'un 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'UI Compose et inversement nous permet d'utiliser toutes les fonctionnalités de l'UI Compose et de les intégrer de manière transparente au système de caméras sous-jacent. Avec les animations, les graphismes avancés de l'UI, la gestion simple de l'état de l'UI 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 les API adaptatives et le framework d'animation Compose pour passer facilement d'une UI de caméra à une autre sur les appareils pliables. Restez attentifs !


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 leurs commentaires et leur avis. Grâce au travail acharné de Yasith Vidanaarachch.

 

Écrit par :

Lire la suite