صمِّم تطبيق Android للتصنيف الرقمي بخط اليد مع تطبيق MediaPipe Tasks

1. مقدمة

ما هو MediaPipe؟

تتيح لك MediaPipe Solutions تطبيق حلول تعلُّم الآلة (ML) على تطبيقاتك. توفّر المنصة إطارًا لإعداد مسارات المعالجة المصمَّمة مسبقًا والتي توفّر للمستخدمين نتائج فورية وجذّابة ومفيدة. يمكنك أيضًا تخصيص هذه الحلول باستخدام MediaPipe model Maker لتعديل النماذج التلقائية.

تصنيف الصور هو إحدى مهام رؤية تعلّم الآلة التي تقدمها MediaPipe Solutions. تتوفر خدمة MediaPipe Tasks لأجهزة Android وiOS وPython (بما في ذلك Raspberry Pi!) والويب.

في هذا الدرس التطبيقي، ستبدأ بتطبيق Android يتيح لك رسم أرقام رقمية على الشاشة، ثم ستضيف وظيفة تصنف تلك الأرقام المرسومة كقيمة واحدة من 0 إلى 9.

ما ستتعرَّف عليه

  • كيفية دمج مهمة تصنيف الصور في تطبيق Android باستخدام MediaPipe Tasks

المتطلبات

  • إصدار مثبَّت من Android Studio (تمت كتابة هذا الدرس التطبيقي حول الترميز واختباره باستخدام Android Studio Giraffe)
  • جهاز Android أو محاكي لتشغيل التطبيق
  • هي معرفة أساسية بتطوير تطبيقات Android (هذه ليست عبارة عن كلمة "مرحبًا بكم"، ولكنها ليست بعيدة جدًا).

2. إضافة "مهام MediaPipe" إلى تطبيق Android

تنزيل تطبيق Android Starter

سيبدأ هذا الدرس التطبيقي حول الترميز بعيّنة معدّة مسبقًا تتيح لك الرسم على الشاشة. يمكنك العثور على خيار بدء تشغيل التطبيق في المستودع الرسمي لعيّنات MediaPipe هنا. استنسِخ المستودع أو نزِّل الملف zip من خلال النقر على الرمز > تنزيل ZIP.

استيراد التطبيق إلى "استوديو Android"

  1. افتح "استوديو Android".
  2. من شاشة مرحبًا بك في "استوديو Android"، انقر على فتح في أعلى يسار الصفحة.

a0b5b070b802e4ea.png

  1. انتقِل إلى المكان الذي استنسعت فيه المستودع أو نزّلته، وافتح دليل codelabs/digitalclassifier/android/start.
  2. تأكَّد من فتح كل العناصر بشكل صحيح من خلال النقر على السهم تشغيل الأخضر ( 7e15a9c9e1620fe7.png) في أعلى يسار صفحة "استوديو Android".
  3. من المفترض أن يظهر التطبيق بشاشة سوداء يمكنك الرسم عليها، بالإضافة إلى زر محو لإعادة ضبط هذه الشاشة. وعلى الرغم من أنّه بإمكانك الرسم على تلك الشاشة، لن يؤثّر ذلك في أي طريقة أخرى، لذا سنبدأ الآن بإصلاحها.

11a0f6fe021fdc92.jpeg

الطراز

عند تشغيل التطبيق لأول مرة، قد تلاحظ أنّه يتم تنزيل ملف باسم mnist.tflite وتخزينه في دليل Assets الخاص بتطبيقك. ولتبسيط الأمر، استخدمنا النص البرمجي download_models.gradle في المشروع، وهو النموذج الذي يصنف الأرقام. إذا قرّرت تدريب نموذج مخصّص، مثل نموذج مخصّص بأحرف مكتوبة بخط اليد، عليك إزالة الملف download_models.gradle، وحذف الإشارة إليه في ملف build.gradle على مستوى التطبيق، وتغيير اسم النموذج لاحقًا في الرمز البرمجي (وخصوصًا في ملف DigitClassifierHelper.kt).

تعديل Build.gradle

قبل أن تتمكَّن من البدء في استخدام "مهام MediaPipe"، عليك استيراد المكتبة.

  1. افتح ملف build.gradle في وحدة التطبيق، ثم انتقِل للأسفل إلى مجموعة التبعيات.
  2. من المفترض أن يظهر لك تعليق في أسفل هذه المجموعة مفاده // استيراد الخطوة 1.
  3. استبدِل هذا السطر بعملية التنفيذ التالية.
implementation("com.google.mediapipe:tasks-vision:latest.release")
  1. انقر على زر المزامنة الآن الذي يظهر في البانر أعلى "استوديو Android" لتنزيل هذه الاعتمادية.

3- إنشاء مساعد مصنّف أرقام في "مهام MediaPipe"

