Petunjuk

Membuat efek sorotan dengan CameraX dan Jetpack Compose

Waktu baca: 8 menit
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 menempatkan elemen UI Compose di atas pratinjau kamera untuk pengalaman pengguna yang lebih kaya.

📂 Bagian 4: Menggunakan API adaptif dan framework animasi Compose untuk menganimasikan transisi ke dan dari mode di atas meja dengan lancar di ponsel perangkat lipat.

Dalam postingan ini, kita akan membahas sesuatu yang lebih menarik secara visual, yaitu menerapkan efek sorotan di atas pratinjau kamera, menggunakan deteksi wajah sebagai dasar untuk efek tersebut. Mengapa, katamu? 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 langsung oleh CameraX. Kita perlu melakukan perubahan berikut:

  • Buat StateFlow yang berisi batas wajah sebagai daftar Rect.
  • Tetapkan opsi permintaan pengambilan foto STATISTICS_FACE_DETECT_MODE ke FULL, yang memungkinkan deteksi wajah.
  • Tetapkan CaptureCallback untuk mendapatkan informasi wajah dari hasil pengambilan gambar.
  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 merepresentasikan 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 harus:

  • Ubah 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 bantuan 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 bekerja dengan cara lain, yaitu 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 merekomposisi seluruh layar setiap kali daftar wajah yang terdeteksi berubah, kita menggunakan derivedStateOf untuk melacak apakah ada wajah yang terdeteksi. Kemudian, hal ini dapat digunakan dengan AnimatedVisibility untuk menganimasikan masuk dan keluarnya overlay berwarna.
  • 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 permukaan, dan menghapus pemroses ini saat composable keluar dari hierarki komposisi.
  • Kita menggunakan Canvas untuk menggambar persegi panjang merah muda transparan 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 transparan.
  • Kita menggunakan BlendMode.DstOut untuk memastikan bahwa kita memotong gradien dari persegi panjang merah muda, sehingga menciptakan efek sorotan.

Catatan: Saat Anda mengubah kamera ke DEFAULT_FRONT_CAMERA, Anda akan melihat bahwa sorotan dicerminkan. Ini adalah masalah umum, yang dilacak di Pelacak Masalah Google.

Hasil

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

Efek ini hanyalah permulaan — dengan menggunakan kecanggihan Compose, Anda dapat menciptakan berbagai pengalaman kamera yang memukau secara visual. Dengan dapat mengubah koordinat sensor dan buffer menjadi koordinat UI Compose dan sebaliknya, kita dapat memanfaatkan semua fitur UI Compose dan mengintegrasikannya secara lancar dengan sistem kamera yang mendasarinya. Dengan animasi, grafis UI tingkat lanjut, pengelolaan status UI sederhana, dan kontrol gestur penuh, imajinasi Anda adalah batasnya.

Di postingan terakhir dalam seri ini, kita akan mempelajari cara menggunakan API adaptif dan framework animasi Compose untuk melakukan transisi yang lancar antara berbagai UI kamera di perangkat foldable. Nantikan kabar terbarunya.


Cuplikan kode dalam 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 peninjauan dan masukan yang diberikan. Dihadirkan berkat kerja keras Yasith Vidanaarachch.

 

Ditulis oleh:

Lanjutkan membaca