Memulai CameraX

Dalam codelab ini, kita akan mempelajari cara membuat aplikasi kamera yang menggunakan CameraX untuk menampilkan jendela bidik, mengambil foto, dan menganalisis aliran gambar dari kamera.

Untuk mencapai hal ini, kami akan memperkenalkan konsep kasus penggunaan dalam CameraX, yang dapat digunakan untuk berbagai operasi kamera, mulai dari menampilkan jendela bidik hingga menganalisis frame secara real time.

Yang akan kami pelajari

  • Cara menambahkan dependensi CameraX.
  • Cara menampilkan pratinjau kamera dalam aktivitas. (Kasus penggunaan pratinjau)
  • Cara mengambil foto, menyimpannya ke penyimpanan. (Kasus penggunaan ImageCapture)
  • Cara menganalisis frame dari kamera secara real time. (Kasus penggunaan ImageAnalysis)

Perangkat keras yang kita butuhkan

  • Perangkat Android, meskipun emulator Android Studio akan berfungsi dengan baik. API level minimum yang didukung adalah 21.

Software yang kami butuhkan

  • Android Studio 3.3 atau yang lebih baru.

Menggunakan menu Android Studio, mulai project baru dan pilih Empty Activity jika diminta.

Selanjutnya, kita dapat memilih nama apa pun yang diinginkan. Dengan cerdas kami memilih "CameraX App". Kita harus memastikan bahwa bahasa disetel ke Kotlin, API level minimum adalah 21 (yang merupakan minimum yang diperlukan untuk CameraX) dan kita menggunakan artefak AndroidX.

Untuk memulai, mari kita tambahkan dependensi CameraX ke file Gradle aplikasi kita, di dalam bagian dependencies :

// Use the most recent version of CameraX, currently that is alpha04
def camerax_version = "1.0.0-alpha04"
implementation "androidx.camera:camera-core:${camerax_version}"
implementation "androidx.camera:camera-camera2:${camerax_version}"

Saat diminta, klik Sync Now, dan kita akan siap menggunakan CameraX di aplikasi.

Kita akan menggunakan SurfaceTexture untuk menampilkan jendela bidik kamera. Dalam codelab ini, kita akan menampilkan jendela bidik dalam format persegi dengan ukuran tetap. Untuk contoh yang lebih komprehensif yang menampilkan jendela bidik responsif, lihat contoh resmi.

Mari kita edit file tata letak activity_main di bagian res > layout > activity_main.xml:

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
        xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:tools="http://schemas.android.com/tools"
        xmlns:app="http://schemas.android.com/apk/res-auto"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:context=".MainActivity">

    <TextureView
            android:id="@+id/view_finder"
            android:layout_width="640px"
            android:layout_height="640px"
            app:layout_constraintTop_toTopOf="parent"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintEnd_toEndOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>

Bagian penting dalam menambahkan fungsi apa pun dalam project kami yang menggunakan kamera adalah meminta izin KAMERA yang sesuai. Pertama, kita harus mendeklarasikannya dalam manifes, sebelum tag Aplikasi:

<uses-permission android:name="android.permission.CAMERA" />

Kemudian, di dalam MainActivity, kita perlu meminta izin pada runtime. Kita akan membuat perubahan pada file MainActivity di bawah java > com.example.cameraxapp > MainActivity.kt:

Di bagian atas file, di luar definisi class MainActivity, mari kita tambahkan konstanta dan impor berikut:

// Your IDE likely can auto-import these classes, but there are several
// different implementations so we list them here to disambiguate
import android.Manifest
import android.util.Size
import android.graphics.Matrix
import java.util.concurrent.TimeUnit

// This is an arbitrary number we are using to keep tab of the permission
// request. Where an app has multiple context for requesting permission,
// this can help differentiate the different contexts
private const val REQUEST_CODE_PERMISSIONS = 10

// This is an array of all the permission specified in the manifest
private val REQUIRED_PERMISSIONS = arrayOf(Manifest.permission.CAMERA)

Di dalam class MainActivity, tambahkan kolom dan metode bantuan berikut yang digunakan untuk meminta izin dan memicu kode setelah kita mengetahui bahwa semua izin telah diberikan:

class MainActivity : AppCompatActivity(), LifecycleOwner {

    override fun onCreate(savedInstanceState: Bundle?) {
        ...
    }

    // Add this after onCreate

    private lateinit var viewFinder: TextureView

    private fun startCamera() {
        // TODO: Implement CameraX operations
    }

