חלוקת תמונות למקטעים במכשיר ב-C++ באמצעות LiteRT

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

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

בשיעור Codelab הזה נסביר איך ליצור קובץ בינארי של חלוקת תמונות למקטעים ב-C++‎ שפועל ישירות במכשיר Android באמצעות זמן הריצה במכשיר של Google,‏ LiteRT. במקום להשתמש ב-Kotlin או ב-Android Studio, בשיעור Codelab הזה נתמקד בבניית קובץ בינארי של C++. תבצעו קומפילציה צולבת באמצעות CMake או Bazel ותפרסו אותו באמצעות ADB. אותו LiteRT C++ API פועל בכל פלטפורמה (Android,‏ Linux, מוטמעת), ולכן הוא מהווה בסיס שימושי לאפליקציות קריטיות לביצועים, לרובוטיקה ולמערכות קצה.

תעברו על כל תהליך הצינור:

  • הגדרת סביבת build (CMake ‏+ Android NDK או Bazel).
  • קישור LiteRT C++ SDK – מגרסה מוכנה מראש או ממקור.
  • שימוש בOpenGL ES compute shaders לעיבוד מראש ולעיבוד סופי של תמונות באמצעות מעבד גרפי.
  • הפעלת מודל הפילוח selfie_multiclass באמצעות LiteRT C++ API.
  • האצת ההסקה ב-CPU, ב-GPU (OpenCL) וב-NPU (Qualcomm / MediaTek).
  • עיבוד שלאחר יצירת הפלט הגולמי של המודל לתמונה של פילוח עם מיזוג צבעים.
  • פריסה למכשיר Android פיזי באמצעות ADB ואחזור התוצאה.

בסופו של דבר, תקבלו משהו דומה לתמונה הבאה – תמונה סטטית שעברה עיבוד דרך כל הצינור, כאשר כל אחת מ-6 קלאסיפיקציות הפילוח מונחתת בצבע שונה:

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

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

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

  • יסודות C++‎ (מצביעים, וקטורים, כולל).
  • מושגים בסיסיים ב-Android/ADB (adb push, adb shell).
  • שימוש בטרמינל ובסקריפטים של מעטפת ב-Linux או ב-macOS.

מה תלמדו

  • איך לבצע קומפילציה צולבת של קובץ בינארי של C++‎ ל-Android arm64-v8a באמצעות CMake + NDK או Bazel.
  • איך משתמשים ב-LiteRT C++ API‏ (Environment, ‏ CompiledModel, ‏ TensorBuffer) להסקת מסקנות יעילה במכשיר.
  • איך שיידרים של מחשוב OpenGL ES 3.1 מאיצים את העיבוד המקדים והעיבוד שלאחר מכן, באופן מלא ב-GPU.
  • איך מגדירים את LiteRT להאצת CPU,‏ GPU‏ (OpenCL) ו-NPU‏ (Qualcomm HTP,‏ MediaTek APU,‏ Google Tensor).
  • ההבדל בין הסקה סינכרונית (Run) לבין הסקה אסינכרונית (RunAsync).
  • איך פורסים ומריצים קובץ בינארי של C++‎ ב-Android באמצעות ADB.

הדרישות

  • מחשב Linux או macOS (משתמשי Windows צריכים להשתמש ב-WSL2).
  • ‫Android NDK גרסה r25c ואילך (להורדה).
  • בקטע CMake path (נתיב CMake):‏ CMake ≥ 3.22 ‏(sudo apt-get install cmake).
  • בקטע Bazel path: Bazel מותקן, בנוסף למאגר הדוגמאות המלא של LiteRT.
  • ‫ADB ב-PATH (Android Platform Tools).
  • מכשיר Android פיזי – מומלץ לבדוק במכשירי Galaxy S24/S25 או Pixel.

2. חלוקת תמונות למקטעים

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

בשיעור Codelab הזה נשתמש במודל selfie_multiclass_256x256, שמסווג כל פיקסל לאחת מ-6 קטגוריות:

אינדקס הכיתה

Segment

0

רקע

1

שיער

2

עור הגוף

3

עור הפנים

4

בגדים

5

