使用 LiteRT 在 C++ 中进行设备端图像分割

1. 准备工作

输入代码是锻炼肌肉记忆和加深对材料理解的好方法。虽然复制粘贴可以节省时间,但从长远来看,投资于这种实践可以提高效率并增强编码技能。

在此 Codelab 中,您将学习如何使用 Google 的高性能设备端运行时 LiteRT 构建直接在 Android 设备上运行的 C++ 图像分割二进制文件。此 Codelab 侧重于构建 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 个分割类别中的每一个都以不同的颜色叠加显示:

分割输出:一个人,头发、皮肤、背景和衣服上覆盖着半透明的彩色蒙版

前提条件

本 Codelab 专为熟悉 C++ 且希望获得在 C++ 层级上于 Android 设备上运行机器学习模型经验的开发者而设计。您应熟悉以下内容:

  • C++ 基础知识(指针、向量、include)。
  • 基本的 Android/ADB 概念(adb pushadb shell)。
  • 在 Linux 或 macOS 上使用终端和 shell 脚本。

学习内容

  • 如何使用 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 示例代码库。
  • PATH(Android 平台工具)中的 ADB
  • Android 实体设备 - 最好在 Galaxy S24/S25 或 Pixel 上进行测试。

2. 图像分割

图像分割是一项计算机视觉任务,可为图像中的每个像素分配一个类别标签。与绘制边界框的对象检测不同,分割功能可以精确到像素级地了解每个对象的开始和结束位置。

此 Codelab 使用 selfie_multiclass_256x256 模型,该模型将每个像素分类为 6 个类别之一:

类索引

Segment

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

本 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

选择一条路径并按照该路径操作。这两个目录中的代码完全相同,只有构建系统和依赖项策略不同。如果您想以最快的速度完成设置,请选择 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.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. 加载 - 使用 STB 图片库将 JPEG 解码到 CPU 内存中。ImageUtils::LoadImage()
  2. 上传 - processor.CreateOpenGLTexture() 将原始像素上传到 GPU 纹理 (OpenGL RGBA8)。
  3. 预处理 - processor.PreprocessInputForSegmentation() 运行一个 GLSL 计算着色器,该着色器将纹理调整为 256x256,并将像素值从 [0, 1] 归一化为 [-1, 1]。结果会存储在 GPU SSBO 中。
  4. 推理 - SSBO 数据写入 LiteRT TensorBuffercompiled_model.Run()(或 RunAsync())执行模型。
  5. 后处理 - 模型的 6 通道浮点输出被解交织为 6 个单通道掩码 SSBO,然后将这些掩码重新进行颜色混合,以生成最终图像。
  6. 保存 - ImageUtils::SaveImage() 将最终 RGBA 图像写入为 PNG。

5. 核心 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 配置和构建 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 build 的工作区。
  • 将最低 Android NDK 级别设置为至少 26
  • 提供 Android SDK 的路径。
  • 将 Android SDK API 级别设置为默认值 (36),并将 build tools 设置为 36.0.0

终端显示 LiteRT 示例工作区的 ./configure 提示和回答

第 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 v2.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

终端显示 bazel build 完成了 cpp_segmentation_cpu 和 cpp_segmentation_gpu 的构建

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 路径中没有的两个概念:用于配置 GPU 加速器(使用 OpenCL 后端)的 litert::Options 和异步执行。

配置 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 devices 输出,其中包含一个已连接的设备

使用 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-11

AOT/JIT

不定

AOT(预先)使用设备专属的预编译模型(例如 selfie_multiclass_256x256_SM8650.tflite)。这是最快的选项,但仅适用于特定芯片。

JIT(即时)使用标准 selfie_multiclass_256x256.tflite,并在运行时编译到 NPU - 首次运行速度较慢,但与芯片无关。

额外前提条件

Qualcomm HTP

  • QAIRT SDK v2.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 runfiles 树中提取 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 推理延迟时间。

了解详情