博客 / 詳情

返回

京東金融鴻蒙端部署AI超分模型實踐

1. 背景

這可能是全網第一篇完整講解鴻蒙端使用CANN部署AI模型的文章, 滿滿乾貨。

社區作為用户交流、信息傳遞的核心載體,圖片內容(如理財產品截圖、投資經驗分享配圖、用户互動評論圖片等)的展示質量直接影響用户的信息獲取效率與平台信任感。從京東金融App社區的業務需求來看,當前用户上傳圖片普遍存在多樣性失真問題:部分用户通過老舊設備拍攝的圖片分辨率較低,部分用户為節省流量選擇低畫質壓縮上傳,還有部分截圖類內容因原始來源清晰度不足導致信息模糊(如理財產品收益率數字、合同條款細節等),這些問題不僅降低了內容可讀性,還可能因信息傳遞不清晰引發用户誤解。

京東金融App團隊已完成Real-ESRGAN-General-x4v3超分辨率模型在安卓端的部署,能夠針對性提升評論區、內容詳情頁、個人主頁等核心場景的圖片清晰度,從視覺體驗層面優化用户留存與互動意願。

ESRGAN-General-x4v3模型在安卓端的部署,採用的是ONNX框架,該方案已有大量公開資料可參考,且取得顯著業務成效。但鴻蒙端部署面臨核心技術瓶頸:鴻蒙系統不支持ONNX框架,部署端側AI僅能使用華為自研的CANN(Compute Architecture for Neural Networks)架構,且當前行業內缺乏基於CANN部署端側AI的公開資料與成熟方案,全程需技術團隊自主探索。接下來我會以ESRGAN-General-x4v3為例, 分享從模型轉換(NPU親和性改造)到端側離線模型部署的全部過程。

2. 部署前期準備

2.1 離線模型轉換

CANN Kit當前僅支持Caffe、TensorFlow、ONNX和MindSpore模型轉換為離線模型,其他格式的模型需要開發者自行轉換為CANN Kit支持的模型格式。模型轉換為OM離線模型,移動端AI程序直接讀取離線模型進行推理。

2.1.1 下載CANN工具

從鴻蒙開發者官網下載 DDK-tools-5.1.1.1 , 解壓使用Tools下的OMG工具,將ONNX、TensorFlow模型轉換為OM模型。(OMG工具位於Tools下載的tools/tools\_omg下,僅可運行在64位Linux平台上。)



2.1.2 下載ESRGAN-General-x4v3模型文件

從https://aihub.qualcomm.com/compute/models/real\_esrgan\_general\_x4v3 下載模型的onnx文件.

注意: 下載鏈接中的a8a8的量化模型使用了高通的算子(親測無法轉換), CANN工具無法進行轉換, 因此請下載float的量化模型。

下載後有兩個文件:

•model.onnx文件 (模型結構): 包含計算圖、opset版本、節點配置等,文件較小。

•model.data文件 (權重數據): 包含神經網絡參數、權重等,文件較大。

現在我們需要把這種分離文件格式的模型合併成一個文件,後續的操作都使用這個。

合併文件:

請使用JoyCode寫個合併腳本即可, 提示詞: 請寫一個腳本, 把onnx模型文件的.onnx和.data文件合併。

2.1.3 OM模型轉換

1. ONNX opset 版本轉換

當前使用CANN進行模型轉換, 支持ONNX opset版本7\~18(最高支持到V1.13.1), 首先需要查看原始的onnx模型的opset版本是否在支持範圍, 這裏我們使用Netron(點擊下載)可視化工具進行查看。

在這裏插入圖片描述





目前該模型使用的opset版本是20, 因此我們需要把該模型的opset版本轉成18, 才可以用CANN轉換成鴻蒙上可部署的模型。請使用JoyCode寫個opset轉換腳本即可, 提示詞: 請寫一個腳本, 把onnx模型文件的opset版本從20轉換成18。



2. OM離線模型****

命令行中的參數説明請參見OMG參數,轉換命令:

./tools/tools_omg/omg --model new_model_opset18.onnx --framework 5 --output ./model

轉換完成後, 生成model.om的模型文件, 該模型文件就是鴻蒙上可以正常使用的模型文件

2.2 查看模型的輸入/輸出張量信息

