การแบ่งกลุ่มรูปภาพในอุปกรณ์ใน C++ ด้วย LiteRT

1. ก่อนเริ่มต้น

การพิมพ์โค้ดเป็นวิธีที่ยอดเยี่ยมในการสร้างความจำของกล้ามเนื้อและเพิ่มความเข้าใจในเนื้อหา แม้ว่าการคัดลอกและวางจะช่วยประหยัดเวลาได้ แต่การลงทุนในแนวทางปฏิบัตินี้จะช่วยเพิ่มประสิทธิภาพและทักษะการเขียนโค้ดในระยะยาว

ใน Codelab นี้ คุณจะได้เรียนรู้วิธีสร้างไบนารีการแบ่งกลุ่มรูปภาพใน C++ ที่ทำงานบนอุปกรณ์ Android โดยตรงโดยใช้รันไทม์ในอุปกรณ์ที่มีประสิทธิภาพสูงของ Google ซึ่งก็คือ LiteRT Codelab นี้มุ่งเน้นการสร้างไบนารี C++ แทนการใช้ Kotlin หรือ Android Studio คุณจะคอมไพล์ข้ามด้วย CMake หรือ Bazel และติดตั้งใช้งานโดยใช้ ADB LiteRT C++ API เดียวกันนี้ใช้ได้ในทุกแพลตฟอร์ม (Android, Linux, ระบบฝังตัว) จึงเป็นรากฐานที่มีประโยชน์สำหรับแอปพลิเคชันที่สำคัญต่อประสิทธิภาพ หุ่นยนต์ และระบบ Edge

คุณจะได้รับคำแนะนำเกี่ยวกับไปป์ไลน์ทั้งหมด

  • การตั้งค่าสภาพแวดล้อมของบิลด์ (CMake + Android NDK หรือ Bazel)
  • การลิงก์ LiteRT C++ SDK ไม่ว่าจะจากรุ่นที่สร้างไว้ล่วงหน้าหรือจากแหล่งที่มา
  • ใช้เชดเดอร์การประมวลผล OpenGL ES สำหรับการประมวลผลรูปภาพก่อนและหลังที่มีการเร่ง GPU
  • เรียกใช้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) เพื่อการอนุมานในอุปกรณ์อย่างมีประสิทธิภาพ
  • Compute Shader ของ 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: CMake ≥ 3.22 (sudo apt-get install cmake)
  • สำหรับเส้นทาง Bazel: ติดตั้ง Bazel และที่เก็บตัวอย่าง LiteRT แบบเต็ม
  • ADB ใน PATH (Android Platform Tools)
  • อุปกรณ์ Android จริง - ทดสอบได้ดีที่สุดใน Galaxy S24/S25 หรือ Pixel

2. การแบ่งกลุ่มรูปภาพ

การแบ่งกลุ่มรูปภาพคืองานด้านคอมพิวเตอร์วิทัศน์ที่กำหนดป้ายกำกับคลาสให้กับทุกพิกเซลในรูปภาพ การแบ่งกลุ่มจะสร้างความเข้าใจที่แม่นยำระดับพิกเซลเกี่ยวกับจุดเริ่มต้นและจุดสิ้นสุดของแต่ละออบเจ็กต์ ซึ่งต่างจากการตรวจหาออบเจ็กต์ที่วาดกรอบล้อมรอบ

Codelab นี้ใช้โมเดล selfie_multiclass_256x256 ซึ่งจัดประเภทแต่ละพิกเซลเป็น 1 ใน 6 คลาสต่อไปนี้

ดัชนีชั้นเรียน

กลุ่ม

0

ฉากหลัง

1

ทำผม

2

ผิวหนัง

3

ผิวหน้า

4

เสื้อผ้า

5

เครื่องประดับ (แว่นตา เครื่องเพชรพลอย ฯลฯ)

โมเดลจะแสดงผลเทนเซอร์จำนวนลอยตัวที่มีรูปร่าง [1, 256, 256, 6] สำหรับแต่ละพิกเซลขนาด 256x256 จะมีคะแนนความเชื่อมั่น 6 คะแนน ซึ่งเป็นคะแนนสำหรับแต่ละคลาส คลาสที่มีคะแนนสูงสุดจะเป็นผู้ชนะพิกเซลนั้น (argmax)

LiteRT: ประสิทธิภาพที่ Edge

LiteRT คือรันไทม์ประสิทธิภาพสูงรุ่นถัดไปของ Google สำหรับโมเดล TFLite API ของ C++ ช่วยให้คุณเข้าถึงตัวเร่งฮาร์ดแวร์ได้โดยตรงโดยมีค่าใช้จ่ายต่ำและมีอินเทอร์เฟซที่สอดคล้องกันในทั้ง 3 อย่าง

  • CPU - เข้ากันได้กับทุกอุปกรณ์ การอนุมานใช้เวลาประมาณ 128 มิลลิวินาทีในอุปกรณ์ระดับกลาง
  • 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/

