تقسيم الصور إلى شرائح في الوقت الفعلي على 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.git

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

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

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

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

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

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

  1. افتح Android Studio وانقر على فتح (Open).

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

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

Android Starter

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

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

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

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

تشغيل تطبيق المبتدئين

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

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

زر التشغيل

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

Project Dir

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

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

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

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

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

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: لتشغيل النموذج على وحدة المعالجة المركزية (CPU) للجهاز هذا هو الخيار الأكثر توافقًا مع جميع الأجهزة.
    • Accelerator.GPU: لتشغيل النموذج على وحدة معالجة الرسومات (GPU) في الجهاز ويكون ذلك غالبًا أسرع بكثير من وحدة المعالجة المركزية (CPU) بالنسبة إلى النماذج المستندة إلى الصور.
  • مخازن الإدخال والإخراج المؤقتة (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 بما يلي (السطر 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, ...) بما يلي (~السطر 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) هي المكان الذي نتفاعل فيه مباشرةً مع طريقة LiteRT run(). نكتب البيانات التي تمت معالجتها مسبقًا في مخزن الإدخال المؤقت، ونشغّل النموذج، ونقرأ النتائج من مخزن الإخراج المؤقت. استبدِل TODO في هذه الدالة بما يلي (~line 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. التعامل مع اتجاه التراكب: ابحث عن 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. اختبار الميزات: بعد تشغيل التطبيق، من المفترض أن يظهر لك بث مباشر من الكاميرا مع طبقة تقسيم ملونة.
    • تبديل الكاميرات: انقر على رمز قلب العدسة في أعلى الشاشة للتبديل بين الكاميرا الأمامية والكاميرا الخلفية. لاحظ كيف يتم توجيه التراكب بشكل صحيح.
    • تغيير المسرِّع: انقر على الزر "وحدة المعالجة المركزية" أو "وحدة معالجة الرسومات" في أسفل الشاشة لتبديل مسرِّع الأجهزة. لاحظ التغيير في وقت الاستدلال المعروض في أسفل الشاشة. يجب أن تكون وحدة معالجة الرسومات أسرع بكثير.
    • استخدام صورة من "معرض الصور": انقر على علامة التبويب "معرض الصور" في أعلى الشاشة لاختيار صورة من معرض الصور في جهازك. سيُجري التطبيق تقسيمًا على الصورة الثابتة المحدّدة.

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

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

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

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

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

لاستخدام مفوّض NPU، عليك التسجيل في برنامج استخدام المنتج قبل إطلاقه.

12. تهانينا!

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

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

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

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

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