Poradniki

Tworzenie efektu reflektora za pomocą CameraX i Jetpack Compose

Czas czytania: 8 min
Wyświetl profil Jolandy Verhoef
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: używanie systemu gestów, grafiki i współprogramów Compose do implementowania wizualnego dotykowego ustawiania ostrości.

🔦 Część 3 (ten post): omawianie sposobu nakładania elementów interfejsu Compose na podgląd z kamery w celu zapewnienia użytkownikom lepszych wrażeń.

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

W tym poście zajmiemy się czymś bardziej atrakcyjnym wizualnie – implementacją efektu reflektora na podglądzie z kamery, używając do tego wykrywania twarzy. 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 CameraPreviewViewModel, aby włączyć wykrywanie twarzy. Użyjemy interfejsu Camera2Interop API, który umożliwia interakcję z podstawowym 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ę Rects.
  • Ustaw opcję żądania przechwytywania STATISTICS_FACE_DETECT_MODE na FULL, co włącza wykrywanie twarzy.
  • Ustaw CaptureCallback, aby pobierać 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

Ramki ograniczające wykryte twarze, które zapisaliśmy w poprzedniej sekcji, używają współrzędnych w układzie współrzędnych czujnika. Aby narysować ramki ograniczające w interfejsie, musimy przekształcić te współrzędne tak, aby były prawidłowe w układzie współrzędnych Compose. Musimy:

  • przekształcić współrzędne czujnika na współrzędne bufora podglądu,
  • przekształcić współrzędne bufora podglądu na współrzędne interfejsu Compose.

Te przekształcenia są wykonywane za pomocą macierzy przekształceń. Każde przekształcenie 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
}
  • Iterujemy po liście wykrytych twarzy i dla każdej z nich wykonujemy przekształcenie.
  • Domyślnie CoordinateTransformer.transformMatrix, który pobieramy z CameraXViewfinder, przekształca współrzędne z interfejsu na współrzędne bufora. W naszym przypadku chcemy, aby macierz działała w odwrotnym kierunku, przekształcając współrzędne bufora na współrzędne interfejsu. Dlatego używamy metody invert() do odwrócenia macierzy.
  • Najpierw przekształcamy twarz ze współrzędnych czujnika na współrzędne bufora za pomocą sensorToBufferTransformMatrix, a następnie przekształcamy te współrzędne bufora na współrzędne interfejsu za pomocą bufferToUiTransformMatrix.

Implementowanie efektu reflektora

Teraz zaktualizujmy element kompozycyjny CameraPreviewContent, aby narysować efekt reflektora. Użyjemy elementu kompozycyjnego Canvas, aby narysować maskę 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
                    )
                }
            }
        }
    }
}

Działa to w następujący sposób:

  • Zbieramy listę twarzy z modelu widoku.
  • Aby nie rekomponować całego ekranu za każdym razem, gdy zmienia się lista wykrytych twarzy, używamy derivedStateOf, aby śledzić, czy w ogóle wykryto jakieś twarze. Można go używać z 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. Używamy funkcji produceState, aby skonfigurować odbiornik w żądaniu powierzchni i wyczyścić go, gdy element 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, aż 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 Google Issue Tracker.

Wynik

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

Ten efekt to dopiero początek – dzięki możliwościom Compose możesz tworzyć niezliczone wizualnie oszałamiające efekty związane z aparatem. 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 podstawowym systemem aparatu. Dzięki animacjom, zaawansowanej grafice interfejsu, prostemu zarządzaniu stanem interfejsu i pełnej kontroli gestów 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 do płynnego przechodzenia między różnymi interfejsami aparatu na urządzeniach składanych. Więcej informacji już wkrótce.


Fragmenty kodu w tym poście na 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. Umożliwiła to ciężka praca Yasitha Vidanaarachcha.

 

Scenariusz:
Czytaj dalej