标签 ONNX 下的文章

基于YOLOv8的工业织物瑕疵检测识别|完整源码数据集+PyQt5界面+完整训练流程+开箱即用!

源码包含:完整YOLOv8训练代码+数据集(带标注)+权重文件+直接可允许检测的yolo检测程序+直接部署教程/训练教程

基本功能演示

https://www.bilibili.com/video/BV1G1r6BuEga/

源码在哔哩哔哩视频简介处。

项目摘要

在纺织制造与高端材料加工过程中,织物表面瑕疵直接影响产品质量等级与出厂合格率。尤其对于 C1 类高精细织物(如粘胶纤维、丝绸等),其表面纹理极弱、结构特征不明显,传统基于规则或人工经验的检测方法在复杂光照与高速产线条件下,往往难以实现稳定、精准的瑕疵识别。

本项目基于 YOLOv8 目标检测模型,构建了一套 工业织物瑕疵智能检测与识别系统,面向弱纹理背景下的织物表面缺陷场景,实现对 洞(Hole)异物(Foreign Object)油斑(Oil Stain)织线错误(Weaving Defect) 四类典型工业瑕疵的自动检测与定位。系统集成 PyQt5 图形化界面,支持图片、文件夹、视频及摄像头等多种输入方式,便于在实验环境与实际产线场景中使用。

项目提供 完整可运行源码、标准化标注数据集、训练权重文件以及详细的训练与部署说明,实现从模型训练到检测应用的完整闭环,适用于工业视觉检测研究、质量控制系统原型开发及相关课程与毕业设计。

前言

随着制造业向高端化与智能化方向持续升级,基于计算机视觉的自动缺陷检测已成为工业质量控制中的核心技术之一。相比具有明显纹理与结构特征的金属或印刷表面,高精细织物表面往往呈现弱纹理、低对比度、特征细微等特点,对检测算法的特征提取能力与鲁棒性提出了更高要求。

在实际生产中,洞、油斑或织线错误等缺陷尺寸较小、形态多变,且在不同光照条件下视觉特征差异明显,传统机器视觉方法依赖人工设定阈值与规则,泛化能力有限。而深度学习目标检测模型,尤其是以 YOLO 系列为代表的端到端检测框架,在复杂背景与小目标检测任务中展现出显著优势。

YOLOv8 在网络结构设计、特征融合与训练策略方面进行了多项优化,在保证检测精度的同时兼顾推理速度与工程可部署性,非常适合工业产线实时或准实时检测需求。本项目结合真实工业织物瑕疵数据,对 YOLOv8 在弱纹理缺陷检测场景下的应用进行系统化实践,为工业视觉检测提供可复现的工程参考。

一、软件核心功能介绍及效果演示

1. 多类别工业织物瑕疵检测

系统基于 YOLOv8 目标检测模型,实现对工业织物表面多种缺陷的自动识别与定位,支持以下四类瑕疵:

  • Hole(洞)
  • Foreign Object(异物)
  • Oil Stain(油斑)
  • Weaving Defect(织线错误)

检测结果以边界框形式叠加显示在原始图像或视频画面上,并同步标注瑕疵类别与置信度,便于质量检测人员快速判断缺陷类型与位置。


2. 多输入源缺陷检测模式

系统支持多种输入方式,满足不同应用阶段的检测需求:

  • 单张图片检测:用于样本分析与算法验证
  • 图片文件夹批量检测:适用于离线质量抽检
  • 视频文件检测:模拟连续产线检测过程
  • 实时摄像头检测:满足工业现场在线检测需求

所有检测模式均可通过图形界面一键切换,无需修改代码。


3. PyQt5 工业视觉检测界面

项目基于 PyQt5 构建桌面端可视化界面,主要功能包括:

  • 模型权重加载与管理
  • 检测模式与输入源选择
  • 实时检测画面显示
  • 缺陷识别结果与运行状态提示

该界面降低了模型使用门槛,使算法工程人员与现场技术人员均可快速完成检测任务。


4. 完整训练流程与工程复现能力

项目提供完整的 YOLOv8 训练与推理流程,包含:

  • 标准 YOLO 格式的工业织物瑕疵数据集
  • 类别配置文件与训练参数示例
  • 模型训练、验证与测试脚本
  • 训练完成的权重文件与推理程序

用户可基于现有数据进行二次训练或扩展新瑕疵类别,具备良好的工程扩展性与研究价值。


5. 实际检测效果说明

在弱纹理、高相似度背景的工业织物图像中,系统能够稳定检测不同类型的细微瑕疵,对小尺寸缺陷与低对比度异常具有较好的识别能力,适用于织物质量检测、生产过程监控及缺陷数据统计分析等工业应用场景。

二、软件效果演示

为了直观展示本系统基于 YOLOv8 模型的检测能力,我们设计了多种操作场景,涵盖静态图片、批量图片、视频以及实时摄像头流的检测演示。

(1)单图片检测演示

用户点击“选择图片”,即可加载本地图像并执行检测:

image-20260113004758526


(2)多文件夹图片检测演示

用户可选择包含多张图像的文件夹,系统会批量检测并生成结果图。

image-20260113004907933


(3)视频检测演示

支持上传视频文件,系统会逐帧处理并生成目标检测结果,可选保存输出视频:

image-20260113004922807


(4)摄像头检测演示

实时检测是系统中的核心应用之一,系统可直接调用摄像头进行检测。由于原理和视频检测相同,就不重复演示了。

image-20260113004937406


(5)保存图片与视频检测结果

