تقسيم الصور إلى شرائح في الوقت الفعلي على Android باستخدام LiteRT

1. قبل البدء

كتابة الرموز البرمجية هي طريقة رائعة لتعزيز الذاكرة العضلية وتعميق فهمك للمادة. على الرغم من أنّ عملية النسخ واللصق يمكن أن توفّر الوقت، إلا أنّ الاستثمار في هذه الممارسة يمكن أن يؤدي إلى زيادة الكفاءة وتعزيز مهارات الترميز على المدى الطويل.

في هذا الدرس التطبيقي حول الترميز، ستتعلّم كيفية إنشاء تطبيق Android ينفّذ تقسيم الصور في الوقت الفعلي على خلاصة كاميرا مباشرة باستخدام وقت التشغيل الجديد من Google لـ TensorFlow Lite، وهو LiteRT. ستستخدم تطبيق Android أساسيًا وتضيف إليه إمكانات تقسيم الصور. سنتناول أيضًا خطوات المعالجة المسبقة والاستنتاج والمعالجة اللاحقة. سيكون عليك إجراء ما يلي:

  • إنشاء تطبيق Android يقسّم الصور في الوقت الفعلي
  • دمج نموذج LiteRT مُدرَّب مسبقًا لتقسيم الصور
  • معالجة صورة الإدخال مسبقًا للنموذج
  • استخدِم بيئة تشغيل LiteRT لتسريع وحدة المعالجة المركزية ووحدة معالجة الرسومات.
  • تعرَّف على كيفية معالجة نتائج النموذج لعرض قناع التقسيم الدلالي.
  • التعرّف على كيفية ضبط الكاميرا الأمامية

في النهاية، ستنشئ شيئًا مشابهًا للصورة أدناه:

التطبيق المكتمل

المتطلبات الأساسية

تم تصميم هذا الدرس التطبيقي حول الترميز للمطوّرين المتمرّسين في تطوير تطبيقات الأجهزة الجوّالة الذين يريدون اكتساب خبرة في تعلُّم الآلة. يجب أن تكون على دراية بما يلي:

  • تطوير تطبيقات Android باستخدام Kotlin و"استوديو Android"
  • المفاهيم الأساسية لمعالجة الصور

ما ستتعلمه

  • كيفية دمج واستخدام وقت تشغيل LiteRT في تطبيق Android
  • كيفية تنفيذ تقسيم الصور باستخدام نموذج LiteRT مُدرَّب مسبقًا
  • كيفية معالجة الصورة المدخلة مسبقًا للنموذج
  • كيفية تنفيذ الاستدلال للنموذج
  • كيفية معالجة ناتج نموذج تقسيم الصور لعرض النتائج
  • كيفية استخدام CameraX لمعالجة خلاصة الكاميرا في الوقت الفعلي

المتطلبات

  • إصدار حديث من "استوديو Android" (تم اختباره على الإصدار 2025.1.1)
  • جهاز Android حقيقي من الأفضل اختبارها على أجهزة Galaxy وPixel.
  • التعليمات البرمجية النموذجية (من GitHub)
  • معرفة أساسية بتطوير تطبيقات Android باستخدام لغة Kotlin

2. تقسيم الصور إلى شرائح

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

على سبيل المثال، بدلاً من مجرد معرفة أنّ هناك "شخصًا" في مربّع، يمكنك معرفة وحدات البكسل التي تخصّ هذا الشخص تحديدًا. يوضّح هذا البرنامج التعليمي كيفية إجراء تقسيم الصور في الوقت الفعلي على جهاز Android باستخدام نموذج تعلُّم آلة مدرَّب مسبقًا.

مثال على التقسيم إلى شرائح

‫LiteRT: تعزيز إمكانات تعلُّم الآلة على الجهاز فقط

LiteRT هي إحدى التقنيات الرئيسية التي تتيح تقسيم الصور بدقة عالية وفي الوقت الفعلي على الأجهزة الجوّالة. ‫LiteRT هو جيل جديد من وقت التشغيل العالي الأداء من Google في TensorFlow Lite، وهو مصمّم لتحقيق أفضل أداء ممكن من الأجهزة الأساسية.

