鸿蒙开发--CANNKit-AscendC-sobel
AscendC 算子开发:基于 NPU 的 Sobel 边缘检测实现 摘要: 本文介绍了使用华为 AscendC 框架在 NPU 上实现 Sobel 边缘检测算子的完整开发流程。内容涵盖: AscendC 算子开发环境搭建(需 HarmonyOS 5.0.5+) 项目结构解析(Host/Kernel 分离架构) Sobel 算法原理(XY 方向滤波核及曼哈顿距离融合) 关键实现技术: 使用 Vec
HarmonyOS AscendC 算子:用 NPU 实现图像边缘检测
什么是 AscendC 算子
前面我们介绍了很多图形渲染相关的技术,这篇来看看 AI 领域的东西。AscendC 是华为提供的一种 NPU(神经网络处理器)编程框架,让你可以自己写算子在 NPU 上运行。
算子是什么?简单说就是一种计算操作。比如加法是一个算子,乘法也是一个算子。在 AI 和图像处理领域,有很多复杂的算子,比如卷积、池化等。AscendC 让你可以自己实现这些算子,然后在 NPU 上高效运行。
这篇我们用一个实际的例子来说明:图像边缘检测。边缘检测是图像处理中最基础的操作之一,用 Sobel 算子来实现。边缘检测的结果可以用于物体识别、图像分割等场景。
为什么要用 NPU
你可能会问:图像处理用 CPU 不就行了,为什么要用 NPU?
答案是性能。NPU 是专门为 AI 和图像处理设计的硬件,它有大量的计算单元,可以并行处理很多数据。对于 Sobel 这种需要对每个像素做相同操作的任务,NPU 的优势非常明显。
举个例子:CPU 就像一个很聪明的人,什么都能做,但一次只能做一件事;NPU 就像一群不太聪明但很勤奋的人,每个人只做很简单的事,但大家一起做,速度就快多了。
环境搭建
硬件要求
- 设备类型:请参考 CANN Kit 开发指南的约束与限制
- HarmonyOS 系统:HarmonyOS 5.0.5 Release 及以上
软件要求
- DevEco Studio 版本:DevEco Studio 6.1.0 Release 及以上
- HarmonyOS SDK 版本:HarmonyOS 6.1.0 Release SDK 及以上
开发环境要求
AscendC 的开发环境比较特殊,需要在 Linux 上搭建:
- Ubuntu 版本:22.04 及以上(仅支持 x86)
- Python 版本:3.7 到 3.10 之间
- GCC/G++ 版本:7.0 及以上
- CMake 版本:3.22.1 及以上
搭建步骤
- 安装 DevEco Studio:去华为开发者官网下载安装
- 下载 DDK Tools:下载 DDK_tools_5.1.1.0,并在 Linux 环境上解压
- 下载平台插件:下载需要的平台插件包,解压后拷贝到
ddk_external/tools/platform目录下 - 安装工具:进入
ddk_external/tools/tools_ascendc目录,执行安装脚本 - 设置环境变量:执行
set_ascendc_env.sh设置环境变量 - 安装 Python 依赖:安装 toml、jinja2、numpy、torch 等依赖库
项目结构
├── build.sh // 编译入口脚本
├── build_devices.sh // 编译devices侧交付件脚本
├── cmake
│ ├── config.cmake
│ └── util // 算子工程编译所需脚本及公共编译文件存放目录
├── CMakeLists.txt // 算子工程的CMakeLists.txt
├── CMakePresets.json // 编译配置项
├── framework // 算子插件实现文件目录
├── op_host // host侧实现文件
│ ├── sobel_custom_tiling.h // 算子Tiling定义文件
│ ├── sobel_custom.cpp // 算子原型注册、shape推导、信息库、tiling实现等内容文件
│ └── CMakeLists.txt
├── op_kernel // Kernel侧实现文件
│ ├── CMakeLists.txt
│ ├── sobel_custom_base.h // 算子代码定义文件
│ └── sobel_custom.cpp // 算子代码实现文件
└── scripts // 自定义算子工程打包相关脚本所在目录
这个项目结构和前面的图形渲染项目很不一样。主要分为两部分:
op_host:Host 侧代码,负责算子的注册、shape 推导、Tiling 策略等op_kernel:Kernel 侧代码,负责在 NPU 上实际执行的计算逻辑
边缘检测算法原理
在看代码之前,先了解一下 Sobel 边缘检测的原理。
处理流程
整个处理流程分四步:
- RGB 转灰度:把彩色图像转成灰度图像
- Sobel X 方向滤波:检测水平方向的边缘
- Sobel Y 方向滤波:检测垂直方向的边缘
- XY 方向融合:把两个方向的边缘信息合并
Sobel 滤波核
Sobel 算子使用 3x3 的滤波核。X 方向的滤波核是:
-1 0 1
-2 0 2
-1 0 1
Y 方向的滤波核是:
-1 -2 -1
0 0 0
1 2 1
简单说,X 方向的滤波核会检测左右像素的差异,Y 方向的滤波核会检测上下像素的差异。
XY 方向融合
融合的方式很简单:用曼哈顿距离 G = |Gx| + |Gy| 来替代欧几里得距离。这样计算更快,效果也差不多。
第一步:实现 RGB 转灰度
RGB 转灰度的公式是:Gray = 0.299 * R + 0.587 * G + 0.114 * B。
在 AscendC 中,这个操作可以用向量指令高效完成。
第二步:实现 Sobel 滤波
这是核心部分。Sobel 滤波需要对每个像素,用它周围的 3x3 邻域和滤波核做卷积。
使用 Gather 操作
由于数据不是 32 字节对齐的,需要用 Gather 操作来获取特定位置的数据。
// 生成index: [2, 3, 4, ..., w - 1]
AscendC::CreateVecIndex(index, int16_t(2), w - 2);
AscendC::Muls(index, index, int16_t(2), w - 2);
// index:int16->uint32
AscendC::Cast(newindex, index, AscendC::RoundMode::CAST_NONE, w - 2);
这段代码生成一个索引数组,用来获取每个像素右侧第 2 个位置的数据。CreateVecIndex 创建一个连续的索引序列,Muls 把索引乘以 2(因为每个像素有两个字节),Cast 把索引转成 uint32 类型。
for (int i = 0; i < h - 2; i++) {
// 将data[i][2,3,4,...,w-1]存储到tmpBuf0[0, 1, 2, ..., w-2]
AscendC::Gather(tmpBuf0, data[i * w], newindex, 0, w - 2);
AscendC::Gather(tmpBuf1, data[(i + 1) * w], newindex, 0, w - 2);
AscendC::Gather(tmpBuf2, data[(i + 2) * w], newindex, 0, w - 2);
对每一行,用 Gather 获取右侧第 2 个位置的像素值。这样就得到了 Sobel 滤波核中 “j+2” 位置的数据。
// part1
AscendC::Muls(dx[i * w], data[i * w], half(-1), w - 2);
AscendC::Muls(tmpBuf3, data[(i + 1) * w], half(-2), w - 2);
AscendC::Muls(tmpBuf4, data[(i + 2) * w], half(-1), w - 2);
AscendC::Add(dx[i * w], dx[i * w], tmpBuf3, w - 2);
AscendC::Add(dx[i * w], dx[i * w], tmpBuf4, w - 2);
计算 Sobel X 方向的第一部分:左侧列的加权和。对应滤波核的 [-1, -2, -1] 这一列。
// part2
AscendC::Muls(tmpBuf1, tmpBuf1, half(2), w - 2);
AscendC::Add(dx[i * w], dx[i * w], tmpBuf0, w - 2);
AscendC::Add(dx[i * w], dx[i * w], tmpBuf1, w - 2);
AscendC::Add(dx[i * w], dx[i * w], tmpBuf2, w - 2);
}
计算 Sobel X 方向的第二部分:右侧列的加权和。对应滤波核的 [1, 2, 1] 这一列。两部分加起来就是完整的 Sobel X 方向滤波结果。
第三步:实现 XY 方向融合
// G = |Gx| + |Gy|
AscendC::Abs(dx, dx, totalSize);
AscendC::Abs(dy, dy, totalSize);
AscendC::Add(result, dx, dy, totalSize);
先对 Gx 和 Gy 取绝对值,然后相加。这就是曼哈顿距离融合。
第四步:归一化
// 归一化为[0,255]
AscendC::Cast(output, result, AscendC::RoundMode::CAST_NONE, totalSize);
把 half 类型的结果转成 uint8 类型,输出范围 [0, 255]。
Vector 编程范式
AscendC 使用 Vector 编程范式,把算子的实现分为三个基本任务:
- CopyIn:把输入数据从 Global Memory 搬运到 Local Memory
- Compute:在 Local Memory 上进行计算
- CopyOut:把计算结果从 Local Memory 搬运回 Global Memory
__aicore__ inline void Process()
{
cntH = SobelCustom::CeilDiv(this->H, h);
cntW = SobelCustom::CeilDiv(this->W, w);
for (int32_t i = 0; i < cntH; i++) {
for (int32_t j = 0; j < cntW; j++) {
CopyIn(i, j);
Compute(i, j);
CopyOut(i, j);
}
}
}
整个处理过程是:把图像分成很多小块(tile),对每个小块依次执行 CopyIn、Compute、CopyOut。
内存管理
AscendC 使用 TQue 和 TBuf 来管理内存:
TQue:队列,用于 CopyIn 和 CopyOut 的数据传输TBuf:缓冲区,用于计算过程中的临时数据
constexpr int32_t BUFFER_NUM = 2;
const uint32_t h = 9;
const uint32_t w = 256;
pipe.InitBuffer(inQueueX, BUFFER_NUM, tileLength * sizeof(T));
pipe.InitBuffer(outQueueY, BUFFER_NUM, tileLength * sizeof(T));
这里使用 double buffer(BUFFER_NUM = 2),可以在计算当前块的同时搬运下一块的数据,提高效率。
Tiling 策略
Tiling 是把大任务分成小块的策略。每个 tile 的大小是 h x w,其中:
- w 必须是 32 的倍数(对齐约束)
- h * w * 34 + w * 12 <= 120KB(UB 大小限制)
选 h=9, w=256 是一个合理的值。
数据搬运
__aicore__ inline void CopyIn(int32_t i, int32_t j)
{
LocalTensor<T> xLocal = inQueueX.AllocTensor<T>();
DataCopyExtParams dataCopyParams;
// ... 设置参数
DataCopyPadExtParams<T> padParams(false, 0, 0, 0);
DataCopyPad(xLocal, xGm[offset], dataCopyParams, padParams);
inQueueX.EnQue(xLocal);
}
DataCopyPad 是一个支持非对齐搬运的接口。因为图像的宽度不一定是 32 的倍数,所以需要用这个接口来处理边界情况。
dataCopyParams 设置了搬运的参数:
blockCount:搬运多少行blockLen:每行搬运多少字节srcStride:源地址的行间距dstStride:目标地址的行间距
编译和运行
创建算子工程
msopgen gen -i ./SobelCustom.json -c ai_core-kirin9020 -f ONNX -out ./SobelCustom
用 msopgen 工具根据配置文件自动创建算子工程。
编译算子
cd SobelCustom/SobelCustom
./build.sh
编译成功后会显示 “Install the project…Build and install success”。
调试算子
# NPU 仿真调试
ascendebug kernel --backend simulator --repo-type customize --json-file ../ascendebug/temp.json --core-type AiCore --chip-version kirin9020 --work-dir ../ascendebug/myWorkDir/ --block-num 1 --rel-err-thd 0.5
# NPU 实际调试
ascendebug kernel --backend npu --repo-type customize --json-file ../ascendebug/temp.json --core-type AiCore --chip-version kirin9020 --work-dir ../ascendebug/myWorkDir/ --block-num 1 --rel-err-thd 0.5
可以先在仿真器上调试,再在实际 NPU 上调试。调试成功后会生成 .omc 文件,这就是编译好的算子。
集成到应用
把生成的 .omc 文件放到应用的 resources/rawfile 目录下,就可以在应用中调用了。
AscendC 的优势
- 性能:在 NPU 上运行,比 CPU 快很多
- 可复用:端侧和云侧的算子代码可以复用
- 可调试:提供了仿真器和实际 NPU 两种调试方式
- 标准化:使用统一的编程范式,学习成本低
适用场景
AscendC 算子开发适合以下场景:
- 图像处理:边缘检测、滤波、变换等
- AI 推理:自定义的神经网络层
- 信号处理:FFT、滤波器等
- 科学计算:矩阵运算、数值计算等
注意事项
- 环境搭建:AscendC 的开发环境比较复杂,需要在 Linux 上搭建
- 对齐约束:数据宽度必须是 32 的倍数,否则需要特殊处理
- 内存管理:UB 大小有限(120KB),要合理规划 Tiling 策略
- 调试工具:建议先用仿真器调试,再在实际 NPU 上调试
- 平台插件:不同芯片需要不同的平台插件,要确保下载正确
核心流程图
Sobel 边缘检测算法的处理流程:
AscendC 算子开发的完整流程:
总结
AscendC 是一个强大的 NPU 编程框架,让你可以自己实现算子在 NPU 上运行。核心流程:
- 搭建开发环境(Linux + DDK Tools)
- 设计算法(RGB 转灰度、Sobel 滤波、XY 融合)
- 实现算子(CopyIn、Compute、CopyOut)
- 配置 Tiling 策略
- 编译、调试、集成
如果你的应用需要高性能的图像处理或 AI 推理,AscendC 是一个值得学习的技术。
鲲鹏昇腾开发者社区是面向全社会开放的“联接全球计算开发者,聚合华为+生态”的社区,内容涵盖鲲鹏、昇腾资源,帮助开发者快速获取所需的知识、经验、软件、工具、算力,支撑开发者易学、好用、成功,成为核心开发者。
更多推荐



所有评论(0)