פילוח תמונות מואץ בזמן אמת ב-Android באמצעות LiteRT

1. לפני שמתחילים

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

בשיעור Codelab הזה נלמד איך ליצור אפליקציה ל-Android שמבצעת חלוקת תמונות למקטעים בזמן אמת בפיד המצלמה החי באמצעות זמן הריצה החדש של Google ל-TensorFlow Lite‏, LiteRT. תקבלו אפליקציית Android בסיסית ותוסיפו לה יכולות של פילוח תמונות. בנוסף, נסביר על שלבי העיבוד המקדים, ההסקה והעיבוד שלאחר מכן. תצטרכו:

  • ליצור אפליקציה ל-Android שמפלח תמונות בזמן אמת.
  • שילוב של מודל LiteRT שעבר אימון מראש לחלוקת תמונות למקטעים.
  • מעבדים מראש את תמונת הקלט עבור המודל.
  • משתמשים בסביבת זמן הריצה LiteRT להאצת מעבד (CPU) ו-GPU.
  • הסבר על עיבוד הפלט של המודל כדי להציג את מסכת הפילוח.
  • הסבר על כוונון המצלמה הקדמית.

בסופו של דבר, תיצרו משהו דומה לתמונה הבאה:

אפליקציה שהושלמה

דרישות מוקדמות

ה-Codelab הזה מיועד למפתחים מנוסים של אפליקציות לנייד שרוצים להתנסות בלמידת מכונה. חשוב שתכירו את:

  • פיתוח ל-Android באמצעות Kotlin ו-Android Studio
  • מושגים בסיסיים בעיבוד תמונה

מה תלמדו

  • איך לשלב את זמן הריצה של LiteRT באפליקציה ל-Android ואיך להשתמש בו.
  • הדרכה: איך לבצע חלוקת תמונות למקטעים באמצעות מודל LiteRT שעבר אימון מראש.
  • איך לעבד מראש את תמונת הקלט עבור המודל.
  • איך להריץ היסק למודל.
  • איך מעבדים את הפלט של מודל סגמנטציה כדי להציג את התוצאות באופן חזותי.
  • איך משתמשים ב-CameraX לעיבוד של פיד המצלמה בזמן אמת.

