สร้างแอปตัวแยกประเภทตัวเลขที่เขียนด้วยลายมือสำหรับ Android ด้วย MediaPipe Tasks

1. บทนำ

MediaPipe คืออะไร

โซลูชัน MediaPipe ช่วยให้คุณใช้โซลูชันแมชชีนเลิร์นนิง (ML) กับแอปได้ โดยมีเฟรมเวิร์กสำหรับการกำหนดค่าไปป์ไลน์การประมวลผลที่สร้างไว้ล่วงหน้า ซึ่งจะให้เอาต์พุตที่น่าสนใจและมีประโยชน์แก่ผู้ใช้ได้ทันที คุณยังปรับแต่งโซลูชันเหล่านี้ด้วย MediaPipe Model Maker เพื่ออัปเดตโมเดลเริ่มต้นได้ด้วย

การจัดประเภทรูปภาพเป็นหนึ่งในงานด้านวิชันซิสเต็ม ML หลายอย่างที่โซลูชัน MediaPipe มีให้บริการ MediaPipe Tasks พร้อมใช้งานสำหรับ Android, iOS, Python (รวมถึง Raspberry Pi!) และเว็บ

ใน Codelab นี้ คุณจะเริ่มต้นด้วยแอป Android ที่ให้คุณวาดตัวเลขบนหน้าจอ จากนั้นจะเพิ่มฟังก์ชันที่จัดประเภทตัวเลขที่วาดเป็นค่าเดียวตั้งแต่ 0 ถึง 9

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

  • วิธีรวมงานการแยกประเภทรูปภาพในแอป Android ด้วย MediaPipe Tasks

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

  • Android Studio เวอร์ชันที่ติดตั้ง (Codelab นี้เขียนและทดสอบด้วย Android Studio Giraffe)
  • อุปกรณ์ Android หรือโปรแกรมจำลองสำหรับเรียกใช้แอป
  • มีความรู้พื้นฐานเกี่ยวกับการพัฒนา Android (นี่ไม่ใช่ "Hello World" แต่ก็ไม่ได้ไกลกันมากนัก)

2. เพิ่ม MediaPipe Tasks ลงในแอป Android

ดาวน์โหลดแอปเริ่มต้นสำหรับ Android

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

นำเข้าแอปไปยัง Android Studio

  1. เปิด Android Studio
  2. จากหน้าจอยินดีต้อนรับสู่ Android Studio ให้เลือกเปิดที่มุมขวาบน

a0b5b070b802e4ea.png

  1. ไปที่ตำแหน่งที่คุณโคลนหรือดาวน์โหลดที่เก็บ แล้วเปิดcodelabs/digitclassifier/android/start directory
  2. ตรวจสอบว่าทุกอย่างเปิดขึ้นอย่างถูกต้องโดยคลิกลูกศรเรียกใช้สีเขียว ( 7e15a9c9e1620fe7.png) ที่ด้านขวาบนของ Android Studio
  3. คุณควรเห็นแอปเปิดขึ้นพร้อมกับหน้าจอสีดำที่วาดได้ รวมถึงปุ่มล้างเพื่อรีเซ็ตหน้าจอนั้น แม้ว่าคุณจะวาดบนหน้าจอนั้นได้ แต่ก็ทำอย่างอื่นไม่ได้มากนัก ดังนั้นเราจะเริ่มแก้ไขปัญหานี้ในตอนนี้

11a0f6fe021fdc92.jpeg

รุ่น

เมื่อเรียกใช้แอปเป็นครั้งแรก คุณอาจสังเกตเห็นว่าระบบจะดาวน์โหลดและจัดเก็บไฟล์ชื่อ mnist.tflite ไว้ในไดเรกทอรี assets ของแอป เพื่อความสะดวก เราได้ใช้โมเดลที่รู้จักกันดีอย่าง MNIST ซึ่งจัดประเภทตัวเลข และเพิ่มลงในแอปผ่านการใช้สคริปต์ download_models.gradle ในโปรเจ็กต์แล้ว หากตัดสินใจฝึกโมเดลที่กำหนดเอง เช่น โมเดลสำหรับตัวอักษรที่เขียนด้วยลายมือ คุณจะต้องนำไฟล์ download_models.gradle ออก ลบการอ้างอิงถึงไฟล์ดังกล่าวในไฟล์ build.gradle ระดับแอป และเปลี่ยนชื่อโมเดลในโค้ดในภายหลัง (โดยเฉพาะในไฟล์ DigitClassifierHelper.kt)

อัปเดต build.gradle

