การแบ่งกลุ่มรูปภาพแบบเรียลไทม์ที่เร่งความเร็วใน Android ด้วย LiteRT

1. ก่อนเริ่มต้น

การพิมพ์โค้ดเป็นวิธีที่ยอดเยี่ยมในการสร้างความจำของกล้ามเนื้อและเพิ่มความเข้าใจในเนื้อหา แม้ว่าการคัดลอกและวางจะช่วยประหยัดเวลาได้ แต่การลงทุนในแนวทางปฏิบัตินี้จะช่วยเพิ่มประสิทธิภาพและทักษะการเขียนโค้ดในระยะยาวได้

ในโค้ดแล็บนี้ คุณจะได้เรียนรู้วิธีสร้างแอปพลิเคชัน Android ที่ทำการแบ่งกลุ่มรูปภาพแบบเรียลไทม์ในฟีดกล้องสดโดยใช้รันไทม์ใหม่ของ Google สำหรับ TensorFlow Lite ซึ่งก็คือ LiteRT คุณจะใช้แอปพลิเคชัน Android เริ่มต้นและเพิ่มความสามารถในการแบ่งกลุ่มรูปภาพลงในแอป นอกจากนี้ เราจะดูขั้นตอนการประมวลผลเบื้องต้น การอนุมาน และการประมวลผลภายหลังด้วย คุณจะได้รับสิ่งต่อไปนี้

  • สร้างแอป Android ที่แบ่งกลุ่มรูปภาพแบบเรียลไทม์
  • ผสานรวมโมเดลการแบ่งกลุ่มรูปภาพ LiteRT ที่ฝึกไว้ล่วงหน้า
  • ประมวลผลล่วงหน้าของรูปภาพอินพุตสำหรับโมเดล
  • ใช้รันไทม์ LiteRT สำหรับการเร่งความเร็ว CPU และ GPU
  • ทำความเข้าใจวิธีประมวลผลเอาต์พุตของโมเดลเพื่อแสดงมาสก์การแบ่งกลุ่ม
  • ทำความเข้าใจวิธีปรับสำหรับกล้องหน้า

สุดท้ายแล้ว คุณจะสร้างผลงานที่คล้ายกับรูปภาพด้านล่าง

แอปที่เสร็จสมบูรณ์

ข้อกำหนดเบื้องต้น

Codelab นี้ออกแบบมาสำหรับนักพัฒนาแอปบนอุปกรณ์เคลื่อนที่ที่มีประสบการณ์ซึ่งต้องการได้รับประสบการณ์ด้านแมชชีนเลิร์นนิง คุณควรคุ้นเคยกับ:

  • การพัฒนาแอป Android โดยใช้ Kotlin และ Android Studio
  • แนวคิดพื้นฐานของการประมวลผลรูปภาพ

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

  • วิธีผสานรวมและใช้รันไทม์ LiteRT ในแอปพลิเคชัน Android
  • วิธีทำการแบ่งกลุ่มรูปภาพโดยใช้โมเดล LiteRT ที่ฝึกไว้ล่วงหน้า
  • วิธีประมวลผลล่วงหน้ารูปภาพอินพุตสำหรับโมเดล
  • วิธีเรียกใช้การอนุมานสำหรับโมเดล
  • วิธีประมวลผลเอาต์พุตของโมเดลการแบ่งกลุ่มเพื่อแสดงผลลัพธ์เป็นภาพ
  • วิธีใช้ CameraX เพื่อประมวลผลฟีดกล้องแบบเรียลไทม์

สิ่งที่คุณต้องมี

  • Android Studio เวอร์ชันล่าสุด (ทดสอบใน v2025.1.1)
  • อุปกรณ์ Android จริง โดยเราได้ทดสอบฟีเจอร์นี้ในอุปกรณ์ Galaxy และ Pixel แล้ว
  • โค้ดตัวอย่าง (จาก GitHub)
  • มีความรู้พื้นฐานเกี่ยวกับการพัฒนาแอป Android ใน Kotlin

2. การแบ่งกลุ่มรูปภาพ

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

เช่น แทนที่จะรู้แค่ว่ามี "คน" อยู่ในกรอบ คุณจะรู้ได้ว่าพิกเซลใดเป็นของบุคคลนั้น บทแนะนำนี้แสดงวิธีทำการแบ่งกลุ่มรูปภาพแบบเรียลไทม์ในอุปกรณ์ Android โดยใช้โมเดลแมชชีนเลิร์นนิงที่ฝึกไว้ล่วงหน้า

