Anleitungen

Mit CameraX und Jetpack Compose einen Spotlight-Effekt erstellen

Lesezeit: 8 Minuten
Profil von Jolanda Verhoef ansehen
Jolanda Verhoef Developer Relations Engineer

Hallo! Willkommen zurück bei unserer Reihe über CameraX und Jetpack Compose. In den vorherigen Beiträgen haben wir die Grundlagen für die Einrichtung einer Kameravorschau behandelt und die Funktion „Zum Fokussieren tippen“ hinzugefügt.

🧱 Teil 1:Eine einfache Kameravorschau mit dem neuen camera-compose-Artefakt erstellen. Wir haben die Berechtigungsverwaltung und die grundlegende Integration behandelt.

👆 Teil 2:Implementieren einer visuellen Tippen-zum-Fokussieren-Funktion mit dem Compose-Gesten-System, Grafiken und Coroutinen.

🔦 Teil 3 (dieser Beitrag): Wir sehen uns an, wie Sie Compose-UI-Elemente über die Kameravorschau legen können, um die Nutzerfreundlichkeit zu verbessern.

📂 Teil 4: Adaptive APIs und das Compose-Animationsframework verwenden, um auf faltbaren Smartphones reibungslose Animationen in den und aus dem Modus „Auf dem Tisch“ zu ermöglichen.

In diesem Beitrag geht es um etwas Visuell Ansprechenderes: Wir implementieren einen Spotlight-Effekt über unserer Kameravorschau und verwenden die Gesichtserkennung als Grundlage für den Effekt. Warum? Keine Ahnung. Aber es sieht auf jeden Fall cool aus 🙂. Und noch wichtiger: Es zeigt, wie wir ganz einfach Sensorkoordinaten in UI-Koordinaten umwandeln können, um sie in Compose zu verwenden.

face-detection.gif

Gesichtserkennung aktivieren

Ändern wir zuerst das CameraPreviewViewModel, um die Gesichtserkennung zu aktivieren. Wir verwenden die Camera2Interop API, mit der wir über CameraX mit der zugrunde liegenden Camera2 API interagieren können. So können wir Kamerafunktionen nutzen, die nicht direkt von CameraX bereitgestellt werden. Wir müssen die folgenden Änderungen vornehmen:

  • Erstellen Sie einen StateFlow, der die Gesichtsgrenzen als Liste von Rects enthält.
  • Setzen Sie die Option für die Erfassungsanfrage STATISTICS_FACE_DETECT_MODE auf FULL, um die Gesichtserkennung zu aktivieren.
  • Legen Sie ein CaptureCallback fest, um die Gesichtsinformationen aus dem Erfassungsresultat abzurufen.
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 {
    ...
}

Nach diesen Änderungen gibt unser Ansichtsmodell jetzt eine Liste von Rect-Objekten aus, die die Begrenzungsrahmen der erkannten Gesichter in Sensorkoordinaten darstellen.

Sensorkoordinaten in Koordinaten der Benutzeroberfläche umwandeln

Die Begrenzungsrahmen der erkannten Gesichter, die wir im letzten Abschnitt gespeichert haben, verwenden Koordinaten im Sensorkoordinatensystem. Damit die Begrenzungsrahmen in unserer Benutzeroberfläche gezeichnet werden können, müssen wir diese Koordinaten so transformieren, dass sie im Compose-Koordinatensystem korrekt sind. Wir müssen:

  • Sensorkoordinaten in Koordinaten des Vorschaupuffers umwandeln
  • Koordinaten des Vorschau-Puffers in Compose-UI-Koordinaten umwandeln

Diese Transformationen werden mithilfe von Transformationsmatrizen durchgeführt. Jede der Transformationen hat eine eigene Matrix:

