CameraX'i Kullanmaya Başlama

Bu codelab'de vizör göstermek, fotoğraf çekmek ve kameradaki görüntü akışını analiz etmek için CameraX'i kullanan bir kamera uygulamasının nasıl oluşturulacağını öğreneceğiz.

Bu hedefe ulaşmak için CameraX'te kullanım alanları kavramını ele alacağız. Bu kavram, vizör görüntülemeden kareleri gerçek zamanlı olarak analiz etmeye kadar çeşitli kamera işlemleri için kullanılabilir.

Öğreneceklerimiz

  • CameraX bağımlılıklarını ekleme.
  • Kamera etkinliğinin bir etkinlikte gösterilmesi. (Kullanım alanını önizleyin)
  • Fotoğraf çekme, depolama alanına kaydetme. (ImageCapture kullanım alanı)
  • Kameradaki kareleri gerçek zamanlı olarak analiz edebilirsiniz. (Resim Analizi kullanım alanı)

İhtiyacınız olan donanım

  • Android cihaz emülatörü her şeyi sorunsuz yapabilmesi için bir Android cihaz kullanıyor. Desteklenen minimum API düzeyi 21'dir.

İhtiyacınız olacak yazılımlar

  • Android Studio 3.3 veya sonraki sürümler.

Android Studio menüsünü kullanarak yeni bir proje başlatın ve istendiğinde Boş Etkinlik'i seçin.

Sonra, istediğimiz adı seçebiliyoruz. Acemi Kamera Kamera Uygulamasını seçiyoruz. Dilin Kotlin olarak ayarlandığından, minimum API düzeyinin 21 (KameraX için gereken minimum düzeyde) ve AndroidX yapıları kullanıldığından emin olmalıyız.

Başlamak için, Camera Gradle bağımlılıklarını uygulama Gradle dosyamıza dependencies bölümünde ekleyelim:

// 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}"

İstendiğinde, Şimdi Senkronize Et'i tıklayın. Uygulamamızda CameraX'i kullanmaya hazır olacağız.

Kamera vizörünü görüntülemek için bir SurfaceTexture kullanacağız. Bu codelab'de vizörü sabit boyutlu kare biçiminde göstereceğiz. Duyarlı vizör gösteren daha kapsamlı bir örnek için resmi örneğe göz atın.

event_main düzen dosyasını res > scheme > activity_main.xml altında düzenleyelim:

<?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>

Projemize kamera kullanan herhangi bir işlev eklemenin önemli bir parçası, uygun CAMERA izinlerini istemektir. Öncelikle, bunları Uygulama etiketinden önce manifest dosyasında beyan etmemiz gerekir:

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

Ardından, MainActivity içinden çalışma zamanında izinler istememiz gerekiyor. Değişiklikleri MainActivity dosyasında java > com.example.cameraxapp > MainActivity.kt altında yapacağız:

Dosyanın üst kısmında, MainActivity sınıf tanımının dışında aşağıdaki sabit ve içe aktarma işlemlerini ekleyelim:

// 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)

MainActivity sınıfına, tüm izinler verildiğini öğrendikten sonra izin istemek ve kodumuzu tetiklemek için kullanılan aşağıdaki alanları ve yardımcı yöntemleri ekleyin:

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

Son olarak, uygun durumlarda izin isteğini tetiklemek için her şeyi onCreate'de bir araya getiririz:

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

Uygulama başladığında artık uygun kamera izinlerine sahip olup olmadığını kontrol edecek. Çalışırsa doğrudan `startcamera()` çağrısı yapar. Aksi takdirde izinler istenir ve verildikten sonra "startcamera()" yöntemi çağrılır.

Çoğu kamera uygulamasında kullanıcılara vizör göstermek çok önemlidir. Aksi takdirde kullanıcıların kamerayı doğru yere yönlendirmesi çok zordur. Vizör, CameraX "Önizleme" sınıfı kullanılarak uygulanabilir.

Önizlemeyi kullanmak için öncelikle bir yapılandırma tanımlamamız gerekir. Bu yapılandırmalar, daha sonra kullanım alanı örneği oluşturmak için kullanılır. Elde edilen örnek, CameraX yaşam döngüsüne bağlamamız gerekir. Bu işlemi "startcamera()" yönteminde gerçekleştireceğiz. Uygulamayı aşağıdaki kodla doldurun:

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

Bu noktada, gizemli 'updateConversion()' yöntemini uygulamamız gerekir. "updateConversion()" ifadesinin hedefi, vizörümüzü dik rotasyonda görüntülemek için cihaz yönündeki değişiklikleri telafi etmektir:

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

