شروع کار با CameraX

در این کد لبه، ما یاد خواهیم گرفت که چگونه یک برنامه دوربین بسازیم که از CameraX برای نمایش منظره یاب، گرفتن عکس و تجزیه و تحلیل جریان تصویر از دوربین استفاده می کند.

برای دستیابی به این هدف، مفهوم موارد استفاده را در CameraX معرفی خواهیم کرد، که می تواند برای انواع عملیات دوربین از نمایش منظره یاب گرفته تا تجزیه و تحلیل فریم ها در زمان واقعی استفاده شود.

چیزی که یاد خواهیم گرفت

  • نحوه اضافه کردن وابستگی های CameraX
  • نحوه نمایش پیش نمایش دوربین در یک فعالیت (پیش نمایش مورد استفاده)
  • نحوه گرفتن عکس، ذخیره آن در حافظه. (مورد استفاده از ImageCapture)
  • چگونه فریم های دوربین را در زمان واقعی تجزیه و تحلیل کنیم. (مورد استفاده از تحلیل تصویر)

سخت افزاری که نیاز داریم

  • یک دستگاه اندرویدی، اگرچه شبیه ساز اندروید استودیو به خوبی کار می کند. حداقل سطح API پشتیبانی شده 21 است.

نرم افزاری که نیاز داریم

  • اندروید استودیو 3.3 یا بالاتر.

با استفاده از منوی Android Studio، یک پروژه جدید را شروع کنید و زمانی که از شما خواسته شد، Empty Activity را انتخاب کنید.

در مرحله بعد، ما می‌توانیم هر نامی را که می‌خواهیم انتخاب کنیم - ما هوشمندانه "CameraX App" را انتخاب کردیم. ما باید مطمئن شویم که زبان روی Kotlin تنظیم شده است، حداقل سطح API 21 است (که حداقل مورد نیاز برای CameraX است) و از مصنوعات AndroidX استفاده می کنیم.

برای شروع، بیایید وابستگی های CameraX را به فایل Gradle برنامه خود در داخل بخش وابستگی ها اضافه کنیم:

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

هنگامی که از شما خواسته شد، روی Sync Now کلیک کنید، و ما آماده استفاده از CameraX در برنامه خود خواهیم بود.

ما از SurfaceTexture برای نمایش منظره یاب دوربین استفاده خواهیم کرد. در این کد لبه، منظره یاب را در قالب مربع با اندازه ثابت نمایش خواهیم داد. برای مثال جامع‌تری که نمایاب واکنش‌گرا را نشان می‌دهد، نمونه رسمی را بررسی کنید.

بیایید فایل layout activity_main را در زیر 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>

بخش مهمی از افزودن هر قابلیتی در پروژه ما که از دوربین استفاده می کند، درخواست مجوزهای مناسب دوربین است. ابتدا باید آنها را در مانیفست قبل از تگ Application اعلام کنیم:

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

سپس در داخل MainActivity باید در زمان اجرا مجوز درخواست کنیم. ما تغییرات را در فایل MainActivity در java > com.example.cameraxapp > MainActivity.kt انجام خواهیم داد:

در بالای فایل، خارج از تعریف کلاس MainActivity، اجازه دهید ثابت‌ها و واردات زیر را اضافه کنیم:

// 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، فیلدها و متدهای کمکی زیر را اضافه کنید که برای درخواست مجوز استفاده می‌شوند و زمانی که متوجه شدیم همه مجوزها اعطا شده‌اند، کد ما را فعال می‌کنند:

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

در نهایت، ما همه چیز را در داخل onCreate قرار می دهیم تا در صورت لزوم درخواست مجوز را راه اندازی کنیم:

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

اکنون، هنگامی که برنامه شروع می شود، بررسی می کند که آیا مجوزهای دوربین مناسب را دارد یا خیر. اگر این کار را کرد، مستقیماً «startCamera()» را فراخوانی می کند. در غیر این صورت، مجوزها را درخواست می‌کند و پس از دریافت، «startCamera()» را فراخوانی می‌کند.

برای اکثر برنامه های دوربین، نشان دادن منظره یاب به کاربران بسیار مهم است -- در غیر این صورت برای کاربران بسیار دشوار است که دوربین را به جای مناسب نشان دهند. یک منظره یاب را می توان با استفاده از کلاس «پیش نمایش» CameraX پیاده سازی کرد.

برای استفاده از Preview، ابتدا باید یک پیکربندی را تعریف کنیم که سپس برای ایجاد یک نمونه از use case استفاده می شود. نمونه به دست آمده چیزی است که باید به چرخه عمر CameraX متصل کنیم. ما این کار را در روش «startCamera()» انجام خواهیم داد. پیاده سازی را با این کد پر کنید:

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

در این مرحله، ما باید متد مرموز "updateTransform()" را پیاده سازی کنیم. در داخل «updateTransform()» هدف جبران تغییرات در جهت دستگاه برای نمایش منظره یاب ما در چرخش عمودی است:

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

