Instrukcje

Tworzenie efektu punktowego za pomocą CameraX i Jetpack Compose

Czas czytania: 8 minut
Jolanda Verhoef
Inżynier ds. relacji z deweloperami

Cześć! Witamy ponownie w naszej serii o CameraX i Jetpack Compose. W poprzednich postach omówiliśmy podstawy konfigurowania podglądu z kamery i dodawania funkcji ustawiania ostrości przez dotknięcie.

🧱 Część 1: tworzenie podstawowego podglądu z kamery za pomocą nowego artefaktu camera-compose. Omówiliśmy obsługę uprawnień i podstawową integrację.

👆Część 2: implementowanie wizualnego dotknięcia w celu ustawienia ostrości za pomocą systemu gestów, grafiki i korutyn Compose.

🔦 Część 3 (ten post): dowiedz się, jak nakładać elementy interfejsu Compose na podgląd z kamery, aby zapewnić użytkownikom lepsze wrażenia.

📂 Część 4: używanie adaptacyjnych interfejsów API i platformy animacji Compose do płynnego przechodzenia do trybu stołowego i z niego na telefonach składanych.

W tym poście zajmiemy się czymś bardziej atrakcyjnym wizualnie – wdrożeniem efektu reflektora na podglądzie z kamery z wykorzystaniem wykrywania twarzy jako podstawy tego efektu. Dlaczego? Nie wiem. Ale wygląda świetnie 🙂. A co ważniejsze, pokazuje, jak łatwo możemy przekształcić współrzędne czujnika na współrzędne interfejsu, co pozwala nam używać ich w Compose.

face-detection.gif

Włączanie wykrywania twarzy

Najpierw zmodyfikujmy klasę CameraPreviewViewModel, aby włączyć wykrywanie twarzy. Użyjemy interfejsu Camera2Interop API, który umożliwia interakcję z interfejsem Camera2 API z poziomu CameraX. Dzięki temu możemy korzystać z funkcji aparatu, które nie są bezpośrednio udostępniane przez CameraX. Musimy wprowadzić te zmiany:

  • Utwórz StateFlow, który zawiera granice twarzy jako listę obiektów Rect.
  • Ustaw opcję żądania przechwytywania STATISTICS_FACE_DETECT_MODE na FULL, co włącza wykrywanie twarzy.
  • Ustaw CaptureCallback, aby uzyskać informacje o twarzy z wyniku przechwytywania.
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 {
    ...
}

Po wprowadzeniu tych zmian nasz model widoku emituje listę obiektów Rect reprezentujących ramki ograniczające wykryte twarze we współrzędnych czujnika.

Przekształcanie współrzędnych czujnika na współrzędne interfejsu

Pola ograniczające wykrytych twarzy, które zostały zapisane w ostatniej sekcji, używają współrzędnych w układzie współrzędnych czujnika. Aby narysować ramki w naszym interfejsie, musimy przekształcić te współrzędne, aby były prawidłowe w systemie współrzędnych Compose. Musimy:

  • Przekształć współrzędne czujnika we współrzędne bufora podglądu.
  • Przekształć współrzędne bufora podglądu na współrzędne interfejsu Compose.

Przekształcenia te są wykonywane za pomocą macierzy przekształceń. Każda z tych transformacji ma własną macierz:

Możemy utworzyć metodę pomocniczą, która wykona przekształcenie:

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
}
  • Przechodzimy przez listę wykrytych twarzy i w przypadku każdej z nich wykonujemy przekształcenie.
  • CoordinateTransformer.transformMatrix, które otrzymujemy z CameraXViewfinder, domyślnie przekształca współrzędne z interfejsu w współrzędne bufora. W naszym przypadku chcemy, aby macierz działała w drugą stronę, przekształcając współrzędne bufora na współrzędne interfejsu. Dlatego do odwrócenia macierzy używamy metody invert().
  • Najpierw przekształcamy twarz ze współrzędnych czujnika na współrzędne bufora za pomocą funkcji sensorToBufferTransformMatrix, a następnie przekształcamy te współrzędne bufora na współrzędne interfejsu za pomocą funkcji bufferToUiTransformMatrix.

Wdrażanie efektu wyróżnienia

Teraz zaktualizujmy funkcję kompozycyjną CameraPreviewContent, aby narysować efekt wyróżnienia. Użyjemy komponentu Canvas, aby narysować maskę w postaci gradientu na podglądzie, dzięki czemu wykryte twarze będą widoczne:

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

Jak to działa:

  • Listę twarzy pobieramy z modelu widoku.
  • Aby nie odświeżać całego ekranu za każdym razem, gdy zmieni się lista wykrytych twarzy, używamy zmiennej derivedStateOf, która śledzi, czy w ogóle wykryto jakieś twarze. Możesz go użyć z parametrem AnimatedVisibility, aby animować kolorową nakładkę.
  • surfaceRequest zawiera informacje potrzebne do przekształcenia współrzędnych czujnika na współrzędne bufora w SurfaceRequest.TransformationInfo. Funkcji produceState używamy do skonfigurowania odbiornika w żądaniu powierzchni i wyczyszczenia go, gdy komponent kompozycyjny opuści drzewo kompozycji.
  • Używamy Canvas, aby narysować półprzezroczysty różowy prostokąt, który pokrywa cały ekran.
  • Odczytanie zmiennej sensorFaceRects odkładamy do momentu, gdy znajdziemy się w bloku rysowania Canvas. Następnie przekształcamy współrzędne na współrzędne interfejsu.
  • Iterujemy po wykrytych twarzach i dla każdej z nich rysujemy gradient promieniowy, który sprawi, że wnętrze prostokąta twarzy będzie przezroczyste.
  • Używamy BlendMode.DstOut, aby wyciąć gradient z różowego prostokąta i utworzyć efekt reflektora.

Uwaga: gdy zmienisz kamerę na DEFAULT_FRONT_CAMERA, zauważysz, że reflektor jest odwrócony. Jest to znany problem, który jest śledzony w narzędziu Google Issue Tracker.

Wynik

Dzięki temu kodowi mamy w pełni funkcjonalny efekt punktowy, który wyróżnia wykryte twarze. Pełny fragment kodu znajdziesz tutaj.

Ten efekt to dopiero początek – dzięki Compose możesz tworzyć niezliczone, oszałamiające wizualnie funkcje aparatu. Możliwość przekształcania współrzędnych czujnika i bufora na współrzędne interfejsu Compose i z powrotem oznacza, że możemy korzystać ze wszystkich funkcji interfejsu Compose i bezproblemowo integrować je z systemem aparatu. Animacje, zaawansowana grafika interfejsu, proste zarządzanie stanem interfejsu i pełna kontrola gestami – ogranicza Cię tylko wyobraźnia.

W ostatnim poście z tej serii omówimy, jak używać adaptacyjnych interfejsów API i platformy animacji Compose, aby płynnie przełączać się między różnymi interfejsami aparatu na urządzeniach składanych. Więcej informacji już wkrótce.


Fragmenty kodu na tym blogu są objęte tą licencją:

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

Dziękujemy Nickowi Butcherowi, Alexowi Vanyo, Trevorowi McGuire, Donowi Turnerowi i Lauren Ward za sprawdzenie i przekazanie opinii. Dzięki ciężkiej pracy Yasitha Vidanaarachchiego.

 

Autor:

Czytaj dalej