Guide pratiche

Creare un effetto di messa a fuoco con CameraX e Jetpack Compose

Lettura di 8 minuti
Visualizza il profilo di Jolanda Verhoef
Jolanda Verhoef Developer Relations Engineer

Ciao! È un piacere rivederti nella nostra serie su CameraX e Jetpack Compose. Nei post precedenti abbiamo trattato i concetti di base per la configurazione di un'anteprima della videocamera e abbiamo aggiunto la funzionalità di tocco per mettere a fuoco.

🧱 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, della grafica e delle coroutine di Compose per implementare un tocco per mettere a fuoco visivo.

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

📂 Parte 4: utilizzo delle API adattive e del framework di animazione di Compose per animare senza problemi la modalità da tavolo sui telefoni pieghevoli.

In questo post, ci occuperemo di qualcosa di un po' più coinvolgente dal punto di vista visivo: l'implementazione di un effetto di messa a fuoco sull'anteprima della videocamera, utilizzando il rilevamento dei volti come base per l'effetto. Perché, ti chiederai? Non ne sono sicuro. Ma è sicuramente fantastico 🙂. 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 rilevamento dei volti

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 possiamo utilizzare le funzionalità della videocamera che non sono esposte direttamente da CameraX. Dobbiamo apportare le seguenti modifiche:

  • Crea un StateFlow che contenga i limiti dei volti come elenco di Rect.
  • Imposta l'opzione della richiesta di acquisizione STATISTICS_FACE_DETECT_MODE su FULL, che attiva il rilevamento dei volti.
  • Imposta un CaptureCallback per ottenere le informazioni sui volti 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 emette un elenco di Rect oggetti che rappresentano i riquadri di delimitazione dei volti rilevati nelle coordinate del sensore.

Tradurre le coordinate del sensore in coordinate dell'interfaccia utente

I riquadri di delimitazione dei volti rilevati che abbiamo memorizzato nella sezione precedente utilizzano le coordinate nel sistema di coordinate del sensore. Per disegnare i riquadri di delimitazione 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.
  • Trasformare le coordinate del buffer di anteprima in coordinate dell'interfaccia utente di Compose.

Queste trasformazioni vengono eseguite utilizzando le matrici di trasformazione. Ogni trasformazione ha la sua 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 ed eseguiamo la trasformazione per ogni volto.
  • Il CoordinateTransformer.transformMatrix che otteniamo dal nostro CameraXViewfinder trasforma le coordinate dall'interfaccia utente alle coordinate del buffer per impostazione predefinita. Nel nostro caso, vogliamo che la matrice funzioni in modo opposto, trasformando le coordinate del buffer in coordinate dell'interfaccia utente. Pertanto, utilizziamo il metodo invert() per invertire la matrice.
  • Innanzitutto, trasformiamo il volto dalle coordinate del sensore alle coordinate del buffer utilizzando sensorToBufferTransformMatrix, quindi trasformiamo queste coordinate del buffer in coordinate dell'interfaccia utente utilizzando bufferToUiTransformMatrix.

Implementare l'effetto di messa a fuoco

Ora aggiorniamo il composable CameraPreviewContent per disegnare l'effetto di messa a fuoco. Utilizzeremo un Canvas composable per disegnare una maschera di 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 funziona:

  • Raccogliamo l'elenco dei volti dal modello di visualizzazione.
  • Per assicurarci di non ricomporre l'intero schermo ogni volta che l'elenco dei volti rilevati cambia, utilizziamo derivedStateOf per tenere traccia se vengono rilevati volti. Questo può essere utilizzato con AnimatedVisibility per animare la sovrapposizione colorata in entrata e in uscita.
  • 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 cancellare questo listener quando il composable lascia l'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. Quindi trasformiamo le coordinate in coordinate dell'interfaccia utente.
  • Iteriamo i volti rilevati e, per ogni volto, disegniamo una sfumatura radiale che renderà trasparente l'interno del rettangolo del volto.
  • Utilizziamo BlendMode.DstOut per assicurarci di ritagliare la sfumatura dal rettangolo rosa, creando l'effetto di messa a fuoco.

Nota: quando cambi la videocamera in DEFAULT_FRONT_CAMERA noterai che la messa a fuoco è specchiata. Si tratta di un problema noto, monitorato nel Google Issue Tracker.

Risultato

Con questo codice, abbiamo un effetto di messa a fuoco 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 con la videocamera visivamente straordinarie. La possibilità di trasformare le coordinate del sensore e del buffer in coordinate dell'interfaccia utente di Compose e viceversa ci consente di utilizzare tutte le funzionalità dell'interfaccia utente di Compose e di integrarle senza problemi con il sistema della 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.

Nell'ultimo post della serie, vedremo come utilizzare le API adattive e il framework di animazione di Compose per passare senza problemi da un'interfaccia utente della videocamera all'altra sui dispositivi pieghevoli. Torna presto a trovarci.


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 i feedback. Reso possibile grazie al duro lavoro di Yasith Vidanaarachch.

 

Scritto da:
Continua a leggere