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

1. مقدمة

ما هو MediaPipe؟

تتيح لك حلول MediaPipe تطبيق حلول تعلُّم الآلة على تطبيقاتك. ويقدّم إطار عمل لضبط مسارات معالجة مُعدّة مسبقًا توفّر للمستخدمين نتائج فورية وتفاعلية ومفيدة. يمكنك أيضًا تخصيص هذه الحلول باستخدام أداة إنشاء النماذج في MediaPipe لتعديل النماذج التلقائية.

يُعدّ تصنيف الصور إحدى مهام الرؤية بالاستناد إلى الذكاء الاصطناعي (ML) المتعددة التي تقدّمها حلول MediaPipe. تتوفّر أداة MediaPipe Tasks لنظامَي التشغيل Android وiOS وPython (بما في ذلك Raspberry Pi) والويب.

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

المُعطيات

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

المتطلبات

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

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

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

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

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

  1. افتح "استوديو Android".
  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"، عليك استيراد المكتبة.

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

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. انتقِل إلى السطر الذي يشير إلى // STEP 3 define classifier، وأضِف السطر التالي لإنشاء عنصر نائب لفئة ImageClassifier التي سيتم استخدامها لهذا التطبيق:

// الخطوة 3: تحديد المصنِّف

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 و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 قبل استخدامه، يمكنك إضافة كتلة بدء لطلب 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)
    }
}

هذا كل ما في الأمر بخصوص ملف المساعدة. في القسم التالي، عليك ملء الخطوات الأخيرة لبدء تصنيف الأرقام التي تمّ رسمها.

4. تنفيذ الاستنتاج باستخدام مهام MediaPipe

يمكنك بدء هذا القسم من خلال فتح فئة DigitCanvasFragment في Android Studio، وهو المكان الذي سيتم فيه تنفيذ جميع الإجراءات.

  1. في أسفل هذا الملف، من المفترض أن يظهر لك تعليق نصه // STEP 6 Set up listener. ستضيف هنا الدالتَين 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. في أعلى الصف، من المفترض أن يظهر لك تعليق نصه // STEP 7a Initialize classifier. هذا هو المكان الذي ستضع فيه بيان DigitClassifierHelper.
// STEP 7a Initialize classifier.
private lateinit var digitClassifierHelper: DigitClassifierHelper
  1. الانتقال إلى // الخطوة 7(ب) إعداد المصنِّف، يمكنك إعداد 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()‎. سيؤدي ذلك إلى استخراج صورة نقطية من اللوحة، ثم تمريرها إلى 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- تهانينا!

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

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

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

نتطلّع إلى مشاهدة فيديوهاتك الرائعة.