คุณต้องนำเข้าไลบรารีก่อนจึงจะเริ่มใช้ MediaPipe Tasks ได้

  1. เปิดไฟล์ build.gradle ที่อยู่ในโมดูล app จากนั้นเลื่อนลงไปที่บล็อก dependencies
  2. คุณควรเห็นความคิดเห็นที่ด้านล่างของบล็อกนั้นซึ่งระบุว่า // STEP 1 Dependency Import
  3. แทนที่บรรทัดดังกล่าวด้วยการติดตั้งใช้งานต่อไปนี้
implementation("com.google.mediapipe:tasks-vision:latest.release")
  1. คลิกปุ่มซิงค์เลยที่ปรากฏในแบนเนอร์ที่ด้านบนของ Android Studio เพื่อดาวน์โหลดทรัพยากร Dependency นี้

3. สร้างตัวช่วยแยกประเภทตัวเลขของ MediaPipe Tasks

ในขั้นตอนถัดไป คุณจะต้องกรอกข้อมูลในคลาสที่จะช่วยจัดการงานหนักสำหรับการจัดประเภทแมชชีนเลิร์นนิง เปิด DigitClassifierHelper.kt แล้วมาเริ่มกันเลย

  1. ค้นหาความคิดเห็นที่ด้านบนของชั้นเรียนซึ่งระบุว่า // ขั้นตอนที่ 2 สร้าง Listener
  2. แทนที่บรรทัดดังกล่าวด้วยโค้ดต่อไปนี้ ซึ่งจะสร้าง Listener ที่ใช้เพื่อส่งผลลัพธ์จากคลาส DigitClassifierHelper กลับไปยังที่ใดก็ตามที่รอรับผลลัพธ์เหล่านั้น (ในกรณีนี้คือคลาส DigitCanvasFragment แต่เราจะไปถึงจุดนั้นในเร็วๆ นี้)
// STEP 2 Create listener

interface DigitClassifierListener {
    fun onError(error: String)
    fun onResults(
        results: ImageClassifierResult,
        inferenceTime: Long
    )
}
  1. นอกจากนี้ คุณยังต้องยอมรับ DigitClassifierListener เป็นพารามิเตอร์ที่ไม่บังคับสำหรับคลาสด้วย
