Anleitungen

Mit CameraX und Jetpack Compose einen Spotlight-Effekt erstellen

Lesezeit: 8 Minuten
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 Artifact „camera-compose“ erstellen. Wir haben die Berechtigungsverwaltung und die grundlegende Integration behandelt.

👆 Teil 2: Das Compose-Gesten-System, Grafiken und Coroutinen verwenden, um eine visuelle Tippen-zum-Fokus-Funktion zu implementieren.

🔦 Teil 3 (dieser Beitrag): Compose-UI-Elemente über der Kameravorschau einblenden, um die Nutzerfreundlichkeit zu verbessern.

📂 Teil 4: Adaptive APIs und das Compose-Animations-Framework verwenden, um auf faltbaren Smartphones reibungslose Animationen in den und aus dem Tischmodus zu ermöglichen.

In diesem Beitrag geht es um etwas visuell Ansprechenderes: die Implementierung eines Spotlight-Effekts über der Kameravorschau, wobei als Grundlage für den Effekt die Gesichtserkennung dienen soll. 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

Zuerst ändern wir 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 von CameraX nicht direkt bereitgestellt werden. Wir müssen die folgenden Änderungen vornehmen:

  • Erstellen Sie einen StateFlow, der die Gesichtsgrenzen als Liste von Rects enthält.
  • Legen Sie die Option für die Erfassungsanfrage STATISTICS_FACE_DETECT_MODE auf „FULL“ fest, um die Gesichtserkennung zu aktivieren.
  • Legen Sie einen CaptureCallback fest, um die Gesichtsinformationen aus dem Erfassungsergebnis 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 Viewmodel eine Liste von Rect-Objekten aus, die die Begrenzungsrahmen der erkannten Gesichter in Sensorkoordinaten darstellen.

Sensorkoordinaten in UI-Koordinaten umwandeln

Die Begrenzungsrahmen der erkannten Gesichter, die wir im letzten Abschnitt gespeichert haben, verwenden Koordinaten im Sensorkoordinatensystem. Um die Begrenzungsrahmen in unserer Benutzeroberfläche zu zeichnen, müssen wir diese Koordinaten so transformieren, dass sie im Compose-Koordinatensystem korrekt sind. Dazu müssen wir Folgendes tun:

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

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

Wir können eine Hilfsmethode erstellen, die die Transformation für uns durchfü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 unserem CameraXViewfinder erhalten, transformiert standardmäßig Koordinaten von der Benutzeroberfläche in Pufferkoordinaten. In unserem Fall soll die Matrix umgekehrt funktionieren und Pufferkoordinaten in UI-Koordinaten transformieren. Daher verwenden wir die Methode invert(), um die Matrix zu invertieren.
  • Zuerst transformieren wir das Gesicht mit der sensorToBufferTransformMatrix von Sensorkoordinaten in Pufferkoordinaten und dann mit der bufferToUiTransformMatrix von Pufferkoordinaten in UI-Koordinaten.

Spotlight-Effekt implementieren

Jetzt aktualisieren wir das zusammensetzbare Element CameraPreviewContent, um den Spotlight-Effekt zu zeichnen. Wir verwenden ein Canvas zusammensetzbares Element, um eine Farbverlaufsmaske über die Vorschau zu zeichnen, sodass die erkannten Gesichter sichtbar sind:

  @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 Viewmodel ab.
  • Damit nicht jedes Mal der gesamte Bildschirm neu zusammengesetzt wird, wenn sich die Liste der erkannten Gesichter ändert, verwenden wir derivedStateOf, um zu verfolgen, ob überhaupt Gesichter erkannt werden. Dies kann dann mit AnimatedVisibility verwendet werden, um das farbige Overlay ein- und auszublenden.
  • Die surfaceRequest enthält die Informationen, die wir zum Transformieren von Sensorkoordinaten in Pufferkoordinaten in der SurfaceRequest.TransformationInfo benötigen. Wir verwenden die Funktion produceState, um einen Listener in der Oberflächenanfrage einzurichten und diesen Listener zu löschen, wenn das zusammensetzbare Element den Kompositionsbaum verlässt.
  • Wir verwenden ein Canvas, um ein durchscheinendes rosa Rechteck zu zeichnen, das den gesamten Bildschirm abdeckt.
  • Wir verschieben das Lesen der Variablen sensorFaceRects, bis wir uns im Zeichenblock Canvas befinden. Dann transformieren wir die Koordinaten in UI-Koordinaten.
  • Wir iterieren über 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 ausgeschnitten wird und so der Spotlight-Effekt entsteht.

Hinweis: Wenn Sie die Kamera in DEFAULT_FRONT_CAMERA ändern, wird der Spotlight-Effekt gespiegelt. Dies ist ein bekanntes Problem, das in der Google-Problemverfolgung. dokumentiert ist.

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 nur der Anfang. Mit Compose können Sie eine Vielzahl visuell beeindruckender 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 nahtlos in das zugrunde liegende Kamerasystem integrieren. Mit Animationen, erweiterten UI-Grafiken, einfacher UI-Statusverwaltung und vollständiger Gestensteuerung sind Ihrer Fantasie keine Grenzen gesetzt.

Im letzten Beitrag der Reihe erfahren Sie, wie Sie adaptive APIs und das Compose-Animations-Framework verwenden können, um nahtlos zwischen verschiedenen Kamera-UIs auf faltbaren Geräten zu wechseln. Bleiben Sie dran!


Für die Code-Snippets in diesem Blog gilt 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 die Überprüfung und das Feedback. Möglich gemacht durch die harte Arbeit von Yasith Vidanaarachch.

 

Verfasst von:

Weiterlesen