用户可通过按钮勾选是否保存检测结果,所有检测图像自动加框标注并保存至指定文件夹,支持后续数据分析与复审。

image-20260113004954026

三、模型的训练、评估与推理

YOLOv8是Ultralytics公司发布的新一代目标检测模型,采用更轻量的架构、更先进的损失函数(如CIoU、TaskAlignedAssigner)与Anchor-Free策略,在COCO等数据集上表现优异。
其核心优势如下:

  • 高速推理,适合实时检测任务
  • 支持Anchor-Free检测
  • 支持可扩展的Backbone和Neck结构
  • 原生支持ONNX导出与部署

3.1 YOLOv8的基本原理

YOLOv8 是 Ultralytics 发布的新一代实时目标检测模型,具备如下优势:

  • 速度快:推理速度提升明显;
  • 准确率高:支持 Anchor-Free 架构;
  • 支持分类/检测/分割/姿态多任务
  • 本项目使用 YOLOv8 的 Detection 分支,训练时每类表情均标注为独立目标。

YOLOv8 由Ultralytics 于 2023 年 1 月 10 日发布,在准确性和速度方面具有尖端性能。在以往YOLO 版本的基础上,YOLOv8 引入了新的功能和优化,使其成为广泛应用中各种物体检测任务的理想选择。

image-20250526165954475

YOLOv8原理图如下:

image-20250526170118103

3.2 数据集准备与训练

采用 YOLO 格式的数据集结构如下:

dataset/
├── images/
│   ├── train/
│   └── val/
├── labels/
│   ├── train/
│   └── val/

每张图像有对应的 .txt 文件,内容格式为:

4 0.5096721233576642 0.352838390077821 0.3947600423357664 0.31825755058365757

分类包括(可自定义):

、

image-20260113005044200

3.3. 训练结果评估

训练完成后,将在 runs/detect/train 目录生成结果文件,包括:

  • results.png:损失曲线和 mAP 曲线;
  • weights/best.pt:最佳模型权重;
  • confusion_matrix.png:混淆矩阵分析图。
若 mAP@0.5 达到 90% 以上,即可用于部署。

在深度学习领域,我们通常通过观察损失函数下降的曲线来评估模型的训练状态。YOLOv8训练过程中,主要包含三种损失:定位损失(box_loss)、分类损失(cls_loss)和动态特征损失(dfl_loss)。训练完成后,相关的训练记录和结果文件会保存在runs/目录下,具体内容如下:

image-20260113005059867

3.4检测结果识别

使用 PyTorch 推理接口加载模型:

import cv2
from ultralytics import YOLO
import torch
from torch.serialization import safe_globals
from ultralytics.nn.tasks import DetectionModel

# 加入可信模型结构
safe_globals().add(DetectionModel)

# 加载模型并推理
model = YOLO('runs/detect/train/weights/best.pt')
results = model('test.jpg', save=True, conf=0.25)

# 获取保存后的图像路径
# 默认保存到 runs/detect/predict/ 目录
save_path = results[0].save_dir / results[0].path.name

# 使用 OpenCV 加载并显示图像
img = cv2.imread(str(save_path))
cv2.imshow('Detection Result', img)
cv2.waitKey(0)
cv2.destroyAllWindows()

预测结果包含类别、置信度、边框坐标等信息。

image-20260113005141716

四.YOLOV8+YOLOUI完整源码打包

本文涉及到的完整全部程序文件:包括python源码、数据集、训练代码、UI文件、测试图片视频等(见下图),获取方式见【4.2 完整源码下载】:

4.1 项目开箱即用

作者已将整个工程打包。包含已训练完成的权重,读者可不用自行训练直接运行检测。

运行项目只需输入下面命令。

python main.py

读者也可自行配置训练集,或使用打包好的数据集直接训练。

自行训练项目只需输入下面命令。

yolo detect train data=datasets/expression/loopy.yaml model=yolov8n.yaml pretrained=yolov8n.pt epochs=100 batch=16 lr0=0.001

4.2 完整源码

至项目实录视频下方获取:https://www.bilibili.com/video/BV1G1r6BuEga/

image-20250801135823301

包含:

📦完整项目源码

📦 预训练模型权重

🗂️ 数据集地址(含标注脚本)

总结

本文围绕 基于 YOLOv8 的工业织物瑕疵检测识别系统,从数据集特点、模型选型到系统工程实现进行了系统性阐述。项目针对 C1 类高精细、弱纹理织物表面这一工业视觉中的典型难点场景,实现了对 洞、异物、油斑及织线错误 等多类微小缺陷的自动检测与精准定位,有效提升了织物质量检测的稳定性与一致性。

在工程实践层面,项目不仅验证了 YOLOv8 在弱纹理缺陷检测任务中的适用性,还通过 PyQt5 图形化界面将算法能力转化为可直接使用的检测工具,支持多输入源与完整训练流程,具备良好的可复现性与可扩展性。整体方案可作为工业视觉检测、制造业质量控制系统原型以及相关教学与科研实验的参考实现,为推动传统织物检测向智能化、自动化方向升级提供了可落地的技术路径。

在这里插入代码片作者:高阔

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技术深度集成到移动设备中,构建起无需联网即可提供智能服务的自主计算生态,推动人机交互从被动响应向主动感知、预测和服务的范式转变,最终开启真正意义上的普惠人工智能时代。

在这里插入代码片作者:高阔

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技术深度集成到移动设备中,构建起无需联网即可提供智能服务的自主计算生态,推动人机交互从被动响应向主动感知、预测和服务的范式转变,最终开启真正意义上的普惠人工智能时代。