ويتم تحقيق ذلك من خلال الاستخدام الذكي والمحسّن لمسرّعات الأجهزة، مثل وحدة معالجة الرسومات (GPU) ووحدة المعالجة العصبية (NPU). من خلال نقل عبء العمل الحسابي المكثّف لنموذج التقسيم من وحدة المعالجة المركزية للأغراض العامة إلى هذه المعالِجات المتخصّصة، يقلّل LiteRT بشكل كبير من وقت الاستدلال. هذا التسارع هو ما يتيح تشغيل النماذج المعقّدة بسلاسة على خلاصة الكاميرا المباشرة، ما يوسّع حدود ما يمكننا تحقيقه باستخدام تعلُّم الآلة مباشرةً على هاتفك. وبدون هذا المستوى من الأداء، سيكون تقسيم الجمهور في الوقت الفعلي بطيئًا جدًا ومتقطعًا، ما يؤدي إلى تجربة مستخدم سيئة.

3- طريقة الإعداد

إنشاء نسخة طبق الأصل من المستودع

أولاً، أنشئ نسخة طبق الأصل من مستودع LiteRT:

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

litert-samples/compiled_model_api/image_segmentation هو الدليل الذي يتضمّن جميع الموارد التي ستحتاج إليها. في هذا الدرس التطبيقي، لن تحتاج إلا إلى مشروع kotlin_cpu_gpu/android_starter. يمكنك مراجعة المشروع المكتمل إذا واجهتك مشكلة: kotlin_cpu_gpu/android

ملاحظة حول مسارات الملفات

يحدّد هذا الدليل التوجيهي مسارات الملفات بتنسيق Linux/macOS. إذا كنت تستخدم نظام التشغيل Windows، عليك تعديل المسارات وفقًا لذلك.

من المهم أيضًا ملاحظة الفرق بين طريقة عرض المشروع في "استوديو Android" وطريقة عرض نظام الملفات العادية. طريقة عرض المشروع في "استوديو Android" هي تمثيل منظَّم لملفات مشروعك، ويتم ترتيبها لتطوير تطبيقات Android. تشير مسارات الملفات في هذا البرنامج التعليمي إلى مسارات نظام الملفات، وليس إلى المسارات في طريقة عرض المشروع في "استوديو Android".

استيراد تطبيق البداية

لنبدأ باستيراد تطبيق أوّلي إلى استوديو Android.

  1. افتح "استوديو Android" وانقر على فتح (Open).

فتح "استوديو Android"

  1. انتقِل إلى الدليل kotlin_cpu_gpu/android_starter وافتحه.

Android Starter

للتأكّد من توفّر جميع التبعيات لتطبيقك، عليك مزامنة مشروعك مع ملفات Gradle بعد انتهاء عملية الاستيراد.

  1. انقر على مزامنة المشروع مع ملفات Gradle من شريط أدوات "استوديو Android".

مزامنة القائمة

  1. يُرجى عدم تخطّي هذه الخطوة، لأنّ عدم إتمامها سيؤدي إلى عدم فهم بقية البرنامج التعليمي.

تشغيل تطبيق البداية

بعد استيراد المشروع إلى "استوديو Android"، يمكنك الآن تشغيل التطبيق للمرة الأولى.

اربط جهاز Android بجهاز الكمبيوتر باستخدام كابل USB وانقر على تشغيل في شريط أدوات استوديو Android.

زر التشغيل

يجب أن يتم تشغيل التطبيق على جهازك. ستظهر لك خلاصة كاميرا مباشرة، ولكن لن يتم تقسيم الجسم بعد. ستكون جميع تعديلات الملفات التي ستجريها في هذا البرنامج التعليمي ضمن الدليل litert-samples/compiled_model_api/image_segmentation/kotlin_cpu_gpu/android_starter/app/src/main/java/com/google/ai/edge/examples/image_segmentation (أنت تعرف الآن سبب إعادة "استوديو Android" هيكلة هذا الدليل 😃).

Project Dir

ستظهر لك أيضًا تعليقات TODO في ملفات ImageSegmentationHelper.kt وMainViewModel.kt وview/SegmentationOverlay.kt. في الخطوات التالية، ستنفّذ وظيفة تقسيم الصور عن طريق ملء TODOs هذه.

4. التعرّف على التطبيق التجريبي

يحتوي التطبيق المبدئي على واجهة مستخدم أساسية ومنطق معالجة الكاميرا. في ما يلي نظرة عامة سريعة على الملفات الرئيسية:

  • ‫app/src/main/java/com/google/ai/edge/examples/image_segmentation/MainActivity.kt: هذه هي نقطة الدخول الرئيسية للتطبيق. ويتم إعداد واجهة المستخدم باستخدام Jetpack Compose والتعامل مع أذونات الكاميرا.
  • ‫app/src/main/java/com/google/ai/edge/examples/image_segmentation/MainViewModel.kt: يدير هذا ViewModel حالة واجهة المستخدم وينسّق عملية تقسيم الصور.
  • 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 وإضافة التبعيات