הדרישות

  • גרסה עדכנית של Android Studio (נבדקה בגרסה 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 הוא הספרייה עם כל המשאבים שתצטרכו. ב-Codelab הזה תצטרכו רק את הפרויקט kotlin_cpu_gpu/android_starter. אם נתקעתם, כדאי לעיין בפרויקט המוגמר: kotlin_cpu_gpu/android

הערה לגבי נתיבי קבצים

במדריך הזה, נתיבי הקבצים מצוינים בפורמט Linux/macOS. אם אתם משתמשים ב-Windows, תצטרכו לשנות את הנתיבים בהתאם.

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

ייבוא האפליקציה לתחילת הדרך

נתחיל בייבוא אפליקציית המתחילים אל Android Studio.

  1. פותחים את Android Studio ובוחרים באפשרות Open (פתיחה).

פתיחה של Android Studio

  1. מנווטים לספרייה kotlin_cpu_gpu/android_starter ופותחים אותה.

Android Starter

כדי לוודא שכל התלות זמינה לאפליקציה, צריך לסנכרן את הפרויקט עם קובצי gradle אחרי שתהליך הייבוא מסתיים.

  1. בסרגל הכלים של Android Studio, בוחרים באפשרות סנכרון הפרויקט עם קובצי Gradle.

סנכרון תפריטים

  1. חשוב לא לדלג על השלב הזה – אם הוא לא יעבוד, לא תהיה משמעות לשאר המדריך.

הפעלת אפליקציה לתחילת הדרך

אחרי שייבאתם את הפרויקט ל-Android Studio, אתם יכולים להריץ את האפליקציה בפעם הראשונה.

מחברים את מכשיר Android למחשב באמצעות USB ולוחצים על הפעלה בסרגל הכלים של Android Studio.

כפתור ההרצה

האפליקציה אמורה להיפתח במכשיר. יוצג פיד המצלמה בשידור חי, אבל עדיין לא יתבצע פילוח. כל העריכות שתבצעו בקובץ במדריך הזה יהיו בספרייה litert-samples/compiled_model_api/image_segmentation/kotlin_cpu_gpu/android_starter/app/src/main/java/com/google/ai/edge/examples/image_segmentation (עכשיו אתם יודעים למה Android Studio משנה את המבנה הזה 😃).

Project Dir

תראו גם TODO תגובות בקבצים ImageSegmentationHelper.kt, ‏MainViewModel.kt ו-view/SegmentationOverlay.kt. בשלבים הבאים תטמיעו את הפונקציונליות של חלוקת תמונות למקטעים על ידי מילוי התוכן בTODO.

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 על ידי לחיצה על הלחצן Sync Now (סנכרון עכשיו) שמופיע בפינה השמאלית העליונה של Android Studio.

סנכרן עכשיו

2. הסבר על ממשקי API מרכזיים של LiteRT

פתיחהImageSegmentationHelper.kt

לפני שכותבים את קוד ההטמעה, חשוב להבין את רכיבי הליבה של LiteRT API שבהם תשתמשו. מוודאים שמייבאים מחבילת com.google.ai.edge.litert, ומוסיפים את הייבוא הבא לחלק העליון של ImageSegmentationHelper.kt:

import com.google.ai.edge.litert.Accelerator
import com.google.ai.edge.litert.CompiledModel
  • ‫CompiledModel: זוהי המחלקה המרכזית לאינטראקציה עם מודל TFLite. הוא מייצג מודל שעבר קומפילציה מראש ואופטימיזציה למאיץ חומרה ספציפי (כמו CPU או GPU). הקומפילציה מראש היא תכונה מרכזית של LiteRT שמובילה להסקת מסקנות מהירה ויעילה יותר.
  • ‫CompiledModel.Options: משתמשים במחלקה הזו של Builder כדי להגדיר את CompiledModel. ההגדרה הכי חשובה היא ציון מאיץ החומרה שרוצים להשתמש בו להרצת המודל.
  • ‫Accelerator: באמצעות ה-enum הזה אפשר לבחור את החומרה להסקת מסקנות. פרויקט המתחילים כבר מוגדר לטיפול באפשרויות האלה:
    • ‫Accelerator.CPU: להרצת המודל במעבד של המכשיר. זו האפשרות הכי תואמת לכל המכשירים.
    • ‫Accelerator.GPU: להרצת המודל ב-GPU של המכשיר. לרוב, התהליך הזה מהיר משמעותית יותר מאשר במעבד (CPU) במודלים שמבוססים על תמונות.
  • מאגרי קלט ופלט (TensorBuffer): LiteRT משתמש ב-TensorBuffer עבור נתוני הקלט והפלט של המודל. כך אתם יכולים לשלוט במדויק בזיכרון ולמנוע העתקות מיותרות של נתונים. תקבלו את המאגרים האלה ישירות ממופע CompiledModel באמצעות model.createInputBuffers() ו-model.createOutputBuffers(), ואז תכתבו את נתוני הקלט ותקראו את התוצאות.
  • ‫model.run(): זו הפונקציה שמבצעת את ההסקה. מעבירים אליו את מאגרי הקלט והפלט, ו-LiteRT מטפל במשימה המורכבת של הפעלת המודל במאיץ החומרה שנבחר.

6. סיום ההטמעה הראשונית של ImageSegmentationHelper

עכשיו הגיע הזמן לכתוב קוד. תשלימו את ההטמעה הראשונית של ImageSegmentationHelper.kt. התהליך כולל הגדרה של Segmenter private class כדי להכיל את מודל LiteRT והטמעה של הפונקציה cleanup() כדי לשחרר אותו בצורה תקינה.

  1. משלימים את המחלקה Segmenter ואת הפונקציה cleanup(): בקובץ ImageSegmentationHelper.kt יש תבנית של מחלקה פרטית בשם Segmenter ופונקציה בשם cleanup(). קודם כול, משלימים את Segmenter המחלקה על ידי הגדרת בנאי שיכיל את המודל, יצירת מאפיינים למאגרי הקלט והפלט והוספת שיטת close() לשחרור המודל. לאחר מכן, מטמיעים את הפונקציה cleanup() כדי להפעיל את ה-method החדש 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 שהגדרתם עכשיו. הקוד הזה מגדיר את המודל עם המאיץ שצוין (CPU או GPU) ומכין אותו להסקת מסקנות. מחליפים את 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 שלנו. מחליפים את TODOs בשתי הפונקציות 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: הפונקציה הזו משמשת כ-wrapper שקורא לפונקציית segment private בתוך המחלקה 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, ...) בערך בשורה 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 בפונקציה הזו ב- (~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. ‫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 ולוחצים על Run (הפעלה) בסרגל הכלים של Android Studio.

כפתור ההרצה

  1. בדיקת התכונות: אחרי שהאפליקציה תופעל, אמור להופיע פיד המצלמה בשידור חי עם שכבת-על צבעונית של פילוח.
    • החלפת מצלמות: מקישים על סמל החלפת המצלמה בחלק העליון כדי לעבור בין המצלמה הקדמית למצלמה האחורית. שימו לב איך שכבת העל מתמקמת בצורה נכונה.
    • שינוי המאיץ: מקישים על הלחצן 'CPU' או 'GPU' בתחתית כדי להחליף את מאיץ החומרה. בודקים את השינוי בזמן ההסקה שמוצג בתחתית המסך. ה-GPU צריך להיות מהיר משמעותית.
    • שימוש בתמונה מהגלריה: מקישים על הכרטיסייה 'גלריה' בחלק העליון כדי לבחור תמונה מגלריית התמונות במכשיר. האפליקציה תריץ פילוח על התמונה הסטטית שנבחרה.

ממשק משתמש אחר

עכשיו יש לכם אפליקציה פונקציונלית לחלוטין לחלוקת תמונות למקטעים בזמן אמת, שמבוססת על LiteRT!

11. מתקדם (אופציונלי): שימוש ב-NPU

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

כדי לנסות את הגרסה של NPU, פותחים את הפרויקט kotlin_npu/android ב-Android Studio. הקוד דומה מאוד לגרסת ה-CPU/GPU והוא מוגדר לשימוש בנציג NPU.

כדי לראות דוגמה להטמעה של נציג NPU, אפשר לעיין באפליקציה לדוגמה.

12. מעולה!

יצרתם בהצלחה אפליקציית Android שמבצעת חלוקת תמונות למקטעים בזמן אמת באמצעות LiteRT. למדתם איך:

  • משלבים את זמן הריצה של LiteRT באפליקציה ל-Android.
  • טעינה והרצה של מודל TFLite לחלוקת תמונות למקטעים.
  • מעבדים מראש את הקלט של המודל.
  • מעבדים את הפלט של המודל כדי ליצור מסכת פילוח.
  • אפשר להשתמש ב-CameraX כדי ליצור אפליקציית מצלמה בזמן אמת.

השלבים הבאים

  • אפשר לנסות מודל אחר של חלוקת תמונות למקטעים.
  • מתנסים בנציגי LiteRT שונים (CPU, ‏ GPU, ‏ NPU).

מידע נוסף