Panduan

Membuat efek sorotan dengan CameraX dan Jetpack Compose

8 menit membaca
Lihat profil Jolanda Verhoef
Jolanda Verhoef Developer Relations Engineer

Halo! Selamat datang kembali di seri kami tentang CameraX dan Jetpack Compose. Di postingan sebelumnya, kita telah membahas dasar-dasar penyiapan pratinjau kamera dan menambahkan fungsi ketuk untuk memfokuskan.

🧱 Bagian 1: Membangun pratinjau kamera dasar menggunakan artefak camera-compose baru. Kita telah membahas penanganan izin dan integrasi dasar.

👆 Bagian 2: Menggunakan sistem gestur, grafis, dan coroutine Compose untuk menerapkan ketuk untuk memfokuskan visual.

🔦 Bagian 3 (postingan ini): Mempelajari cara melapisi elemen UI Compose di atas pratinjau kamera untuk pengalaman pengguna yang lebih kaya.

📂 Bagian 4: Menggunakan API adaptif dan framework animasi Compose untuk menganimasikan secara lancar ke dan dari mode tabletop di ponsel foldable.

Dalam postingan ini, kita akan mempelajari sesuatu yang lebih menarik secara visual — menerapkan efek sorotan di atas pratinjau kamera, menggunakan deteksi wajah sebagai dasar untuk efek tersebut. Mengapa? Saya tidak yakin. Namun, tampilannya keren 🙂. Dan yang lebih penting, hal ini menunjukkan cara kita dapat dengan mudah menerjemahkan koordinat sensor ke koordinat UI, sehingga kita dapat menggunakannya di Compose.

face-detection.gif

Mengaktifkan deteksi wajah

Pertama, mari kita ubah CameraPreviewViewModel untuk mengaktifkan deteksi wajah. Kita akan menggunakan Camera2Interop API, yang memungkinkan kita berinteraksi dengan Camera2 API yang mendasarinya dari CameraX. Hal ini memberi kita kesempatan untuk menggunakan fitur kamera yang tidak diekspos oleh CameraX secara langsung. Kita perlu melakukan perubahan berikut:

  • Membuat StateFlow yang berisi batas wajah sebagai daftar Rect.
  • Menetapkan opsi permintaan pengambilan STATISTICS_FACE_DETECT_MODE ke FULL, yang mengaktifkan deteksi wajah.
  • Menetapkan CaptureCallback untuk mendapatkan informasi wajah dari hasil pengambilan.
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 {
    ...
}

Dengan perubahan ini, model tampilan kita kini memancarkan daftar objek Rect yang mewakili kotak pembatas wajah yang terdeteksi dalam koordinat sensor.

Menerjemahkan koordinat sensor ke koordinat UI

Kotak pembatas wajah yang terdeteksi yang kita simpan di bagian terakhir menggunakan koordinat dalam sistem koordinat sensor. Untuk menggambar kotak pembatas di UI, kita perlu mengubah koordinat ini agar benar dalam sistem koordinat Compose. Kita perlu:

  • Mengubah koordinat sensor menjadi koordinat buffer pratinjau
  • Mengubah koordinat buffer pratinjau menjadi koordinat UI Compose

Transformasi ini dilakukan menggunakan matriks transformasi. Setiap transformasi memiliki matriksnya sendiri:

Kita dapat membuat metode helper yang dapat melakukan transformasi untuk kita:

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
}
  • Kita melakukan iterasi melalui daftar wajah yang terdeteksi, dan untuk setiap wajah, kita menjalankan transformasi.
  • CoordinateTransformer.transformMatrix yang kita dapatkan dari CameraXViewfinder kita mengubah koordinat dari UI ke koordinat buffer secara default. Dalam kasus ini, kita ingin matriks berfungsi sebaliknya, mengubah koordinat buffer menjadi koordinat UI. Oleh karena itu, kita menggunakan metode invert() untuk membalikkan matriks.
  • Pertama, kita mengubah wajah dari koordinat sensor ke koordinat buffer menggunakan sensorToBufferTransformMatrix, lalu mengubah koordinat buffer tersebut ke koordinat UI menggunakan bufferToUiTransformMatrix.

Menerapkan efek sorotan

Sekarang, mari kita perbarui composable CameraPreviewContent untuk menggambar efek sorotan. Kita akan menggunakan composable Canvas untuk menggambar mask gradien di atas pratinjau, sehingga wajah yang terdeteksi terlihat:

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

Berikut cara kerjanya:

  • Kita mengumpulkan daftar wajah dari model tampilan.
  • Untuk memastikan kita tidak mengomposisi ulang seluruh layar setiap kali daftar wajah yang terdeteksi berubah, kita menggunakan derivedStateOf untuk melacak apakah ada wajah yang terdeteksi sama sekali. Kemudian, hal ini dapat digunakan dengan AnimatedVisibility untuk menganimasikan overlay berwarna masuk dan keluar.
  • surfaceRequest berisi informasi yang kita butuhkan untuk mengubah koordinat sensor menjadi koordinat buffer di SurfaceRequest.TransformationInfo. Kita menggunakan fungsi produceState untuk menyiapkan pemroses dalam permintaan platform, dan menghapus pemroses ini saat composable keluar dari pohon komposisi.
  • Kita menggunakan Canvas untuk menggambar persegi panjang berwarna merah muda tembus cahaya yang menutupi seluruh layar.
  • Kita menunda pembacaan variabel sensorFaceRects hingga kita berada di dalam blok gambar Canvas. Kemudian, kita mengubah koordinat menjadi koordinat UI.
  • Kita melakukan iterasi pada wajah yang terdeteksi, dan untuk setiap wajah, kita menggambar gradien radial yang akan membuat bagian dalam persegi panjang wajah menjadi transparan.
  • Kita menggunakan BlendMode.DstOut untuk memastikan bahwa kita memotong gradien dari persegi panjang berwarna merah muda, sehingga membuat efek sorotan.

Catatan: Saat Anda mengubah kamera menjadi DEFAULT_FRONT_CAMERA Anda akan melihat bahwa sorotan dicerminkan. Masalah ini telah diketahui dan dilacak di Google Issue Tracker.

Hasil

Dengan kode ini, kita memiliki efek sorotan yang berfungsi penuh dan menyoroti wajah yang terdeteksi. Anda dapat menemukan cuplikan kode lengkap di sini.

Efek ini hanyalah permulaan — dengan menggunakan kemampuan Compose, Anda dapat membuat berbagai pengalaman kamera yang menakjubkan secara visual. Dengan kemampuan mengubah koordinat sensor dan buffer menjadi koordinat UI Compose dan kembali, kita dapat menggunakan semua fitur UI Compose dan mengintegrasikannya dengan lancar dengan sistem kamera yang mendasarinya. Dengan animasi, grafis UI canggih, pengelolaan status UI sederhana, dan kontrol gestur penuh, imajinasi Anda adalah batasnya.

Di postingan terakhir dalam seri ini, kita akan membahas cara menggunakan API adaptif dan framework animasi Compose untuk bertransisi dengan lancar antara UI kamera yang berbeda di perangkat foldable. Nantikan kabar terbarunya.


Cuplikan kode di blog ini memiliki lisensi berikut:

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

Terima kasih banyak kepada Nick Butcher, Alex Vanyo, Trevor McGuire, Don Turner dan Lauren Ward atas tinjauan dan masukan yang diberikan. Dapat dilakukan berkat kerja keras Yasith Vidanaarachch.

 

Ditulis oleh:
Lanjutkan membaca