لنبدأ الآن بإضافة وظيفة تقسيم الصور إلى التطبيق الأوّلي.

1. إضافة اعتمادية LiteRT

أولاً، يجب إضافة مكتبة LiteRT إلى مشروعك. هذه هي الخطوة الأولى الحاسمة لتفعيل ميزة تعلُّم الآلة على الجهاز فقط باستخدام وقت التشغيل المحسّن من Google.

افتح ملف app/build.gradle.kts وأضِف السطر التالي إلى الحزمة dependencies:

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

بعد إضافة الاعتمادية، زامِن مشروعك مع ملفات Gradle من خلال النقر على الزر مزامنة الآن الذي يظهر في أعلى يسار "استوديو Android".

المزامنة الآن

2. التعرّف على واجهات برمجة التطبيقات الرئيسية في LiteRT

فتح ImageSegmentationHelper.kt

قبل كتابة رمز التنفيذ، من المهم فهم المكوّنات الأساسية لواجهة برمجة التطبيقات LiteRT التي ستستخدمها. تأكَّد من أنّك تستورد من حزمة com.google.ai.edge.litert، وأضِف عمليات الاستيراد التالية إلى أعلى ImageSegmentationHelper.kt:

import com.google.ai.edge.litert.Accelerator
import com.google.ai.edge.litert.CompiledModel
  • ‫CompiledModel: هذه هي الفئة المركزية للتفاعل مع نموذج TFLite. يمثّل هذا النوع نموذجًا تم تجميعه مسبقًا وتحسينه لمسرِّع أجهزة معيّن (مثل وحدة المعالجة المركزية أو وحدة معالجة الرسومات). وتُعدّ عملية التجميع المُسبَق هذه ميزة رئيسية في LiteRT تؤدي إلى استنتاج أسرع وأكثر كفاءة.
  • ‫CompiledModel.Options: يمكنك استخدام فئة أداة الإنشاء هذه لإعداد CompiledModel. أهم إعداد هو تحديد أداة تسريع الأجهزة التي تريد استخدامها لتشغيل النموذج.
  • ‫Accelerator: يتيح لك هذا النوع من التعداد اختيار الجهاز للاستدلال. تمّ إعداد المشروع المبدئي مسبقًا للتعامل مع هذه الخيارات:
    • ‫Accelerator.CPU: لتشغيل النموذج على وحدة المعالجة المركزية للجهاز هذا هو الخيار الأكثر توافقًا مع جميع الأجهزة.
    • ‫Accelerator.GPU: لتشغيل النموذج على وحدة معالجة الرسومات (GPU) في الجهاز غالبًا ما تكون هذه العملية أسرع بكثير من وحدة المعالجة المركزية بالنسبة إلى النماذج المستندة إلى الصور.
  • مخازن الإدخال والإخراج المؤقتة (TensorBuffer): تستخدم LiteRT TensorBuffer لإدخال البيانات إلى النموذج وإخراجها منه. يمنحك ذلك تحكّمًا دقيقًا في الذاكرة ويتجنّب نسخ البيانات غير الضرورية. ستحصل على هذه المخازن المؤقتة مباشرةً من مثيل CompiledModel باستخدام model.createInputBuffers() وmodel.createOutputBuffers()، ثم ستكتب بيانات الإدخال فيها وتقرأ النتائج منها.
  • ‫model.run(): هذه هي الدالة التي تنفّذ الاستدلال. يمكنك تمرير مخازن الإدخال والإخراج المؤقتة إليه، ويتولّى LiteRT المهمة المعقّدة المتمثّلة في تشغيل النموذج على مسرِّع الأجهزة المحدّد.

6. إكمال عملية التنفيذ الأولية لـ ImageSegmentationHelper

