Nasıl yapılır? rehberleri

CameraX ve Jetpack Compose ile spot ışığı efekti oluşturma

Okuma süresi: 8 dakika
Jolanda Verhoef
Geliştirici İlişkileri Mühendisi

Merhaba. CameraX ve Jetpack Compose ile ilgili serimize tekrar hoş geldiniz. Önceki gönderilerde kamera önizlemesi ayarlamanın temellerini ele almış ve dokunarak odaklanma işlevini eklemiştik.

🧱 1. Bölüm: Yeni camera-compose yapısını kullanarak temel bir kamera önizlemesi oluşturma. İzin işleme ve temel entegrasyon konularını ele aldık.

👆 2. bölüm: Görsel bir dokunarak odaklanma özelliği uygulamak için Compose hareket sistemini, grafikleri ve eşzamanlı rutinleri kullanma.

🔦 3. Bölüm (bu gönderi): Daha zengin bir kullanıcı deneyimi için Compose kullanıcı arayüzü öğelerini kamera önizlemenizin üzerine yerleştirme yöntemlerini keşfedin.

📂 4. Bölüm: Katlanabilir telefonlarda masaüstü moduna sorunsuz bir şekilde geçiş yapmak için uyarlanabilir API'leri ve Compose animasyon çerçevesini kullanma.

Bu yayında, biraz daha görsel olarak ilgi çekici bir konuya, yani efektin temeli olarak yüz algılamayı kullanarak kamera önizlememizin üzerine bir spot ışığı efekti uygulama konusuna değineceğiz. Neden mi? Emin değilim. Ancak bu özellik kesinlikle çok havalı görünüyor 🙂. Daha da önemlisi, sensör koordinatlarını kullanıcı arayüzü koordinatlarına nasıl kolayca çevirebileceğimizi gösteriyor. Böylece bu koordinatları Compose'da kullanabiliyoruz.

face-detection.gif

Yüz algılamayı etkinleştirme

İlk olarak, yüz algılamayı etkinleştirmek için CameraPreviewViewModel'ı değiştirelim. CameraX'ten temel Camera2 API ile etkileşim kurmamıza olanak tanıyan Camera2Interop API'yi kullanacağız. Bu sayede, doğrudan CameraX tarafından sunulmayan kamera özelliklerini kullanabiliriz. Aşağıdaki değişiklikleri yapmamız gerekiyor:

  • Yüz sınırlarını Rect listesi olarak içeren bir StateFlow oluşturun.
  • STATISTICS_FACE_DETECT_MODE yakalama isteği seçeneğini FULL olarak ayarlayın. Bu seçenek, yüz algılamayı etkinleştirir.
  • Yakalama sonucundaki yüz bilgilerini almak için CaptureCallback ayarlayın.
  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 {
    ...
}

Bu değişiklikler uygulandıktan sonra görünüm modelimiz artık sensör koordinatlarında algılanan yüzlerin sınırlayıcı kutularını temsil eden bir Rect nesne listesi yayınlıyor.

Sensör koordinatlarını kullanıcı arayüzü koordinatlarına çevirme

Son bölümde depoladığımız algılanan yüzlerin sınırlayıcı kutuları, sensör koordinat sistemindeki koordinatları kullanır. Kullanıcı arayüzümüzde sınırlayıcı kutuları çizmek için bu koordinatları Compose koordinat sisteminde doğru olacak şekilde dönüştürmemiz gerekir. Yapmamız gerekenler:

  • Sensör koordinatlarını önizleme arabelleği koordinatlarına dönüştürme
  • Önizleme arabelleği koordinatlarını Compose kullanıcı arayüzü koordinatlarına dönüştürme

Bu dönüşümler, dönüşüm matrisleri kullanılarak yapılır. Her dönüşümün kendi matrisi vardır:

