使用 LiteRT 在 C++ 中進行裝置端圖像區隔

1. 事前準備

手動輸入程式碼是培養肌肉記憶的好方法,也有助於加深對教材的理解。雖然複製貼上可以節省時間,但長期而言,投入這項做法可提高效率,並增進程式設計技能。

在本程式碼研究室中,您將瞭解如何使用 Google 的高效能裝置端執行階段 LiteRT,建構直接在 Android 裝置上執行的 C++ 影像分割二進位檔。本程式碼研究室的重點是建構 C++ 二進位檔,而非使用 Kotlin 或 Android Studio。您將使用 CMake 或 Bazel 進行跨平台程式碼編譯,並透過 ADB 部署。LiteRT C++ API 適用於任何平台 (Android、Linux、嵌入式),因此是效能關鍵應用程式、機器人和邊緣系統的實用基礎。

您將瞭解整個管道:

  • 設定建構環境 (CMake + Android NDK Bazel)。
  • 連結 LiteRT C++ SDK,可從預先建構的版本或來源連結。
  • 使用 OpenGL ES 運算著色器,透過 GPU 加速進行影像前處理和後處理。
  • 使用 LiteRT C++ API 執行 selfie_multiclass 分割模型。
  • CPUGPU (OpenCL)NPU (Qualcomm / MediaTek) 上加速推論。
  • 將原始模型輸出後處理為顏色混合的區隔圖片。
  • 使用 ADB 部署至 Android 實體裝置,並擷取結果。

最後,您會產生類似下圖的內容:經過完整管道處理的靜態圖片,其中 6 個區隔類別分別以不同顏色疊加顯示:

分割輸出內容:人物的頭髮、皮膚、背景和衣服上覆蓋半透明彩色遮罩

必要條件

本程式碼研究室適合熟悉 C++ 的開發人員,他們想在 C++ 層級的 Android 上執行機器學習模型。您必須已經熟悉下列項目:

  • C++ 基礎知識 (指標、向量、包含項目)。
  • Android/ADB 基本概念 (adb pushadb shell)。
  • 在 Linux 或 macOS 上使用終端機和殼層指令碼。

課程內容

  • 如何使用 CMake + NDK 或 Bazel,為 Android arm64-v8a 跨平台程式碼編譯 C++ 二進位檔。
  • 如何使用 LiteRT C++ API (EnvironmentCompiledModelTensorBuffer) 在裝置端進行高效推論。
  • 瞭解 OpenGL ES 3.1 運算著色器如何完全在 GPU 上加速預先處理和後續處理。
  • 如何設定 LiteRT,以透過 CPU、GPU (OpenCL) 和 NPU (Qualcomm HTP、MediaTek APU、Google Tensor) 加速。
  • 同步 (Run) 和非同步 (RunAsync) 推論的差異。
  • 如何使用 ADB 在 Android 上部署及執行 C++ 二進位檔。

軟硬體需求

  • Linux 或 macOS 機器 (Windows 使用者應使用 WSL2)。
  • Android NDK r25c 以上版本 (下載)。
  • CMake 路徑:CMake ≥ 3.22 (sudo apt-get install cmake)。
  • Bazel 路徑:已安裝 Bazel,以及完整的 LiteRT 範例存放區。
  • ADB 位於 PATH (Android 平台工具)。
  • 實體 Android 裝置,建議使用 Galaxy S24/S25 或 Pixel 進行測試。

2. 影像分割

影像分割是電腦視覺工作,會為圖片中的每個像素指派類別標籤。與繪製定界框的物件偵測不同,影像分割功能可精確瞭解每個物件的起點和終點,達到像素級的精準度。

本程式碼研究室使用 selfie_multiclass_256x256 模型,將每個像素分類為 6 個類別之一:

類別索引

區隔

0

背景

1

美髮

2

身體肌膚

3

臉部肌膚

4

衣服

5

配件 (眼鏡、珠寶等)

模型會輸出形狀為 [1, 256, 256, 6] 的浮點張量。每個 256×256 像素都有 6 個信賴分數,每個類別各一個。得分最高的類別會贏得該像素 (argmax)。

LiteRT:邊緣效能