部署AI模式時, 我們需要確認模型的輸入張量和輸出張量信息, 請使用JoyCode編寫一個腳本, 確定輸入輸出張量信息, 提示詞: 寫一個腳本查看onnx模型的輸入輸出張量信息。

在這裏插入圖片描述

2.2.1 輸入張量

BCHW格式, 是深度學習中常見的張量維度排列格式, 在圖像處理場景中:

•B (Batch): 批次大小 - 一次處理多少個樣本。

•C (Channel): 通道數 - 圖像的顏色通道數。

•H (Height): 高度 - 圖像的像素高度。

•W (Width): 寬度 - 圖像的像素寬度。

由此可以得出結論, 該模型1個批次處理1張寬高為128*128的RGB圖片(因為C是3,因此不包含R通道)。



2.2.2 輸出張量

該模型1個批次輸出1張寬高為512*512的RGB圖片。



2.2.3 BCHW和BHWC格式的區別:

超分模型中的BCHW和BHWC是兩種不同的張量存儲格式,主要區別在於通道維度的位置:



BCHW格式(Batch-Channel-Height-Width)

◦維度順序:[批次, 通道, 高度, 寬度]

◦內存佈局:通道維度在空間維度之前

◦常用框架:PyTorch、TensorRT等

示例: 形狀為 (1, 3, 256, 256) 的RGB圖像

內存中的存儲順序: R通道的所有像素 -> G通道的所有像素 -> B通道的所有像素

tensor_bchw = torch.randn(1, 3, 256, 256)
訪問第一個像素的RGB值需要跨越不同的內存區域
pixel_0_0_r = tensor_bchw[0, 0, 0, 0]  # R通道
pixel_0_0_g = tensor_bchw[0, 1, 0, 0]  # G通道  
pixel_0_0_b = tensor_bchw[0, 2, 0, 0]  # B通道

BHWC格式(Batch-Height-Width-Channel)

◦維度順序:[批次, 高度, 寬度, 通道]

◦內存佈局:通道維度在最後,像素的所有通道連續存儲

◦常用框架:TensorFlow、OpenCV等

示例:形狀為 (1, 256, 256, 3) 的RGB圖像

內存中的存儲順序:像素(0,0)的RGB -> 像素(0,1)的RGB -> ... -> 像素(0,255)的RGB -> 像素(1,0)的RGB...

tensor_bhwc = tf.random.normal([1, 256, 256, 3])
# 訪問第一個像素的RGB值在連續的內存位置
pixel_0_0_rgb = tensor_bhwc[0, 0, 0, :]  # [R, G, B]



3. 鴻蒙端部署核心步驟

3.1 創建項目

1.創建DevEco Studio項目,選擇“Native C++”模板,點擊“Next”。

在這裏插入圖片描述



2.按需填寫“Project name”、“Save location”和“Module name”,選擇“Compile SDK”為“5.1.0(18)”及以上版本,點擊“Finish”。

在這裏插入圖片描述

3.2 配置項目NAPI

CANN部署只提供了C++接口, 因此需要使用NAPI, 編譯HAP時,NAPI層的so需要編譯依賴NDK中的libneural\_network\_core.so和libhiai\_foundation.so。



頭文件引用

按需引用NNCore和CANN Kit的頭文件。

#include "neural_network_runtime/neural_network_core.h"
#include "CANNKit/hiai_options.h"

編寫CMakeLists.txt

CMakeLists.txt示例代碼如下。

cmake_minimum_required(VERSION 3.5.0)
project(myNpmLib)

set(NATIVERENDER_ROOT_PATH ${CMAKE_CURRENT_SOURCE_DIR})

include_directories(${NATIVERENDER_ROOT_PATH}
                    ${NATIVERENDER_ROOT_PATH}/include)

include_directories(${HMOS_SDK_NATIVE}/sysroot/usr/lib)
FIND_LIBRARY(cann-lib hiai_foundation)

add_library(imagesr SHARED HIAIModelManager.cpp ImageSuperResolution.cpp)
target_link_libraries(imagesr PUBLIC libace_napi.z.so
    libhilog_ndk.z.so
    librawfile.z.so
    ${cann-lib}
    libneural_network_core.so
    )

3.3 集成模型

模型的加載、編譯和推理主要是在native層實現,應用層主要作為數據傳遞和展示作用。模型推理之前需要對輸入數據進行預處理以匹配模型的輸入,同樣對於模型的輸出也需要做處理獲取自己期望的結果