Dönüşümü bizim için yapabilecek bir yardımcı yöntem oluşturabiliriz:

  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
}
  • Tespit edilen yüzlerin listesini tekrarlayarak her yüz için dönüşümü gerçekleştiririz.
  • CameraXViewfinder adlı uygulamamızdan aldığımız CoordinateTransformer.transformMatrix, varsayılan olarak kullanıcı arayüzündeki koordinatları arabellek koordinatlarına dönüştürür. Bizim durumumuzda, arabelleğe alma koordinatlarını kullanıcı arayüzü koordinatlarına dönüştürerek matrisin ters yönde çalışmasını istiyoruz. Bu nedenle, matrisi tersine çevirmek için invert() yöntemini kullanırız.
  • Önce sensorToBufferTransformMatrix kullanarak yüzü sensör koordinatlarından arabellek koordinatlarına, ardından bufferToUiTransformMatrix kullanarak arabellek koordinatlarını kullanıcı arayüzü koordinatlarına dönüştürürüz.

Ön plana alma efektini uygulama

Şimdi, CameraPreviewContent composable'ı güncelleyerek spot ışığı efektini çizelim. Algılanan yüzleri görünür hale getirmek için önizlemenin üzerine gradyan maskesi çizmek amacıyla bir Canvas composable'ı kullanacağız:

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

İşleyiş şekli:

  • Görünüm modelinden yüzlerin listesini toplarız.
  • Algılanan yüzlerin listesi her değiştiğinde ekranın tamamını yeniden oluşturmamak için derivedStateOf kullanarak yüz algılanıp algılanmadığını takip ederiz. Bu değer, renkli yer paylaşımını animasyonla içeri ve dışarı taşımak için AnimatedVisibility ile birlikte kullanılabilir.
  • surfaceRequest, sensör koordinatlarını SurfaceRequest.TransformationInfo içinde arabellek koordinatlarına dönüştürmek için ihtiyacımız olan bilgileri içerir. Yüzey isteğinde bir dinleyici ayarlamak ve composable, kompozisyon ağacından ayrıldığında bu dinleyiciyi temizlemek için produceState işlevini kullanırız.
  • Ekranın tamamını kaplayan yarı saydam pembe bir dikdörtgen çizmek için Canvas kullanırız.
  • sensorFaceRects değişkeninin okunmasını, Canvas çizim bloğunun içine girene kadar erteleriz. Ardından, koordinatları kullanıcı arayüzü koordinatlarına dönüştürürüz.
  • Algılanan yüzleri yineleriz ve her yüz için, yüz dikdörtgeninin içini şeffaf hale getirecek dairesel renk geçişi çizeriz.
  • BlendMode.DstOut aracını kullanarak pembe dikdörtgenin gradyanını kesip çıkararak spot ışığı efekti oluştururuz.

Not: Kamerayı DEFAULT_FRONT_CAMERA olarak değiştirdiğinizde spot ışığının yansıtıldığını fark edeceksiniz. Bu bilinen bir sorundur ve Google Sorun İzleyici'de takip edilmektedir.

Sonuç

Bu kodla, algılanan yüzleri vurgulayan tam işlevsel bir spot ışığı efekti elde ederiz. Kod snippet'inin tamamını burada bulabilirsiniz.

Bu efekt yalnızca başlangıç. Compose'un gücünü kullanarak görsel açıdan çarpıcı sayısız kamera deneyimi oluşturabilirsiniz. Sensör ve arabellek koordinatlarını Compose kullanıcı arayüzü koordinatlarına ve tekrar geri dönüştürebilmek, tüm Compose kullanıcı arayüzü özelliklerini kullanabileceğimiz ve bunları temel kamera sistemiyle sorunsuz bir şekilde entegre edebileceğimiz anlamına gelir. Animasyonlar, gelişmiş kullanıcı arayüzü grafikleri, basit kullanıcı arayüzü durumu yönetimi ve tam hareket kontrolü ile hayal gücünüzü sınırlamayın.

Serinin son yayınında, katlanabilir cihazlarda farklı kamera kullanıcı arayüzleri arasında sorunsuz geçiş yapmak için uyarlanabilir API'leri ve Compose animasyon çerçevesini nasıl kullanacağımızı ayrıntılı olarak ele alacağız. Bizi izlemeye devam edin!


Bu blogdaki kod snippet'leri aşağıdaki lisansa sahiptir:

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

İnceleme ve geri bildirimleri için Nick Butcher, Alex Vanyo, Trevor McGuire, Don Turner ve Lauren Ward'a teşekkür ederiz. Yasith Vidanaarachch'ın sıkı çalışması sayesinde mümkün olmuştur.

 

Yazan:

Okumaya devam edin