Wir können eine Hilfsmethode erstellen, die die Transformation für uns ausführt:

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
}
  • Wir iterieren durch die Liste der erkannten Gesichter und führen für jedes Gesicht die Transformation aus.
  • Die CoordinateTransformer.transformMatrix, die wir von unserer CameraXViewfinder erhalten, transformiert Koordinaten standardmäßig von der UI in Pufferkoordinaten. In unserem Fall soll die Matrix umgekehrt funktionieren und Pufferkoordinaten in UI-Koordinaten umwandeln. Daher verwenden wir die invert()-Methode, um die Matrix zu invertieren.
  • Zuerst transformieren wir das Gesicht mithilfe von sensorToBufferTransformMatrix von Sensorkoordinaten in Pufferkoordinaten und dann mithilfe von bufferToUiTransformMatrix von Pufferkoordinaten in UI-Koordinaten.

Spotlight-Effekt implementieren

Aktualisieren wir nun die CameraPreviewContent-Composable, um den Spotlight-Effekt zu zeichnen. Wir verwenden eine Canvas-Composable, um eine Farbverlaufsmaske über die Vorschau zu zeichnen und die erkannten Gesichter sichtbar zu machen:

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

So funktionierts:

  • Wir rufen die Liste der Gesichter aus dem Ansichtsmodell ab.
  • Damit wir nicht jedes Mal den gesamten Bildschirm neu rendern, wenn sich die Liste der erkannten Gesichter ändert, verwenden wir derivedStateOf, um zu verfolgen, ob überhaupt Gesichter erkannt werden. Diese kann dann mit AnimatedVisibility verwendet werden, um das farbige Overlay ein- und auszublenden.
  • Die surfaceRequest enthält die Informationen, die wir benötigen, um Sensorkoordinaten in Pufferkoordinaten in der SurfaceRequest.TransformationInfo zu transformieren. Wir verwenden die produceState-Funktion, um einen Listener in der Oberflächenanfrage einzurichten, und löschen diesen Listener, wenn die komponierbare Funktion den Kompositionsbaum verlässt.
  • Wir verwenden ein Canvas, um ein durchscheinendes rosa Rechteck zu zeichnen, das das gesamte Display abdeckt.
  • Das Lesen der sensorFaceRects-Variablen wird aufgeschoben, bis wir uns im Canvas-Block befinden. Anschließend werden die Koordinaten in UI-Koordinaten umgewandelt.
  • Wir durchlaufen die erkannten Gesichter und zeichnen für jedes Gesicht einen radialen Farbverlauf, der das Innere des Gesichtsrechtecks transparent macht.
  • Wir verwenden BlendMode.DstOut, um sicherzustellen, dass der Farbverlauf aus dem rosa Rechteck herausgeschnitten wird, wodurch der Spotlight-Effekt entsteht.

Hinweis : Wenn Sie die Kamera auf DEFAULT_FRONT_CAMERA ändern, wird das Spotlight gespiegelt. Dieses Problem ist bekannt und wird im Google Issue Tracker erfasst.

Ergebnis

Mit diesem Code haben wir einen voll funktionsfähigen Spotlight-Effekt, der erkannte Gesichter hervorhebt. Das vollständige Code-Snippet finden Sie hier.

Dieser Effekt ist erst der Anfang. Mit Compose können Sie eine Vielzahl von visuell beeindruckenden Kamerafunktionen erstellen. Da wir Sensor- und Pufferkoordinaten in Compose-UI-Koordinaten und zurück transformieren können, können wir alle Compose-UI-Funktionen nutzen und sie nahtlos in das zugrunde liegende Kamerasystem einbinden. Mit Animationen, erweiterten UI-Grafiken, einfacher Verwaltung des UI-Status und vollständiger Gestensteuerung sind Ihrer Kreativität keine Grenzen gesetzt.

Im letzten Beitrag der Reihe erfahren Sie, wie Sie adaptive APIs und das Compose-Animationsframework verwenden, um nahtlos zwischen verschiedenen Kamera-UIs auf faltbaren Geräten zu wechseln. Mehr dazu demnächst!


Die Code-Snippets in diesem Blog haben die folgende Lizenz:

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

Vielen Dank an Nick Butcher, Alex Vanyo, Trevor McGuire, Don Turner und Lauren Ward für das Überprüfen und Bereitstellen von Feedback. Möglich gemacht durch die harte Arbeit von Yasith Vidanaarachchi.

 

Geschrieben von:
Weiterlesen