ไดเรกทอรีนี้มีโปรเจ็กต์ย่อย 2 โปรเจ็กต์ ซึ่งแต่ละโปรเจ็กต์เป็นบิลด์ที่สมบูรณ์ของตัวอย่างเดียวกัน

ไดเรกทอรี

ระบบบิลด์

ทรัพยากร Dependency ของ LiteRT

use_prebuilt_litert/

CMake + Android NDK

litert_cc_sdk.zip + libLiteRt.so ที่สร้างไว้ล่วงหน้า

build_from_source/

Bazel

คอมไพล์ LiteRT จากแหล่งที่มา

เลือกเส้นทางใดเส้นทางหนึ่งแล้วทำตาม โค้ดในทั้ง 2 ไดเรกทอรีจะเหมือนกันทุกประการ มีเพียงระบบบิลด์และกลยุทธ์การอ้างอิงเท่านั้นที่แตกต่างกัน หากต้องการตั้งค่าให้เร็วที่สุด ให้เลือก use_prebuilt_litert/ หากต้องการแก้ไข LiteRT เองหรือทำงานภายในที่เก็บข้อมูลแบบ Monorepo ของ Bazel ที่มีอยู่ ให้ใช้ build_from_source/

หมายเหตุเกี่ยวกับเส้นทางของไฟล์

เส้นทางของไฟล์ทั้งหมดในบทแนะนำนี้ใช้รูปแบบ Linux/macOS ผู้ใช้ Windows ควรใช้ WSL2

ภาพรวมไดเรกทอรี

ทั้ง 2 โปรเจ็กต์ย่อยใช้เลย์เอาต์แหล่งที่มาเดียวกัน

<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. ทำความเข้าใจโครงสร้างโปรเจ็กต์

จุดแรกเข้า 3 จุด ไปป์ไลน์เดียว

main_cpu.cc, main_gpu.cc และ main_npu.cc แต่ละรายการมีฟังก์ชัน main() ที่ขับเคลื่อนไปป์ไลน์การแบ่งกลุ่มทั้งหมด ไปป์ไลน์เหมือนกันทั้ง 3 รุ่น มีเพียงการกำหนดค่าตัวเร่ง LiteRT และกลยุทธ์บัฟเฟอร์เท่านั้นที่แตกต่างกัน

ไฟล์

Accelerator

กลยุทธ์การบัฟเฟอร์

main_cpu.cc

kCpu

หน่วยความจำ CPU

main_gpu.cc

kGpu | kCpu

หน่วยความจำ CPU ที่มีแบ็กเอนด์ OpenCL

main_npu.cc

kNpu | kCpu

หน่วยความจำ CPU ที่มี CPU สำรอง

ทั้ง 3 รายการใช้ยูทิลิตี ImageProcessor (OpenGL ES Compute Shader สำหรับการประมวลผลล่วงหน้าและหลังการประมวลผล) และ ImageUtils (STB Image I/O) เดียวกัน

ไปป์ไลน์ทั้งหมด

ทุกจุดแรกเข้ามีโครงสร้าง 5 เฟสเดียวกัน ดังนี้

Load  GPU upload  Preprocess (shader)  Inference (LiteRT)  Postprocess (shader)  Save
  1. โหลด - ImageUtils::LoadImage() ถอดรหัส JPEG ลงในหน่วยความจำ CPU โดยใช้ไลบรารีรูปภาพ STB
  2. อัปโหลด - processor.CreateOpenGLTexture() อัปโหลดพิกเซลดิบไปยังพื้นผิว GPU (OpenGL RGBA8)
  3. ประมวลผลล่วงหน้าprocessor.PreprocessInputForSegmentation() เรียกใช้ GLSL Compute Shader ที่ปรับขนาดเท็กซ์เจอร์เป็น 256x256 และปรับค่าพิกเซลให้เป็นมาตรฐานจาก [0, 1] เป็น [-1, 1] ผลลัพธ์จะอยู่ใน SSBO ของ GPU
  4. อนุมาน - ข้อมูล SSBO จะเขียนลงใน LiteRT TensorBuffer และ compiled_model.Run() (หรือ RunAsync()) จะเรียกใช้โมเดล
  5. การประมวลผลภายหลัง - เอาต์พุตแบบลอยตัว 6 แชแนลของโมเดลจะถูกแยกออกเป็น SSBO ของมาสก์แบบแชแนลเดียว 6 รายการ จากนั้นจะมีการผสมสีกลับไปยังรูปภาพต้นฉบับ
  6. บันทึกImageUtils::SaveImage() เขียนรูปภาพ RGBA สุดท้ายเป็น PNG

