注:作者使用的平台为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号正是太空飞船,故推理成功。