Üretime hazır bir uygulama uygulamak için resmi örneğe göz atarak işlenmesi gereken diğer unsurları inceleyin. Bu codelab'i kısa tutmak için birkaç kısayol uyguluyoruz. Örneğin, düzen değişikliği dinleyicimizi tetiklemeyen 180 derece cihaz döndürme gibi bazı yapılandırma değişikliklerini takip etmiyoruz. Kare olmayan vizörler de cihaz döndürüldüğünde en boy oranını değiştirmeleri gerekir.

Uygulamayı geliştirip çalıştırırsak artık bir yaşam önizlemesi görmemiz gerekiyor. Güzel!

Kullanıcıların görüntü yakalamasına izin vermek için düzenin bir parçası olarak > düzen > event_main.xml içinde bir düğme sunacağız:

<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" />

Diğer kullanım alanları, Önizleme'ye kıyasla çok benzer bir şekilde çalışır. Öncelikle, gerçek kullanım alanı nesnesini gerçekleştirmek için kullanılan bir yapılandırma nesnesi tanımlamamız gerekir. Fotoğraf çekmek için, yakalama düğmesine basıldığında "startcamera()" yöntemini güncellememiz ve CameraX.bindToLifecycle çağrısından önce, sonuna birkaç satır kod eklememiz gerekir:

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

Ardından, CameraX.bindToLifecycle çağrısını yeni kullanım alanını içerecek şekilde güncelleyin:

CameraX.bindToLifecycle(this, preview, imageCapture)

Bu nedenle, işlevsel bir fotoğraf çekme düğmesi uygulamaya başladık.

CameraX'in çok ilginç bir özelliği, ImageAnalysis sınıfıdır. Bu araç, gelen kamera çerçeveleri ile çağrılacak olan ImageAnalysis.analyticsr arayüzünü uygulayan özel bir sınıf tanımlamamıza olanak sağlar. CameraX'in temel vizyonu doğrultusunda kamera oturumunun durumunu yönetme, hatta resimleri imha etme konusunda endişelenmemiz gerekmez. Uygulamamızın tercih ettiği yaşam döngüsüne bağlı olmak, diğer yaşam döngüsüne duyarlı bileşenler gibi yeterlidir.

Öncelikle, özel bir resim analiz ediciyi uygulamaya alacağız. Analizcimiz oldukça basittir. Bu rapor, yalnızca resmin ortalama lümenini (parlaklığı) günlüğe kaydeder, ancak rastgele kullanım alanları için yapılması gerekenlere dair örnek verir. Tek yapmamız gereken, ImageAnalysis.analyticsr arayüzünü uygulayan bir sınıftaki "analyze" işlevini geçersiz kılmaktır. Uygulamamızı MainActivity içinde dahili bir sınıf olarak tanımlayabiliriz:

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

Sınıfımızın ImageAnalysis.analyticsr arayüzünü uyguladığında, tüm diğer kullanım örnekleri gibi ImageAnalysis'i örneklendirmek ve CameraX.bindToLifecycle çağrısından önce `start Camera()’ işlevini bir kez daha güncellemek istiyoruz:

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

Ayrıca, yeni kullanım alanını bağlamak için CameraX.bindtoLifecycle çağrısını güncelliyoruz:

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

Uygulamayı şimdi çalıştırırsanız yaklaşık olarak her saniyede logcat'te şuna benzer bir mesaj oluşturulur:

D/CameraXApp: Average luminosity: ...

Uygulamayı test etmek için Android Studio'daki Çalıştır düğmesini tıklamanız yeterlidir. Projemiz, seçilen cihazda veya emülatörde oluşturulur, dağıtılır ve kullanıma sunulur. Uygulama yüklendikten sonra, daha önce eklediğimiz yön işleme kodu sayesinde cihazı döndürdükten sonra bile dik konumda kalacak olan vizörü görebilmemiz gerekir. Ayrıca bu düğmeyi kullanarak fotoğraf da çekebilir:

Kod laboratuvarını başarıyla tamamladınız. Aşağıdakileri sıfırdan yeni bir Android uygulamasına uyguladınız:

  • Projenize CameraX bağımlılıkları eklendi.
  • Kamera vizörü görüntülendi (Önizleme kullanım alanı kullanılarak)
  • Resim depolama uygulandı, resimler depolama alanına kaydedildi (ImageCapture kullanım alanı kullanılarak)
  • Kameradaki çerçevelerin gerçek zamanlı analizi uygulandı (ImageAnalysis kullanım alanı kullanılarak)

CameraX ve bununla yapabilecekleriniz hakkında daha fazla bilgi edinmek istiyorsanız belgeleri inceleyin veya resmi örneği klonlayın.