class DigitClassifierHelper(
    val context: Context,
    val digitClassifierListener: DigitClassifierListener?
) {
  1. ไปที่บรรทัดที่ระบุว่า // STEP 3 define classifier แล้วเพิ่มบรรทัดต่อไปนี้เพื่อสร้างตัวยึดตำแหน่งสำหรับ ImageClassifier ที่จะใช้กับแอปนี้

// ขั้นตอนที่ 3 กำหนดตัวแยกประเภท

private var digitClassifier: ImageClassifier? = null
  1. เพิ่มฟังก์ชันต่อไปนี้ในตำแหน่งที่คุณเห็นความคิดเห็น // ขั้นตอนที่ 4 ตั้งค่าเครื่องมือคัดแยก
// STEP 4 set up classifier
private fun setupDigitClassifier() {

    val baseOptionsBuilder = BaseOptions.builder()
        .setModelAssetPath("mnist.tflite")

    // Describe additional options
    val optionsBuilder = ImageClassifierOptions.builder()
        .setRunningMode(RunningMode.IMAGE)
        .setBaseOptions(baseOptionsBuilder.build())

    try {
        digitClassifier =
            ImageClassifier.createFromOptions(
                context,
                optionsBuilder.build()
            )
    } catch (e: IllegalStateException) {
        digitClassifierListener?.onError(
            "Image classifier failed to initialize. See error logs for " +
                    "details"
        )
        Log.e(TAG, "MediaPipe failed to load model with error: " + e.message)
    }
}

ในส่วนด้านบนมีหลายสิ่งเกิดขึ้น ดังนั้นเรามาดูส่วนย่อยๆ เพื่อให้เข้าใจสิ่งที่เกิดขึ้นจริงๆ กัน

val baseOptionsBuilder = BaseOptions.builder()
    .setModelAssetPath("mnist.tflite")

// Describe additional options
val optionsBuilder = ImageClassifierOptions.builder()
    .setRunningMode(RunningMode.IMAGE)
    .setBaseOptions(baseOptionsBuilder.build())

บล็อกนี้จะกำหนดพารามิเตอร์ที่ใช้โดย ImageClassifier ซึ่งรวมถึงโมเดลที่จัดเก็บไว้ในแอป (mnist.tflite) ภายใต้ BaseOptions และ RunningMode ภายใต้ ImageClassifierOptions ซึ่งในกรณีนี้คือ IMAGE แต่ VIDEO และ LIVE_STREAM เป็นตัวเลือกเพิ่มเติมที่ใช้ได้ พารามิเตอร์อื่นๆ ที่ใช้ได้คือ MaxResults ซึ่งจำกัดโมเดลให้แสดงผลลัพธ์ได้สูงสุด และ ScoreThreshold ซึ่งกำหนดความน่าเชื่อถือขั้นต่ำที่โมเดลต้องมีในผลลัพธ์ก่อนที่จะแสดงผลลัพธ์นั้น

try {
    digitClassifier =
        ImageClassifier.createFromOptions(
            context,
            optionsBuilder.build()
        )
} catch (e: IllegalStateException) {
    digitClassifierListener?.onError(
        "Image classifier failed to initialize. See error logs for " +
                "details"
    )
    Log.e(TAG, "MediaPipe failed to load model with error: " + e.message)
}

หลังจากสร้างตัวเลือกการกำหนดค่าแล้ว คุณจะสร้าง ImageClassifier ใหม่ได้โดยส่งบริบทและตัวเลือก หากกระบวนการเริ่มต้นใช้งานดังกล่าวเกิดข้อผิดพลาด ระบบจะแสดงข้อผิดพลาดผ่าน DigitClassifierListener

  1. เนื่องจากเราต้องการเริ่มต้น ImageClassifier ก่อนที่จะใช้งาน คุณจึงเพิ่มบล็อก init เพื่อเรียกใช้ setupDigitClassifier() ได้
init {
    setupDigitClassifier()
}
  1. สุดท้าย ให้เลื่อนลงไปที่ความคิดเห็นที่ระบุว่า // ขั้นตอนที่ 5 สร้างฟังก์ชันการจัดประเภท แล้วเพิ่มโค้ดต่อไปนี้ ฟังก์ชันนี้จะยอมรับ Bitmap ซึ่งในกรณีนี้คือตัวเลขที่วาด แปลงเป็นออบเจ็กต์รูปภาพ MediaPipe (MPImage) จากนั้นจัดประเภทรูปภาพนั้นโดยใช้ ImageClassifier รวมถึงบันทึกระยะเวลาที่ใช้ในการอนุมาน ก่อนที่จะส่งคืนผลลัพธ์เหล่านั้นผ่าน DigitClassifierListener
// STEP 5 create classify function
fun classify(image: Bitmap) {
    if (digitClassifier == null) {
        setupDigitClassifier()
    }

    // Convert the input Bitmap object to an MPImage object to run inference.
    // Rotating shouldn't be necessary because the text is being extracted from
    // a view that should always be correctly positioned.
    val mpImage = BitmapImageBuilder(image).build()

    // Inference time is the difference between the system time at the start and finish of the
    // process
    val startTime = SystemClock.uptimeMillis()

    // Run image classification using MediaPipe Image Classifier API
    digitClassifier?.classify(mpImage)?.also { classificationResults ->
        val inferenceTimeMs = SystemClock.uptimeMillis() - startTime
        digitClassifierListener?.onResults(classificationResults, inferenceTimeMs)
    }
}

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

4. เรียกใช้การอนุมานด้วย MediaPipe Tasks

คุณเริ่มต้นส่วนนี้ได้โดยเปิดคลาส DigitCanvasFragment ใน Android Studio ซึ่งเป็นที่ที่จะเกิดการทำงานทั้งหมด

  1. ที่ด้านล่างสุดของไฟล์นี้ คุณควรเห็นความคิดเห็นที่ระบุว่า // ขั้นตอนที่ 6 ตั้งค่า Listener คุณจะเพิ่มฟังก์ชัน onResults() และ onError() ที่เชื่อมโยงกับ Listener ได้ที่นี่
// STEP 6 Set up listener
override fun onError(error: String) {
    activity?.runOnUiThread {
        Toast.makeText(requireActivity(), error, Toast.LENGTH_SHORT).show()
        fragmentDigitCanvasBinding.tvResults.text = ""
    }
}

override fun onResults(
    results: ImageClassifierResult,
    inferenceTime: Long
) {
    activity?.runOnUiThread {
        fragmentDigitCanvasBinding.tvResults.text = results
            .classificationResult()
            .classifications().get(0)
            .categories().get(0)
            .categoryName()

        fragmentDigitCanvasBinding.tvInferenceTime.text = requireActivity()
            .getString(R.string.inference_time, inferenceTime.toString())
    }
}

onResults() มีความสําคัญอย่างยิ่งเนื่องจากจะแสดงผลลัพธ์ที่ได้รับจาก ImageClassifier เนื่องจากมีการทริกเกอร์ Callback นี้จากเธรดเบื้องหลัง คุณจึงต้องเรียกใช้การอัปเดต UI ในเธรด UI ของ Android ด้วย

  1. ขณะเพิ่มฟังก์ชันใหม่จากอินเทอร์เฟซในขั้นตอนด้านบน คุณจะต้องเพิ่มการประกาศการใช้งานที่ด้านบนของคลาสด้วย
class DigitCanvasFragment : Fragment(), DigitClassifierHelper.DigitClassifierListener
  1. ที่ด้านบนของคลาส คุณควรเห็นความคิดเห็นที่ระบุว่า // STEP 7a Initialize classifier นี่คือส่วนที่คุณจะวางประกาศสำหรับ DigitClassifierHelper
// STEP 7a Initialize classifier.
private lateinit var digitClassifierHelper: DigitClassifierHelper
  1. เลื่อนลงไปที่ // STEP 7b Initialize classifier คุณสามารถเริ่มต้น digitClassifierHelper ภายในฟังก์ชัน onViewCreated() ได้
// STEP 7b Initialize classifier
// Initialize the digit classifier helper, which does all of the
// ML work. This uses the default values for the classifier.
digitClassifierHelper = DigitClassifierHelper(
    context = requireContext(), digitClassifierListener = this
)
  1. สำหรับขั้นตอนสุดท้าย ให้ค้นหาความคิดเห็น // STEP 8a*: classify* แล้วเพิ่มโค้ดต่อไปนี้เพื่อเรียกใช้ฟังก์ชันใหม่ที่คุณจะเพิ่มในอีกสักครู่ โค้ดบล็อกนี้จะทริกเกอร์การจัดประเภทเมื่อคุณยกนิ้วออกจากพื้นที่วาดในแอป
// STEP 8a: classify
classifyDrawing()
  1. สุดท้าย ให้มองหาความคิดเห็น // STEP 8b classify เพื่อเพิ่มฟังก์ชัน classifyDrawing() ใหม่ ซึ่งจะดึงบิตแมปจาก Canvas จากนั้นส่งไปยัง DigitClassifierHelper เพื่อทำการจัดประเภทเพื่อรับผลลัพธ์ในฟังก์ชันอินเทอร์เฟซ onResults()
// STEP 8b classify
private fun classifyDrawing() {
    val bitmap = fragmentDigitCanvasBinding.digitCanvas.getBitmap()
    digitClassifierHelper.classify(bitmap)
}

5. ทำให้แอปใช้งานได้และทดสอบแอป

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

  1. คลิกเรียกใช้ ( 7e15a9c9e1620fe7.png) ในแถบเครื่องมือ Android Studio เพื่อเรียกใช้แอป
  2. วาดตัวเลขใดก็ได้ลงในกระดานวาดภาพ แล้วดูว่าแอปจดจำได้หรือไม่ โดยควรแสดงทั้งตัวเลขที่โมเดลเชื่อว่าวาด และระยะเวลาที่ใช้ในการคาดการณ์ตัวเลขนั้น

7f37187f8f919638.gif

6. ยินดีด้วย

สำเร็จแล้ว! ใน Codelab นี้ คุณได้เรียนรู้วิธีเพิ่มการจัดประเภทรูปภาพลงในแอป Android และโดยเฉพาะอย่างยิ่งวิธีจัดประเภทตัวเลขที่วาดด้วยมือโดยใช้โมเดล MNIST

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

  • เมื่อแยกประเภทตัวเลขได้แล้ว คุณอาจต้องการฝึกโมเดลของคุณเองเพื่อแยกประเภทตัวอักษรที่วาด สัตว์ หรือรายการอื่นๆ อีกมากมาย ดูเอกสารประกอบสำหรับการฝึกโมเดลการแยกประเภทรูปภาพใหม่ด้วย MediaPipe Model Maker ได้ที่หน้า developers.google.com/mediapipe
  • ดูข้อมูลเกี่ยวกับ MediaPipe Tasks อื่นๆ ที่พร้อมใช้งานสำหรับ Android ซึ่งรวมถึงการตรวจจับจุดสังเกตบนใบหน้า การจดจำท่าทางสัมผัส และการแยกประเภทเสียง

เราตั้งตารอชมผลงานเจ๋งๆ ของคุณ