في الخطوة التالية، سوف تكمل صفًا سيتولى المهام الصعبة لتصنيف التعلم الآلي لديك. افتح DigitClassifierHelper.kt وابدأ الآن.

  1. ابحث في أعلى الصف عن التعليق التالي: // الخطوة 2 إنشاء مستمع
  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. بالانتقال إلى السطر التالي: // الخطوة 3 تحديد المصنِّف، أضِف السطر التالي لإنشاء عنصر نائب لـ 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 وRunMode ضمن 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. أخيرًا، مرر لأسفل إلى التعليق الذي يقول // STEP 5 إنشاء دالة تصنيف وإضافة التعليمة البرمجية التالية. ستقبل هذه الدالة دالة Bitmap، وهي في هذه الحالة الرقم المرسوم، وتحوِّله إلى كائن صورة MediaPipe Image (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"

يمكنك بدء هذا القسم بفتح الصف DigitCanvasFragment في "استوديو Android"، حيث ستتم جميع هذه الإجراءات.

  1. في أسفل هذا الملف، من المفترض أن يظهر تعليق بعنوان // الخطوة 6 إعداد المستمع. ستضيف الدالتين onResults() وonError() المرتبطين بالمستمع هنا.
// 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. نظرًا لتشغيل رد الاتصال هذا من سلسلة محادثات في الخلفية، ستحتاج أيضًا إلى تشغيل تحديثات واجهة المستخدم على سلسلة واجهة المستخدم في Android.

  1. عند إضافة دوال جديدة من واجهة في الخطوة أعلاه، ستحتاج أيضًا إلى إضافة بيان التنفيذ في أعلى الفئة.
class DigitCanvasFragment : Fragment(), DigitClassifierHelper.DigitClassifierListener
  1. في أعلى الصف، من المفترض أن يظهر تعليق مفاده // الخطوة 7a إعداد المصنِّف. في هذه الصفحة، ستضع بيان DigitClassifierHelper.
// STEP 7a Initialize classifier.
private lateinit var digitClassifierHelper: DigitClassifierHelper
  1. بالانتقال للأسفل إلى // الخطوة 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. في الخطوات الأخيرة، ابحث عن التعليق // الخطوة 8a*: التصنيف* وأضِف الرمز التالي لاستدعاء دالة جديدة ستضيفها فورًا. وستبدأ مجموعة الرموز هذه بتصنيفك عندما ترفع إصبعك من منطقة الرسم في التطبيق.
// STEP 8a: classify
classifyDrawing()
  1. أخيرًا، ابحث عن التعليق // STEP 8b classify لإضافة دالة classifyDrawing() الجديدة. سيؤدي هذا إلى استخراج صورة نقطية من لوحة الرسم، ثم تمريرها إلى DigitClassifierHelper لإجراء التصنيف والحصول على النتائج في دالة واجهة onResults().
// STEP 8b classify
private fun classifyDrawing() {
    val bitmap = fragmentDigitCanvasBinding.digitCanvas.getBitmap()
    digitClassifierHelper.classify(bitmap)
}

5- نشر التطبيق واختباره

بعد كل ذلك، يُفترض أن يكون لديك تطبيق عمل يمكنه تصنيف الأرقام المرسومة على الشاشة! يمكنك المتابعة ونشر التطبيق على محاكي Android أو على جهاز Android فعلي لاختباره.

  1. انقر على "تشغيل" (7e15a9c9e1620fe7.png) في شريط أدوات "استوديو Android" لتشغيل التطبيق.
  2. ارسم أي رقم على لوحة الرسم واكتشف ما إذا كان يمكن للتطبيق التعرف عليه. ويجب أن يعرض كل من الرقم الذي يعتقد النموذج أنه تم رسمه، وكذلك المدة التي يستغرقها التنبؤ بهذا الرقم.

7f37187f8f919638.gif

6- تهانينا!

أحسنت. في هذا الدرس التطبيقي حول الترميز، تعلّمت كيفية إضافة تصنيف الصور إلى تطبيق Android، وخاصةً طريقة تصنيف الأرقام المرسومة باليد باستخدام نموذج MNIST.

الخطوات التالية

  • والآن بعد أن أصبح بإمكانك تصنيف الأرقام، قد تحتاج إلى تدريب نموذجك الخاص على تصنيف الحروف المرسومة، أو تصنيف الحيوانات، أو عدد لا نهائي من العناصر الأخرى. يمكنك العثور على وثائق تدريب نموذج جديد لتصنيف الصور باستخدام MediaPipe Model Maker على صفحة developers.google.com/mediapipe.
  • تعرَّف على مهام MediaPipe الأخرى المتاحة لنظام التشغيل Android، بما في ذلك ميزة "رصد معالم الوجه" و"التعرّف على الإيماءات" و"تصنيف الصوت".

نحن نتطلع إلى كل الأشياء الرائعة التي ستنشئها!