RKNPU2从入门到实践 ---- 【9】使用RKNPU2的C API接口将RKNN模型部署在RK3588开发板上

server/2024/12/21 21:19:09/

注:作者使用的平台为Ubuntu20.04虚拟系统,开发板为RK3588,开发板上面的系统为Ubuntu22.04。 

前言

      本博文我们要学习使用 RKNPU2 提供的 C API 接口将RKNN模型部署在RK3588开发板上,完成测试图片在开发板上的推理工作。C API接口可以根据帧数据的更新方式分为通用API和零拷贝API。而这一篇博文主要介绍通用 API 接口。

项目文件包

项目文件包以百度网盘链接的形式给出
链接:https://pan.baidu.com/s/1n9M3BwMKDO3NhyfJzBvORQ 
提取码:1234
,整体文件夹如下图所示:

进入到该文件夹中,如下图所示:

一、cmake架构

      由于在后续程序编写的过程中,会涉及到一些第三方库,且瑞芯微提供的例程都是以cmake自动化构建工具来生成可执行文件、库和其他构建目标的。
      因此,在此之前,我们需要先了解一下cmake架构。
      打开Ubuntu虚拟系统,打开终端,创建一个work目录,用来存放后续的cmake工程。

然后将 01_Cmake工程示例 中的 00_example文件夹拷贝到work目录下。

使用 vscode 软件打开 work 这个工程。
打开work工程,如下图所示:

该工程下有两个目录,一个是model目录,另一个是src目录。
model目录存放了测试图片以及适用于RK3568和RK3588的RKNN模型。
src目录下存放了要编译的源码。
build.sh 脚本文件中设置了一些基本的环境变量,以及开始cmake的构建。

注意编译器的路径:

交叉编译器设置步骤如下所示:

1 安装 gcc 交叉编译器,拷贝 gcc-arm-10.3-2021.07-x86_64-aarch64-none-linux-gnu.tar.gz 到 Ubuntu虚拟系统 的/usr/local/arm64/目录下,这里拷贝的路径要和作者保持一致,后面要用到交叉编译器的绝对路径。如下图所示:

2 解压交叉编译器压缩包 tar -vxf gcc-arm-10.3-2021.07-x86_64-aarch64-none-linux-gnu.tar.gz ,解压完成后即可!


      CMakeLists.txt 文件表示cmake构建的配置文件,如下所示:

在上图中(CMakeLists.txt 文件中),我们截取第11、17行的内容,如下图所示:


我们发现,在设置第三方库rknn_api、opencv的CMAKE_SOURCE_DIR时,需要用到3rdparty这个文件夹。
因此我们将3rdparty这个文件夹放至work目录下,如下图所示。


这一步结束后,work目录下的情况如下图所示:

接下来我们运行build.sh脚本进行cmake工程的构建,运行结果如下:

生成新的目录,如下所示:

      从上图可以看到,多出了build目录和install目录,build目录用来存放工程构建过程中生成的中间文件。install目录用来存放编译完成之后的可执行程序、运行所用到的库以及RKNN模型和推理图片。
      到这里,关于cmake工程结构就结束了,接下来我们来学习通用API部署RKNN模型。

二、通用API部署RKNN模型

RKNPU2通用API使用流程如下所示: 

2.1 前奏工作 

回到vscode软件中,在work工程目录中创建 01_resnet18 目录。

将 00_example目录下的四个文件拷贝到 01_resnet18 目录下,如下所示:


首先,将CmakeLists.txt中的项目名称由:

修改为:

然后将main.cc代码清空,根据RKNPU2通用API使用流程图从零开始编写代码:

程序编写到该步骤(第7行)时可能会报错,这是因为我们还没有添加rknn的头文件。
我们在3rdparty目录中找到rknn头文件rknn_api.h所在的文件夹include的路径,如下图所示:

点击复制路径,然后使用快捷键 ctrl+shift+p 打开搜索框搜索
C/C++: Edit ConFigurations(JSON) ,如下所示:

如果输入C/C++: Edit ConFigurations(JSON)后并没有上图中红色框中的内容弹出,大概率是你的vscode中没有装C/C++ debug拓展,此时需要去拓展库装debug,如下图所示:

进入到 josn 文件后,内容如下所示:

我们需要将刚刚复制的RKNN库文件的路径添加在如下所示的位置中:

添加之后,返回代码处添加rknn头文件,如下图所示:

此时我们发现rknn_context从原先的灰色变为高亮色了,如下图所示:

这说明已经配置成功了。
接下来,就要按流程图进行编写代码了,请看下面