برای پیاده سازی یک برنامه آماده تولید، به نمونه رسمی نگاهی بیندازید تا ببینید چه چیز دیگری باید انجام شود. به منظور کوتاه نگه داشتن این کد لبه، چند میانبر را انتخاب می کنیم. به عنوان مثال، ما برخی از تغییرات پیکربندی مانند چرخش 180 درجه ای دستگاه را که شنونده تغییر طرح ما را تحریک نمی کند، پیگیری نمی کنیم. منظره یاب های غیرمربع نیز باید تغییرات نسبت تصویر را هنگام چرخش دستگاه جبران کنند.

اگر برنامه را بسازیم و اجرا کنیم، اکنون باید پیش نمایش زندگی را ببینیم. خوب!

برای اینکه کاربران بتوانند تصاویر را ثبت کنند، پس از نمای بافت، دکمه ای را به عنوان بخشی از طرح بندی در 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" />

سایر موارد استفاده در مقایسه با پیش نمایش به روشی بسیار مشابه عمل می کنند. ابتدا باید یک شیء پیکربندی را تعریف کنیم که برای نمونه سازی شی مورد استفاده واقعی استفاده می شود. برای گرفتن عکس، زمانی که دکمه عکس برداری فشار داده می شود، باید روش «startCamera()» را به روز کنیم و چند خط کد دیگر را در پایان، قبل از تماس با 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)
}

سپس، تماس را به CameraX.bindToLifecycle به‌روزرسانی کنید تا مورد استفاده جدید را نیز شامل شود:

CameraX.bindToLifecycle(this, preview, imageCapture)

و دقیقاً مانند آن، ما یک دکمه عکس برداری کاربردی را پیاده سازی کرده ایم.

ویژگی بسیار جالب CameraX کلاس ImageAnalysis است. این به ما امکان می دهد یک کلاس سفارشی برای پیاده سازی رابط ImageAnalysis.Analyzer تعریف کنیم که با فریم های دوربین ورودی فراخوانی می شود. مطابق با دیدگاه اصلی CameraX، ما نباید نگران مدیریت وضعیت جلسه دوربین یا حتی از بین بردن تصاویر باشیم. اتصال به چرخه عمر مورد نظر برنامه ما مانند سایر مؤلفه های آگاه از چرخه حیات کافی است.

ابتدا یک تحلیلگر تصویر سفارشی پیاده سازی می کنیم. آنالایزر ما بسیار ساده است -- فقط میانگین لوما (درخشندگی) تصویر را ثبت می کند، اما نمونه کارهایی است که باید برای موارد استفاده پیچیده دلخواه انجام شود. تنها کاری که باید انجام دهیم این است که تابع «analyze» را در کلاسی که رابط ImageAnalysis.Analyzer را پیاده سازی می کند، لغو کنیم. ما می توانیم پیاده سازی خود را به عنوان یک کلاس داخلی در 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
        }
    }
}

با استفاده از کلاس ما که رابط ImageAnalysis.Analyzer را پیاده سازی می کند، تنها کاری که باید انجام دهیم این است که ImageAnalysis را مانند تمام موارد استفاده دیگر نمونه سازی کنیم و قبل از فراخوانی به CameraX.bindToLifecycle یک بار دیگر تابع «startCamera()» را به روز کنیم:

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

و همچنین تماس را به CameraX.bindtoLifecycle به‌روزرسانی می‌کنیم تا مورد استفاده جدید را متصل کنیم:

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

اجرای برنامه اکنون تقریباً هر ثانیه یک پیام مشابه در logcat تولید می کند:

D/CameraXApp: Average luminosity: ...

برای تست برنامه کافی است روی دکمه Run در اندروید استودیو کلیک کنید تا پروژه ما در دستگاه یا شبیه ساز انتخاب شده ساخته، مستقر و راه اندازی شود. پس از بارگیری برنامه، ما باید منظره یاب را ببینیم، که حتی پس از چرخاندن دستگاه به لطف کد جهت گیری که قبلا اضافه کردیم، عمودی باقی می ماند و همچنین باید بتواند با استفاده از دکمه عکس بگیرد:

شما آزمایشگاه کد را با موفقیت به پایان رساندید! با نگاهی به گذشته، موارد زیر را از ابتدا در یک برنامه اندروید جدید پیاده سازی کردید:

  • شامل وابستگی های CameraX به پروژه شما.
  • یک منظره یاب دوربین نمایش داده شد (با استفاده از جعبه استفاده از پیش نمایش)
  • ضبط عکس پیاده‌سازی شده، ذخیره تصاویر در حافظه (با استفاده از ImageCapture)
  • تحلیل پیاده‌سازی فریم‌ها از دوربین در زمان واقعی (با استفاده از ImageAnalysis مورد استفاده)

اگر علاقه مند به خواندن اطلاعات بیشتر در مورد CameraX و کارهایی هستید که می توانید با آن انجام دهید، اسناد را بررسی کنید یا نمونه رسمی را شبیه سازی کنید.