    private fun updateTransform() {
        // TODO: Implement camera viewfinder transformations
    }

    /**
     * Process result from permission request dialog box, has the request
     * been granted? If yes, start Camera. Otherwise display a toast
     */
    override fun onRequestPermissionsResult(
        requestCode: Int, permissions: Array<String>, grantResults: IntArray) {
        if (requestCode == REQUEST_CODE_PERMISSIONS) {
            if (allPermissionsGranted()) {
                viewFinder.post { startCamera() }
            } else {
                Toast.makeText(this,
                    "Permissions not granted by the user.", 
                    Toast.LENGTH_SHORT).show()
                finish()
            }
        }
    }

    /**
     * Check if all permission specified in the manifest have been granted
     */
    private fun allPermissionsGranted() = REQUIRED_PERMISSIONS.all {
        ContextCompat.checkSelfPermission(
               baseContext, it) == PackageManager.PERMISSION_GRANTED
    }
}

Terakhir, kita menggabungkan semuanya dalam onCreate untuk memicu permintaan izin jika diperlukan:

override fun onCreate(savedInstanceState: Bundle?) {
    ...

    // Add this at the end of onCreate function

    viewFinder = findViewById(R.id.view_finder)

    // Request camera permissions
    if (allPermissionsGranted()) {
        viewFinder.post { startCamera() }
    } else {
        ActivityCompat.requestPermissions(
            this, REQUIRED_PERMISSIONS, REQUEST_CODE_PERMISSIONS)
    }

    // Every time the provided texture view changes, recompute layout
    viewFinder.addOnLayoutChangeListener { _, _, _, _, _, _, _, _, _ ->
        updateTransform()
    }
}

Sekarang, saat aplikasi dimulai, aplikasi akan memeriksa apakah aplikasi memiliki izin kamera yang sesuai. Jika ya, fungsi tersebut akan memanggil `startCamera()` secara langsung. Jika tidak, kode akan meminta izin dan, setelah diberikan, memanggil `startCamera()`.

Untuk sebagian besar aplikasi kamera, menampilkan jendela bidik kepada pengguna sangatlah penting. Jika tidak, sangat sulit bagi pengguna untuk mengarahkan kamera ke tempat yang tepat. Viewfinder dapat diterapkan menggunakan class `Preview` CameraX.

Untuk menggunakan Pratinjau, pertama-tama kita harus menentukan konfigurasi yang kemudian digunakan untuk membuat instance kasus penggunaan. Instance yang dihasilkan adalah yang perlu kita ikat ke siklus proses CameraX. Kita akan melakukannya dalam metode `startCamera()`; isi implementasi dengan kode ini:

private fun startCamera() {

    // Create configuration object for the viewfinder use case
    val previewConfig = PreviewConfig.Builder().apply {
        setTargetAspectRatio(Rational(1, 1))
        setTargetResolution(Size(640, 640))
    }.build()

    // Build the viewfinder use case
    val preview = Preview(previewConfig)

    // Every time the viewfinder is updated, recompute layout
    preview.setOnPreviewOutputUpdateListener {

        // To update the SurfaceTexture, we have to remove it and re-add it
        val parent = viewFinder.parent as ViewGroup
        parent.removeView(viewFinder)
        parent.addView(viewFinder, 0)

        viewFinder.surfaceTexture = it.surfaceTexture
        updateTransform()
    }

    // Bind use cases to lifecycle
    // If Android Studio complains about "this" being not a LifecycleOwner
    // try rebuilding the project or updating the appcompat dependency to
    // version 1.1.0 or higher.
    CameraX.bindToLifecycle(this, preview)
}

Pada titik ini, kita perlu mengimplementasikan metode `updateTransform()` yang misterius. Di dalam `updateTransform()`, sasarannya adalah mengompensasi perubahan pada orientasi perangkat untuk menampilkan jendela bidik dalam rotasi tegak:

private fun updateTransform() {
    val matrix = Matrix()

    // Compute the center of the view finder
    val centerX = viewFinder.width / 2f
    val centerY = viewFinder.height / 2f

    // Correct preview output to account for display rotation
    val rotationDegrees = when(viewFinder.display.rotation) {
        Surface.ROTATION_0 -> 0
        Surface.ROTATION_90 -> 90
        Surface.ROTATION_180 -> 180
        Surface.ROTATION_270 -> 270
        else -> return
    }
    matrix.postRotate(-rotationDegrees.toFloat(), centerX, centerY)

    // Finally, apply transformations to our TextureView
    viewFinder.setTransform(matrix)
}