在這裏插入圖片描述

3.3.1 加載離線模型

為了讓App運行時能夠讀取到模型文件和處理推理結果,需要先把離線模型和模型對應的結果標籤文件預置到工程的“entry/src/main/resources/rawfile”目錄中。

在這裏插入圖片描述



在App應用創建時加載模型:

1.native層讀取模型的buffer。

const char* modelPath = "imagesr.om";
RawFile *rawFile = OH_ResourceManager_OpenRawFile(resourceMgr, modelPath);
long modelSize = OH_ResourceManager_GetRawFileSize(rawFile);
std::unique_ptr<uint8_t[]> modelData = std::make_unique<uint8_t[]>(modelSize);
int res = OH_ResourceManager_ReadRawFile(rawFile, modelData.get(), modelSize);

2.使用模型的buffer, 調用OH\_NNCompilation\_ConstructWithOfflineModelBuffer創建模型的編譯實例

HiAI_Compatibility compibility = HMS_HiAICompatibility_CheckFromBuffer(modelData, modelSize);
OH_NNCompilation *compilation = OH_NNCompilation_ConstructWithOfflineModelBuffer(modelData, modelSize);

3.(可選)根據需要調用HMS\_HiAIOptions\_SetOmOptions接口,打開維測功能(如Profiling)。

const char *out_path = "/data/storage/el2/base/haps/entry/files";
HiAI_OmType omType = HIAI_OM_TYPE_PROFILING;
OH_NN_ReturnCode ret = HMS_HiAIOptions_SetOmOptions(compilation, omType, out_path);     

4.設置模型的deviceID。

size_t deviceID = 0;
const size_t *allDevicesID = nullptr;
uint32_t deviceCount = 0;
OH_NN_ReturnCode ret = OH_NNDevice_GetAllDevicesID(&allDevicesID, &deviceCount);

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;
    }
}

ret = OH_NNCompilation_SetDevice(compilation, deviceID);

5.調用OH\_NNCompilation\_Build,執行模型編譯。

ret = SetModelBuildOptions(compilation);
ret = OH_NNCompilation_Build(compilation);

6.調用OH\_NNExecutor\_Construct,創建模型執行器。

executor_ = OH_NNExecutor_Construct(compilation);

7.調用OH\_NNCompilation\_Destroy,釋放模型編譯實例。



3.3.2 準備輸入輸出****Tensor

1.處理模型的輸入,模型的輸入為13128*128格式(BCHW) Float類型的數據, 需要把RGB 數據轉成BCHW格式並進行歸一化。

從圖片中讀取的RGB數據為BHWC,需要轉換成模型可以識別的BCHW
/**
 * 把bhwc轉成bchw
 */
uint8_t *rgbData = static_cast<uint8_t*>(data);
uint8_t *floatData_tmp = new uint8_t[length];
for (int c = 0; c < 3; ++c) {
    for (int h = 0; h < 128; ++h) {
        for (int w = 0; w < 128; ++w) {
            // HWC 索引: h * width * channels + w * channels +c 
            int hwc_index = h * 128 * 3 + w * 3 + c;
            // CHW 索引: C * height * width + h* width + W
            int chw_index = c * 128 * 128 + h * 128 + w;
            floatData_tmp[chw_index] = rgbData[hwc_index];
        }
    }
}
//歸一化
float *floatData = new float[length];
for (size_t i = 0; i < length; ++i) {
    floatData[i] = static_cast<float>(floatData_tmp[i])/ 255.0f;
}

2.創建模型的輸入和輸出Tensor,並把應用層傳遞的數據填充到輸入的Tensor中

// 準備輸入張量
size_t inputCount = 0;
OH_NN_ReturnCode ret = OH_NNExecutor_GetInputCount(executor_, &inputCount);
for (size_t i = 0; i < inputCount; ++i) {
    NN_TensorDesc *tensorDesc = OH_NNExecutor_CreateInputTensorDesc(executor_, i);
    NN_Tensor *tensor = OH_NNTensor_Create(deviceID_, tensorDesc);
    if (tensor != nullptr) {
        inputTensors_.push_back(tensor);
    }
    OH_NNTensorDesc_Destroy(&tensorDesc);
}


ret = SetInputTensorData(inputTensors_, inputData);

// 準備輸出張量
size_t outputCount = 0;
ret = OH_NNExecutor_GetOutputCount(executor_, &outputCount);