ตัวอย่างการแบ่งกลุ่ม

LiteRT: การผลักดันขีดจำกัดของ ML บนอุปกรณ์

เทคโนโลยีสำคัญที่ช่วยให้การแบ่งกลุ่มแบบเรียลไทม์ที่มีความเที่ยงตรงสูงในอุปกรณ์เคลื่อนที่คือ LiteRT LiteRT เป็นรันไทม์ประสิทธิภาพสูงรุ่นถัดไปของ Google สำหรับ TensorFlow Lite ซึ่งได้รับการออกแบบมาเพื่อให้ได้ประสิทธิภาพที่ดีที่สุดจากฮาร์ดแวร์พื้นฐาน

ทำได้สำเร็จโดยการใช้ตัวเร่งความเร็วฮาร์ดแวร์อย่างชาญฉลาดและเหมาะสมที่สุด เช่น GPU (หน่วยประมวลผลกราฟิก) และ NPU (หน่วยประมวลผลประสาท) การโอนภาระงานการคำนวณที่เข้มข้นของโมเดลการแบ่งกลุ่มจาก CPU แบบอเนกประสงค์ไปยังโปรเซสเซอร์เฉพาะทางเหล่านี้ทำให้ LiteRT ลดเวลาการอนุมานได้อย่างมาก การเร่งความเร็วนี้ช่วยให้เราสามารถเรียกใช้โมเดลที่ซับซ้อนได้อย่างราบรื่นในฟีดกล้องแบบสด ซึ่งเป็นการขยายขีดจำกัดของสิ่งที่เราทำได้ด้วยแมชชีนเลิร์นนิงโดยตรงในโทรศัพท์ หากไม่มีประสิทธิภาพระดับนี้ การแบ่งกลุ่มแบบเรียลไทม์จะช้าและไม่ราบรื่นเกินไปจนทำให้ผู้ใช้ได้รับประสบการณ์ที่ไม่ดี

3. ตั้งค่า

โคลนที่เก็บ

ขั้นแรก โคลนที่เก็บข้อมูลสำหรับ LiteRT:

git clone https://github.com/google-ai-edge/litert-samples.git

litert-samples/v2/image_segmentation คือไดเรกทอรีที่มีทรัพยากรทั้งหมดที่คุณจะต้องใช้ สำหรับโค้ดแล็บนี้ คุณจะต้องใช้โปรเจ็กต์ kotlin_cpu_gpu/android_starter เท่านั้น คุณอาจต้องตรวจสอบโปรเจ็กต์ที่เสร็จสมบูรณ์แล้วหากติดขัด โดยทำดังนี้ kotlin_cpu_gpu/android

หมายเหตุเกี่ยวกับเส้นทางของไฟล์

บทแนะนำนี้จะระบุเส้นทางของไฟล์ในรูปแบบ Linux/macOS หากใช้ Windows คุณจะต้องปรับเส้นทางให้เหมาะสม

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

นำเข้าแอปเริ่มต้น

เริ่มต้นด้วยการนำเข้าแอปเริ่มต้นไปยัง Android Studio

  1. เปิด Android Studio แล้วเลือกเปิด

เปิด Android Studio

  1. ไปที่ไดเรกทอรี kotlin_cpu_gpu/android_starter แล้วเปิด

แอนดรอยด์สตาร์ทเตอร์

หากต้องการให้แน่ใจว่าแอปของคุณมีทรัพยากร Dependency ทั้งหมด คุณควรซิงค์โปรเจ็กต์กับไฟล์ Gradle เมื่อกระบวนการนำเข้าเสร็จสิ้น

  1. เลือกซิงค์โปรเจ็กต์กับไฟล์ Gradle จากแถบเครื่องมือของ Android Studio

การซิงค์เมนู

  1. โปรดอย่าข้ามขั้นตอนนี้ หากไม่ได้ผล บทแนะนำที่เหลือจะไม่มีประโยชน์

เรียกใช้แอปเริ่มต้น

ตอนนี้คุณได้นำเข้าโปรเจ็กต์ไปยัง Android Studio แล้ว และพร้อมที่จะเรียกใช้แอปเป็นครั้งแรก

เชื่อมต่ออุปกรณ์ Android กับคอมพิวเตอร์ผ่าน USB แล้วคลิกเรียกใช้ในแถบเครื่องมือของ Android Studio

ปุ่มเรียกใช้