אביזרים (משקפיים, תכשיטים וכו')

הפלט של המודל הוא טנסור של מספרים ממשיים בצורה [1, 256, 256, 6]. לכל אחד מהפיקסלים בגודל 256x256 יש 6 ציוני מהימנות – אחד לכל סיווג. הסיווג עם הניקוד הכי גבוה מנצח את הפיקסל הזה (argmax).

‫LiteRT: ביצועים ב-Edge

‫LiteRT הוא זמן הריצה של הדור הבא של Google, עם ביצועים גבוהים למודלים של TFLite. ממשק ה-API שלו ב-C++‎ מאפשר גישה ישירה עם תקורה נמוכה למאיצי חומרה עם ממשק עקבי בכל שלושת המקרים:

  • ‫CPU – תואם לכל המכשירים; הסקת מסקנות של ‎~128 ms במכשיר בינוני.
  • ‫GPU (OpenCL) – הסקת מסקנות של כ-1 אלפית השנייה; כ-17 עד 43 אלפיות השנייה מקצה לקצה, בהתאם לאסטרטגיית המאגר.
  • NPU – ‏9 עד 28 אלפיות השנייה מקצה לקצה במכשירי Qualcomm Snapdragon,‏ MediaTek Dimensity 9400 ו-Google Tensor, בהתאם ל-AOT לעומת. הידור JIT.

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

3. להגדרה

שכפול המאגר

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

כל המשאבים של ה-Codelab הזה נמצאים ב:

litert-samples/compiled_model_api/image_segmentation/c++_segmentation/

בספרייה הזו יש שני תת-פרויקטים, שכל אחד מהם הוא בנייה מלאה של אותו מדגם:

ספרייה

מערכת build

תלות ב-LiteRT

use_prebuilt_litert/

‫CMake + Android NDK

litert_cc_sdk.zip + libLiteRt.so

build_from_source/

Bazel

הידור של LiteRT מהמקור

בוחרים באחת מהאפשרויות ופועלים לפי ההוראות. הקוד זהה בשתי הספריות – רק מערכת ה-build ואסטרטגיית התלות שונות. אם רוצים לבצע את ההגדרה הכי מהר, בוחרים באפשרות use_prebuilt_litert/. אם אתם צריכים לשנות את LiteRT עצמו או לעבוד בתוך מאגר Bazel monorepo קיים, אתם יכולים להשתמש ב-build_from_source/.

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

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

סקירה כללית של הספרייה

לשני תת-הפרויקטים יש את אותו פריסת מקור:

<variant>/
├── main_cpu.cc              # CPU inference entry point
├── main_gpu.cc              # GPU (OpenCL) inference entry point
├── main_npu.cc              # NPU (Qualcomm / MediaTek) entry point
├── image_processor.h/.cc    # OpenGL ES preprocessing and postprocessing
├── image_utils.h/.cc        # STB-based image load / save utilities
├── timing_utils.h/.cc       # Profiling helpers
├── shaders/                 # GLSL ES 3.1 compute shaders
│   ├── preprocess_compute.glsl
│   ├── resize_compute.glsl
│   ├── mask_blend_compute.glsl
│   ├── deinterleave_masks.glsl
│   └── passthrough_shader.vert
├── models/
│   ├── selfie_multiclass_256x256.tflite        (CPU / GPU / NPU JIT)
│   ├── selfie_multiclass_256x256_SM8650.tflite (Qualcomm S24 AOT)
│   └── selfie_multiclass_256x256_SM8750.tflite (Qualcomm S25 AOT)
└── test_images/
    └── image.jpeg

בנוסף:

  • ‫use_prebuilt_litert/ הוסיף/ה את CMakeLists.txt,‏ build_prebuilt.sh,‏ deploy_and_run_on_android.sh ו-third_party/stb/.
  • ‫build_from_source/ מוסיף קובץ BUILD של Bazel ומשתמש ב-deploy_and_run_on_android.sh שמצביע על bazel-bin/.

טרמינל שמציג את עץ הספרייה use_prebuilt_litert

4. הסבר על מבנה הפרויקט

שלוש נקודות כניסה, צינור אחד

כל אחד מהקבצים main_cpu.cc, main_gpu.cc ו-main_npu.cc מכיל פונקציה main() שמפעילה את כל תהליך הפילוח. הצינור זהה בכל שלושת המקרים, רק ההגדרה של מאיץ LiteRT ואסטרטגיית המאגר שונות:

קובץ

Accelerator

אסטרטגיית מאגר

main_cpu.cc

kCpu

זיכרון CPU

main_gpu.cc

kGpu | kCpu

זיכרון מעבד עם קצה עורפי של OpenCL

main_npu.cc

kNpu | kCpu

זיכרון המעבד עם מעבר חזרה למעבד

לשלושתם יש את אותם כלי עזר של ImageProcessor (OpenGL ES compute shaders לעיבוד מקדים ועיבוד סופי) ושל ImageUtils (STB image I/O).

הפייפליין המלא

לכל נקודת כניסה יש את אותו מבנה של חמישה שלבים:

Load → GPU upload → Preprocess (shader) → Inference (LiteRT) → Postprocess (shader) → Save
  1. ‫Load – ImageUtils::LoadImage() מפענח את ה-JPEG לזיכרון המעבד באמצעות ספריית התמונות STB.
  2. העלאה – processor.CreateOpenGLTexture() מעלה את הפיקסלים הגולמיים למרקם GPU (OpenGL RGBA8).
  3. ‫Preprocess – processor.PreprocessInputForSegmentation() מפעיל הצללה לחישוב GLSL שמשנה את גודל הטקסטורה ל-256x256 ומנרמל את ערכי הפיקסלים מ-[0, 1] ל-[-1, 1]. התוצאה נשמרת ב-SSBO של ה-GPU.
  4. ‫Infer – נתוני SSBO נכתבים ל-LiteRT TensorBuffer ו-compiled_model.Run() (או RunAsync()) מפעיל את המודל.
  5. עיבוד שלאחר יצירת התמונה – הפלט של המודל, שהוא מספר צף של 6 ערוצים, מפוצל ל-6 מאגרי SSBO של מסכות בעלות ערוץ יחיד, ואז מתבצע מיזוג צבעים בחזרה לתמונה המקורית.
  6. שמירה – ImageUtils::SaveImage() כותב את תמונת ה-RGBA הסופית כקובץ PNG.

5. ממשקי ה-API העיקריים של LiteRT C++

לפני שמתחילים לבנות, כדאי להכיר את שלושת הסוגים העיקריים של LiteRT C++‎ שמשמשים בכל נקודות הכניסה. כל הפריטים נמצאים במרחב השמות litert::.

litert::Environment

‫Environment הוא ההקשר הבסיסי לכל הפעולות של LiteRT. יוצרים אותו פעם אחת ומעבירים אותו אל CompiledModel::Create. כדי להשתמש ב-NPU, צריך להגדיר אותו באמצעות ספריית הפלאגין של הספק.

// For CPU or GPU - no extra options needed
LITERT_ASSIGN_OR_ABORT(auto env, litert::Environment::Create({}));

// For NPU: point at the vendor dispatch library directory on the device
std::vector<litert::Environment::Option> opts;
opts.push_back({litert::Environment::OptionTag::DispatchLibraryDir,
                "/data/local/tmp/cpp_segmentation_android/npu/"});
LITERT_ASSIGN_OR_ABORT(auto env,
    litert::Environment::Create(std::move(opts)));

litert::CompiledModel

‫CompiledModel טוען ומבצע קומפילציה מראש של מודל TFLite עבור החומרה המבוקשת בזמן הבנייה. ההסקה מצטמצמת למילוי מאגרי נתונים זמניים ולקריאה ל-Run().

// CPU
LITERT_ASSIGN_OR_ABORT(auto model,
    litert::CompiledModel::Create(env, model_path,
                                  litert::HwAccelerators::kCpu));

// GPU (pass an Options object with GpuOptions configured)
LITERT_ASSIGN_OR_ABORT(auto model,
    litert::CompiledModel::Create(env, model_path, gpu_options));

// NPU (pass an Options object with kNpu | kCpu and vendor options)
LITERT_ASSIGN_OR_ABORT(auto model,
    litert::CompiledModel::Create(env, model_path, npu_options));

litert::TensorBuffer

מאגרי טנסורים מכילים נתוני קלט ופלט. תמיד יוצרים אותם מ-CompiledModel כדי שהגודל והמיקום שלהם יתאימו לחומרה של המכשיר.

LITERT_ASSIGN_OR_ABORT(auto input_buffers,
                       compiled_model.CreateInputBuffers());
LITERT_ASSIGN_OR_ABORT(auto output_buffers,
                       compiled_model.CreateOutputBuffers());

// Write preprocessed float data, run, read results
LITERT_ABORT_IF_ERROR(
    input_buffers[0].Write(absl::MakeConstSpan(preprocessed_data)));
LITERT_ABORT_IF_ERROR(
    compiled_model.Run(input_buffers, output_buffers));
LITERT_ABORT_IF_ERROR(
    output_buffers[0].Read(absl::MakeSpan(output_data)));

פקודות מאקרו לטיפול בשגיאות

Macro

התנהגות

LITERT_ASSIGN_OR_ABORT(var, expr)

הפונקציה מקצה או קוראת ל-abort() אם הפעולה נכשלת

LITERT_ABORT_IF_ERROR(expr)

הפונקציה abort() מופעלת אם הביטוי מחזיר שגיאה

LITERT_ASSIGN_OR_RETURN(var, expr)

הקצאה או הפצה של שגיאה למתקשר

6. Build — Option A: Prebuilt LiteRT C++ SDK (CMake)

זהו המסלול המומלץ אם לא צריך לשנות את LiteRT עצמו. סקריפט הבנייה מטפל בהורדה של כותרות ה-SDK, בהעתקה של .so, באחזור של STB ובהפעלת CMake + NDK בפקודה אחת.

שלב 1 – מקבלים את libLiteRt.so מ-Maven

זמן הריצה של LiteRT נשלח כספרייה משותפת בתוך AAR של Android ב-Google Maven. מורידים את הקובץ ומחלצים את arm64-v8a .so:

# Download the AAR
wget -O litert.aar \
    "https://dl.google.com/dl/android/maven2/com/google/ai/edge/litert/litert/2.1.3/litert-2.1.3.aar"

# Extract the runtime library
unzip litert.aar "jni/arm64-v8a/libLiteRt.so" -d extracted/

כדי לתמוך ב-GPU, צריך גם לחלץ את מאיץ OpenCL/GL:

unzip litert.aar "jni/arm64-v8a/libLiteRtClGlAccelerator.so" -d extracted/

חלון Terminal שבו מוצגת הפקודה wget להורדת קובץ ה-AAR של LiteRT והפקודה unzip לחילוץ libLiteRt.so

שלב 2 – הפעלת build_prebuilt.sh

cd litert-samples/compiled_model_api/image_segmentation/c++_segmentation/use_prebuilt_litert/

bash build_prebuilt.sh \
    --litert_version=2.1.3 \
    --ndk_path=/path/to/android-ndk \
    --litert_so=extracted/jni/arm64-v8a/libLiteRt.so

הסקריפט:

  1. הורדה של litert_cc_sdk.zip (כותרות SDK + קובצי cmake) מגרסת ה-LiteRT ב-GitHub – אם הקבצים כבר קיימים, השלב הזה ידלג על ההורדה בהרצות הבאות.
  2. מעתיקים את libLiteRt.so אל litert_cc_sdk/.
  3. הורדה של כותרות תמונות של STB אל third_party/stb/ – אם הן קיימות, המערכת מדלגת על השלב הזה.
  4. מגדירים ויוצרים באמצעות CMake באמצעות Android NDK toolchain ל-arm64-v8a ב-android-26.

אם הפעולה תצליח, יופיעו שלושה קבצים בינאריים ב-build/:

build/cpp_segmentation_cpu
build/cpp_segmentation_gpu
build/cpp_segmentation_npu

מסוף שמציג את הפלט של build_prebuilt.sh, שמושלם עם שלושה קבצים בינאריים שמופיעים ב-build/

מה CMakeLists.txt עושה

פתיחת CMakeLists.txt. הוא דורש C++20, שולף את LiteRT SDK דרך add_subdirectory, מקשר OpenGL ES 3 (GLESv3) ו-EGL, ואז משתמש במאקרו של כלי עזר כדי ליצור כל קובץ בינארי ממקור main_*.cc שלו:

macro(add_segmentation_target target_name main_source)
  add_executable(${target_name} ${main_source})
  target_link_libraries(${target_name}
    PRIVATE
      image_processor image_utils timing_utils litert_cc_api
      absl::log absl::check EGL GLESv3 android log
  )
endmacro()

add_segmentation_target(cpp_segmentation_cpu main_cpu.cc)
add_segmentation_target(cpp_segmentation_gpu main_gpu.cc)
add_segmentation_target(cpp_segmentation_npu main_npu.cc)

7. בנייה – אפשרות ב': בנייה באמצעות Bazel (ממקור)

כדאי לבחור באפשרות הזו אם אתם מעדיפים את Bazel כמערכת build שלכם, שמקמפלת את זמן הריצה של LiteRT מהמקור, או אם אתם צריכים לעבוד בסביבת עבודה קיימת של Bazel.

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

בנוסף ל-NDK ול-ADB שמופיעים בקטע 'לפני שמתחילים', תצטרכו:

  • ‫Bazel מותקן ב-PATH.
  • שיבוט מלא של מאגר המקור של הדוגמאות של LiteRT.

שלב 1 – הגדרת סביבת העבודה של דוגמאות LiteRT

כל הפקודות מופעלות מהשורש של מאגר הדוגמאות של LiteRT

cd /path/to/litert-samples
./configure

כשמופיעה בקשה:

  • מאשרים את ברירות המחדל עבור Python ונתיב הספרייה של Python.
  • תשובה N לתמיכה ב-ROCm וב-CUDA.
  • בוחרים באפשרות clang (נבדק עם גרסה 18.1.3) כקומפיילר.
  • מאשרים את דגלי האופטימיזציה שמוגדרים כברירת מחדל.
  • משיבים Y כדי להגדיר את WORKSPACE לגרסאות build של Android.
  • מגדירים את הרמה המינימלית של Android NDK ל-26 לפחות.
  • מזינים את הנתיב אל Android SDK.
  • מגדירים את רמת ה-API של Android SDK לערך ברירת המחדל (36) ואת כלי הבנייה ל-36.0.0.

מסוף שבו מוצגים הנחיות ותשובות של ‎ ./configure עבור סביבת העבודה של דוגמאות LiteRT

שלב 2 – בניית יעדי המעבד (CPU) והמעבד הגרפי (GPU)

# CPU
bazel build \
  //compiled_model_api/image_segmentation/c++_segmentation/build_from_source:cpp_segmentation_cpu \
  --config=android_arm64

# GPU
bazel build \
  //compiled_model_api/image_segmentation/c++_segmentation/build_from_source:cpp_segmentation_gpu \
  --config=android_arm64

שלב 3 – בניית היעד של ה-NPU

Qualcomm HTP

  1. מורידים את QAIRT SDK גרסה 2.41 ואילך ומחלצים אותו.
  2. מוודאים שהתוכן שחולץ מ-SDK נמצא בתוך ספריית משנה בשם latest/:
    /path/to/qairt_sdk/
      └── latest/
          ├── include/
          ├── lib/
          └── ...
    
  3. ‫Build, העברת נתיב האב שמסתיים ב-/:
    bazel build \
      //compiled_model_api/image_segmentation/c++_segmentation/build_from_source:cpp_segmentation_npu \
      --config=android_arm64 \
      --nocheck_visibility \
      --action_env LITERT_QAIRT_SDK=/path/to/qairt_sdk/
    

הדגל --nocheck_visibility נדרש כי לחלק מיעדי LiteRT במעלה הזרם יש הגדרות ברירת מחדל של חשיפה מוגבלת.

MediaTek APU

אין צורך ב-SDK נוסף. זמן הריצה של NeuroPilot הוא ספריית מערכת במכשירי Dimensity 9400.

bazel build \
  //compiled_model_api/image_segmentation/c++_segmentation/build_from_source:cpp_segmentation_npu_mtk \
  --config=android_arm64 \
  --nocheck_visibility

חלון Terminal שבו מוצגת השלמת בניית bazel עבור cpp_segmentation_cpu ו-cpp_segmentation_gpu

הקובץ BUILD

פתיחת build_from_source/BUILD. הוא מגדיר ארבעה cc_binary יעדים – אחד לכל מאיץ ועוד יעד ייעודי של MediaTek NPU – כל אחד מהם תלוי ביעדי הספרייה המשותפת image_processor, image_utils ו-timing_utils:

cc_binary(
    name = "cpp_segmentation_cpu",
    srcs = ["main_cpu.cc"],
    deps = [
        ":image_processor",
        ":image_utils",
        ":timing_utils",
        "@litert_archive//litert/cc:litert_api_with_dynamic_runtime",
        "@com_google_absl//absl/time",
        "@com_google_absl//absl/types:span",
    ] + gles_deps() + gl_native_deps(),
    ...
)

יעד ה-GPU מוסיף את libLiteRtClGlAccelerator.so כתלות בנתונים, כך ש-Bazel כולל אותו בקבצים להרצה. היעד NPU מוסיף קבצים של שיגור ספקים ותוסף קומפיילר .so כתלות בנתונים.

8. עיבוד מקדים מואץ באמצעות GPU עם Compute Shaders

כל שלוש נקודות הכניסה משתמשות באותו צינור (pipeline) של shader לחישוב OpenGL ES לעיבוד מקדים. הבנת התהליך הזה היא המפתח להבנת הסיבה לכך שהנתיב של ה-GPU מהיר בהרבה מהנתיב של ה-CPU.

הגדרת הקשר EGL ללא ראש

‫ImageProcessor::InitializeGL() יוצר הקשר EGL ללא ראש – הקשר OpenGL ללא חלון או תצוגה מצורפים. זוהי שיטה סטנדרטית לחישוב GPU מחוץ למסך ב-Android. לאחר מכן, הוא מהדר את חמש התוכניות של shader לחישוב GLSL מהדיסק:

processor.InitializeGL(
    "shaders/passthrough_shader.vert",
    "shaders/mask_blend_compute.glsl",
    "shaders/resize_compute.glsl",
    "shaders/preprocess_compute.glsl",
    "shaders/deinterleave_masks.glsl");

מעלים את תמונת הקלט ל-GPU

קובץ ה-JPEG מפוענח לזיכרון ה-CPU על ידי ImageUtils::LoadImage() (באמצעות ספריית ה-STB), ואז מועלה לטקסטורה של ה-GPU:

auto img_data_cpu = ImageUtils::LoadImage(
    input_file, width_orig, height_orig, channels_file, /*desired=*/3);

GLuint tex_id_orig = processor.CreateOpenGLTexture(
    img_data_cpu, width_orig, height_orig, loaded_channels);

ImageUtils::FreeImageData(img_data_cpu);  // CPU copy no longer needed

מנקודה זו, התמונה המקורית נמצאת בזיכרון של ה-GPU כטקסטורה של OpenGL.

ה-shader של החישוב לעיבוד מקדים

‫shaders/preprocess_compute.glsl dispatches 8×8 thread groups across the 256×256 output grid. כל שרשור מטפל בפיקסל פלט אחד: הוא דוגם את מרקם הקלט באמצעות סינון דו-ליניארי (שינוי גודל חומרה בחינם), ממיר את ערך ה-RGB‏ [0, 1] ל-[-1, 1] וכותב ל-SSBO של הפלט:

vec2 uv = vec2(float(pos.x) / float(out_width - 1),
               float(pos.y) / float(out_height - 1));
vec4 color_0_1 = texture(inputTexture, uv);
vec3 color_neg1_1 = (color_0_1.rgb * 2.0) - 1.0;

int base = (pos.y * out_width + pos.x) * num_channels;
preprocessed_output.data[base + 0] = color_neg1_1.r;
preprocessed_output.data[base + 1] = color_neg1_1.g;
preprocessed_output.data[base + 2] = color_neg1_1.b;

בנתיב הרגיל (לא אפס-עותק), ה-SSBO הזה נקרא בחזרה ל-CPU ונכתב לטנזור LiteRT:

std::vector<float> preprocessed(256 * 256 * num_channels);
processor.ReadBufferData(preprocessed_buffer_id, 0,
                         preprocessed.size() * sizeof(float),
                         preprocessed.data());
LITERT_ABORT_IF_ERROR(
    input_buffers[0].Write(absl::MakeConstSpan(preprocessed)));

9. הסקת מסקנות ב-CPU

פתיחת main_cpu.cc. ההגדרה של LiteRT היא שלוש שורות:

// Create the root environment
LITERT_ASSIGN_OR_ABORT(auto env, litert::Environment::Create({}));

// Compile the model for the CPU
LITERT_ASSIGN_OR_ABORT(auto compiled_model,
    litert::CompiledModel::Create(
        env, model_path, litert::HwAccelerators::kCpu));

// Allocate input and output tensor buffers
LITERT_ASSIGN_OR_ABORT(auto input_buffers,
                       compiled_model.CreateInputBuffers());
LITERT_ASSIGN_OR_ABORT(auto output_buffers,
                       compiled_model.CreateOutputBuffers());

אחרי העיבוד המקדים, ההסקה היא קריאה סינכרונית יחידה:

LITERT_ABORT_IF_ERROR(compiled_model.Run(input_buffers, output_buffers));

‫Run() חוסם עד להשלמת ההסקה. מודל הנקודה הצפה selfie_multiclass_256x256.tflite פועל על ליבות ARM Cortex ונמשך בדרך כלל כ-116 עד 128 אלפיות השנייה במכשיר בינוני.

Binary usage:

cpp_segmentation_cpu <model_path> <input_image> <output_image>

10. הסקת מסקנות ב-GPU‏ (OpenCL)

פתיחת main_gpu.cc. בנתיב ה-GPU יש שני מושגים שלא קיימים בנתיב ה-CPU: ‏ litert::Options להגדרת מאיץ ה-GPU (עם קצה העורף של OpenCL), והרצה אסינכרונית.

הגדרת אפשרויות ה-GPU

litert::Options CreateGpuOptions() {
  LITERT_ASSIGN_OR_ABORT(litert::Options options, litert::Options::Create());
  LITERT_ASSIGN_OR_ABORT(auto& gpu_options, options.GetGpuOptions());

  LITERT_ABORT_IF_ERROR(
      gpu_options.SetBackend(litert::GpuOptions::Backend::kOpenCl));

  // Allow CPU fallback for any ops not supported by the GPU delegate
  options.SetHardwareAccelerators(litert::HwAccelerators::kGpu |
                                  litert::HwAccelerators::kCpu);
  return options;
}

הסקת מסקנות אסינכרונית

נתיב ה-GPU משתמש ב-RunAsync() במקום ב-Run(). הפעולה הזו שולחת את העבודה לתור הפקודות של ה-GPU וחוזרת מיד. לאחר מכן, מבצעים סנכרון לפני שקוראים את התוצאות:

bool async = false;
LITERT_ABORT_IF_ERROR(
    compiled_model.RunAsync(0, input_buffers, output_buffers, async));

if (output_buffers[0].HasEvent()) {
  LITERT_ASSIGN_OR_ABORT(auto event, output_buffers[0].GetEvent());
  event.Wait();
}

העיצוב הזה מאפשר לכם לבצע עבודת CPU במקביל להרצת GPU בצינור עיבוד נתונים בזמן אמת.

Binary usage:

cpp_segmentation_gpu <model_path> <input_image> <output_image>

11. עיבוד שאחרי — ביטול שזירה ומיזוג

אחרי ש-Run() או RunAsync() מסתיימים, output_buffers[0] מכיל מערך שטוח של מספרים ממשיים בצורה [256 × 256 × 6] בסדר משולב. ‫6 ציוני הסיווג של פיקסל (row, col) נמצאים באינדקסים (row * 256 + col) * 6 עד (row * 256 + col) * 6 + 5.

פירוק ל-6 מאגרי SSBO של מסכות

כדי לעזור למעבד, המערך המשולב מפולח ל-6 מערכים של מספרים ממשיים בערוץ יחיד, וכל אחד מהם מועלה ל-SSBO משלו ב-GPU:

std::vector<float> data(256 * 256 * 6);
output_buffers[0].Read(absl::MakeSpan(data));

std::vector<GLuint> mask_ids(6);
for (int i = 0; i < 6; ++i)
  mask_ids[i] = processor.CreateOpenGLBuffer(nullptr, 256 * 256 * sizeof(float));

processor.DeinterleaveMasksCpu(data.data(), 256, 256, mask_ids);

מיזוג צבעים של מסכות עם התמונה המקורית

processor.ApplyColoredMasks() מריצה את תוכנת ההצללה mask_blend_compute.glsl. לכל פיקסל פלט, האלגוריתם מוצא את המחלקה עם הציון הכי גבוה (argmax בכל 6 ה-SSBO של המסכה) ומבצע שילוב אלפא של הצבע המתאים על פיקסל התמונה המקורי. ששת הצבעים מוגדרים בכל נקודת כניסה:

std::vector<RGBAColor> mask_colors = {
    {1.0f, 0.0f, 0.0f, 0.1f},  // red     - background
    {0.0f, 1.0f, 0.0f, 0.1f},  // green   - hair
    {0.0f, 0.0f, 1.0f, 0.1f},  // blue    - body skin
    {1.0f, 1.0f, 0.0f, 0.1f},  // yellow  - face skin
    {1.0f, 0.0f, 1.0f, 0.1f},  // magenta - clothes
    {0.0f, 1.0f, 1.0f, 0.1f},  // cyan    - accessories
};

ערך האלפא של 0.1f שומר על גוון עדין כך שהתמונה המקורית נשארת גלויה.

שמירת הפלט

ה-SSBO הסופי של RGBA float נקרא בחזרה, מוגבל ל-[0, 1], מומר ל-unsigned char ונשמר כ-PNG:

for (size_t i = 0; i < float_data.size(); ++i)
  uchar_data[i] = static_cast<unsigned char>(
      std::max(0.0f, std::min(1.0f, float_data[i])) * 255.0f);
ImageUtils::SaveImage(output_file, width, height, 4, uchar_data.data());

12. פריסה והפעלה במכשיר

מחברים את מכשיר Android באמצעות USB ומוודאים שיש קישוריות ADB:

adb devices

טרמינל שמציג פלט של מכשירי adb עם מכשיר מחובר אחד

שימוש ב-deploy_and_run_on_android.sh

לכל וריאנט יש סקריפט פריסה משלו. הווריאנט של CMake מצביע על הספרייה build/, והווריאנט של Bazel מצביע על bazel-bin/. שני הסקריפטים:

  1. יוצרים /data/local/tmp/cpp_segmentation_android/ במכשיר.
  2. דוחפים את הקבצים הבינאריים, את ההצללות של GLSL, את המודל, את תמונת הבדיקה ואת קובצי זמן הריצה .so.
  3. מריצים הסקה באמצעות adb shell.
  4. מושכים את output_segmented.png בחזרה למחשב.

גרסת CMake (use_prebuilt_litert/)

# CPU
./deploy_and_run_on_android.sh --accelerator=cpu --phone=s25 build/

# GPU
./deploy_and_run_on_android.sh --accelerator=gpu --phone=s25 build/

וריאנט Bazel ‏ (build_from_source/)

מריצים את הפקודות האלה מהספרייה הראשית של מאגר הדוגמאות של LiteRT:

# CPU
./compiled_model_api/image_segmentation/c++_segmentation/build_from_source/deploy_and_run_on_android.sh \
    --accelerator=cpu --phone=s25 bazel-bin/

# GPU
./compiled_model_api/image_segmentation/c++_segmentation/build_from_source/deploy_and_run_on_android.sh \
    --accelerator=gpu --phone=s25 bazel-bin/

הדגל --phone קובע באיזו תבנית ספציפית למכשיר ובאילו ספריות של ספקים נעשה שימוש. ערכים נתמכים: s24 (Snapdragon 8 Gen 3),‏ s25 (Snapdragon 8 Elite),‏ dim9400 (MediaTek Dimensity 9400),‏ pixel8 (Tensor G3),‏ pixel9 (Tensor G4),‏ pixel10 (Tensor G5) ו-pixel11 (Tensor G6).

תזמון ההסקה

אחרי ההסקה, PrintTiming() מדפיס פירוט מלא של הפרופיל:

Load image:    X ms
Preprocess:    X ms
Inference:     X ms
Postprocess:   X ms
E2E:           X ms
Save image:    X ms

ביצועים לדוגמה במכשיר Samsung S25 Ultra‏ (Snapdragon 8 Elite):

Accelerator

סוג הביצוע

הסקת מסקנות

E2E

מעבד (CPU)

סנכרון

‫~116–128 אלפיות השנייה

‫~157 אלפיות השנייה

GPU ‏ (OpenCL)

אסינכרוני

‫0.95 אלפיות השנייה

‫~35–43 אלפיות השנייה

13. מתקדם (אופציונלי): הסקת מסקנות ב-NPU

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

מכשירים ומצבים נתמכים

Chip

דוגמה למכשיר

מצב

E2E

Qualcomm SM8650

Galaxy S24

‫AOT

‫~17 אלפיות השנייה

Qualcomm SM8750

Galaxy S25

‫AOT

‫~17 אלפיות השנייה

Qualcomm (כל ערך)

—

JIT

‫~28 אלפיות השנייה

MediaTek Dimensity 9400

—

JIT

‫~9 אלפיות השנייה

‫Google Tensor G3-G6

‫Pixel 8-11

AOT/JIT

משתנה

‫AOT (Ahead-of-Time) משתמש במודל שעבר קומפילציה מראש וספציפי למכשיר (למשל, selfie_multiclass_256x256_SM8650.tflite). זו האפשרות הכי מהירה, אבל היא ספציפית לשבב.

‫JIT (Just-in-Time) משתמש ב-selfie_multiclass_256x256.tflite הסטנדרטי ומבצע הידור ל-NPU בזמן הריצה – ההפעלה הראשונה איטית יותר, לא תלויה בשבב.

דרישות מוקדמות נוספות

‫Qualcomm HTP:

  • ‫QAIRT SDK גרסה 2.41 ואילך (מספק קובצי libQnnHtp.so, stub או skel .so).
  • ‫libLiteRtDispatch_Qualcomm.so מגרסת LiteRT NPU runtime libraries ב-GitHub.

‫MediaTek APU:

  • ‫libLiteRtDispatch_MediaTek.so מגרסת הספריות של זמן הריצה של LiteRT NPU.
  • זמן הריצה של NeuroPilot (כבר ספריית מערכת במכשירי Dimensity 9400 – אין צורך להעביר אותה).

‫Google Tensor:

  • ‫libLiteRtDispatch_GoogleTensor.so מגרסת הספריות של זמן הריצה של LiteRT NPU.

סביבת NPU ואפשרויות

‫main_npu.cc מציין את Environment בספריית השליחה של הספק במכשיר, ואז מגדיר אפשרויות ביצועים ספציפיות לספק:

// Configure LiteRT to find the dispatch library
std::vector<litert::Environment::Option> env_opts;
env_opts.push_back({litert::Environment::OptionTag::DispatchLibraryDir,
                    kQualcommDispatchDir});
LITERT_ASSIGN_OR_ABORT(auto env,
    litert::Environment::Create(std::move(env_opts)));

// Target NPU with CPU fallback
LITERT_ASSIGN_OR_ABORT(litert::Options options, litert::Options::Create());
options.SetHardwareAccelerators(litert::HwAccelerators::kNpu |
                                litert::HwAccelerators::kCpu);

// Qualcomm: burst performance mode
auto& qnn_opts = options.GetQualcommOptions();
qnn_opts.SetLogLevel(litert::qualcomm::QualcommOptions::LogLevel::kOff);
qnn_opts.SetHtpPerformanceMode(
    litert::qualcomm::QualcommOptions::HtpPerformanceMode::kBurst);

LITERT_ASSIGN_OR_ABORT(auto model,
    litert::CompiledModel::Create(env, model_path, options));

ב-MediaTek, מחליפים את הבלוק GetQualcommOptions():

// MediaTek: fast single-answer mode + low-latency hint
auto& mtk_opts = options.GetMediatekOptions();
mtk_opts.SetPerformanceMode(
    litert::mediatek::MediatekOptions::PerformanceMode::kFastSingleAnswer);
mtk_opts.SetOptimizationHint(
    litert::mediatek::MediatekOptions::OptimizationHint::kLowLatency);
mtk_opts.SetNeronSDKVersionType(
    litert::mediatek::MediatekOptions::NeronSDKVersion::kVersion8);

פריסה עבור NPU

‫CMake variant — Qualcomm S25 (AOT)

./deploy_and_run_on_android.sh \
    --accelerator=npu --phone=s25 \
    --host_npu_lib=/path/to/qairt/lib \
    --host_npu_dispatch_lib=/path/to/dir/with/libLiteRtDispatch_Qualcomm.so \
    build/

גרסת CMake – MediaTek Dimensity 9400 (JIT)

./deploy_and_run_on_android.sh \
    --accelerator=npu --phone=dim9400 --jit \
    --host_npu_dispatch_lib=/path/to/dir/with/libLiteRtDispatch_MediaTek.so \
    build/

וריאציית Bazel‏ – Qualcomm S25‏ (AOT)

./compiled_model_api/image_segmentation/c++_segmentation/build_from_source/deploy_and_run_on_android.sh \
    --accelerator=npu --phone=s25 bazel-bin/

וריאנט של Bazel – MediaTek Dimensity 9400 (JIT)

./compiled_model_api/image_segmentation/c++_segmentation/build_from_source/deploy_and_run_on_android.sh \
    --accelerator=npu --phone=dim9400 --jit bazel-bin/

גרסת Bazel‏ – Google Tensor Pixel 9‏ (JIT)

./compiled_model_api/image_segmentation/c++_segmentation/build_from_source/deploy_and_run_on_android.sh \
    --accelerator=npu --phone=pixel9 --jit bazel-bin/

במקרה של וריאציית Bazel, ספריות QAIRT SDK נאספות באופן אוטומטי מעץ bazel-bin runfiles כשמגדירים את LITERT_QAIRT_SDK בזמן ה-build. הווריאנט של CMake דורש שהדגל --host_npu_lib יצביע על QAIRT SDK שחולץ.

14. מעולה!

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

  • קומפילציה צולבת של קובץ בינארי C++ ל-Android arm64-v8a באמצעות CMake + NDK או Bazel.
  • כדי להסיק מסקנות ביעילות במכשיר, אפשר להשתמש ב-LiteRT C++ API‏ (Environment,‏ CompiledModel,‏ TensorBuffer).
  • עיבוד מוקדם של נתוני תמונה ב-GPU באמצעות הצללות חישוב של OpenGL ES 3.1.
  • הפעלת היקש סינכרוני של CPU והיקש אסינכרוני של GPU‏ (OpenCL).
  • הגדרת האצת NPU למכשירי Qualcomm,‏ MediaTek ו-Google Tensor.
  • פריסה והפעלה של קובץ בינארי C++‎ ב-Android באמצעות ADB.

השלבים הבאים

  • להחליף במודל TFLite אחר (למשל, הערכת עומק או זיהוי תנוחה).
  • איך משלבים את צינור עיבוד הנתונים C++‎ באפליקציית Android NDK באמצעות JNI.
  • פרופיל של השימוש בזיכרון באמצעות Android GPU Inspector לצד פלט של תזמון.
  • כדאי לבדוק את האפשרות של כימות מודלים כדי להקטין עוד יותר את זמן האחזור של הסקת מסקנות ב-NPU.

מידע נוסף