CameraX দিয়ে শুরু করা

এই কোডল্যাবে, আমরা শিখব কীভাবে একটি ক্যামেরা অ্যাপ তৈরি করতে হয় যা ক্যামেরাএক্স ব্যবহার করে একটি ভিউফাইন্ডার দেখায়, ফটো তুলতে এবং ক্যামেরা থেকে একটি চিত্র স্ট্রিম বিশ্লেষণ করে।

এটি অর্জনের জন্য আমরা CameraX-এ ব্যবহারের ক্ষেত্রে ধারণাটি প্রবর্তন করব, যা একটি ভিউফাইন্ডার প্রদর্শন থেকে শুরু করে রিয়েল টাইমে ফ্রেম বিশ্লেষণ পর্যন্ত বিভিন্ন ক্যামেরা অপারেশনের জন্য ব্যবহার করা যেতে পারে।

আমরা কি শিখব

  • ক্যামেরাএক্স নির্ভরতা কিভাবে যোগ করবেন।
  • একটি কার্যকলাপে ক্যামেরা প্রিভিউ কিভাবে প্রদর্শন করবেন। (প্রিভিউ ব্যবহারের ক্ষেত্রে)
  • কিভাবে একটি ফটো তুলবেন, স্টোরেজে সংরক্ষণ করুন। (ইমেজ ক্যাপচার ব্যবহারের ক্ষেত্রে)
  • বাস্তব সময়ে ক্যামেরা থেকে ফ্রেম বিশ্লেষণ কিভাবে. (চিত্র বিশ্লেষণ ব্যবহার ক্ষেত্রে)

হার্ডওয়্যার আমাদের প্রয়োজন হবে

  • একটি অ্যান্ড্রয়েড ডিভাইস, যদিও অ্যান্ড্রয়েড স্টুডিওর এমুলেটর ঠিক কাজ করবে। ন্যূনতম সমর্থিত API স্তর হল 21৷

সফটওয়্যার আমাদের প্রয়োজন হবে

  • অ্যান্ড্রয়েড স্টুডিও 3.3 বা তার উপরে।

অ্যান্ড্রয়েড স্টুডিও মেনু ব্যবহার করে, একটি নতুন প্রকল্প শুরু করুন এবং অনুরোধ করা হলে খালি কার্যকলাপ নির্বাচন করুন।

এরপরে, আমরা যে কোনো নাম বাছাই করতে পারি -- আমরা বুদ্ধিমত্তার সাথে "ক্যামেরাএক্স অ্যাপ" বেছে নিয়েছি। আমাদের নিশ্চিত করা উচিত যে ভাষাটি Kotlin-এ সেট করা আছে, ন্যূনতম API স্তর 21 (যা CameraX-এর জন্য সর্বনিম্ন প্রয়োজনীয়) এবং আমরা AndroidX শিল্পকর্ম ব্যবহার করি।

শুরু করার জন্য, আসুন নির্ভরতা বিভাগের ভিতরে, আমাদের অ্যাপ গ্রেডল ফাইলে CameraX নির্ভরতা যোগ করি:

// 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 ব্যবহার করার জন্য প্রস্তুত হব।

ক্যামেরা ভিউফাইন্ডার প্রদর্শন করতে আমরা একটি সারফেসটেক্সচার ব্যবহার করব। এই কোডল্যাবে, আমরা নির্দিষ্ট আকারের একটি বর্গাকার বিন্যাসে ভিউফাইন্ডার প্রদর্শন করব। আরও বিস্তৃত উদাহরণের জন্য যা একটি প্রতিক্রিয়াশীল ভিউফাইন্ডার দেখায়, অফিসিয়াল নমুনা চেকআউট করুন।

চলুন res > লেআউট > activity_main.xml এর অধীনে activity_main লেআউট ফাইলটি সম্পাদনা করি:

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

ক্যামেরা ব্যবহার করে এমন আমাদের প্রকল্পে যেকোনো কার্যকারিতা যোগ করার একটি গুরুত্বপূর্ণ অংশ হল উপযুক্ত CAMERA অনুমতির জন্য অনুরোধ করা। প্রথমে, অ্যাপ্লিকেশন ট্যাগের আগে আমাদের ম্যানিফেস্টে সেগুলি ঘোষণা করতে হবে:

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

তারপর, MainActivity এর ভিতরে আমাদের রানটাইমে অনুমতির অনুরোধ করতে হবে। আমরা java > com.example.cameraxapp > MainActivity.kt-এর অধীনে MainActivity ফাইলে পরিবর্তনগুলি করব:

ফাইলের শীর্ষে, 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()` কল করবে।

বেশিরভাগ ক্যামেরা অ্যাপ্লিকেশনের জন্য, ব্যবহারকারীদের কাছে একটি ভিউফাইন্ডার দেখানো খুবই গুরুত্বপূর্ণ -- অন্যথায় ব্যবহারকারীদের জন্য ক্যামেরাটিকে সঠিক জায়গায় নির্দেশ করা খুবই কঠিন। ক্যামেরাএক্স `প্রিভিউ` ক্লাস ব্যবহার করে একটি ভিউফাইন্ডার প্রয়োগ করা যেতে পারে।

পূর্বরূপ ব্যবহার করার জন্য, আমাদের প্রথমে একটি কনফিগারেশন সংজ্ঞায়িত করতে হবে যা পরে ব্যবহারের ক্ষেত্রে একটি উদাহরণ তৈরি করতে ব্যবহৃত হয়। ফলস্বরূপ উদাহরণ হল যা আমাদের ক্যামেরাএক্স লাইফসাইকেলের সাথে আবদ্ধ করতে হবে। আমরা এটি `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" />