แอปควรเปิดขึ้นในอุปกรณ์ คุณจะเห็นฟีดกล้องแบบสด แต่จะยังไม่มีการแบ่งกลุ่ม การแก้ไขไฟล์ทั้งหมดที่คุณจะทำในบทแนะนำนี้จะอยู่ในไดเรกทอรี litert-samples/v2/image_segmentation/kotlin_cpu_gpu/android_starter/app/src/main/java/com/google/ai/edge/examples/image_segmentation (ตอนนี้คุณคงทราบแล้วว่าเหตุใด Android Studio จึงปรับโครงสร้างนี้ 😃)

ไดเรกทอรีโปรเจ็กต์

นอกจากนี้ คุณจะเห็นTODOความคิดเห็นในไฟล์ ImageSegmentationHelper.kt, MainViewModel.kt และ view/SegmentationOverlay.kt ด้วย ในขั้นตอนต่อไปนี้ คุณจะใช้ฟังก์ชันการแบ่งกลุ่มรูปภาพโดยการกรอกข้อมูลใน TODOs เหล่านี้

4. ทำความเข้าใจแอปเริ่มต้น

แอปเริ่มต้นมี UI พื้นฐานและตรรกะการจัดการกล้องอยู่แล้ว ภาพรวมโดยสรุปของไฟล์สำคัญมีดังนี้

  • app/src/main/java/com/google/ai/edge/examples/image_segmentation/MainActivity.kt: นี่คือจุดแรกเข้าหลักของแอปพลิเคชัน โดยจะตั้งค่า UI โดยใช้ Jetpack Compose และจัดการสิทธิ์เข้าถึงกล้อง
  • app/src/main/java/com/google/ai/edge/examples/image_segmentation/MainViewModel.kt: ViewModel นี้จัดการสถานะ UI และประสานกระบวนการแบ่งกลุ่มรูปภาพ
  • app/src/main/java/com/google/ai/edge/examples/image_segmentation/ImageSegmentationHelper.kt: ส่วนนี้คือที่ที่เราจะเพิ่มตรรกะหลักสำหรับการแบ่งกลุ่มรูปภาพ โดยจะจัดการการโหลดโมเดล การประมวลผลเฟรมกล้อง และการเรียกใช้การอนุมาน
  • app/src/main/java/com/google/ai/edge/examples/image_segmentation/view/CameraScreen.kt: ฟังก์ชันที่ประกอบกันได้นี้จะแสดงตัวอย่างกล้องและการซ้อนทับการแบ่งกลุ่ม
  • app/download_model.gradle: สคริปต์นี้จะดาวน์โหลด selfie_multiclass.tflite นี่คือโมเดลการแบ่งกลุ่มรูปภาพ TensorFlow Lite ที่ได้รับการฝึกมาก่อนซึ่งเราจะใช้

5. ทำความเข้าใจ LiteRT และการเพิ่มทรัพยากร Dependency

ตอนนี้มาเพิ่มฟังก์ชันการทำงานของการแบ่งกลุ่มรูปภาพลงในแอปเริ่มต้นกัน

1. เพิ่มการอ้างอิง LiteRT

ก่อนอื่น คุณต้องเพิ่มไลบรารี LiteRT ลงในโปรเจ็กต์ นี่คือก้าวแรกที่สำคัญในการเปิดใช้แมชชีนเลิร์นนิงในอุปกรณ์ด้วยรันไทม์ที่เพิ่มประสิทธิภาพของ Google

เปิดไฟล์ app/build.gradle.kts แล้วเพิ่มบรรทัดต่อไปนี้ลงในบล็อก dependencies

// LiteRT for on-device ML
implementation(libs.litert)

หลังจากเพิ่มทรัพยากร Dependency แล้ว ให้ซิงค์โปรเจ็กต์กับไฟล์ Gradle โดยคลิกปุ่มซิงค์เลยที่ปรากฏที่มุมขวาบนของ Android Studio

ซิงค์เลย

2. ทำความเข้าใจ API หลักของ LiteRT

เปิด ImageSegmentationHelper.kt

ก่อนเขียนโค้ดการติดตั้งใช้งาน คุณควรทำความเข้าใจคอมโพเนนต์หลักของ LiteRT API ที่จะใช้ ตรวจสอบว่าคุณนำเข้าจากแพ็กเกจ com.google.ai.edge.litert แล้วเพิ่มการนำเข้าต่อไปนี้ที่ด้านบนของ ImageSegmentationHelper.kt

