เริ่มต้นใช้งาน CameraX

เริ่มต้นใช้งาน CameraX

เกี่ยวกับ Codelab นี้

subjectอัปเดตล่าสุดเมื่อ ต.ค. 17, 2019
account_circleเขียนโดย Googler

1 ภาพรวม

ใน Codelab นี้ เราจะดูวิธีสร้างแอปกล้องถ่ายรูปที่ใช้ CameraX เพื่อแสดงช่องมองภาพ ถ่ายภาพ และวิเคราะห์สตรีมรูปภาพจากกล้อง

ในการดําเนินการนี้ เราจะนําเสนอแนวคิดเกี่ยวกับ Use Case ใน CameraX ซึ่งสามารถนําไปใช้กับการทํางานของกล้องได้หลากหลายตั้งแต่การแสดงช่องมองภาพ ไปจนถึงการวิเคราะห์เฟรมแบบเรียลไทม์

สิ่งที่จะได้เรียนรู้

  • วิธีเพิ่มทรัพยากร Dependency ของ CameraX
  • วิธีแสดงตัวอย่างกล้องในกิจกรรม (ตัวอย่างการใช้งาน)
  • วิธีการถ่ายรูปโดยบันทึกลงในพื้นที่เก็บข้อมูล (กรณีการใช้งานของ ImageCapture)
  • วิธีวิเคราะห์เฟรมจากกล้องแบบเรียลไทม์ (กรณีการใช้งานของ Analytics Analysis)

ฮาร์ดแวร์ที่เราต้องใช้

  • อุปกรณ์ Android แม้ว่าโปรแกรมจําลอง Android Studio&#39 จะทํางานได้ดีก็ตาม ระดับ API ต่ําสุดที่รองรับคือ 21

ซอฟต์แวร์ที่เราต้องการ

  • Android Studio 3.3 ขึ้นไป

2 เริ่มต้นโครงการใหม่

เมื่อใช้โปรเจ็กต์ Android Studio ให้เริ่มโปรเจ็กต์ใหม่และเลือกกิจกรรมที่ว่างเปล่าเมื่อระบบแจ้ง

จากนั้น คุณสามารถเลือกชื่อใดก็ได้ที่เราต้องการให้เลือก โดยเราเลือก "CameraX App" คุณควรตรวจสอบว่าตั้งค่าภาษาเป็น Kotlin ระดับ API ขั้นต่ําคือ 21 (ซึ่งเป็นค่าต่ําสุดที่ต้องระบุสําหรับ CameraX) และที่เราใช้อาร์ติแฟกต์ AndroidX

3 เพิ่มทรัพยากร Dependency ของ Gradle

เริ่มต้นด้วยการเพิ่มทรัพยากร Dependency ของ 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 ในแอปของเรา

4 สร้างเลย์เอาต์ช่องมองภาพ

เราจะใช้ SurfaceTexture เพื่อแสดงช่องมองภาพของกล้อง ใน Codelab นี้ เราจะแสดงช่องมองภาพในรูปแบบสี่เหลี่ยมจัตุรัสขนาดคงที่ โปรดดูตัวอย่างที่ครอบคลุมมากขึ้นซึ่งแสดงช่องมองภาพที่ปรับเปลี่ยนตามอุปกรณ์ได้ที่ตัวอย่างอย่างเป็นทางการ

มาแก้ไขไฟล์เลย์เอาต์_กิจกรรมในส่วน 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>

5 ขอสิทธิ์เข้าถึงกล้อง

ส่วนสําคัญในการเพิ่มฟังก์ชันใดๆ ในโครงการของเราที่ใช้กล้องคือการขอสิทธิ์ CAMERA ที่เหมาะสม ก่อนอื่น เราจะต้องประกาศในไฟล์ Manifest ก่อนแท็กแอปพลิเคชัน

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

จากนั้น ใน MainActivity เราจําเป็นต้องขอสิทธิ์ขณะรันไทม์ เราจะทําการเปลี่ยนแปลงในไฟล์ MainActivity ใต้ java > com.example.cameraxapp > MainActivity.kt:

ที่ด้านบนของไฟล์ นอกเหนือจากคําจํากัดความคลาสของ ActivityActivity แล้ว ให้เพิ่มค่าคงที่และนําเข้าต่อไปนี้

// 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()" เมื่อได้รับสิทธิ์

6 ติดตั้งเครื่องมือค้นหาข้อมูลพร็อพเพอร์ตี้

สําหรับแอปพลิเคชันกล้องส่วนใหญ่ การแสดงช่องมองภาพต่อผู้ใช้ถือเป็นเรื่องที่สําคัญอย่างยิ่ง ไม่เช่นนั้นแล้วผู้ใช้ก็จะเล็งกล้องไปยังตําแหน่งที่ถูกต้องได้ยากมาก คุณจะใช้งานช่องมองภาพได้โดยใช้คลาส "แสดงตัวอย่าง" ของ CameraX

ในการใช้พรีวิว เราจะต้องกําหนดการกําหนดค่าก่อน จากนั้นระบบจะใช้เพื่อสร้างอินสแตนซ์ 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)
}