for (size_t i = 0; i < outputCount; i++) {
    NN_TensorDesc *tensorDesc = OH_NNExecutor_CreateOutputTensorDesc(executor_, i);
    NN_Tensor *tensor = OH_NNTensor_Create(deviceID_, tensorDesc);
    if (tensor != nullptr) {
        outputTensors_.push_back(tensor);
    }
    OH_NNTensorDesc_Destroy(&tensorDesc);
}
if (outputTensors_.size() != outputCount) {
    DestroyTensors(inputTensors_);
    DestroyTensors(outputTensors_);
    OH_LOG_ERROR(LOG_APP, "output size mismatch.");
    return OH_NN_FAILED;
}



3.3.3 進行推理

調用OH\_NNExecutor\_RunSync,完成模型的同步推理。

OH_NN_ReturnCode ret = OH_NNExecutor_RunSync(executor_, inputTensors_.data(), inputTensors_.size(),
                                                 outputTensors_.data(), outputTensors_.size());

説明

•如果不更換模型,則首次編譯加載完成後可多次推理,即一次編譯加載,多次推理。

•所有關於模型的操作, 均無法多線程執行。



3.3.4 獲取模型輸出並處理數據

1.調用OH\_NNTensor\_GetDataBuffer,獲取輸出的Tensor,在輸出Tensor中會得到模型的輸出數據。

// 獲取第一個輸出張量
NN_Tensor* tensor = outputTensors_[0];

// 獲取張量數據緩衝區
void *tensorData = OH_NNTensor_GetDataBuffer(tensor);

// 獲取張量大小
size_t size = 0;
OH_NN_ReturnCode ret = OH_NNTensor_GetSize(tensor, &size);

float *tensorDataOutput = (float*)malloc(size);
// 將tensorData的數據一次性複製到tensorDataOutput中
memcpy(tensorDataOutput, tensorData, size);



2.對Tensor輸出數據進行相應的處理

把模型輸出的BCHW轉成BHWC, 並進行反歸一化處理



//把模型輸出的BCHW轉成BHWC
float *outputResult = static_cast<float *>(tensorData);
float *output_tmp = new float[size/sizeof(float)];
for (int h = 0; h < 512; ++h) {
    for (int w = 0; w < 512; ++w) {
        for (int c = 0; c < 3; ++c) {
            output_tmp[h * 512 * 3 + w* 3 + c] = outputResult[c * 512 * 512 + h * 512 + w];
        }
    }
}
std::vector<float> output(size / sizeof(float), 0.0);
for (size_t i = 0; i < size / sizeof(float); ++i) {
    output[i] = output_tmp[i];
}
delete [] output_tmp;


 // 計算總的數據大小
size_t totalSize = output.size();

// 分配結果數據內存
std::unique_ptr<uint8_t[]> result_data = std::make_unique<uint8_t[]>(totalSize);

// 將float數據轉換為uint8_t (反歸一化)
size_t index = 0;
for (float value : result) {
    // 將float值轉換為uint8_t (0-255範圍)
    float scaledValue = value * 255.0f;
    scaledValue = std::max(0.0f, std::min(255.0f, scaledValue));
    result_data[index++] = static_cast<uint8_t>(scaledValue);
}

result_data 就是最終的超分數據,可以正常顯示



4. 總結與技術展望

京東金融App在鴻蒙端部署Real-ESRGAN-General-x4v3超分辨率模型的完整實踐過程,成功解決了ONNX模型到OM離線模型轉換、BCHW與BHWC張量格式處理、以及基於CANN Kit和NAPI的完整部署鏈路等關鍵技術難題。

展望端智能的未來發展,隨着芯片算力的指數級增長、模型壓縮技術的突破性進展以及邊緣計算架構的日趨成熟,端側設備將從單純的數據採集終端演進為具備強大推理能力的智能計算節點,通過實現多模態AI融合、實時個性化學習、隱私保護計算和跨設備協同等核心能力,將大語言模型、計算機視覺、語音識別等AI技術深度集成到移動設備中,構建起無需聯網即可提供智能服務的自主計算生態,推動人機交互從被動響應向主動感知、預測和服務的範式轉變,最終開啓真正意義上的普惠人工智能時代。

user avatar
0 位用戶收藏了這個故事!

發佈 評論

Some HTML is okay.