import com.google.ai.edge.litert.Accelerator
import com.google.ai.edge.litert.CompiledModel
  • CompiledModel: นี่คือคลาสส่วนกลางสำหรับการโต้ตอบกับโมเดล TFLite ซึ่งแสดงถึงโมเดลที่คอมไพล์ล่วงหน้าและเพิ่มประสิทธิภาพสำหรับตัวเร่งฮาร์ดแวร์ที่เฉพาะเจาะจง (เช่น CPU หรือ GPU) การคอมไพล์ล่วงหน้านี้เป็นฟีเจอร์หลักของ LiteRT ที่ช่วยให้การอนุมานเร็วขึ้นและมีประสิทธิภาพมากขึ้น
  • CompiledModel.Options: คุณใช้คลาสเครื่องมือสร้างนี้เพื่อกำหนดค่า CompiledModel การตั้งค่าที่สำคัญที่สุดคือการระบุตัวเร่งฮาร์ดแวร์ที่คุณต้องการใช้เพื่อเรียกใช้โมเดล
  • Accelerator: การแจงนับนี้ช่วยให้คุณเลือกฮาร์ดแวร์สำหรับการอนุมานได้ โปรเจ็กต์เริ่มต้นได้รับการกำหนดค่าให้รองรับตัวเลือกต่อไปนี้แล้ว
    • Accelerator.CPU: สำหรับการเรียกใช้โมเดลใน CPU ของอุปกรณ์ ตัวเลือกนี้เข้ากันได้กับทุกอุปกรณ์มากที่สุด
    • Accelerator.GPU: สำหรับการเรียกใช้โมเดลใน GPU ของอุปกรณ์ ซึ่งมักจะเร็วกว่า CPU สำหรับโมเดลที่อิงตามรูปภาพอย่างมาก
  • บัฟเฟอร์อินพุตและเอาต์พุต (TensorBuffer): LiteRT ใช้ TensorBuffer สำหรับอินพุตและเอาต์พุตของโมเดล ซึ่งจะช่วยให้คุณควบคุมหน่วยความจำได้อย่างละเอียดและหลีกเลี่ยงการคัดลอกข้อมูลที่ไม่จำเป็น คุณจะได้รับบัฟเฟอร์เหล่านี้โดยตรงจากอินสแตนซ์ CompiledModel โดยใช้ model.createInputBuffers() และ model.createOutputBuffers() จากนั้นเขียนข้อมูลอินพุตลงในบัฟเฟอร์เหล่านั้นและอ่านผลลัพธ์จากบัฟเฟอร์
  • model.run(): ฟังก์ชันนี้จะดำเนินการอนุมาน คุณส่งบัฟเฟอร์อินพุตและเอาต์พุตไปยัง LiteRT และ LiteRT จะจัดการงานที่ซับซ้อนในการเรียกใช้โมเดลในฮาร์ดแวร์ Accelerator ที่เลือก

6. การติดตั้งใช้งาน ImageSegmentationHelper เริ่มต้นให้เสร็จสมบูรณ์