2.2 第一步:调用rknn_init接口创建rknn_context对象、加载 RKNN模型

2.2.1 rknn_init API函数介绍 

rknn_init 初始化函数将创建 rknn_context 对象、加载 RKNN 模型以及根据 flag 和 rknn _init_extend 结构体执行特定的初始化行为。

示例代码如下:

2.2.2 实际代码编写

      在实际代码编写中,调用rknn_init函数时,flag 和rknn_init_extend 目前用不到,因此将flag参数赋值为0,将rknn_init_extend赋值为NULL。

2.3 第二步:调用rknn_query接口查询获取到模型输入输出属性、推理时间、SDK版本等信息

2.3.1 rknn_query API函数介绍

具体介绍后续更新!!在本项目中,确实也用到了这个接口函数,如下图所示:

但由于介绍起来内容较多,因此后续以博文的形式单独介绍这个接口函数!!

2.4 第三步:调用rknn_inputs_set接口设置模型的输入数据

2.4.1 opencv读取输入数据

根据下图中的步骤配置好opencv库文件的路径,如下图所示: 


添加opencv的头文件,如下图所示:

使用opencv读取要推理的图像,如下图所示:

至此,opencv部分就结束了。

2.4.2 rknn_inputs_set API介绍

      通过 rknn_inputs_set 函数可以设置模型的输入数据。该函数能够支持多个输入,其中每个输入是 rknn_input 结构体对象,在传入之前用户需要设置该对象。(注:RV1106/RV1103 不支持这个接口)

示例代码如下:

2.4.3 rknn_inputs_set 实际代码编写

 

2.5 第四步:调用rknn_run接口执行模型推理

2.5.1 rknn_run API介绍

      rknn_run 函数将执行一次模型推理,调用之前需要先通过 rknn_inputs_set 函数或者零拷贝的接口设置输入数据。

示例代码如下:

2.5.2 rknn_run 接口实际代码编写
 ​​​​​​

 

2.6 第五步:调用rknn_outputs_get接口获取模型推理的输出数据

2.6.1 API介绍

      rknn_outputs_get 函数可以获取模型推理的输出数据。该函数能够一次获取多个输出数据。 其中每个输出是 rknn_output 结构体对象,在函数调用之前需要依次创建并设置每个 rknn_output 对象。
      对于输出数据的 buffer 存放可以采用两种方式:一种是用户自行申请和释放,此时 rknn_output 对象的 is_prealloc 需要设置为 1,并且将 buf 指针指向用户申请的 buffer;另一种是由 rknn 来进行分配,此时 rknn_output 对象的 is_prealloc 设置为 0 即可,函数执行之后 buf 将指向输出数据。(注:RV1106/RV1103 不支持这个接口) 


示例代码如下:

2.6.2 实际代码编写


      至此,rknn模型推理图像的过程就已经完成了,输出数据会保存到output结构体中的buffer成员之中。 为了得到我们常见的概率信息,还需要经过后处理部分,后处理的代码如下:

static int rknn_GetTop(float* pfProb, float* pfMaxProb, uint32_t* pMaxClass, uint32_t outputCount, uint32_t topNum)
{uint32_t i, j;#define MAX_TOP_NUM 20if (topNum > MAX_TOP_NUM)return 0;memset(pfMaxProb, 0, sizeof(float) * topNum);memset(pMaxClass, 0xff, sizeof(float) * topNum);for (j = 0; j < topNum; j++) {for (i = 0; i < outputCount; i++) {if ((i == *(pMaxClass + 0)) || (i == *(pMaxClass + 1)) || (i == *(pMaxClass + 2)) || (i == *(pMaxClass + 3)) ||(i == *(pMaxClass + 4))) {continue;}if (pfProb[i] > *(pfMaxProb + j)) {*(pfMaxProb + j) = pfProb[i];*(pMaxClass + j) = i;}}}return 1;
}// Post Processfor (int i = 0; i < io_num.n_output; i++) {uint32_t MaxClass[5];float    fMaxProb[5];float*   buffer = (float*)output[i].buf;uint32_t sz     = output[i].size / 4;rknn_GetTop(buffer, fMaxProb, MaxClass, sz, 5);printf(" --- Top5 ---\n");for (int i = 0; i < 5; i++) {printf("%3d: %8.6f\n", MaxClass[i], fMaxProb[i]);}}


 

后处理完成之后,就需要释放前面所创建的资源了。请看下面。

2.7 第六步:调用rknn_outputs_release接口释放推理输出的相关资源

