HarmonyOS NEXT 上基于 CANN Kit 的 NPU 模型推理实战:Sobel 边缘检测应用解析(HDC_Sobel_Demo)
【免费下载链接】cann-recipes-harmony-infer本项目为鸿蒙开发者提供基于CANN平台的业务实践案例,方便开发者参考实现端云能力迁移及端侧推理部署。项目地址: https://gitcode.com/cann/cann-recipes-harmony-infer
导读
本文以 harmony_infer/harmony_os_next/Soble/readme_cn.md 为骨架,深入剖析一个完整的鸿蒙(HarmonyOS NEXT)端侧 AI 推理示例:通过 CANN Kit 提供的 NDK 接口(HiAI Foundation 与 NNCore),在手机 NPU 上运行 Sobel 边缘检测算子模型,并与 CPU 纯软件实现进行直观对比。读完本文,你将掌握:如何组织一个鸿蒙推理应用的工程目录、如何将 ATC 转换后的.omc离线模型放入应用资源、如何在 Native 层完成"设备枚举 → 模型加载 → 编译 → 建执行器 → 输入输出张量初始化 → 同步推理 → 取结果 → 卸载模型"的完整闭环,以及 CPU/NPU 两条推理路径的实现差异。
示例功能概述
本示例(工程名HDC_Sobel_Demo,目录 harmony_infer/harmony_os_next/Soble)展示了使用 CANN API 提供的模型推理能力,对图片中的物体执行 Sobel 边缘检测(滤波识别)。
Sobel 算子是图像处理中最经典的边缘检测算子之一,通过计算图像灰度在水平和垂直方向的一阶导数近似值来提取边缘。本项目中的 Sobel 计算存在两条实现路径:
- NPU 推理路径:将 Ascend C 实现的 Sobel 自定义算子 编译导出模型,在设备 NPU 上完成灰度化与 Sobel 梯度计算;
- CPU 路径:由 Native 层 C++ 代码直接对像素数据做灰度化与 Sobel 滤波,用于与 NPU 推理结果和耗时进行对比。
应用编译依赖 CANN 的两个动态库:libhiai_foundation.so(HiAI Foundation,提供模型管理与编译执行的高层封装)与libneural_network_core.so(AI 领域公共动态库,提供 NNCore 底层张量与执行器接口)。
效果预览
下表展示了示例运行时的三个界面状态(截图均取自仓库 screenshots 目录):
| 主界面 | 推理结果 | 下一张图片 |
|---|---|---|
主界面提供三个操作入口,对应原始文档中的使用说明:
- 点击"NPU推理"按钮:自动加载模型并将图片交由 NPU 处理,处理完成后展示滤波结果图与模型处理时间;
- 点击"CPU推理"按钮:将图片交由 CPU 完成同样的 Sobel 处理,展示处理结果图与 CPU 耗时;
- 点击"Click for next image"按钮:切换到测试图片列表中的下一张图片继续推理。
两个推理按钮共用同一张待处理原图,便于在相同输入下直观比较 CPU 与 NPU 的执行耗时。
工程目录结构
harmony_infer/harmony_os_next/Soble/entry/src/main // 代码区 ├── cpp │ ├── types/libentry │ │ └── Index.d.ts // native层接口注册文件(TS 类型声明) │ ├── SobelCustom.cpp // native api层接口的具体实现函数(NAPI 桥接 + CPU Sobel) │ ├── CMakeLists.txt // native层编译配置(链接 CANN 动态库) │ ├── HIAIModelManager.cpp // 模型管理类的实现(加载/编译/推理/卸载) │ ├── HIAIModelManager.h // 模型管理类的定义 ├── ets │ ├── entryability │ │ └── EntryAbility.ets // 程序入口类 │ ├── entrybackupability │ │ └── EntryBackupAbility.ets // 备份扩展能力 │ ├── pages │ │ └── Index.ets // 主界面展示类(UI 与推理调度逻辑) └── resources ├── base/media // 图片资源(cup.jpg、guitar.jpg 等测试图片) │ ├── cup.jpg │ └── guitar.jpg └── rawfile └── SobelCustom.omc // ATC 转换后的离线模型文件工程顶层还包含 AppScope/app.json5、module.json5(声明设备类型phone/tablet/2in1及 EntryAbility 等)与 oh-package.json5 等鸿蒙工程标配文件。
构建与使用说明
按原始文档,使用步骤如下:
- 准备模型:使用 DevEco 构建应用前,先将 ATC 工具转换后的
.omc离线模型文件放置到应用entry/src/main/resources/rawfile目录下(本示例中的模型文件名为SobelCustom.omc),再进行应用构建和安装。模型缺失或未放入该目录会导致运行时模型加载失败。 - 启动应用:在手机主屏幕点击应用图标(应用界面标题为 "CANN SobelFilter Demo"),启动后自动加载模型并等待推理操作。
- NPU 推理:点击 "NPU推理",将图片交给 NPU 处理,页面展示处理后的边缘检测结果图与模型处理时间。
- CPU 推理:点击 "CPU推理",将图片交由 CPU 完成同样的 Sobel 处理,页面展示结果与 CPU 处理时间。
- 切换图片:点击 "Click for next image",展示测试图片列表中的下一张图片。
- 退出清理:退出应用时自动卸载模型,释放 NPU 侧资源。
编译依赖配置
Native 层的链接关系定义在 entry/src/main/cpp/CMakeLists.txt 中,关键内容如下:
cmake_minimum_required(VERSION 3.5.0) project(HDC_Sobel_Demo) set(NATIVERENDER_ROOT_PATH ${CMAKE_CURRENT_SOURCE_DIR}) if(DEFINED PACKAGE_FIND_FILE) include(${PACKAGE_FIND_FILE}) endif() include_directories(${NATIVERENDER_ROOT_PATH} ${NATIVERENDER_ROOT_PATH}/include) include_directories(${HMOS_SDK_NATIVE}/sysroot/usr/lib) FIND_LIBRARY(hiai_foundation-lib hiai_foundation) add_library(entry SHARED SobelCustom.cpp HIAIModelManager.cpp) target_link_libraries(entry PUBLIC libace_napi.z.so libhilog_ndk.z.so librawfile.z.so ${hiai_foundation-lib} libneural_network_core.so)其中libace_napi.z.so提供 NAPI 桥接能力,libhilog_ndk.z.so提供 hilog 日志能力,librawfile.z.so用于读取 rawfile 中的模型文件,hiai_foundation与libneural_network_core.so即 CANN 的两个核心依赖库。
具体实现与 API 解析
本示例在 Native 层使用了两组核心接口:HiAI Foundation中hiai_options.h/hiai_helper.h/hiai_aipp_param.h/hiai_tensor.h定义的模型与选项接口,以及NNCore(neural_network_core.h)中定义的基础推理接口。完整涉及的 API 如下(与原始文档一致):
HiAI Foundation 扩展接口:
OH_NN_ReturnCode HMS_HiAIOptions_SetBandMode(OH_NNCompilation* compilation, HiAI_BandMode bandMode):设置编译选项的带宽模式(本示例使用HIAI_BANDMODE_NORMAL);HiAI_BandMode HMS_HiAIOptions_GetBandMode(const OH_NNCompilation* compilation):查询当前带宽模式;OH_NN_ReturnCode HMS_HiAIOptions_SetModelDeviceOrder(OH_NNCompilation* compilation, HiAI_ExecuteDevice* executeDevices, size_t deviceCount):设置模型执行设备顺序(本示例指定HIAI_EXECUTE_DEVICE_NPU);HiAI_Compatibility HMS_HiAICompatibility_CheckFromBuffer(const void* data, size_t size):校验离线模型缓冲与当前设备/框架的兼容性。
NNCore 设备与张量接口:
OH_NN_ReturnCode OH_NNDevice_GetAllDevicesID(const size_t **allDevicesID, uint32_t *deviceCount):枚举当前设备上所有可用的 NPU 设备 ID;OH_NN_ReturnCode OH_NNDevice_GetName(size_t deviceID, const char **name):查询指定设备 ID 的名称;void *OH_NNTensor_GetDataBuffer(const NN_Tensor *tensor):获取张量数据缓冲区地址;OH_NN_ReturnCode OH_NNTensor_GetSize(const NN_Tensor *tensor, size_t *size):获取张量数据字节数;OH_NN_ReturnCode OH_NNTensor_Destroy(NN_Tensor **tensor):销毁张量对象;NN_Tensor *OH_NNTensor_Create(size_t deviceID, NN_TensorDesc *tensorDesc):在指定设备上按描述符创建张量。
NNCore 编译与执行接口:
OH_NNCompilation *OH_NNCompilation_ConstructWithOfflineModelBuffer(const void *modelBuffer, size_t modelSize):基于离线模型内存缓冲创建编译对象;OH_NN_ReturnCode OH_NNCompilation_SetDevice(OH_NNCompilation *compilation, size_t deviceID):为编译对象指定执行设备;OH_NN_ReturnCode OH_NNCompilation_Build(OH_NNCompilation *compilation):执行模型编译;void OH_NNCompilation_Destroy(OH_NNCompilation **compilation):销毁编译对象;OH_NNExecutor *OH_NNExecutor_Construct(OH_NNCompilation *compilation):由编译结果创建执行器(同时完成模型加载);OH_NN_ReturnCode OH_NNExecutor_GetInputCount(const OH_NNExecutor *executor, size_t *inputCount):获取输入张量个数;NN_TensorDesc *OH_NNExecutor_CreateInputTensorDesc(const OH_NNExecutor *executor, size_t index):按索引创建输入张量描述符;OH_NN_ReturnCode OH_NNExecutor_GetOutputCount(const OH_NNExecutor *executor, size_t *outputCount):获取输出张量个数;NN_TensorDesc *OH_NNExecutor_CreateOutputTensorDesc(const OH_NNExecutor *executor, size_t index):按索引创建输出张量描述符;OH_NN_ReturnCode OH_NNExecutor_RunSync(OH_NNExecutor *executor, NN_Tensor *inputTensor[], size_t inputCount, NN_Tensor *outputTensor[], size_t outputCount):同步执行一次推理;void OH_NNExecutor_Destroy(OH_NNExecutor **executor):销毁执行器;OH_NN_ReturnCode OH_NNTensorDesc_Destroy(NN_TensorDesc **tensorDesc):销毁张量描述符。
模型管理类 HIAIModelManager
HIAIModelManager.h 定义了单例模型管理类(HIAIModelManager::GetInstance()),对外提供LoadModelFromBuffer、InitIOTensors、RunModel、GetResult、UnloadModel五个核心方法,内部维护执行器executor_、输入/输出张量数组以及设备 ID。
1. 设备选择
推理前需要先确定使用哪个 NPU 设备。GetDeviceID()(HIAIModelManager.cpp)通过OH_NNDevice_GetAllDevicesID枚举设备,再逐一用OH_NNDevice_GetName查询名称,匹配名为"HIAI_F"的设备:
size_t deviceID = 0; const size_t *allDevicesID = nullptr; uint32_t deviceCount = 0; OH_NN_ReturnCode ret = OH_NNDevice_GetAllDevicesID(&allDevicesID, &deviceCount); if (ret != OH_NN_SUCCESS || allDevicesID == nullptr) { OH_LOG_ERROR(LOG_APP, "OH_NNDevice_GetAllDevicesID failed"); return deviceID; } for (uint32_t i = 0; i < deviceCount; i++) { const char *name = nullptr; ret = OH_NNDevice_GetName(allDevicesID[i], &name); if (ret != OH_NN_SUCCESS || name == nullptr) { OH_LOG_ERROR(LOG_APP, "OH_NNDevice_GetName failed"); return deviceID; } if (std::string(name) == "HIAI_F") { deviceID = allDevicesID[i]; break; } } return deviceID;2. 模型加载与编译(LoadModelFromBuffer)
LoadModelFromBuffer(HIAIModelManager.cpp)实现了完整的模型初始化流程:
- 首先调用
HMS_HiAICompatibility_CheckFromBuffer校验模型缓冲的兼容性; - 调用
OH_NNCompilation_ConstructWithOfflineModelBuffer基于模型内存缓冲创建编译对象; - 调用
OH_NNCompilation_SetDevice将编译绑定到前面枚举到的HIAI_F设备; - 调用
SetModelBuildOptions设置编译选项(带宽模式 + 设备顺序,见下); - 调用
OH_NNCompilation_Build完成编译; - 调用
OH_NNExecutor_Construct创建执行器并加载模型; - 销毁编译对象,将设备 ID 保存到成员变量。
编译选项的设置在SetModelBuildOptions(HIAIModelManager.cpp)中完成:
// set bandmode OH_NN_ReturnCode ret = HMS_HiAIOptions_SetBandMode(compilation, HiAI_BandMode::HIAI_BANDMODE_NORMAL); if (ret != OH_NN_SUCCESS) { OH_LOG_ERROR(LOG_APP, "HMS_HiAIOptions_SetBandMode failed"); return ret; } HiAI_BandMode bandMode = HMS_HiAIOptions_GetBandMode(compilation); // set model execute device std::vector<HiAI_ExecuteDevice> executeDevices {HiAI_ExecuteDevice::HIAI_EXECUTE_DEVICE_NPU}; ret = HMS_HiAIOptions_SetModelDeviceOrder(compilation, executeDevices.data(), executeDevices.size()); if (ret != OH_NN_SUCCESS) { OH_LOG_ERROR(LOG_APP, "HMS_HiAIOptions_SetModelDeviceOrder failed"); return ret; } return OH_NN_SUCCESS;3. 输入输出张量初始化(InitIOTensors)
InitIOTensors(HIAIModelManager.cpp)根据执行器的输入/输出描述符创建张量,并写入输入数据:
- 通过
OH_NNExecutor_GetInputCount/OH_NNExecutor_CreateInputTensorDesc获取输入数量并创建输入描述符,再用OH_NNTensor_Create(deviceID_, tensorDesc)在 NPU 设备上创建输入张量; - 通过
SetInputTensorData获取每个输入张量的数据缓冲区(OH_NNTensor_GetDataBuffer)并校验大小后memcpy写入像素数据; - 对称地,通过
OH_NNExecutor_GetOutputCount/OH_NNExecutor_CreateOutputTensorDesc/OH_NNTensor_Create创建输出张量; - 每个张量创建完毕后调用
OH_NNTensorDesc_Destroy释放描述符。
4. 同步推理(RunModel)
RunModel(HIAIModelManager.cpp)核心只有一行调用:
OH_NN_ReturnCode ret = OH_NNExecutor_RunSync(executor_, inputTensors_.data(), inputTensors_.size(), outputTensors_.data(), outputTensors_.size());RunSync是同步接口,推理完成后输出张量缓冲区即包含推理结果。
5. 结果获取与模型卸载
GetResult(HIAIModelManager.cpp)遍历所有输出张量,将每个张量的数据缓冲区内容依次memcpy到调用方提供的outputDat中。UnloadModel(HIAIModelManager.cpp)先销毁输入/输出张量,再调用OH_NNExecutor_Destroy释放执行器,实现退出时的模型卸载。
NAPI 桥接层 SobelCustom.cpp
SobelCustom.cpp 通过 NAPI 将 Native 能力暴露给 ArkTS 侧,注册的接口声明在 types/libentry/Index.d.ts:
export const LoadModel : (resMgr : resourceManager.ResourceManager) => number export const processImageWithSobel: (buffer: Uint8Array, width: number, height: number) => Uint8Array; export const InitIOTensors : (input : Uint8Array) => number export const GetSobelResult : () => Uint8Array export const RunModel : () => number export const GetResult : () => string[] export const UnloadModel : () => numberNAPI 层共实现 7 个函数(SobelCustom.cpp):
LoadModel:通过OH_ResourceManager_InitNativeResourceManager/OH_ResourceManager_OpenRawFile从 rawfile 中读取SobelCustom.omc模型文件,构造模型缓冲后调用HIAIModelManager::GetInstance().LoadModelFromBuffer;processImageWithSobel:CPU 路径的 Sobel 实现,接收Uint8Array(BGRA_8888 格式)与宽高,返回处理后的图像数据,并记录 CPU 耗时到全局变量cpuRunTime;InitIOTensors/RunModel/GetSobelResult/GetResult/UnloadModel:分别桥接模型管理类的对应方法;其中RunModel同时记录 NPU 推理耗时到全局变量npuRunTime,GetResult将 CPU/NPU 耗时以字符串数组形式返回给 ArkTS 侧展示。
CPU 路径的 Sobel 算法实现
CPU 路径(NAPI_Global_processImageWithSobel,SobelCustom.cpp)在 Native 层用纯 C++ 实现了经典 Sobel 边缘检测三步流程:
- BGRA → 灰度:解析 BGRA 通道(B 为通道 0、G 为通道 1、R 为通道 2),按 ITU-R BT.601 标准加权
gray = 0.299R + 0.587G + 0.114B; - Sobel 梯度计算:使用 X 方向卷积核
[-1 0 1; -2 0 2; -1 0 1]与 Y 方向卷积核[-1 -2 -1; 0 0 0; 1 2 1],对每个像素(边缘留白 1 像素)计算sobelX与sobelY; - 融合与量化:以曼哈顿距离
|dx| + |dy|作为边缘强度,乘系数0.3f并限制最大值到 1.0,再量化回 0~255,写入 BGRA_8888 输出缓冲(Alpha 置 255 不透明)。
从源码结构可以推断,该 CPU 实现与本仓库 Sobel 自定义算子 中 NPU 上RGB2Gray(0.299*r + 0.587*g + 0.114*b)与 Sobel-x/y 梯度融合(|dx| + |dy|)的计算逻辑是一致的,这保证了 CPU 与 NPU 两条路径产出的边缘检测结果在算法语义上可相互对照。
主界面调度逻辑 Index.ets
Index.ets 是 ArkTS 侧的 UI 与推理调度入口,核心逻辑如下:
- NPU 推理流程(Index.ets):
- 通过
resourceManager.getMediaContent读取当前测试图片; - 用 ImageKit 创建 PixelMap 并按预处理尺寸1024×763缩放(
npuPreProcessImageWidth/Height); - 将 RGBA 像素重排为NHWC 的 RGB 三通道数据(注释标明该输入格式与 Sobel 算子的
1*763*1024*3规格对应); - 依次调用
testNapi.InitIOTensors(inputData)→testNapi.RunModel()→testNapi.GetResult(); - 调用
testNapi.GetSobelResult()取得输出灰度图(尺寸 1022×761,npuPostProcessImageWidth/Height),经grayToRGB888转成 RGB_888 后创建 PixelMap 并缩放回原图尺寸展示,同时显示 "NPU运行时间"。
- 通过
- CPU 推理流程(Index.ets):读取图片 PixelMap 后直接调用
testNapi.processImageWithSobel,将返回的图像缓冲创建为 PixelMap 展示,并显示 "CPU运行时间"。 - 图片切换:
imagesList中预置了 5 张测试图片(cup、guitar、airplane、wall_after、home_after,均位于 resources/base/media),点击 "Click for next image" 在列表中循环切换。
算子侧:SobelCustom 自定义算子
NPU 路径所用的模型源于本仓库的 Ascend C 自定义算子工程 ops/ascendc/src/sobel_custom,相关说明见 custom-npu_sobel.md:
- 背景:图像 CV 前处理计算灵活多变,常规 NN 模型难以在 NPU 上直接处理;该方案使用 Ascend C 实现 CV 领域的 Sobel 计算,将前处理搬移到 NPU 上执行;
- 算子规格:支持
1*763*1024*3输入规格,输出为1*761*1022(高宽各减 2,对应 3×3 卷积核的边界裁剪); - 支持的处理器:Kirin X90 与 Kirin 9030 系列产品;
- 算子实现:kernel 侧(sobel_custom.cpp)按
Process → CopyIn → Compute → CopyOut流水完成 NHWC→NCHW 转置、u8→half 转换、RGB2Gray(0.299R+0.587G+0.114B)、Sobel-x/y 梯度计算、|dx|+|dy|融合与 half→u8 量化;host 侧(sobel_custom.cpp)通过 Tiling 将数据按9×256×3的 tile 切分,并定义了 sobel_custom_tiling.h 中的 Tiling 数据结构。
该算子经 ATC 工具转换导出为.omc离线模型(即SobelCustom.omc)后,即为本示例 NPU 推理路径的模型来源,形成"Ascend C 算子开发 → 模型转换 → 鸿蒙应用端侧推理"的完整链路。
相关权限与依赖
- 相关权限:不涉及(本示例无需申请任何系统敏感权限)。
- 依赖:不涉及第三方组件依赖,仅依赖 HarmonyOS SDK 提供的 CANN NDK 动态库(
libhiai_foundation.so、libneural_network_core.so)。
约束与限制
- 本示例仅支持标准系统上运行,支持设备:华为手机、平板和 2in1(对应 module.json5 中声明的
deviceTypes:phone、tablet、2in1); - HarmonyOS 系统版本:HarmonyOS 5.0.1 Release 及以上;
- DevEco Studio 版本:DevEco Studio 6.0.0 Release 及以上;
- HarmonyOS SDK 版本:HarmonyOS 6.0.0 Release SDK 及以上。
同时需注意:NPU 推理依赖设备具备 NPU 能力(枚举HIAI_F设备成功),且模型文件与算子规格(1*763*1024*3)需与运行时预处理尺寸保持一致,否则张量大小校验(SetInputTensorData中的dataSize != inputData[i].second)会返回失败。
总结
通过本示例可以看到一条清晰的鸿蒙端侧 AI 推理落地路径:Ascend C 自定义算子 → ATC 转换导出 .omc 离线模型 → 鸿蒙应用 rawfile 资源打包 → NNCore/HiAI Foundation NDK 接口加载编译 → OH_NNExecutor_RunSync 同步推理 → 结果回传 ArkTS 展示。其中模型管理类 HIAIModelManager.cpp 与 NAPI 桥接 SobelCustom.cpp 是可直接复用的核心骨架,CPU 与 NPU 双路径的设计也为评估 NPU 加速收益提供了可对比的基线实现。
【免费下载链接】cann-recipes-harmony-infer本项目为鸿蒙开发者提供基于CANN平台的业务实践案例,方便开发者参考实现端云能力迁移及端侧推理部署。项目地址: https://gitcode.com/cann/cann-recipes-harmony-infer
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考