অন্যান্য ব্যবহারের ক্ষেত্রে প্রিভিউয়ের তুলনায় খুব অনুরূপভাবে কাজ করে। প্রথমত, আমাদের অবশ্যই একটি কনফিগারেশন অবজেক্ট সংজ্ঞায়িত করতে হবে যা প্রকৃত ব্যবহারের কেস অবজেক্টকে ইনস্ট্যান্ট করতে ব্যবহৃত হয়। ফটো ক্যাপচার করার জন্য, যখন ক্যাপচার বোতামটি চাপানো হয়, তখন CameraX.bindToLifecycle এ কল করার আগে আমাদের `startCamera()` পদ্ধতি আপডেট করতে হবে এবং শেষে কোডের আরও কয়েকটি লাইন যোগ করতে হবে:

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.Analyser ইন্টারফেস প্রয়োগ করে একটি কাস্টম ক্লাস সংজ্ঞায়িত করার অনুমতি দেয়, যা ইনকামিং ক্যামেরা ফ্রেমের সাথে বলা হবে। CameraX এর মূল দৃষ্টিভঙ্গির সাথে সামঞ্জস্য রেখে, ক্যামেরা সেশনের অবস্থা পরিচালনা বা এমনকি ছবি নিষ্পত্তি করার বিষয়ে আমাদের চিন্তা করতে হবে না; অন্যান্য জীবনচক্র-সচেতন উপাদানগুলির মতো আমাদের অ্যাপের পছন্দসই জীবনচক্রের সাথে আবদ্ধ হওয়া যথেষ্ট।

প্রথমত, আমরা একটি কাস্টম ইমেজ বিশ্লেষক প্রয়োগ করব। আমাদের বিশ্লেষকটি বেশ সহজ -- এটি কেবল চিত্রের গড় লুমা (উজ্জ্বলতা) লগ করে, তবে নির্বিচারে জটিল ব্যবহারের ক্ষেত্রে কী করা দরকার তার উদাহরণ দেয়৷ আমাদের যা করতে হবে তা হল একটি ক্লাসে `বিশ্লেষণ` ফাংশনটিকে ওভাররাইড করে যা ImageAnalysis.Analyser ইন্টারফেস প্রয়োগ করে। আমরা মেইনঅ্যাক্টিভিটির মধ্যে একটি অভ্যন্তরীণ শ্রেণী হিসাবে আমাদের বাস্তবায়নকে সংজ্ঞায়িত করতে পারি:

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.Analyser ইন্টারফেস বাস্তবায়নের সাথে, আমাদের যা করতে হবে তা হল অন্য সমস্ত ব্যবহারের ক্ষেত্রে ইমেজ অ্যানালাইসিসকে ইনস্ট্যান্টিয়েট করা এবং 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)

এখন অ্যাপটি চালানোর ফলে প্রায় প্রতি সেকেন্ডে লগক্যাটে এর মতো একটি বার্তা তৈরি হবে:

D/CameraXApp: Average luminosity: ...

অ্যাপটি পরীক্ষা করার জন্য, আমাদের যা করতে হবে তা হল অ্যান্ড্রয়েড স্টুডিওতে রান বোতামে ক্লিক করুন এবং আমাদের প্রকল্পটি নির্বাচিত ডিভাইস বা এমুলেটরে তৈরি, স্থাপন এবং চালু করা হবে। একবার অ্যাপ লোড হয়ে গেলে, আমাদের ভিউফাইন্ডার দেখতে হবে, যা ডিভাইসটিকে ঘোরানোর পরেও সোজা থাকবে আমাদের আগে যোগ করা ওরিয়েন্টেশন-হ্যান্ডলিং কোডের জন্য ধন্যবাদ, এবং বোতামটি ব্যবহার করে ফটো তুলতেও সক্ষম হওয়া উচিত:

আপনি কোড ল্যাব এবং সফলভাবে শেষ করেছেন! পিছনে তাকিয়ে, আপনি স্ক্র্যাচ থেকে একটি নতুন অ্যান্ড্রয়েড অ্যাপে নিম্নলিখিতগুলি প্রয়োগ করেছেন:

  • আপনার প্রকল্পে CameraX নির্ভরতা অন্তর্ভুক্ত করা হয়েছে।
  • একটি ক্যামেরা ভিউফাইন্ডার প্রদর্শিত হয়েছে (প্রিভিউ ব্যবহারের ক্ষেত্রে ব্যবহার করে)
  • বাস্তবায়িত ফটো ক্যাপচার, সঞ্চয়স্থানে ছবি সংরক্ষণ করা (ইমেজক্যাপচার ব্যবহারের ক্ষেত্রে ব্যবহার করে)
  • বাস্তব সময়ে ক্যামেরা থেকে ফ্রেমের বাস্তবায়িত বিশ্লেষণ (ইমেজ অ্যানালাইসিস ব্যবহার কেস ব্যবহার করে)

আপনি যদি CameraX সম্পর্কে আরও পড়তে আগ্রহী হন এবং এটি দিয়ে আপনি যা করতে পারেন , ডকুমেন্টেশন চেকআউট করুন বা অফিসিয়াল নমুনা ক্লোন করুন।