ตอนนี้ถึงเวลาเขียนโค้ดแล้ว คุณจะติดตั้งใช้งาน ImageSegmentationHelper.kt ในช่วงแรกให้เสร็จสมบูรณ์ ซึ่งเกี่ยวข้องกับการตั้งค่าSegmenterคลาสส่วนตัวเพื่อเก็บโมเดล LiteRT และการใช้ฟังก์ชัน cleanup() เพื่อเผยแพร่โมเดลอย่างถูกต้อง

  1. ทําให้คลาส Segmenter และฟังก์ชัน cleanup() เสร็จสมบูรณ์: ในไฟล์ ImageSegmentationHelper.kt คุณจะเห็นโครงสร้างของคลาสส่วนตัวชื่อ Segmenter และฟังก์ชันชื่อ cleanup() ก่อนอื่น ให้ทําSegmenterคลาสclose()ให้เสร็จสมบูรณ์โดยกําหนดตัวสร้างเพื่อเก็บโมเดล สร้างพร็อพเพอร์ตี้สําหรับบัฟเฟอร์อินพุต/เอาต์พุต และเพิ่มเมธอด close()เพื่อปล่อยโมเดล จากนั้นใช้ฟังก์ชัน cleanup() เพื่อเรียกใช้เมธอด close() ใหม่นี้ แทนที่คลาส Segmenter และฟังก์ชัน cleanup() ที่มีอยู่ด้วยรายการต่อไปนี้ (~บรรทัดที่ 83)
    private class Segmenter(
        // Add this argument
        private val model: CompiledModel,
        private val coloredLabels: List<ColoredLabel>,
    ) {
        // Add these private vals
        private val inputBuffers = model.createInputBuffers()
        private val outputBuffers = model.createOutputBuffers()
    
        fun cleanup() {
          // cleanup buffers
          inputBuffers.forEach { it.close() }
          outputBuffers.forEach { it.close() }
          // cleanup model
          model.close()
        }
    }
    
  2. กำหนดเมธอด toAccelerator: เมธอดนี้จะแมปการแจงนับตัวเร่งที่กำหนดจากเมนูตัวเร่งไปยังการแจงนับตัวเร่งที่เฉพาะเจาะจงสำหรับโมดูล LiteRT ที่นำเข้า (~บรรทัดที่ 225)
    fun toAccelerator(acceleratorEnum: AcceleratorEnum): Accelerator {
      return when (acceleratorEnum) {
        AcceleratorEnum.CPU -> Accelerator.CPU
        AcceleratorEnum.GPU -> Accelerator.GPU
      }
    }
    
  3. เริ่มต้น CompiledModel: ตอนนี้ให้ค้นหาฟังก์ชัน initSegmenter คุณจะสร้างอินสแตนซ์ CompiledModel และใช้เพื่อสร้างอินสแตนซ์ของคลาส Segmenter ที่กำหนดไว้ในตอนนี้ โค้ดนี้จะตั้งค่าโมเดลด้วยตัวเร่งที่ระบุ (CPU หรือ GPU) และเตรียมโมเดลสำหรับการอนุมาน แทนที่ TODO ใน initSegmenter ด้วยการติดตั้งใช้งานต่อไปนี้ (Cmd/Ctrl+f ‘initSegmenter` หรือ ~บรรทัดที่ 62):
    cleanup()
    try {
      withContext(singleThreadDispatcher) {
        val model =
          CompiledModel.create(
            context.assets,
            "selfie_multiclass.tflite",
            CompiledModel.Options(toAccelerator(acceleratorEnum)),
            null,
          )
        segmenter = Segmenter(model, coloredLabels)
        Log.d(TAG, "Created an image segmenter")
      }
    } catch (e: Exception) {
      Log.i(TAG, "Create LiteRT from selfie_multiclass is failed: ${e.message}")
      _error.emit(e)
    }
    

7. เริ่มการแบ่งกลุ่มและการประมวลผลล่วงหน้า

ตอนนี้เรามีโมเดลแล้ว จึงต้องทริกเกอร์กระบวนการแบ่งกลุ่มและเตรียมข้อมูลอินพุตสำหรับโมเดล

การแบ่งกลุ่มตามทริกเกอร์

กระบวนการแบ่งกลุ่มจะเริ่มต้นใน MainViewModel.kt ซึ่งรับเฟรมจากกล้อง

เปิด MainViewModel.kt

  1. ทริกเกอร์การแบ่งกลุ่มจากเฟรมกล้อง: ฟังก์ชัน segment ใน MainViewModel คือจุดแรกเข้าสำหรับงานการแบ่งกลุ่มของเรา ระบบจะเรียกใช้ฟังก์ชันเหล่านี้ทุกครั้งที่มีรูปภาพใหม่จากกล้องหรือเลือกจากแกลเลอรี จากนั้นฟังก์ชันเหล่านี้จะเรียกใช้เมธอด segment ใน ImageSegmentationHelper แทนที่ TODO ในฟังก์ชัน segment ทั้ง 2 รายการด้วยข้อความต่อไปนี้ (บรรทัดที่ ~107)
    // For ImageProxy (from CameraX)
    fun segment(imageProxy: ImageProxy) {
        segmentJob =
            viewModelScope.launch {
                imageSegmentationHelper.segment(imageProxy.toBitmap(), imageProxy.imageInfo.rotationDegrees)
                imageProxy.close()
            }
    }
    
    // For Bitmaps (from gallery)
    fun segment(bitmap: Bitmap, rotationDegrees: Int) {
        segmentJob =
            viewModelScope.launch {
                val argbBitmap = bitmap.copy(Bitmap.Config.ARGB_8888, true)
                imageSegmentationHelper.segment(argbBitmap, rotationDegrees)
            }
    }
    

ประมวลผลรูปภาพล่วงหน้า

ตอนนี้เรากลับไปที่ ImageSegmentationHelper.kt เพื่อจัดการการประมวลผลล่วงหน้าของรูปภาพกัน

เปิด ImageSegmentationHelper.kt

  1. ใช้ฟังก์ชัน Public segment: ฟังก์ชันนี้ทำหน้าที่เป็น Wrapper ที่เรียกใช้ฟังก์ชัน Private segment ภายในคลาส Segmenter แทนที่ TODO ด้วย (~บรรทัดที่ 95):
    try {
      withContext(singleThreadDispatcher) {
        segmenter?.segment(bitmap, rotationDegrees)?.let { if (isActive) _segmentation.emit(it) }
      }
    } catch (e: Exception) {
      Log.i(TAG, "Image segment error occurred: ${e.message}")
      _error.emit(e)
    }
    
  2. ใช้การประมวลผลล่วงหน้า: segmentฟังก์ชันส่วนตัวภายในคลาส Segmenter คือที่ที่เราจะทำการแปลงที่จำเป็นกับรูปภาพอินพุตเพื่อเตรียมรูปภาพให้พร้อมสำหรับโมเดล ซึ่งรวมถึงการปรับขนาด การหมุน และการปรับรูปภาพให้เป็นปกติ จากนั้นฟังก์ชันนี้จะเรียกใช้ฟังก์ชัน segment ส่วนตัวอีกฟังก์ชันหนึ่งเพื่อทำการอนุมาน แทนที่ TODO ในฟังก์ชัน segment(bitmap: Bitmap, ...) ด้วย (~บรรทัดที่ 121):
    val totalStartTime = SystemClock.uptimeMillis()
    val rotation = -rotationDegrees / 90
    val (h, w) = Pair(256, 256)
    
    // Preprocessing
    val preprocessStartTime = SystemClock.uptimeMillis()
    var image = bitmap.scale(w, h, true)
    image = rot90Clockwise(image, rotation)
    val inputFloatArray = normalize(image, 127.5f, 127.5f)
    Log.d(TAG, "Preprocessing time: ${SystemClock.uptimeMillis() - preprocessStartTime} ms")
    
    // Inference
    val inferenceStartTime = SystemClock.uptimeMillis()
    val segmentResult = segment(inputFloatArray)
    Log.d(TAG, "Inference time: ${SystemClock.uptimeMillis() - inferenceStartTime} ms")
    
    Log.d(TAG, "Total segmentation time: ${SystemClock.uptimeMillis() - totalStartTime} ms")
    return SegmentationResult(segmentResult, SystemClock.uptimeMillis() - inferenceStartTime)
    

8. การอนุมานหลักด้วย LiteRT

เมื่อประมวลผลข้อมูลอินพุตล่วงหน้าแล้ว เราสามารถรันการอนุมานหลักโดยใช้ LiteRT ได้แล้ว

เปิด ImageSegmentationHelper.kt

  1. การใช้งานการดำเนินการโมเดล: ฟังก์ชัน segment(inputFloatArray: FloatArray) ส่วนตัวคือที่ที่เราโต้ตอบกับเมธอด LiteRT run() โดยตรง เราจะเขียนข้อมูลที่ประมวลผลล่วงหน้าไปยังบัฟเฟอร์อินพุต เรียกใช้โมเดล และอ่านผลลัพธ์จากบัฟเฟอร์เอาต์พุต แทนที่ TODO ในฟังก์ชันนี้ด้วย (~บรรทัดที่ 188):
    val (h, w, c) = Triple(256, 256, 6)
    
    // MODEL EXECUTION PHASE
    val modelExecStartTime = SystemClock.uptimeMillis()
    
    // Write input data - measure time
    val bufferWriteStartTime = SystemClock.uptimeMillis()
    inputBuffers[0].writeFloat(inputFloatArray)
    val bufferWriteTime = SystemClock.uptimeMillis() - bufferWriteStartTime
    Log.d(TAG, "Buffer write time: $bufferWriteTime ms")
    
    // Optional tensor inspection
    logTensorStats("Input tensor", inputFloatArray)
    
    // Run model inference - measure time
    val modelRunStartTime = SystemClock.uptimeMillis()
    model.run(inputBuffers, outputBuffers)
    val modelRunTime = SystemClock.uptimeMillis() - modelRunStartTime
    Log.d(TAG, "Model.run() time: $modelRunTime ms")
    
    // Read output data - measure time
    val bufferReadStartTime = SystemClock.uptimeMillis()
    val outputFloatArray = outputBuffers[0].readFloat()
    val outputBuffer = FloatBuffer.wrap(outputFloatArray)
    val bufferReadTime = SystemClock.uptimeMillis() - bufferReadStartTime
    Log.d(TAG, "Buffer read time: $bufferReadTime ms")
    
    val modelExecTime = SystemClock.uptimeMillis() - modelExecStartTime
    Log.d(TAG, "Total model execution time: $modelExecTime ms")
    
    // Optional tensor inspection
    logTensorStats("Output tensor", outputFloatArray)
    
    // POSTPROCESSING PHASE
    val postprocessStartTime = SystemClock.uptimeMillis()
    
    // Process mask from model output
    val inferenceData = InferenceData(width = w, height = h, channels = c, buffer = outputBuffer)
    val mask = processImage(inferenceData)
    
    val postprocessTime = SystemClock.uptimeMillis() - postprocessStartTime
    Log.d(TAG, "Postprocessing time (mask creation): $postprocessTime ms")
    
    return Segmentation(
      listOf(Mask(mask, inferenceData.width, inferenceData.height)),
      coloredLabels,
    )
    

9. การประมวลผลหลังการประมวลผลและการแสดงโอเวอร์เลย์

หลังจากเรียกใช้การอนุมาน เราจะได้รับเอาต์พุตดิบจากโมเดล เราต้องประมวลผลเอาต์พุตนี้เพื่อสร้างมาสก์การแบ่งกลุ่มภาพ แล้วจึงแสดงบนหน้าจอ

เปิด ImageSegmentationHelper.kt

  1. ใช้การประมวลผลเอาต์พุต: ฟังก์ชัน processImage จะแปลงเอาต์พุตจุดลอยตัวดิบจากโมเดลเป็น ByteBuffer ที่แสดงถึงมาสก์การแบ่งกลุ่ม โดยจะพิจารณาจากคลาสที่มีโอกาสสูงสุดสำหรับแต่ละพิกเซล แทนที่ TODO ด้วย (~บรรทัดที่ 238):
    val mask = ByteBuffer.allocateDirect(inferenceData.width * inferenceData.height)
    for (i in 0 until inferenceData.height) {
        for (j in 0 until inferenceData.width) {
            val offset = inferenceData.channels * (i * inferenceData.width + j)
    
            var maxIndex = 0
            var maxValue = inferenceData.buffer.get(offset)
    
            for (index in 1 until inferenceData.channels) {
                if (inferenceData.buffer.get(offset + index) > maxValue) {
                    maxValue = inferenceData.buffer.get(offset + index)
                    maxIndex = index
                }
            }
            mask.put(i * inferenceData.width + j, maxIndex.toByte())
        }
    }
    return mask
    

เปิด MainViewModel.kt

  1. รวบรวมและประมวลผลผลลัพธ์การแบ่งกลุ่ม: ตอนนี้เรากลับไปที่ MainViewModel เพื่อประมวลผลผลลัพธ์การแบ่งกลุ่มจาก ImageSegmentationHelper segmentationUiShareFlow จะรวบรวมSegmentationResult แปลงมาสก์เป็นBitmap ที่มีสีสัน และแสดงใน UI แทนที่ TODO ในพร็อพเพอร์ตี้ segmentationUiShareFlow ด้วย (~บรรทัดที่ 63) อย่าแทนที่โค้ดที่มีอยู่แล้ว เพียงแค่กรอกข้อมูลในส่วนเนื้อหา
    viewModelScope.launch {
      imageSegmentationHelper.segmentation
        .filter { it.segmentation.masks.isNotEmpty() }
        .map {
          val segmentation = it.segmentation
          val mask = segmentation.masks[0]
          val maskArray = mask.data
          val width = mask.width
          val height = mask.height
          val pixelSize = width * height
          val pixels = IntArray(pixelSize)
    
          val colorLabels =
            segmentation.coloredLabels.mapIndexed { index, coloredLabel ->
              ColorLabel(index, coloredLabel.label, coloredLabel.argb)
            }
          // Set color for pixels
          for (i in 0 until pixelSize) {
            val colorLabel = colorLabels[maskArray[i].toInt()]
            val color = colorLabel.getColor()
            pixels[i] = color
          }
          // Get image info
          val overlayInfo = OverlayInfo(pixels = pixels, width = width, height = height)
    
          val inferenceTime = it.inferenceTime
          Pair(overlayInfo, inferenceTime)
        }
        .collect { flow.emit(it) }
    }
    

เปิด view/SegmentationOverlay.kt

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

  1. จัดการการวางซ้อนของแฮนเดิล: ค้นหา TODO ในไฟล์ SegmentationOverlay.kt แล้วแทนที่ด้วยโค้ดต่อไปนี้ โค้ดนี้จะตรวจสอบว่ากล้องหน้าทำงานอยู่หรือไม่ หากทำงานอยู่ ก็จะพลิกภาพซ้อนทับ Bitmap ในแนวนอนก่อนที่จะวาดบน Canvas (~บรรทัดที่ 42):
    val orientedBitmap =
      if (lensFacing == CameraSelector.LENS_FACING_FRONT) {
        // Create a matrix for horizontal flipping
        val matrix = Matrix().apply { preScale(-1f, 1f) }
        Bitmap.createBitmap(image, 0, 0, image.width, image.height, matrix, false).also {
          image.recycle()
        }
      } else {
        image
      }
    

10. เรียกใช้และใช้แอปสุดท้าย

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

  1. เรียกใช้แอป: เชื่อมต่ออุปกรณ์ Android แล้วคลิกเรียกใช้ในแถบเครื่องมือของ Android Studio

ปุ่มเรียกใช้

  1. ทดสอบฟีเจอร์: เมื่อเปิดตัวแอปแล้ว คุณจะเห็นฟีดกล้องสดพร้อมการวางซ้อนการแบ่งกลุ่มสีสันสดใส
    • เปลี่ยนกล้อง: แตะไอคอนพลิกกล้องที่ด้านบนเพื่อสลับระหว่างกล้องหน้าและกล้องหลัง สังเกตว่าการซ้อนทับปรับทิศทางของตัวเองอย่างถูกต้องอย่างไร
    • เปลี่ยนตัวเร่ง: แตะปุ่ม "CPU" หรือ "GPU" ที่ด้านล่างเพื่อเปลี่ยนตัวเร่งฮาร์ดแวร์ สังเกตการเปลี่ยนแปลงในเวลาอนุมานที่แสดงที่ด้านล่างของหน้าจอ GPU ควรจะเร็วกว่ามาก
    • ใช้รูปภาพในแกลเลอรี: แตะแท็บ "แกลเลอรี" ที่ด้านบนเพื่อเลือกรูปภาพจากแกลเลอรีรูปภาพของอุปกรณ์ แอปจะเรียกใช้การแบ่งกลุ่มในรูปภาพนิ่งที่เลือก

UI อื่นๆ

ตอนนี้คุณมีแอปการแบ่งกลุ่มรูปภาพแบบเรียลไทม์ที่ทำงานได้อย่างเต็มรูปแบบซึ่งขับเคลื่อนโดย LiteRT แล้ว

11. ขั้นสูง (ไม่บังคับ): การใช้ NPU

นอกจากนี้ ที่เก็บนี้ยังมีแอปเวอร์ชันที่ได้รับการเพิ่มประสิทธิภาพสำหรับหน่วยประมวลผลประสาท (NPU) ด้วย เวอร์ชัน NPU สามารถเพิ่มประสิทธิภาพได้อย่างมากในอุปกรณ์ที่มี NPU ที่เข้ากันได้

หากต้องการลองใช้เวอร์ชัน NPU ให้เปิดkotlin_npu/androidโปรเจ็กต์ใน Android Studio โค้ดมีความคล้ายคลึงกับเวอร์ชัน CPU/GPU มาก และได้รับการกำหนดค่าให้ใช้ตัวแทน NPU

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

12. ยินดีด้วย

คุณสร้างแอป Android ที่ทำการแบ่งกลุ่มรูปภาพแบบเรียลไทม์โดยใช้ LiteRT ได้สำเร็จแล้ว คุณได้เรียนรู้วิธีต่อไปนี้แล้ว

  • ผสานรวมรันไทม์ LiteRT เข้ากับแอป Android
  • โหลดและเรียกใช้โมเดลการแบ่งกลุ่มรูปภาพ TFLite
  • ประมวลผลอินพุตของโมเดลล่วงหน้า
  • ประมวลผลเอาต์พุตของโมเดลเพื่อสร้างมาสก์การแบ่งกลุ่ม
  • ใช้ CameraX สำหรับแอปกล้องแบบเรียลไทม์

ขั้นตอนถัดไป

  • ลองใช้โมเดลการแบ่งกลุ่มรูปภาพอื่น
  • ทดสอบกับตัวแทน LiteRT ต่างๆ (CPU, GPU, NPU)

ดูข้อมูลเพิ่มเติม