Untuk menerapkan aplikasi yang siap produksi, lihat contoh resmi untuk mengetahui apa lagi yang perlu ditangani. Agar codelab ini tetap singkat, kami menggunakan beberapa pintasan. Misalnya, kita tidak mencatat beberapa perubahan konfigurasi, seperti rotasi perangkat 180 derajat, yang tidak memicu pemroses perubahan tata letak. Jendela bidik non-persegi juga perlu mengompensasi perubahan rasio lebar tinggi saat perangkat diputar.

Jika kita mem-build dan menjalankan aplikasi, sekarang kita akan melihat pratinjau kehidupan. Bagus!

Agar pengguna dapat mengambil gambar, kami akan menyediakan tombol sebagai bagian dari tata letak setelah tampilan tekstur di res > layout > activity_main.xml:

<ImageButton
        android:id="@+id/capture_button"
        android:layout_width="72dp"
        android:layout_height="72dp"
        android:layout_margin="24dp"
        app:srcCompat="@android:drawable/ic_menu_camera"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent" />

Kasus penggunaan lainnya berfungsi dengan cara yang sangat mirip dibandingkan dengan Pratinjau. Pertama, kita harus menentukan objek konfigurasi yang digunakan untuk membuat instance objek kasus penggunaan yang sebenarnya. Untuk mengambil foto, saat tombol ambil ditekan, kita perlu memperbarui metode `startCamera()` dan menambahkan beberapa baris kode lagi di akhir, sebelum panggilan ke CameraX.bindToLifecycle:

private fun startCamera() {

    ...

    // Add this before CameraX.bindToLifecycle

    // Create configuration object for the image capture use case
    val imageCaptureConfig = ImageCaptureConfig.Builder()
        .apply {
            setTargetAspectRatio(Rational(1, 1))
            // We don't set a resolution for image capture; instead, we
            // select a capture mode which will infer the appropriate
            // resolution based on aspect ration and requested mode
            setCaptureMode(ImageCapture.CaptureMode.MIN_LATENCY)
    }.build()

    // Build the image capture use case and attach button click listener
    val imageCapture = ImageCapture(imageCaptureConfig)
    findViewById<ImageButton>(R.id.capture_button).setOnClickListener {
        val file = File(externalMediaDirs.first(),
            "${System.currentTimeMillis()}.jpg")
        imageCapture.takePicture(file,
            object : ImageCapture.OnImageSavedListener {
            override fun onError(error: ImageCapture.UseCaseError,
                                 message: String, exc: Throwable?) {
                val msg = "Photo capture failed: $message"
                Toast.makeText(baseContext, msg, Toast.LENGTH_SHORT).show()
                Log.e("CameraXApp", msg)
                exc?.printStackTrace()
            }

            override fun onImageSaved(file: File) {
                val msg = "Photo capture succeeded: ${file.absolutePath}"
                Toast.makeText(baseContext, msg, Toast.LENGTH_SHORT).show()
                Log.d("CameraXApp", msg)
            }
        })
    }

    // Bind use cases to lifecycle
    // If Android Studio complains about "this" being not a LifecycleOwner
    // try rebuilding the project or updating the appcompat dependency to
    // version 1.1.0 or higher.
    CameraX.bindToLifecycle(this, preview)
}

Kemudian, perbarui panggilan ke CameraX.bindToLifecycle untuk menyertakan kasus penggunaan baru:

CameraX.bindToLifecycle(this, preview, imageCapture)

Selain itu, kami telah menerapkan tombol pengambilan foto yang berfungsi.

Fitur CameraX yang sangat menarik adalah class ImageAnalysis. Ini memungkinkan kita untuk menentukan class kustom yang mengimplementasikan antarmuka ImageAnalysis.Analyzer, yang akan dipanggil dengan frame kamera masuk. Sejalan dengan visi inti CameraX, kita tidak perlu khawatir tentang mengelola status sesi kamera atau bahkan membuang gambar; mengikat ke siklus proses yang diinginkan aplikasi kita sudah cukup seperti komponen lifecycle-aware lainnya.

Pertama, kami akan menerapkan penganalisis gambar kustom. Penganalisis kami cukup sederhana -- hanya mencatat luma (luminositas) rata-rata gambar, tetapi mencontohkan apa yang perlu dilakukan untuk kasus penggunaan kompleks secara arbitrer. Hal yang perlu kita lakukan adalah mengganti fungsi `Analyze` di class yang mengimplementasikan antarmuka ImageAnalysis.Analyzer. Kita dapat menentukan implementasi kita sebagai class dalam di dalam MainActivity:

