Istruzioni

Creare un effetto spotlight con CameraX e Jetpack Compose

Lettura di 8 minuti
Jolanda Verhoef
Developer Relations Engineer

Ciao! Ti diamo il bentornato alla nostra serie su CameraX e Jetpack Compose. Nei post precedenti abbiamo trattato le nozioni di base per la configurazione di un'anteprima della videocamera e abbiamo aggiunto la funzionalità di messa a fuoco con tocco.

🧱 Parte 1: creazione di un'anteprima di base della videocamera utilizzando il nuovo artefatto camera-compose. Abbiamo trattato la gestione delle autorizzazioni e l'integrazione di base.

👆 Parte 2: utilizzo del sistema di gesti Compose, della grafica e delle coroutine per implementare un tocco per mettere a fuoco visivo.

🔦 Parte 3 (questo post): esplorare come sovrapporre gli elementi dell'interfaccia utente di Compose all'anteprima della fotocamera per un'esperienza utente più ricca.

📂 Parte 4: utilizzo di API adattive e del framework di animazione Compose per animare in modo fluido il passaggio alla modalità da tavolo e viceversa sugli smartphone pieghevoli.

In questo post, ci concentreremo su qualcosa di più coinvolgente dal punto di vista visivo: l'implementazione di un effetto spotlight sopra l'anteprima della videocamera, utilizzando il rilevamento del volto come base per l'effetto. Perché, ti chiederai. Non saprei. Ma è sicuramente molto bello 🙂. E, cosa ancora più importante, dimostra come possiamo tradurre facilmente le coordinate del sensore in coordinate dell'interfaccia utente, consentendoci di utilizzarle in Compose.

face-detection.gif

Attivare il riconoscimento facciale

Innanzitutto, modifichiamo CameraPreviewViewModel per attivare il rilevamento dei volti. Utilizzeremo l'API Camera2Interop, che ci consente di interagire con l'API Camera2 sottostante da CameraX. In questo modo abbiamo l'opportunità di utilizzare funzionalità della fotocamera che non sono esposte direttamente da CameraX. Dobbiamo apportare le seguenti modifiche:

  • Crea un StateFlow che contenga i limiti del volto come elenco di Rect.
  • Imposta l'opzione di richiesta di acquisizione STATISTICS_FACE_DETECT_MODE su FULL, che attiva il rilevamento dei volti.
  • Imposta un CaptureCallback per ottenere le informazioni sul volto dal risultato dell'acquisizione.
  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 queste modifiche, il nostro modello di visualizzazione ora genera un elenco di oggetti Rect che rappresentano i rettangoli di selezione dei volti rilevati nelle coordinate del sensore.

Tradurre le coordinate del sensore in coordinate dell'interfaccia utente

I riquadri di selezione dei volti rilevati che abbiamo memorizzato nell'ultima sezione utilizzano le coordinate nel sistema di coordinate del sensore. Per disegnare i riquadri di selezione nella nostra UI, dobbiamo trasformare queste coordinate in modo che siano corrette nel sistema di coordinate di Compose. Dobbiamo:

  • Trasformare le coordinate del sensore in coordinate del buffer di anteprima
  • Trasforma le coordinate del buffer di anteprima in coordinate della UI Compose

Queste trasformazioni vengono eseguite utilizzando matrici di trasformazione. Ogni trasformazione ha la propria matrice:

Possiamo creare un metodo helper che esegua la trasformazione per noi:

  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
}
  • Iteriamo l'elenco dei volti rilevati e per ogni volto eseguiamo la trasformazione.
  • Il CoordinateTransformer.transformMatrix che otteniamo dal nostro CameraXViewfinder trasforma le coordinate dall'UI alle coordinate del buffer per impostazione predefinita. Nel nostro caso, vogliamo che la matrice funzioni al contrario, trasformando le coordinate del buffer in coordinate dell'interfaccia utente. Pertanto, utilizziamo il metodo invert() per invertire la matrice.
  • Innanzitutto, trasformiamo il viso dalle coordinate del sensore alle coordinate del buffer utilizzando sensorToBufferTransformMatrix, quindi trasformiamo le coordinate del buffer in coordinate della UI utilizzando bufferToUiTransformMatrix.

Implementare l'effetto riflettore

Ora aggiorniamo il composable CameraPreviewContent per disegnare l'effetto spotlight. Utilizzeremo un elemento componibile Canvas per disegnare una maschera della sfumatura sull'anteprima, rendendo visibili i volti rilevati:

  @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
                    )
                }
            }
        }
    }
}

Ecco come:

  • Raccogliamo l'elenco dei volti dal modello di visualizzazione.
  • Per assicurarci di non ricomporre l'intero schermo ogni volta che cambia l'elenco dei volti rilevati, utilizziamo derivedStateOf per tenere traccia del rilevamento di eventuali volti. Questo valore può essere utilizzato con AnimatedVisibility per animare la visualizzazione e la scomparsa della sovrapposizione colorata.
  • surfaceRequest contiene le informazioni necessarie per trasformare le coordinate del sensore in coordinate del buffer in SurfaceRequest.TransformationInfo. Utilizziamo la funzione produceState per configurare un listener nella richiesta di superficie e lo cancelliamo quando il componibile esce dall'albero di composizione.
  • Utilizziamo un Canvas per disegnare un rettangolo rosa traslucido che copre l'intero schermo.
  • Posticipiamo la lettura della variabile sensorFaceRects fino a quando non ci troviamo all'interno del blocco di disegno Canvas. Poi trasformiamo le coordinate in coordinate dell'interfaccia utente.
  • Iteriamo sui volti rilevati e, per ciascuno, disegniamo una sfumatura radiale che renderà trasparente l'interno del rettangolo del volto.
  • Utilizziamo BlendMode.DstOut per assicurarci di ritagliare il gradiente dal rettangolo rosa, creando l'effetto riflettore.

Nota: quando passi alla videocamera DEFAULT_FRONT_CAMERA, noterai che il riflettore è specchiato. Si tratta di un problema noto, monitorato nel Google Issue Tracker.

Risultato

Con questo codice, abbiamo un effetto spotlight completamente funzionale che mette in evidenza i volti rilevati. Puoi trovare lo snippet di codice completo qui.

Questo effetto è solo l'inizio: utilizzando la potenza di Compose, puoi creare una miriade di esperienze visive straordinarie con la videocamera. La possibilità di trasformare le coordinate del sensore e del buffer in coordinate dell'interfaccia utente Compose e viceversa ci consente di utilizzare tutte le funzionalità dell'interfaccia utente Compose e integrarle perfettamente con il sistema di videocamera sottostante. Con animazioni, grafica avanzata dell'interfaccia utente, gestione semplice dello stato dell'interfaccia utente e controllo completo dei gesti, l'unico limite è la tua immaginazione.

Nel post finale della serie, vedremo come utilizzare le API adattive e il framework di animazione Compose per passare senza problemi da un'interfaccia utente della fotocamera all'altra sui dispositivi pieghevoli. Continuate a seguirmi!


Gli snippet di codice in questo blog hanno la seguente licenza:

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

Un ringraziamento speciale a Nick Butcher, Alex Vanyo, Trevor McGuire, Don Turner e Lauren Ward per la revisione e il feedback. Reso possibile grazie al duro lavoro di Yasith Vidanaarachch.

 

Scritto da:

Continua a leggere