LiteRT 是 Google 的新一代高效能執行階段,適用於 TFLite 模型。透過 C++ API,您可直接存取硬體加速器,且負擔較低,並在以下三種加速器中享有一致的介面:

  • CPU:普遍相容,在中階裝置上推論約需 128 毫秒。
  • GPU (OpenCL):推論時間約 1 毫秒;端對端時間約 17 到 43 毫秒,視緩衝區策略而定。
  • NPU:在 Qualcomm Snapdragon、MediaTek Dimensity 9400 和 Google Tensor 裝置上,端對端延遲時間約為 9 到 28 毫秒,視 AOT 而定。JIT 編譯。

主要抽象化是:CompiledModel模型會在載入時間預先編譯並針對目標硬體進行最佳化,將推論作業減少為對預先配置緩衝區的 Run() 呼叫。

3. 做好準備

複製存放區

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

本程式碼研究室的所有資源都位於:

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

這個目錄有兩個子專案,每個都是相同範例的完整建構版本:

目錄

建構系統

LiteRT 依附元件

use_prebuilt_litert/

CMake + Android NDK

預先建構的 litert_cc_sdk.zip + libLiteRt.so

build_from_source/

Bazel

從來源編譯 LiteRT

選擇其中一個路徑並按照步驟操作。這兩個目錄的程式碼完全相同,只有建構系統和依附元件策略不同。如要以最快速度完成設定,請選擇 use_prebuilt_litert/。如需修改 LiteRT 本身或在現有的 Bazel 單一存放區中工作,請使用 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.txtbuild_prebuilt.shdeploy_and_run_on_android.shthird_party/stb/
  • build_from_source/ 會新增 Bazel BUILD 檔案,並使用指向 bazel-bin/deploy_and_run_on_android.sh

終端機顯示 use_prebuilt_litert 目錄樹狀結構

4. 瞭解專案結構

三個進入點,一個管道

main_cpu.ccmain_gpu.ccmain_npu.cc 各包含一個 main() 函式,可驅動完整區隔管道。這三種管道完全相同,只有 LiteRT 加速器設定和緩衝區策略不同:

檔案

加速器

緩衝區策略

main_cpu.cc

kCpu

CPU 記憶體

main_gpu.cc

kGpu | kCpu

使用 OpenCL 後端的 CPU 記憶體

main_npu.cc

kNpu | kCpu

CPU 記憶體 (CPU 備援)

三者共用相同的 ImageProcessor (用於預先處理和後續處理的 OpenGL ES 運算著色器) 和 ImageUtils (STB 圖片 I/O) 公用程式。

完整管道

每個進入點都遵循相同的五階段結構:

Load  GPU upload  Preprocess (shader)  Inference (LiteRT)  Postprocess (shader)  Save
  1. 載入ImageUtils::LoadImage() 使用 STB 圖片程式庫,將 JPEG 解碼至 CPU 記憶體。
  2. 上傳processor.CreateOpenGLTexture()將原始像素上傳至 GPU 紋理 (OpenGL RGBA8)。
  3. 前置處理 - processor.PreprocessInputForSegmentation() 執行 GLSL 運算著色器,將紋理大小調整為 256x256,並將像素值從 [0, 1] 正規化為 [-1, 1]。結果會儲存在 GPU SSBO 中。
  4. 推論 - SSBO 資料會寫入 LiteRT TensorBuffer,而 compiled_model.Run() (或 RunAsync()) 會執行模型。
  5. 後續處理:模型的 6 通道浮點輸出會去交錯為 6 個單通道遮罩 SSBO,然後色彩混合回原始圖片。
  6. 儲存ImageUtils::SaveImage() 將最終 RGBA 圖片寫入為 PNG。

5. Core LiteRT C++ API

建構前,請先熟悉所有進入點使用的三種主要 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)));

錯誤處理巨集

巨集

行為

LITERT_ASSIGN_OR_ABORT(var, expr)

在失敗時指派或呼叫 abort()

LITERT_ABORT_IF_ERROR(expr)

如果運算式傳回錯誤,則呼叫 abort()

LITERT_ASSIGN_OR_RETURN(var, expr)

將錯誤指派或傳播給呼叫端

6. 建構 - 選項 A:預先建構的 LiteRT C++ SDK (CMake)

如果您不需要修改 LiteRT 本身,建議採用這個路徑。建構指令碼會處理 SDK 標頭的下載作業、複製 .so、擷取 STB,並透過單一指令叫用 CMake + NDK。