หากต้องการใช้แอปที่พร้อมใช้งานจริง โปรดดูตัวอย่างอย่างเป็นทางการเพื่อดูว่าต้องจัดการอะไรอีกบ้าง เราขอแนะนําทางลัดง่ายๆ ในการเขียน Codelab สั้นๆ นี้ ตัวอย่างเช่น เราไม่ติดตามการเปลี่ยนแปลงการกําหนดค่าบางอย่าง เช่น การหมุนอุปกรณ์ 180 องศา ซึ่งจะไม่ทริกเกอร์ Listener การเปลี่ยนแปลงเลย์เอาต์ นอกจากนี้ ช่องมองภาพที่ไม่ใช่รูปสี่เหลี่ยมจัตุรัสจะต้องชดเชยอัตราส่วนที่เปลี่ยนไปเมื่ออุปกรณ์หมุน

หากเราสร้างและเรียกใช้แอป เราก็ควรจะเห็นตัวอย่างชีวิต ดีมาก

7 กรณีการใช้งานการจับภาพ

เพื่อให้ผู้ใช้สามารถจับภาพได้ เราจะให้ปุ่มเป็นส่วนหนึ่งของเลย์เอาต์หลังจากมุมมองพื้นผิวใน 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" />

Use Case อื่นๆ จะทํางานในลักษณะที่คล้ายกันมากเมื่อเทียบกับพรีวิว ก่อนอื่น คุณต้องกําหนดออบเจ็กต์การกําหนดค่าที่ใช้เพื่อจําแนกออบเจ็กต์ Use Case จริง หากต้องการจับภาพเมื่อมีการกดปุ่มบันทึก เราต้องอัปเดตเมธอด "startCamera()" และเพิ่มโค้ดอีก 2-3 บรรทัดในตอนท้ายสุดก่อนเรียกใช้ 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)

ในทํานองเดียวกัน เราได้ดําเนินการปุ่มถ่ายรูปที่ใช้ประโยชน์ได้

8 ใช้กรณีการใช้งานการวิเคราะห์รูปภาพ

ฟีเจอร์ที่น่าสนใจมากของ CameraX คือคลาส ImageAnalysis ซึ่งช่วยให้เรากําหนดคลาสที่กําหนดเองเพื่อใช้อินเทอร์เฟซ ImageAnalysis. Analyzer ได้ ซึ่งจะเรียกใช้พร้อมเฟรมกล้องที่เข้ามาใหม่ ตามวิสัยทัศน์หลักของ CameraX เราจะไม่กังวลเกี่ยวกับการจัดการสถานะเซสชันของกล้องหรือแม้กระทั่งการกําจัดรูปภาพ การเชื่อมโยงกับวงจรที่ต้องการของแอปก็ถือว่าเพียงพอแล้วกับคอมโพเนนต์ที่รับรู้อายุการใช้งานอื่นๆ

ขั้นแรก เราจะใช้เครื่องมือวิเคราะห์รูปภาพที่กําหนดเอง เครื่องมือวิเคราะห์ของเราไม่ซับซ้อน โดยเป็นเพียงการบันทึกความสว่างโดยเฉลี่ย (ความสว่าง) แต่เป็นตัวอย่างว่าต้องทําอะไรกับ Use Case ที่ซับซ้อนอย่างอิสระ สิ่งที่เราต้องทําคือลบล้างฟังก์ชัน "วิเคราะห์" ในชั้นเรียนที่ใช้อินเทอร์เฟซ 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.Analyzerr สิ่งที่คุณต้องทําคือทําให้ ImageAnalysis ทํางานเหมือนกับกรณีการใช้งานอื่นๆ ทั้งหมดและอัปเดตฟังก์ชัน "startCamera()" อีกครั้ง ก่อนเรียกใช้ 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)
}

นอกจากนี้เรายังอัปเดตการเรียกไปยัง CameraX.bindtoLifecycle เพื่อเชื่อมโยงกรณีการใช้งานใหม่ ดังนี้

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

การเรียกใช้แอปจะสร้างข้อความที่คล้ายกันใน Logcat ทุกวินาทีโดยประมาณ

D/CameraXApp: Average luminosity: ...

9 ทดสอบแอป

หากต้องการทดสอบแอป เพียงคลิกปุ่มเรียกใช้ใน Android Studio จากนั้นระบบจะสร้าง ทําให้ใช้งานได้ และเปิดใช้ในอุปกรณ์หรือโปรแกรมจําลองที่เลือก เมื่อแอปโหลดแล้ว เราควรดูช่องมองภาพ ซึ่งจะคงไว้ชั่วคราวหลังจากหมุนอุปกรณ์เนื่องจากรหัสการจัดการการวางแนวที่เราเพิ่มไว้ก่อนหน้านี้ และจะต้องถ่ายภาพโดยใช้ปุ่มได้ด้วย

10 ยินดีด้วย

คุณดูห้องทดลองของโค้ดเสร็จเรียบร้อยแล้ว เมื่อมองย้อนกลับไปแล้ว คุณได้นําฟีเจอร์ต่อไปนี้ไปใช้ในแอป Android ใหม่ตั้งแต่ต้น

  • รวมทรัพยากร Dependency ของ CameraX ไว้ในโปรเจ็กต์ของคุณ
  • แสดงช่องมองภาพของกล้อง (ใช้กรณีการใช้งานตัวอย่าง)
  • ใช้การจับภาพ บันทึกรูปภาพลงในพื้นที่เก็บข้อมูล (โดยใช้ UseCapture Use Case)
  • การวิเคราะห์เฟรมจากกล้องแบบเรียลไทม์ (โดยใช้ Use ImageAalysis)

หากคุณสนใจอ่านเพิ่มเติมเกี่ยวกับ CameraX และสิ่งที่คุณทําได้ด้วย ดูเอกสารหรือคัดลอกตัวอย่างอย่างเป็นทางการ