2.7.1 rknn_outputs_release API介绍

rknn_outputs_release 函数将释放 rknn_outputs_get 函数得到的输出的相关资源。 


示例代码如下所示:

2.7.2 实际代码编写


2.8 第七步:调用rknn_destroy释放传入的rknn_context及其相关资源

2.8.1 rknn_destroy API介绍

rknn_destroy 函数将释放传入的 rknn_context 及其相关资源。 


示例代码如下:

2.8.2 实际代码编写

到此,使用通用 API 加载RKNN模型并推理的程序就编写完成了。 

2.8.3 最终代码

整体代码如下所示:

#include<stdio.h>
#include "rknn_api.h"
#include "opencv2/core.hpp"
#include "opencv2/imgcodecs.hpp"
#include "opencv2/imgproc.hpp"
using namespace cv;static int rknn_GetTop(float* pfProb, float* pfMaxProb, uint32_t* pMaxClass, uint32_t outputCount, uint32_t topNum)
{uint32_t i, j;#define MAX_TOP_NUM 20if (topNum > MAX_TOP_NUM)return 0;memset(pfMaxProb, 0, sizeof(float) * topNum);memset(pMaxClass, 0xff, sizeof(float) * topNum);for (j = 0; j < topNum; j++) {for (i = 0; i < outputCount; i++) {if ((i == *(pMaxClass + 0)) || (i == *(pMaxClass + 1)) || (i == *(pMaxClass + 2)) || (i == *(pMaxClass + 3)) ||(i == *(pMaxClass + 4))) {continue;}if (pfProb[i] > *(pfMaxProb + j)) {*(pfMaxProb + j) = pfProb[i];*(pMaxClass + j) = i;}}}return 1;
}int main(int argc, char *argv[]){/*要求程序传入的第一个参数为RKNN模型,第二个参数为要推理的图片*/char *model_path = argv[1];char *image_path = argv[2];/*调用rknn_init接口将RKNN模型的运行环境和相关信息赋予到context变量中*/rknn_context context;rknn_init(&context,model_path,0,0,NULL);/*使用opencv读取要推理的图像数据*/cv::Mat img = cv::imread(image_path); /*使用cvtColor进行通道转换*/cv::cvtColor(img,img,cv::COLOR_BGR2RGB);/*调用rknn_query接口查询tensor输入输出个数*/rknn_input_output_num io_num;rknn_query(context,RKNN_QUERY_IN_OUT_NUM,&io_num,sizeof(io_num));printf("model input num:%d,output num:%d\n",io_num.n_input,io_num.n_output);/*调用rknn_inputs_set接口设置输入数据*/rknn_input input[1];memset(input,0,sizeof(rknn_input));input[0].index = 0;input[0].buf = img.data;input[0].size = img.rows*img.cols*img.channels()*sizeof(uint8_t);input[0].pass_through = 0;input[0].type = RKNN_TENSOR_UINT8;input[0].fmt = RKNN_TENSOR_NHWC;rknn_inputs_set(context,1,input);/*调用rknn_run接口进行模型推理*/rknn_run(context,NULL);/*调用rknn_outputs_get接口获取模型推理结果*/rknn_output output[1]; memset(output,0,sizeof(rknn_output));output[0].index = 0;output[0].is_prealloc = 0;output[0].want_float = 1; // 表示将输出数据转换为浮点类型rknn_outputs_get(context,1,output,NULL);// Post Processfor (int i = 0; i < io_num.n_output; i++) {uint32_t MaxClass[5];float    fMaxProb[5];float*   buffer = (float*)output[i].buf;uint32_t sz     = output[i].size / 4;rknn_GetTop(buffer, fMaxProb, MaxClass, sz, 5);printf(" --- Top5 ---\n");for (int i = 0; i < 5; i++) {printf("%3d: %8.6f\n", MaxClass[i], fMaxProb[i]);}}/*调用rknn_outputs_release接口释放推理输出的相关资源*/rknn_outputs_release(context,1,output);/*调用rknn_destory接口销毁context变量*/rknn_destroy(context);return 0;
}

2.9 运行build.sh文件进行cmake工程的构建 

      运行build.sh前后注意观察该文件夹内容变化,左图为没有运行build.sh文件之前的,右图为运行build.sh文件之后的。 


运行结束后终端输出信息如下:

      我们可以看到运行build.sh文件后多出了两个目录,一个build目录和install目录。install目录就是我们要放在开发板上运行测试的文件夹。这在博文刚开始的时候已经介绍过了,这里就不再赘述。

2.10 启动开发板、将生成的install目录拷贝到开发板系统上

