当前位置: 首页 > news >正文

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

注:作者使用的平台为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.mrgr.cn/news/15652.html

相关文章:

  • JavaWeb——前端工程化
  • 行得稳,跑得远,美团如何做到长期主义持续发力?
  • 已解决**Java OutOfMemoryError: GC Overhead Limit Overload - 问题分析与解决方法**
  • springboot学习(2)
  • 斯坦福UE4 C++课学习补充24:伤害数值
  • 【动态规划】两个数组 / 字符串的dp问题(子序列、子数组问题、匹配问题、字符串问题)
  • 什么是反应诱导重构
  • YoloV8训练参数篇
  • 【IEEE出版 | 往届会后三个月检索 | 院士杰青领衔】第五届大数据、人工智能与软件工程国际研讨会(ICBASE 2024)
  • tail 和 head命令(查看文件内容
  • 数据分析报告练习作业
  • Mysql基础练习题 595.大的国家 (力扣)
  • 【提分必看!】蓝桥杯单片机提分技巧(国一经验分享)
  • git 更改分支名称
  • SLAM的详细介绍,包括其基本原理、主要组件、算法类型、应用场景以及面临的挑战
  • 92. UE5 RPG 使用C++创建GE实现灼烧的负面效果
  • 使用手机挖掘IDOR漏洞赚取1500美元赏金
  • k8s声明式管理方式(yaml文件实现)
  • 20. Java中的fail-fast机制是什么?它是如何在集合中实现的?
  • Spring Bean加载耗时采集工具