步驟 1:從 Maven 取得 libLiteRt.so

LiteRT 會在 Google Maven 的 Android AAR 中,將執行階段做為共用程式庫出貨。下載並解壓縮 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/

終端機顯示 wget 下載 LiteRT AAR,以及 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 GitHub 版本下載 litert_cc_sdk.zip (SDK 標頭 + cmake 檔案) - 如果已存在,後續執行時會略過。
  2. libLiteRt.so 複製到 litert_cc_sdk/
  3. 將 STB 圖片標頭下載至 third_party/stb/ (如有,則略過)。
  4. 使用 Android NDK 工具鍊為 arm64-v8a 設定及建構 CMake,位置為 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,透過 add_subdirectory 提取 LiteRT SDK,連結 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. 建構 - 選項 B:使用 Bazel 建構 (從來源)

如果您偏好使用 Bazel 做為建構系統 (從來源編譯 LiteRT 執行階段),或是需要在現有的 Bazel 工作區中工作,請選擇這個路徑。

必要條件

除了「事前準備」一節列出的 NDK 和 ADB 之外,您還需要:

  • 已安裝 Bazel,且位於 PATH 中。
  • LiteRT 範例來源存放區的完整副本。

步驟 1:設定 LiteRT 範例工作區

所有指令都是從 LiteRT 範例存放區的根目錄執行

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