2.10.1 开发板与电脑相连 

      将开发板与电脑连接好之后,启动开发板会在虚拟系统上弹出如下界面,按照下图选择并点击确定按键。

点击确定之后,若得到如下图:

      即在虚拟系统任务栏处出现了手机的标识,那么就说明开发板的adb工具已经成功连接至虚拟系统上了。

2.10.2 将生成的install目录拷贝到开发板系统上

      在这一操作中,有很多种方式,例如:用优盘拷贝等,但在这里,有一种更为简单的方式,即使用开发板的adb工具。
打开终端,如下图所示:

进入到 01_resnet18 目录,如下图所示:

使用adb push [xxx] [xxx] ,将install目录拷贝到开发板系统的根目录上,如下图所示:

      我们可以使用 adb shell 命令来进入到开发板的系统中,并查看install目录是否已经拷贝完成,如下图所示:

发现install已经放至开发板系统的根目录上去了。
进入 install目录中,如下图:

进入resnet18_Linux目录下:

接下来,使用./resnet18运行模型,第一个参数为rknn模型的路径,第二个参数为要推理的图片路径,如下所示:

当准备输入model时,会自动弹到如下界面的形式,并继续输入第二个参数剩余部分即可:

输入完成之后,按下回车键,得到运行结果:

我们看到812号的值(具体是什么值,目前有争议,等待后续更新)最大,而812号正是太空飞船,故推理成功。


http://www.ppmy.cn/server/109584.html

相关文章

Golang 教程2

Golang 教程2 注意&#xff0c;该文档只适合有编程基础的同学&#xff0c;这里的go教程只给出有区别的知识点 函数的基本形式 //形式 /* func 函数&#xff08;形参列表&#xff09;&#xff08;返回值类型列表&#xff09;{执行语句return 返回值列表 } */1、 一个返回值…

NV GPU FMA指令测试

NV GPU FMA指令测试 一.小结二.复现步骤1.获取FMA指令的峰值性能、启动开销2.假设固定开销为120个cycle,希望fma pipe利用率超过95%,需要多少条指令呢,求解以下不等式:3.采用1140条fma指令测试4.生成fatbin5.修改SASS指令,删除掉STG.E.STRONG.SYS指令,重新生成fatbin6.准备测试…

学习C语言(19)

整理今天的学习内容 1.memmove使用和模拟实现 void* memmove (void* destination&#xff0c;const void* source&#xff0c;size_t num&#xff09;&#xff1b; 和momcpy的差别是memmove函数处理的源内存块和目标内存块是可以重叠的 memmove的模拟实现&#xff1a; 2.mem…

【HarmonyOS 4.0】@ohos.router 页面路由

注册页面&#xff0c;在src/main/resources/base/profile/main_pages.json文件新增配置。 {"src": ["pages/Index","pages/AnimateTo"] }导入 router 模块 import router from ohos.router1. router.pushUrl 跳转到应用内的指定页面会将当前页面…

使用LinkedHashMap实现固定大小的LRU缓存

使用LinkedHashMap实现固定大小的LRU缓存 1. 什么是LRU&#xff1f; LRU是"Least Recently Used"的缩写&#xff0c;意为"最近最少使用"。LRU缓存是一种常用的缓存淘汰算法&#xff0c;它的核心思想是&#xff1a;当缓存满时&#xff0c;优先淘汰最近最少…

【Vue】Echart渲染数据时页面不显示内容

背景 做的一个对话交互的功能&#xff0c;根据后台返回的数据&#xff0c;渲染成Echart图表展示因为图表种类多&#xff0c;因此根据不同图表单独做了一个个vue组件&#xff0c;将数据根据展示类型传到这些子组件中进行渲染无论哪种图表&#xff0c;第一次展示时都能正常展示&…

SQL 快速参考

SQL 快速参考 引言 SQL(Structured Query Language)是一种用于管理关系数据库管理系统(RDBMS)的标准编程语言。它被广泛用于数据查询、数据更新、数据库维护和访问控制。本快速参考旨在提供SQL的基本概念和常用命令的概览,帮助读者快速理解和应用SQL。 基础概念 数据库…

Sentinel熔断与限流

一、服务雪崩与解决方案 1.1、服务雪崩问题 一句话&#xff1a;微服务之间相互调用&#xff0c;因为调用链中的一个服务故障&#xff0c;引起整个链路都无法访问的情况。 微服务中&#xff0c;服务间调用关系错综复杂&#xff0c;一个微服务往往依赖于多个其它微服务。 如图…