CameraX'i kullanmaya başlama

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

Bunu sağlamak için CameraX'te kullanım alanları kavramını tanıtacağız. Bu kavram, vizör görüntülemekten kareleri gerçek zamanlı olarak analiz etmeye kadar çeşitli kamera işlemleri için kullanılabilir.

Neler öğreneceğiz?

  • CameraX bağımlılıklarını ekleme
  • Kamera önizlemesini bir etkinlikte görüntüleme (Önizleme kullanım alanı)
  • Fotoğraf çekme ve depolama alanına kaydetme (ImageCapture kullanım alanı)
  • Kameradan alınan kareleri gerçek zamanlı olarak analiz etme (ImageAnalysis kullanım alanı)

Gerekli donanım

  • Android Studio'nun emülatörü yeterli olsa da Android cihaz Desteklenen minimum API düzeyi 21'dir.

Gerekli 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 Empty Activity'yi (Boş Etkinlik) seçin.

Ardından, istediğimiz adı seçebiliriz. Biz ustaca bir seçim yaparak "CameraX App"i seçtik. Dilin Kotlin olarak ayarlandığından, minimum API düzeyinin 21 olduğundan (CameraX için gerekli minimum düzey) ve AndroidX yapılarını kullandığımızdan emin olmalıyız.

Başlamak için dependencies bölümünde CameraX bağımlılıklarını uygulama Gradle dosyamıza 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 hale getireceğiz.

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

res > layout > activity_main.xml altında activity_main düzen dosyasını 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 kamerayı kullanan herhangi bir işlev eklemenin önemli bir parçası, uygun KAMERA izinlerini istemektir. Öncelikle bunları, manifest dosyasında Application etiketinden önce bildirmemiz gerekir:

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

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

Dosyanın en üstünde, MainActivity sınıf tanımının dışında aşağıdaki sabitleri ve içe aktarmaları 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ının içine, izin istemek ve tüm izinlerin verildiğini öğrendiğimizde 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 olduğunda izin isteğini tetiklemek için her şeyi onCreate içinde bir araya getiriyoruz:

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şlatıldığında artık uygun kamera izinlerine sahip olup olmadığını kontrol eder. Bu durumda doğrudan `startCamera()` çağrısı yapılır. Aksi takdirde izinleri ister ve izinler verildikten sonra `startCamera()` işlevini çağırı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 zor olur. Vizör, CameraX'in `Preview` sınıfı kullanılarak uygulanabilir.

Önizleme'yi kullanmak için önce bir yapılandırma tanımlamamız gerekir. Bu yapılandırma, kullanım alanının bir örneğini oluşturmak için kullanılır. Elde edilen örnek, CameraX yaşam döngüsüne bağlamamız gereken şeydir. Bu işlemi `startCamera()` yöntemi içinde yapacağız. Uygulamayı bu 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 `updateTransform()` yöntemini uygulamamız gerekiyor. `updateTransform()` işlevi içinde, vizörümüzü dik bir şekilde döndürerek görüntülemek için cihaz yönündeki değişiklikleri telafi etmeyi amaçlıyoruz:

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 oluşturmak için başka hangi işlemlerin yapılması gerektiğini görmek üzere resmi örneğe göz atın. Bu codelab'i kısa tutmak için birkaç kısayol kullanıyoruz. Örneğin, düzen değişikliği dinleyicimizi tetiklemeyen 180 derecelik cihaz döndürmeleri gibi bazı yapılandırma değişikliklerini izlemiyoruz. Kare olmayan vizörler de cihaz döndürüldüğünde en boy oranının değişmesini telafi etmelidir.

Uygulamayı oluşturup çalıştırdığımızda artık canlı önizlemeyi görmemiz gerekir. Güzel!

Kullanıcıların resim çekmesine olanak tanımak için res > layout > activity_main.xml'deki doku görünümünden sonra düzenin bir parçası olarak bir düğme sağlayacağı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ı, önizlemeye kıyasla çok benzer şekilde çalışır. Öncelikle, gerçek kullanım alanı nesnesini oluşturmak için kullanılan bir yapılandırma nesnesi tanımlamamız gerekir. Fotoğraf çekmek için çekim düğmesine basıldığında `startCamera()` yöntemini güncellememiz ve CameraX.bindToLifecycle çağrısından önce sona birkaç satır daha 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, yeni kullanım alanını eklemek için CameraX.bindToLifecycle çağrısını güncelleyin:

CameraX.bindToLifecycle(this, preview, imageCapture)

İşte bu kadar. Artık işlevsel bir fotoğraf çekme düğmemiz var.

CameraX'in çok ilginç bir özelliği ImageAnalysis sınıfıdır. Bu, ImageAnalysis.Analyzer arayüzünü uygulayan özel bir sınıf tanımlamamıza olanak tanır. Bu sınıf, gelen kamera kareleriyle birlikte çağrılır. CameraX'in temel vizyonu doğrultusunda, kamera oturumu durumunu yönetme veya görüntüleri silme konusunda endişelenmemize gerek kalmaz. Diğer yaşam döngüsüne duyarlı bileşenler gibi, uygulamamızın istediği yaşam döngüsüne bağlanmak yeterlidir.

İlk olarak özel bir görüntü analiz aracı uygulayacağız. Analiz aracımız oldukça basittir. Yalnızca görüntünün ortalama parlaklığını (ışıklılık) kaydeder ancak rastgele karmaşık kullanım alanları için ne yapılması gerektiğini gösterir. Tek yapmamız gereken, ImageAnalysis.Analyzer arayüzünü uygulayan bir sınıftaki "analyze" işlevini geçersiz kılmaktır. Uygulamamızı MainActivity içinde bir iç 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
        }
    }
}

ImageAnalysis.Analyzer arayüzünü uygulayan sınıfımızda, diğer tüm kullanım alanlarında olduğu gibi ImageAnalysis'i başlatmamız ve CameraX.bindToLifecycle çağrısından önce `startCamera()` işlevini tekrar güncellememiz yeterlidir:

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ı da güncelleriz:

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

Uygulamayı şimdi çalıştırmak, logcat'te yaklaşık olarak her saniyede bir buna benzer bir mesaj oluşturur:

D/CameraXApp: Average luminosity: ...

Uygulamayı test etmek için Android Studio'da Run (Çalıştır) düğmesini tıklamamız yeterlidir. Projemiz oluşturulur, dağıtılır ve seçilen cihazda veya emülatörde başlatılır. Uygulama yüklendikten sonra, daha önce eklediğimiz yönlendirme işleme kodu sayesinde cihaz döndürüldükten sonra bile dik kalacak olan vizörü görmemiz ve düğmeyi kullanarak fotoğraf çekebilmemiz gerekir:

Code Lab'i başarıyla tamamladınız. Geçmişe baktığımızda, aşağıdaki özellikleri sıfırdan yeni bir Android uygulamasına uyguladığınızı görüyoruz:

  • Projenize CameraX bağımlılıklarını ekleyin.
  • Kamera vizörü gösterildi (önizleme kullanım alanında)
  • Fotoğraf çekme ve görüntüleri depolama alanına kaydetme (ImageCapture kullanım alanını kullanarak) işlevi uygulandı.
  • Kameradan alınan karelerin gerçek zamanlı olarak analiz edilmesi (ImageAnalysis kullanım alanını kullanarak)

CameraX ve bu araçla yapabilecekleriniz hakkında daha fazla bilgi edinmek istiyorsanız belgelere göz atın veya resmi örneği klonlayın.