出現提示時:

  • 接受 Python 和 Python 程式庫路徑的預設值。
  • 回答 ROCm 和 CUDA 支援的 N
  • 選取「clang」 (已使用 18.1.3 測試) 做為編譯器。
  • 接受預設最佳化旗標。
  • 回答 Y,為 Android 建構作業設定 WORKSPACE。
  • 將最低 Android NDK 級別設為至少 26
  • 提供 Android SDK 的路徑。
  • 將 Android SDK API 級別設為預設值 (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. 建構時,請傳遞以 / 結尾的父項路徑:
    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/
    

由於部分上游 LiteRT 目標的預設瀏覽權限受到限制,因此需要 --nocheck_visibility 標記。

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

終端機畫面:顯示 cpp_segmentation_cpu 和 cpp_segmentation_gpu 的 bazel 建構作業已完成

BUILD 檔案

開啟 build_from_source/BUILD。這會定義四個 cc_binary 目標 (每個加速器各一個,加上專用的 MediaTek NPU 目標),每個目標都依附於共用的 image_processorimage_utilstiming_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 加速前處理

這三個進入點都使用相同的 OpenGL ES 運算著色器管道進行前處理。瞭解這項概念是瞭解 GPU 路徑為何比 CPU 路徑快上許多的關鍵。

設定無頭 EGL 環境

ImageProcessor::InitializeGL() 會建立無頭 EGL 情境,也就是沒有附加視窗或螢幕的 OpenGL 情境。這是 Android 螢幕外 GPU 計算的標準做法。接著,從磁碟編譯五個 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 會由 ImageUtils::LoadImage() (透過 STB 程式庫) 解碼至 CPU 記憶體,然後上傳至 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

從這個時間點開始,原始圖片會以 OpenGL 紋理的形式存在於 GPU 記憶體中。

預先處理運算著色器

shaders/preprocess_compute.glsl 會在 256×256 的輸出格線中,調度 8×8 的執行緒群組。每個執行緒會處理一個輸出像素:使用雙線性濾鏡 (免費的硬體調整大小) 對輸入紋理進行取樣、將 [0, 1] RGB 值轉換為 [-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 毫秒。

二進位檔用法:

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 執行作業重疊。

二進位檔用法:

cpp_segmentation_gpu <model_path> <input_image> <output_image>

11. 後置處理 - 去交錯和混合

Run()RunAsync() 完成後,output_buffers[0] 會以交錯順序保存形狀 [256 × 256 × 6] 的平面浮點陣列。像素 (row, col) 的 6 個類別分數位於索引 (row * 256 + col) * 6(row * 256 + col) * 6 + 5

將交錯式資料解交錯至 6 個遮罩 SSBO

CPU 輔助程式會將交錯陣列分割為 6 個單一通道浮點陣列,並將每個陣列上傳至各自的 GPU SSBO:

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 著色器。針對每個輸出像素,系統會找出分數最高的類別 (6 個遮罩 SSBO 的 argmax),並將對應的顏色以 Alpha 合成方式疊加在原始圖片像素上。每個進入點都會定義六種顏色:

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 的 Alpha 值會讓色調保持細微,因此原始圖片仍會顯示。

儲存輸出內容

讀回最終混合的 RGBA 浮點 SSBO,限制取值範圍至 [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. 在裝置上部署及執行

使用 USB 連接 Android 裝置,並確認 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) 的參考效能:

加速器

執行作業類型

推論

E2E

CPU

同步

約 116 至 128 毫秒

~157 毫秒

GPU (OpenCL)

非同步

~0.95 毫秒

約 35 至 43 毫秒

13. 進階 (選用):NPU 推論

為達到最高效能,LiteRT 支援使用供應商專屬外掛程式庫的 NPU 加速功能。NPU 路徑可將端對端延遲時間降至 9 毫秒。

支援的裝置和模式

方塊

裝置範例

模式

E2E

高通 SM8650

Galaxy S24

AOT

約 17 毫秒

高通 SM8750

Galaxy S25

AOT

約 17 毫秒

Qualcomm (任何)

JIT

約 28 毫秒

MediaTek Dimensity 9400

JIT

~9 毫秒

Google Tensor G3-G6

Pixel 8 到 Pixel 11

AOT/JIT

不定

AOT (預先) 使用裝置專用的預先編譯模型 (例如 selfie_multiclass_256x256_SM8650.tflite)。這是最快的選項,但僅適用於特定晶片。

JIT (即時) 使用標準 selfie_multiclass_256x256.tflite,並在執行階段編譯至 NPU,因此首次執行速度較慢,但與晶片無關。

其他必要條件

Qualcomm HTP:

  • QAIRT SDK 2.41 以上版本 (提供 libQnnHtp.so、存根或骨架 .so 檔案)。
  • libLiteRtDispatch_Qualcomm.so,位於 GitHub 上的 LiteRT NPU 執行階段程式庫版本中。

MediaTek APU:

  • 從 LiteRT NPU 執行階段程式庫版本中。libLiteRtDispatch_MediaTek.so
  • NeuroPilot 執行階段 (已是 Dimensity 9400 裝置上的系統程式庫,無須推送)。

Google Tensor:

  • 從 LiteRT NPU 執行階段程式庫版本中。libLiteRtDispatch_GoogleTensor.so

NPU 環境和選項

main_npu.ccEnvironment 指向裝置上的供應商調度程式庫目錄,然後設定供應商專屬的效能選項:

// 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(
    kLiteRtMediatekNeuronAdapterPerformanceModeNeuronPreferFastSingleAnswer);
mtk_opts.SetOptimizationHint(
    kLiteRtMediatekNeuronAdapterOptimizationHintLowLatency);
mtk_opts.SetNeronSDKVersionType(
    kLiteRtMediatekOptionsNeronSDKVersionTypeVersion8);

部署至 NPU

CMake 變體 - 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 變體,當 LITERT_QAIRT_SDK 在建構時間設定,系統會自動從 bazel-bin 執行檔樹狀結構中挑選 QAIRT SDK 程式庫。CMake 變數需要 --host_npu_lib 旗標,才能指向已解壓縮的 QAIRT SDK。

14. 恭喜!

您已成功使用 LiteRT,在 Android 上建構及執行 C++ 影像分割管道。您已學會如何:

  • 使用 CMake + NDK 或 Bazel,為 Android arm64-v8a 跨平台程式碼編譯 C++ 二進位檔。
  • 使用 LiteRT C++ API (EnvironmentCompiledModelTensorBuffer) 進行高效的裝置端推論。
  • 使用 OpenGL ES 3.1 運算著色器,在 GPU 上預先處理圖像資料。
  • 執行同步 CPU 推論,以及非同步 GPU (OpenCL) 推論。
  • 為 Qualcomm、MediaTek 和 Google Tensor 裝置設定 NPU 加速功能。
  • 使用 ADB 在 Android 上部署及執行 C++ 二進位檔。

後續步驟

  • 換用其他 TFLite 模型 (例如深度估算或姿勢偵測)。
  • 使用 JNI 將 C++ 管道整合至 Android NDK 應用程式。
  • 使用 Android GPU 檢查器剖析記憶體用量,並輸出時間資訊。
  • 探索模型量化,進一步縮短 NPU 推論延遲時間。

瞭解詳情