private class LuminosityAnalyzer : ImageAnalysis.Analyzer {
    private var lastAnalyzedTimestamp = 0L

    /**
     * Helper extension function used to extract a byte array from an
     * image plane buffer
     */
    private fun ByteBuffer.toByteArray(): ByteArray {
        rewind()    // Rewind the buffer to zero
        val data = ByteArray(remaining())
        get(data)   // Copy the buffer into a byte array
        return data // Return the byte array
    }

    override fun analyze(image: ImageProxy, rotationDegrees: Int) {
        val currentTimestamp = System.currentTimeMillis()
        // Calculate the average luma no more often than every second
        if (currentTimestamp - lastAnalyzedTimestamp >=
            TimeUnit.SECONDS.toMillis(1)) {
            // Since format in ImageAnalysis is YUV, image.planes[0]
            // contains the Y (luminance) plane
            val buffer = image.planes[0].buffer
            // Extract image data from callback object
            val data = buffer.toByteArray()
            // Convert the data into an array of pixel values
            val pixels = data.map { it.toInt() and 0xFF }
            // Compute average luminance for the image
            val luma = pixels.average()
            // Log the new luma value
            Log.d("CameraXApp", "Average luminosity: $luma")
            // Update timestamp of last analyzed frame
            lastAnalyzedTimestamp = currentTimestamp
        }
    }
}

Dengan class yang mengimplementasikan antarmuka ImageAnalysis.Analyzer, yang perlu kita lakukan adalah membuat instance ImageAnalysis seperti semua kasus penggunaan lainnya dan memperbarui fungsi `startCamera()` sekali lagi, sebelum panggilan ke CameraX.bindToLifecycle:

private fun startCamera() {

    ...

    // Add this before CameraX.bindToLifecycle

    // Setup image analysis pipeline that computes average pixel luminance
    val analyzerConfig = ImageAnalysisConfig.Builder().apply {
        // Use a worker thread for image analysis to prevent glitches
        val analyzerThread = HandlerThread(
            "LuminosityAnalysis").apply { start() }
        setCallbackHandler(Handler(analyzerThread.looper))
        // In our analysis, we care more about the latest image than
        // analyzing *every* image
        setImageReaderMode(
            ImageAnalysis.ImageReaderMode.ACQUIRE_LATEST_IMAGE)
    }.build()

    // Build the image analysis use case and instantiate our analyzer
    val analyzerUseCase = ImageAnalysis(analyzerConfig).apply {
        analyzer = LuminosityAnalyzer()
    }

    // Bind use cases to lifecycle
    // If Android Studio complains about "this" being not a LifecycleOwner
    // try rebuilding the project or updating the appcompat dependency to
    // version 1.1.0 or higher.
    CameraX.bindToLifecycle(this, preview, imageCapture)
}

Dan kita juga memperbarui panggilan ke CameraX.bindtoLifecycle untuk mengikat kasus penggunaan baru:

CameraX.bindToLifecycle(
    this, preview, imageCapture, analyzerUseCase)

Menjalankan aplikasi sekarang akan menghasilkan pesan yang serupa dengan ini di logcat kira-kira setiap detik:

D/CameraXApp: Average luminosity: ...

Untuk menguji aplikasi, yang perlu kita lakukan adalah mengklik tombol Run di Android Studio dan project akan di-build, di-deploy, dan diluncurkan di perangkat atau emulator yang dipilih. Setelah aplikasi dimuat, kita akan melihat jendela bidik, yang akan tetap tegak bahkan setelah memutar perangkat berkat kode penanganan orientasi yang ditambahkan sebelumnya, dan juga akan dapat mengambil foto menggunakan tombol:

Anda telah menyelesaikan codelab dan berhasil menyelesaikan codelab! Melihat ke belakang, Anda telah menerapkan hal berikut ke dalam aplikasi Android baru dari awal:

  • Menyertakan dependensi CameraX ke dalam project Anda.
  • Menampilkan jendela bidik kamera (menggunakan kasus penggunaan Pratinjau)
  • Menerapkan pengambilan foto, menyimpan gambar ke penyimpanan (menggunakan kasus penggunaan ImageCapture)
  • Mengimplementasikan analisis frame dari kamera secara real time (menggunakan kasus penggunaan ImageAnalysis)

Jika Anda tertarik untuk membaca selengkapnya tentang CameraX dan hal-hal yang dapat Anda lakukan dengan CameraX, lihat dokumentasi atau clone contoh resmi.