Creare un effetto di messa a fuoco con CameraX e Jetpack Compose
Tempo di lettura: 8 minuti
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 messa a fuoco con un 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, della grafica e delle coroutine di Compose per implementare una messa a fuoco visiva con un tocco.
🔦 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 transizione verso e dalla modalità da tavolo sugli smartphone pieghevoli.
In questo post, ci occuperemo di qualcosa di 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 un effetto interessante 🙂. E, cosa ancora più importante, dimostra come possiamo tradurre facilmente le coordinate dei sensori in coordinate dell'interfaccia utente, consentendoci di utilizzarle in Compose.
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 un elenco di
Rects. - Imposta l'opzione della richiesta di acquisizione
STATISTICS_FACE_DETECT_MODEsu FULL, che attiva il rilevamento dei volti. - Imposta un
CaptureCallbackper 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 dei sensori.
Tradurre le coordinate dei sensori 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 dei sensori. 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 dei sensori 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 matrici di trasformazione. Ogni trasformazione ha la propria matrice:
- Il nostro
SurfaceRequestcontiene un'istanzaTransformationInfo, che contiene una matricesensorToBufferTranform. - Il nostro
CameraXViewfinderha unCoordinateTransformerassociato. Ricorderai che abbiamo già utilizzato questo trasformatore nel post del blog precedente per trasformare le coordinate di messa a fuoco con un tocco.
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.transformMatrixche otteniamo dal nostroCameraXViewfindertrasforma 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 metodoinvert()per invertire la matrice. - Innanzitutto, trasformiamo il volto dalle coordinate dei sensori alle coordinate del buffer utilizzando
sensorToBufferTransformMatrix, quindi trasformiamo queste coordinate del buffer in coordinate dell'interfaccia utente utilizzandobufferToUiTransformMatrix.
Implementare l'effetto di messa a fuoco
Ora aggiorniamo il componibile CameraPreviewContent per disegnare l'effetto di messa a fuoco. Utilizzeremo un Canvas componibile 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
derivedStateOfper tenere traccia se sono stati rilevati volti. Questo può essere utilizzato conAnimatedVisibilityper animare la sovrapposizione colorata in entrata e in uscita. surfaceRequestcontiene le informazioni necessarie per trasformare le coordinate dei sensori in coordinate del buffer inSurfaceRequest.TransformationInfo. Utilizziamo la funzioneproduceStateper configurare un listener nella richiesta di superficie e cancellare questo listener quando il componibile lascia l'albero di composizione.- Utilizziamo un
Canvasper disegnare un rettangolo rosa traslucido che copra l'intero schermo. - Posticipiamo la lettura della variabile
sensorFaceRectsfino a quando non ci troviamo all'interno del blocco di disegnoCanvas. 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.DstOutper 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 dei sensori 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 il feedback. Reso possibile dal duro lavoro di Yasith Vidanaarachch.
-
Guide praticheIn questo articolo imparerai a utilizzare l'API di test waitUntil in Compose per attendere che vengano soddisfatte determinate condizioni.
Jose Alcérreca • Tempo di lettura: 3 minuti -
Guide praticheJetpacker è un'app di dimostrazione tecnica che il nostro team ha creato da zero per Google I/O di quest'anno (utilizzando Antigravity). In sostanza, Jetpacker aiuta gli utenti a pianificare, esplorare e godersi la loro prossima grande avventura.
Jolanda Verhoef • Tempo di lettura: 4 minuti -
Guide praticheBentornato alla serie di blog post "Creare app per Android intelligenti", in cui prendiamo un'app per Android di base e la trasformiamo in un'esperienza personalizzata, intelligente e agentica.
Thomas Ezan, Jolanda Verhoef, Caren Chang • Tempo di lettura: 8 minuti
Ricevi ogni settimana nella tua casella di posta le ultime informazioni sullo sviluppo di Android.