5. Core LiteRT C++ APIs

ก่อนที่จะสร้าง ให้ทำความคุ้นเคยกับประเภท C++ หลัก 3 ประเภทของ LiteRT ที่ใช้ในทุกจุดแรกเข้า ทั้งหมดอยู่ในเนมสเปซ 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. สร้าง - ตัวเลือก ก: C++ SDK ของ LiteRT ที่สร้างไว้ล่วงหน้า (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/

เทอร์มินัลแสดง 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 สำหรับ arm64-v8a ที่ android-26

เมื่อสำเร็จแล้ว คุณจะเห็นไบนารี 3 รายการใน build/ ดังนี้

build/cpp_segmentation_cpu
build/cpp_segmentation_gpu
build/cpp_segmentation_npu

เทอร์มินัลแสดงเอาต์พุต build_prebuilt.sh ที่เสร็จสมบูรณ์พร้อมไบนารี 3 รายการที่แสดงใน build/

สิ่งที่ CMakeLists.txt ทำ

เปิด CMakeLists.txt โดยต้องใช้ C++20 ดึง SDK ของ LiteRT ผ่าน 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 เป็นระบบบิลด์ ซึ่งจะคอมไพล์รันไทม์ LiteRT จากแหล่งที่มา หรือหากต้องการทำงานภายในพื้นที่ทำงาน Bazel ที่มีอยู่

ข้อกำหนดเบื้องต้น

นอกจาก NDK และ ADB ที่ระบุไว้ในส่วน "ก่อนเริ่มต้น" แล้ว คุณจะต้องมีสิ่งต่อไปนี้

  • Bazel ติดตั้งและอยู่ใน PATH
  • การโคลนที่เก็บแหล่งที่มาของตัวอย่าง LiteRT แบบเต็ม

ขั้นตอนที่ 1 — กำหนดค่าพื้นที่ทำงานตัวอย่าง LiteRT

คำสั่งทั้งหมดจะทำงานจากรูทของที่เก็บตัวอย่าง LiteRT

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

เมื่อได้รับข้อความแจ้ง ให้ทำดังนี้

  • ยอมรับค่าเริ่มต้นสำหรับเส้นทาง Python และ Python lib
  • ตอบ N เพื่อรองรับ ROCm และ CUDA
  • เลือก clang (ทดสอบกับ 18.1.3) เป็นคอมไพเลอร์
  • ยอมรับค่าสถานะการเพิ่มประสิทธิภาพเริ่มต้น
  • ตอบ Y เพื่อกำหนดค่า WORKSPACE สำหรับบิลด์ 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 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/
    

ต้องใช้--nocheck_visibilityเนื่องจากเป้าหมาย LiteRT บางรายการต้นทางมีค่าเริ่มต้นการมองเห็นที่จำกัด

APU ของ MediaTek

ไม่จำเป็นต้องใช้ 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 ที่เสร็จสมบูรณ์สำหรับ cpp_segmentation_cpu และ cpp_segmentation_gpu

ไฟล์ BUILD

เปิด build_from_source/BUILD โดยจะกำหนดcc_binaryเป้าหมาย 4 รายการ ซึ่งเป็นเป้าหมายสำหรับตัวเร่งความเร็ว 1 รายการและเป้าหมาย NPU ของ MediaTek โดยเฉพาะ แต่ละรายการจะขึ้นอยู่กับเป้าหมายไลบรารี 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 Shader

โดยทั้ง 3 จุดแรกเข้าจะใช้ไปป์ไลน์เชดเดอร์การคำนวณ OpenGL ES เดียวกันสำหรับการประมวลผลล่วงหน้า การทำความเข้าใจเรื่องนี้เป็นกุญแจสำคัญในการทำความเข้าใจว่าเหตุใดเส้นทาง GPU จึงเร็วกว่าเส้นทาง CPU มาก

ตั้งค่าบริบท EGL แบบไม่มีส่วนหัว

ImageProcessor::InitializeGL() จะสร้าง headless EGL context ซึ่งเป็น OpenGL context ที่ไม่มีหน้าต่างหรือจอแสดงผลแนบอยู่ นี่เป็นแนวทางปฏิบัติมาตรฐานสำหรับการประมวลผล GPU นอกหน้าจอบน Android จากนั้นจะคอมไพล์โปรแกรม Compute Shader GLSL 5 รายการจากดิสก์

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

อัปโหลดรูปภาพอินพุตไปยัง GPU

ImageUtils::LoadImage() (ผ่านไลบรารี STB) จะถอดรหัส JPEG เป็นหน่วยความจำ 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

จากจุดนี้ รูปภาพต้นฉบับจะอยู่ในหน่วยความจำของ GPU เป็นพื้นผิว OpenGL

Compute Shader ก่อนการประมวลผล

shaders/preprocess_compute.glsl จะส่งกลุ่มเธรด 8×8 ไปยังตารางเอาต์พุต 256×256 แต่ละเทรดจะจัดการพิกเซลเอาต์พุต 1 พิกเซล โดยจะสุ่มตัวอย่างพื้นผิวอินพุตโดยใช้การกรองแบบไบลิเนียร์ (ปรับขนาดฮาร์ดแวร์ฟรี) แปลงค่า [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 มี 3 บรรทัดดังนี้

// 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 จะมีแนวคิด 2 อย่างที่ไม่มีในเส้นทาง 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. Postprocess - Deinterleave and Blend

หลังจาก Run() หรือ RunAsync() เสร็จสมบูรณ์แล้ว output_buffers[0] จะเก็บอาร์เรย์ลอยแบบแบนที่มีรูปร่าง [256 × 256 × 6] ในลำดับที่สลับกัน คะแนน 6 คลาสสำหรับพิกเซล (row, col) อยู่ที่ดัชนี (row * 256 + col) * 6 ถึง (row * 256 + col) * 6 + 5

แยกเป็น SSBO มาสก์ 6 รายการ

ตัวช่วย CPU จะแยกอาร์เรย์ที่สลับกันออกเป็นอาร์เรย์ลอยตัวแบบช่องทางเดียว 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() เรียกใช้ Shader mask_blend_compute.glsl สำหรับพิกเซลเอาต์พุตแต่ละพิกเซล ระบบจะค้นหาคลาสที่มีคะแนนสูงสุด (argmax ใน SSBO ของมาสก์ทั้ง 6) และผสมสีที่เกี่ยวข้องกับพิกเซลรูปภาพต้นฉบับ โดยมีการกำหนดสีทั้ง 6 สีในแต่ละจุดแรกเข้าดังนี้

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 ที่ผสมแล้วกลับมา หนีบเป็น [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 ที่มีอุปกรณ์ที่เชื่อมต่ออยู่ 1 เครื่อง

ใช้ deploy_and_run_on_android.sh

แต่ละตัวแปรจะมีสคริปต์การติดตั้งใช้งานของตัวเอง ตัวแปร CMake จะชี้ไปที่ไดเรกทอรี build/ ส่วนตัวแปร Bazel จะชี้ไปที่ bazel-bin/ ทั้ง 2 สคริปต์

  1. สร้าง /data/local/tmp/cpp_segmentation_android/ ในอุปกรณ์
  2. ส่งไบนารี, Shader 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)

Async

~0.95 มิลลิวินาที

~35–43 มิลลิวินาที

13. ขั้นสูง (ไม่บังคับ): การอนุมาน NPU

LiteRT รองรับการเร่งความเร็ว NPU โดยใช้ไลบรารีปลั๊กอินเฉพาะของผู้ให้บริการเพื่อให้ได้ประสิทธิภาพสูงสุด เส้นทาง NPU สามารถลดเวลาในการตอบสนองตั้งแต่ต้นจนจบได้ต่ำสุดที่ 9 มิลลิวินาที

อุปกรณ์และโหมดที่รองรับ

ชิป

ตัวอย่างอุปกรณ์

โหมด

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 v2.41 ขึ้นไป (มีไฟล์ libQnnHtp.so, สตับ หรือสเกล .so)
  • libLiteRtDispatch_Qualcomm.so จากการเผยแพร่ไลบรารีรันไทม์ของ NPU สำหรับ LiteRT ใน GitHub

APU ของ MediaTek:

  • libLiteRtDispatch_MediaTek.so จากการเปิดตัวไลบรารีรันไทม์ของ NPU ใน LiteRT
  • รันไทม์ NeuroPilot (เป็นไลบรารีระบบในอุปกรณ์ Dimensity 9400 อยู่แล้ว จึงไม่ต้องพุช)

Google Tensor:

  • libLiteRtDispatch_GoogleTensor.so จากการเปิดตัวไลบรารีรันไทม์ของ NPU ใน LiteRT

สภาพแวดล้อมและตัวเลือก 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(
    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 ระบบจะเลือกไลบรารี QAIRT SDK โดยอัตโนมัติจากbazel-binทรี runfiles เมื่อตั้งค่า LITERT_QAIRT_SDK ในเวลาบิลด์ ตัวแปร 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 เพิ่มเติม

ดูข้อมูลเพิ่มเติม