יצירת אפליקציה ל-Android לסיווג ספרות בכתב יד באמצעות MediaPipe Tasks

1. מבוא

מה זה MediaPipe?

בעזרת MediaPipe Solutions אתם יכולים להחיל פתרונות של למידת מכונה (ML) על האפליקציות שלכם. היא מספקת מסגרת להגדרת צינורות עיבוד נתונים מוכנים מראש שמספקים למשתמשים פלט מיידי, מרתק ושימושי. אפשר אפילו להתאים אישית את הפתרונות האלה באמצעות MediaPipe Model Maker כדי לעדכן את המודלים שמוגדרים כברירת מחדל.

סיווג תמונות הוא אחת מכמה משימות של ראיית למידת מכונה שיש ב-MediaPipe Solutions. משימות MediaPipe זמינות ל-Android, ל-iOS, ל-Python (כולל Raspberry Pi! ) ולאינטרנט.

ב-Codelab הזה, נתחיל עם אפליקציה ל-Android שמאפשרת לצייר ספרות מספריות על המסך, ואז נוסיף פונקציונליות שמסווגת את הספרות המצוירות כערך יחיד בין 0 ל-9.

מה תלמדו

  • איך משלבים משימה של סיווג תמונות באפליקציה ל-Android באמצעות משימות MediaPipe

למה תזדקק?

  • גרסה מותקנת של Android Studio (ה-Codelab הזה נכתב ונבדק באמצעות Android Studio Giraffe).
  • מכשיר Android או אמולטור להפעלת האפליקציה.
  • ידע בסיסי בפיתוח Android (לא מדובר ב"שלום עולם", אבל הוא לא רחוק מדי!).

2. הוספת משימות של MediaPipe לאפליקציה ל-Android

הורדת האפליקציה ל-Android לתחילת פעולה

ה-Codelab הזה יתחיל בדוגמה מוכנה מראש שמאפשרת לצייר על המסך. אפשר למצוא את האפליקציה ההתחלתית במאגר הרשמי של MediaPipe Samples כאן. משכפלים את המאגר או מורידים את קובץ ה-ZIP באמצעות לחיצה על 'קוד' > הורדה של קובץ ZIP.

ייבוא האפליקציה ל-Android Studio

  1. פותחים את Android Studio.
  2. במסך ברוכים הבאים אל Android Studio, בוחרים באפשרות פתיחה בפינה השמאלית העליונה.

a0b5b070b802e4ea.png

  1. עוברים למקום שבו שכפולם או הורדתם את המאגר, ופותחים את ספריית Codelabs/digitalclassifier/android/start.
  2. כדי לוודא שהכול נפתח כמו שצריך, לוחצים על החץ הירוק להרצה ( 7e15a9c9e1620fe7.png) בפינה השמאלית העליונה ב-Android Studio.
  3. האפליקציה אמורה להיפתח עם מסך שחור שניתן לצייר עליו, וכן לחצן ניקוי כדי לאפס את המסך. אפשר לשרטט על המסך, אבל לא צריך לעשות הרבה יותר, אז נתחיל לתקן את זה עכשיו.

11a0f6fe021fdc92.jpeg

דגם

כשתפעילו את האפליקציה בפעם הראשונה, ייתכן שתשימו לב שהורדתם קובץ בשם mnist.tflite ונשמר בספריית הנכסים של האפליקציה. כדי לשמור על פשטות, כבר לקחנו מודל מוכר, MNIST, שמסווג ספרות והוספנו אותו לאפליקציה באמצעות הסקריפט download_models.gradle בפרויקט. אם תחליטו לאמן מודל מותאם אישית משלכם, למשל מודל לאותיות בכתב יד, תצטרכו להסיר את הקובץ download_models.gradle, למחוק את ההפניה אליו בקובץ build.gradle ברמת האפליקציה, ולשנות את שם המודל מאוחר יותר בקוד (במיוחד בקובץ DigitClassifierHelper.kt).

עדכון build.gradle

לפני שמתחילים להשתמש ב-MediaPipe Tasks, צריך לייבא את הספרייה.

  1. פותחים את הקובץ build.gradle שנמצא במודול app, וגוללים למטה אל בלוק dependencies.
  2. בחלק התחתון של הבלוק אמורה להופיע הערה שכתוב בה // שלב 1 ייבוא תלות.
  3. מחליפים את השורה הזו בהטמעה הבאה