حان الوقت الآن لكتابة بعض التعليمات البرمجية. ستكمل عملية التنفيذ الأوّلية لـ ImageSegmentationHelper.kt. يتضمّن ذلك إعداد الفئة الخاصة Segmenter لاحتواء نموذج LiteRT وتنفيذ الدالة cleanup() لإصداره بشكل صحيح.

  1. أكمِل الفئة Segmenter والدالة cleanup() : في ملف ImageSegmentationHelper.kt، ستجد هيكلاً لفئة خاصة باسم Segmenter ودالة باسم cleanup(). أولاً، أكمل فئة Segmenter من خلال تحديد الدالة الإنشائية الخاصة بها لاحتواء النموذج، وإنشاء خصائص لمخازن الإدخال/الإخراج المؤقتة، وإضافة طريقة 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 التي تم تحديدها الآن. يُعدّ هذا الرمز النموذج باستخدام المسرّع المحدّد (وحدة المعالجة المركزية أو وحدة معالجة الرسومات) ويجهّزه للاستدلال. استبدِل 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 بما يلي (السطر 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: تعمل هذه الدالة كبرنامج تضمين يستدعي الدالة الخاصة segment ضمن الفئة Segmenter. استبدِل TODO بما يلي (~line 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, ...) بما يلي (~line 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) هي المكان الذي نتفاعل فيه مباشرةً مع طريقة run() في LiteRT. نكتب البيانات التي تمت معالجتها مسبقًا في مخزن الإدخال المؤقت، ونشغّل النموذج، ونقرأ النتائج من مخزن الإخراج المؤقت. استبدِل 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 بما يلي (~line 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 ملون، ثم تقدّمه إلى واجهة المستخدم. استبدِل TODO في السمة segmentationUiShareFlow بـ (~line 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. Handle Overlay Orientation: ابحث عن TODO في ملف SegmentationOverlay.kt واستبدِله بالرمز التالي. يتحقّق هذا الرمز مما إذا كانت الكاميرا الأمامية نشطة، وإذا كان الأمر كذلك، يتم تطبيق قلب أفقي على التراكب Bitmap قبل رسمه على Canvas. (~line 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".

زر التشغيل

  1. اختبار الميزات: بعد تشغيل التطبيق، من المفترض أن تظهر لك خلاصة الكاميرا المباشرة مع تراكب تقسيم ملون.
    • تبديل الكاميرات: انقر على رمز قلب العدسة في أعلى الشاشة للتبديل بين الكاميرا الأمامية والخلفية. لاحظ كيف يتم توجيه التراكب بشكل صحيح.
    • تغيير المسرِّع: انقر على الزر "وحدة المعالجة المركزية" (CPU) أو "وحدة معالجة الرسومات" (GPU) في أسفل الشاشة لتبديل مسرِّع الأجهزة. لاحظ التغيير في وقت الاستنتاج المعروض في أسفل الشاشة. يجب أن تكون وحدة معالجة الرسومات أسرع بكثير.
    • استخدام صورة من معرض الصور: انقر على علامة التبويب "معرض الصور" في أعلى الشاشة لاختيار صورة من معرض الصور في جهازك. سيُجري التطبيق تقسيمًا على الصورة الثابتة المحدّدة.

واجهة مستخدم أخرى

أصبح لديك الآن تطبيق كامل الميزات لتصنيف الصور في الوقت الفعلي يستند إلى LiteRT.

11. الإعدادات المتقدّمة (اختيارية): استخدام وحدة المعالجة العصبية

يحتوي هذا المستودع أيضًا على إصدار من التطبيق تم تحسينه لوحدات المعالجة العصبية (NPU). يمكن أن يوفّر إصدار NPU تحسينًا كبيرًا في الأداء على الأجهزة التي تتضمّن وحدة معالجة عصبية متوافقة.

لتجربة إصدار وحدة المعالجة العصبية، افتح مشروع kotlin_npu/android في "استوديو Android". الرمز البرمجي مشابه جدًا لرمز وحدة المعالجة المركزية/وحدة معالجة الرسومات، وتم إعداده لاستخدام مفوّض وحدة المعالجة العصبية.

يمكنك الاطّلاع على تطبيقنا النموذجي للاطّلاع على مثال على تنفيذ تفويض NPU.

12. تهانينا!

لقد أنشأت بنجاح تطبيق Android يتيح تقسيم الصور إلى شرائح في الوقت الفعلي باستخدام LiteRT. لقد تعلّمت كيفية:

  • ادمج وقت تشغيل LiteRT في تطبيق Android.
  • تحميل نموذج تقسيم الصور في TFLite وتشغيله
  • معالجة إدخال النموذج مسبقًا
  • معالجة ناتج النموذج لإنشاء قناع تقسيم دلالي
  • استخدِم CameraX لإنشاء تطبيق كاميرا في الوقت الفعلي.

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

  • جرِّب نموذجًا مختلفًا لتقسيم الصور.
  • جرِّب مفوّضين مختلفين في LiteRT (وحدة المعالجة المركزية ووحدة معالجة الرسومات ووحدة المعالجة العصبية).

مزيد من المعلومات