implementation("com.google.mediapipe:tasks-vision:latest.release")
  1. כדי להוריד את התלות הזאת, לוחצים על הלחצן סנכרון עכשיו שמופיע בבאנר בחלק העליון של Android Studio.

3. יצירת כלי לסיווג ספרות ב-MediaPipe Tasks

בשלב הבא תמלאו את הכיתה לעשות את העבודה הקשה ביותר לצורכי סיווג למידת המכונה שלכם. כדי להתחיל, צריך לפתוח את DigitClassifierHelper.kt.

  1. מוצאים את התגובה בחלק העליון של הכיתה שכתוב בה // STEP 2 Create Listener
  2. מחליפים את השורה הזו בקוד הבא. הפעולה הזו תיצור אוזן שישמש להעברת תוצאות מהכיתה 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 setup classifier (הגדרת סיווג של שלב 3), מוסיפים את השורה הבאה כדי ליצור placeholder בשביל ImageClassifier, שישמש לאפליקציה הזו:

// STEP 3 configuration classifier

private var digitClassifier: ImageClassifier? = null
  1. מוסיפים את הפונקציה הבאה שבה מופיעה התגובה // STEP 4 set up classifier:
// 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 ובתפריט EnabledMode בקטע ImageClassifierOptions, שבמקרה הזה הוא IMAGE, אבל יש אפשרויות זמינות נוספות: VIDEO ו-LIVE_STREAM. פרמטרים זמינים אחרים הם Max Results (תוצאות מקסימליות), שמגביל את המודל כך שיחזיר מספר מקסימלי של תוצאות, וסף הציון, שקובע את רמת הסמך המינימלית של המודל צריך להיות בתוצאה לפני שהוא מחזיר אותה.

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. לסיום, גוללים למטה לתגובה שבה כתוב // STEP 5 create classify function ומוסיפים את הקוד הבא. הפונקציה הזו מקבלת מפת סיביות, שבמקרה הזה היא הספרה שצולמה, ממירה אותה לאובייקט תמונה של 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)
    }
}

זהו, סיימתם עם קובץ ה-Helper. בקטע הבא תבצעו את השלבים האחרונים כדי להתחיל לסווג את המספרים שציירתם.

4. הרצת מסקנות בעזרת Tasks ב-MediaPipe

כדי להתחיל את הקטע הזה, פותחים את הכיתה DigitCanvasFragment ב-Android Studio, שבה כל העבודה תתבצע.

  1. בחלק התחתון של הקובץ הזה אמורה להופיע תגובה עם הכיתוב // STEP 6 Set up Listener כאן מוסיפים את הפונקציות on Results() ו-onError() שמשויכות ל-listen.
// 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())
    }
}

on Results() חשוב במיוחד כי היא תציג את התוצאות שהתקבלו מ-ImageClassifier. הקריאה החוזרת (callback) הזו מופעלת משרשור ברקע, ולכן צריך גם להריץ עדכונים של ממשק המשתמש בשרשור של ממשק המשתמש ב-Android.

  1. כשמוסיפים פונקציות חדשות מהממשק שמוצג בשלב שלמעלה, צריך גם להוסיף את הצהרת ההטמעה בחלק העליון של המחלקה.
class DigitCanvasFragment : Fragment(), DigitClassifierHelper.DigitClassifierListener
  1. בחלק העליון של הכיתה אמורה להופיע תגובה עם הכיתוב // STEP 7a אתחול מסווג. כאן יש לשלוח את ההצהרה עבור DigitClassifierHelper.
// STEP 7a Initialize classifier.
private lateinit var digitClassifierHelper: DigitClassifierHelper
  1. כשמורידים אל // STEP 7b אתחול מסווג, אפשר לאתחל את digitalClassifierHelper בתוך הפונקציה 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() . הפעולה הזו תחלץ מפת סיביות מאזור העריכה, ולאחר מכן תעביר אותה אל DigitClassifierHelper כדי לבצע סיווג כדי לקבל את התוצאות בפונקציית הממשק on Results() .
// 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 האחרות שזמינות ב-Android, כולל זיהוי פנים של ציוני דרך, זיהוי תנועות וסיווג אודיו.

אנחנו מצפים לכל הדברים המגניבים שיעשו לך!