引言
在本篇教程中,博主将记录国庆假期前在RK3568上部署分割算法的步骤以及代码。首先说一下,RK3568这个开发板本身的算力大概是0.8T(在实际开发中还会用到额外的计算卡,额外的计算卡后面文章再说,本篇文章主要记录在RK3568上的部署过程)。
一、获取rknn模型
1、这步不是很难,我之前也写过BiSeNet的教程,官方提供的代码也很好理解,并且提供了onnx模型的导出代码。教程--从零开始使用BiSeNet(语义分割)网络训练自己的数据集_计算机幻觉的博客-CSDN博客为了从图片分割出我们想要的特征,我们采用BiSeNet作为分割模型,并且在自己制作的数据集上进行训练测试。注:训练是在linux环境下的,Win下训练可能会有点问题。_bisenethttps://blog.csdn.net/qq_39149619/article/details/131882664?spm=1001.2014.3001.55012、将导出的onnx代码进行rknn转换,RK3568需要用到rknn-toolkit2,该环境的安装之前也写过:
RKNN-ToolKit2 1.5.0安装教程_rknn安装-CSDN博客由于种种原因需要用到开发版RK3568,需要预先安装RKNN-Toolkit2进行模型转化等,博主安装的版本是1.5.0,Ubuntu版本是20.04,python版本3.6。1、原本准备采取docker安装,但是文件有点大再加上网速不行,于是我们采用pip方法进行安装。,直接点击下载,得到rknn-toolkit2-master.zip,并且解压到任意文件夹中。_rknn安装https://blog.csdn.net/qq_39149619/article/details/131694631?spm=1001.2014.3001.5501转换代码:mean_values和std_values需要根据自己训练集修改
from rknn.api import RKNN
ONNX_MODEL = '/home/zw/Prg/Pycharm/file/RKNN3568/onnx/yolov5-seg/best_480x480.onnx'
platform = "rk3568"
RKNN_MODEL = '/home/zw/Prg/Pycharm/file/RKNN3568/rknn/BiSeNetV2/BiSeNetv2_320x320_min4_{}_out_opt.rknn'.format(platform)
if __name__ == '__main__':
# Create RKNN object
rknn = RKNN(verbose=False)
# pre-process config
print('--> config model')
rknn.config(mean_values=[82.9835, 93.9795, 82.1893], std_values=[54.02, 54.804, 54.0225], target_platform='rk3568') #BiSeNet
print('done')
# Load tensorflow model
print('--> Loading model')
ret = rknn.load_onnx(model=ONNX_MODEL, outputs=['preds']) # 这里一定要根据onnx模型修改
if ret != 0:
print('Load onnx model failed!')
exit(ret)
print('done')
# Build model
print('--> Building model')
ret = rknn.build(do_quantization=False, dataset='/home/zw/Prg/Pycharm/file/RKNN3568/dataset.txt')
if ret != 0:
print('Build rkmodel failed!')
exit(ret)
print('done')
# rknn.export_rknn_precompile_model(RKNN_MODEL)
rknn.export_rknn(RKNN_MODEL)
rknn.release()
二、在RK3568上进行C++部署
1、首先,从官网下载rknpu2相关文件,官网地址:GitHub - rockchip-linux/rknpu2,该文件包含了rknn相关的接口文件以及提供的示例代码。同时,我们创建bisenetv2的例子,如下图:
model存放转换好的rknn文件,main是推理C++代码。
2、想在3568上部署,需要对程序进行编译,详细的参考官方提供的说明pdf,讲解的简单易懂,这里就不多说。编译需要用到交叉编译工具,这里提供下载地址:Firefly-Linux / prebuilts / gcc / linux-x86 / aarch64 / gcc-buildroot-9.3.0-2020.03-x86_64_aarch64-rockchip-linux-gnu · GitLab
下载到任意位置即可,打开build-linux_RK3566_RK3568.sh文件,修改入下:gcc替换成你自己的地址。
#!/bin/bash
set -e
TARGET_SOC="rk356x"
# for aarch64
# GCC_COMPILER=aarch64-linux-gnu
export TOOL_CHAIN=/home/zw/Downloads/gcc-buildroot-9.3.0-2020.03-x86_64_aarch64-rockchip-linux-gnu-firefly
GCC_COMPILER=/home/zw/Downloads/gcc-buildroot-9.3.0-2020.03-x86_64_aarch64-rockchip-linux-gnu-firefly/bin/aarch64-rockchip-linux-gnu
export LD_LIBRARY_PATH=${TOOL_CHAIN}/lib64:$LD_LIBRARY_PATH
export CC=${GCC_COMPILER}-gcc
export CXX=${GCC_COMPILER}-g++
ROOT_PWD=$( cd "$( dirname $0 )" && cd -P "$( dirname "$SOURCE" )" && pwd )
# build
BUILD_DIR=${ROOT_PWD}/build/build_linux_aarch64
if [[ ! -d "${BUILD_DIR}" ]]; then
mkdir -p ${BUILD_DIR}
fi
cd ${BUILD_DIR}
cmake ../.. \
-DTARGET_SOC=${TARGET_SOC} \
-DCMAKE_C_COMPILER=${GCC_COMPILER}-gcc \
-DCMAKE_CXX_COMPILER=${GCC_COMPILER}-g++
make -j4
make install
cd -
cmakelist文件爱你没啥要改的,修改好自己项目名称即可:
cmake_minimum_required(VERSION 3.4.1)
project(rknn_bisenetv2_demo)
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS}")
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++11")
# rknn api
if(TARGET_SOC STREQUAL "rk356x")
set(RKNN_API_PATH ${CMAKE_SOURCE_DIR}/../../runtime/RK356X/${CMAKE_SYSTEM_NAME}/librknn_api)
elseif(TARGET_SOC STREQUAL "rk3588")
set(RKNN_API_PATH ${CMAKE_SOURCE_DIR}/../../runtime/RK3588/${CMAKE_SYSTEM_NAME}/librknn_api)
else()
message(FATAL_ERROR "TARGET_SOC is not set, ref value: rk356x or rk3588")
endif()
if (CMAKE_SYSTEM_NAME STREQUAL "Android")
set(RKNN_RT_LIB ${RKNN_API_PATH}/${CMAKE_ANDROID_ARCH_ABI}/librknnrt.so)
else()
if (CMAKE_C_COMPILER MATCHES "aarch64")
set(LIB_ARCH aarch64)
else()
set(LIB_ARCH armhf)
endif()
set(RKNN_RT_LIB ${RKNN_API_PATH}/${LIB_ARCH}/librknnrt.so)
endif()
include_directories(${RKNN_API_PATH}/include)
include_directories(${CMAKE_SOURCE_DIR}/../3rdparty)
# opencv
if (CMAKE_SYSTEM_NAME STREQUAL "Android")
set(OpenCV_DIR ${CMAKE_SOURCE_DIR}/../3rdparty/opencv/OpenCV-android-sdk/sdk/native/jni/abi-${CMAKE_ANDROID_ARCH_ABI})
else()
if(LIB_ARCH STREQUAL "armhf")
set(OpenCV_DIR ${CMAKE_SOURCE_DIR}/../3rdparty/opencv/opencv-linux-armhf/share/OpenCV)
else()
set(OpenCV_DIR ${CMAKE_SOURCE_DIR}/../3rdparty/opencv/opencv-linux-aarch64/share/OpenCV)
endif()
endif()
find_package(OpenCV REQUIRED)
set(CMAKE_INSTALL_RPATH "lib")
add_executable(rknn_bisenetv2_demo
src/main.cc
)
target_link_libraries(rknn_bisenetv2_demo
${RKNN_RT_LIB}
${OpenCV_LIBS}
)
# install target and libraries
set(CMAKE_INSTALL_PREFIX ${CMAKE_SOURCE_DIR}/install/rknn_bisenetv2_demo_${CMAKE_SYSTEM_NAME})
install(TARGETS rknn_bisenetv2_demo DESTINATION ./)
install(DIRECTORY model DESTINATION ./)
install(PROGRAMS ${RKNN_RT_LIB} DESTINATION lib)
3、废话不多说了,直接提供C++代码,需要注意的是代码中的图像尺寸这里写死了,根据自己需要来修改代码即可。
#include <stdio.h>
#include <stdint.h>
#include <stdlib.h>
#include <queue>
#include "rknn_api.h"
#include "opencv2/core/core.hpp"
#include "opencv2/imgproc/imgproc.hpp"
#include "opencv2/highgui/highgui.hpp"
#include <chrono>
#include <sys/time.h>
using namespace cv;
using namespace std;
/* 打印结构体rknn_tensor_attr所代表的张量信息,这个结构体包含了关于张量的信息;
rknn_tensor_attr *attr:指向 rknn_tensor_attr 结构体的指针,这个结构体包含了关于张量的信息。
%d 整数 %s 字符串 %f 浮点数
*/
void printRKNNTensor(rknn_tensor_attr *attr) {
printf("index=%d name=%s n_dims=%d dims=[%d %d %d %d] n_elems=%d size=%d "
"fmt=%d type=%d qnt_type=%d fl=%d zp=%d scale=%f\n",
attr->index, // 张量的索引
attr->name, // 张量的名字
attr->n_dims, //张量的维度数
attr->dims[3], attr->dims[2],
attr->dims[1], attr->dims[0], // 张量在每个维度上的大小
attr->n_elems, // 张量中元素的总数
attr->size, // 张量的大小
0,
attr->type, // 张量的数据类型
attr->qnt_type, // 张量的量化类型
attr->fl, // 浮点层(与量化有关)
attr->zp, //零点(与量化有关)
attr->scale); //缩放值(与量化有关)
}
/*
这段代码是一个用于后处理的函数,主要是将一个输入数组 input0 中的数据转换成伪彩色图像,
然后将伪彩色图像与原始图像 resize_img 进行融合,
最后保存三张图像:伪彩色图像、原始图像、和融合后的图像。
int:函数返回一个整数值作为结果。
float *input0:指向 float 类型的数组,存储了后处理前的数据。
cv::Mat resize_img:OpenCV 中的 cv::Mat 类型,代表原始图像。
*/
int post_process_u8(float *input0,cv::Mat resize_img,int w,int h){
//将 float 类型的数组转换为 int 类型的向量 vec_host_scores,并将 input0 数组中的数据逐个添加到这个向量中。
std::vector<int> vec_host_scores;
for(int i=0;i<w*h;i++){
vec_host_scores.emplace_back(input0[i]);
}
/*
根据预设的 num_class 值(256),生成颜色映射表 color_map,
用于将 input0 中的整数值映射为伪彩色值。
在这里,每个整数值被视为一个类别标签,然后将其转换为对应的伪彩色值。
这个过程是通过位操作来实现的,根据 input0 中的整数值生成对应的 R、G、B 分量值。
*/
int num_class = 256;//提取到外面 只执行一次即可,自己改吧
vector<int> color_map(num_class * 3);
for (int i = 0; i < num_class; i++) {
int j = 0;
int lab = i;
while (lab) {
color_map[i * 3] |= ((lab >> 0 & 1) << (7 - j));
color_map[i * 3 + 1] |= (((lab >> 1) & 1) << (7 - j));
color_map[i * 3 + 2] |= (((lab >> 2) & 1) << (7 - j));
j += 1;
lab >>= 3;
}
}
/*
创建一个 cv::Mat 类型的 pseudo_img 对象,用于存储生成的伪彩色图像。
在这里,该图像的尺寸与输入的 w 和 h 相同,通道数为 3(代表 RGB 颜色通道)。
用于创建大小为 w x h 的 cv::Mat 对象 pseudo_img 并将所有像素设置为黑色。
*/
cv::Mat pseudo_img(w, h, CV_8UC3, cv::Scalar(0, 0, 0));
for (int r = 0; r < w; r++) {
for (int c = 0; c < h; c++) {
int idx = vec_host_scores[r*h + c];
pseudo_img.at<Vec3b>(r, c)[0] = color_map[idx * 3];
pseudo_img.at<Vec3b>(r, c)[1] = color_map[idx * 3 + 1];
pseudo_img.at<Vec3b>(r, c)[2] = color_map[idx * 3 + 2];
}
}
cv::Mat result;
cv::Mat resize_result;
cv::addWeighted(resize_img, 0.4, pseudo_img, 0.6, 0, result, 0);
cv::resize(result, resize_result, cv::Size(640, 480));
// cv::imshow("pseudo_img", pseudo_img);
cv::imwrite("pseudo_img.jpg", pseudo_img);
// cv::imshow("bgr", resize_img);
cv::imwrite("resize_img.jpg", resize_img);
// cv::imshow("result", result);
cv::imwrite("result.jpg", resize_result);
// cv::waitKey(0);
return 0;
}
double __get_us(struct timeval t) { return (t.tv_sec * 1000000 + t.tv_usec); }
int main(int argc, char **argv) {
const char *img_path = argv[2];
const char *model_path = argv[1];
const char *post_process_type = "fp";//fp
struct timeval start_time, stop_time;
// const int target_width = 960;
// const int target_height = 720;
if (argc != 3) {
printf("Usage: %s <rknn model> <image_path> \n", argv[0]);
return -1;
}
// Load image
cv::Mat bgr = cv::imread(img_path);
if (!bgr.data) {
printf("cv::imread %s fail!\n", img_path);
return -1;
}
cv::Mat rgb;
//BGR->RGB
cv::cvtColor(bgr, rgb, cv::COLOR_BGR2RGB);
//调整rgb图像的大小
cv::Mat img_resize;
cv::resize(rgb,img_resize,cv::Size(320,320));
int width=img_resize.cols; //获取原始bgr图像的大小
int height=img_resize.rows;
// Load model
FILE *fp = fopen(model_path, "rb");
if (fp == NULL) {
printf("fopen %s fail!\n", model_path);
return -1;
}
fseek(fp, 0, SEEK_END); // 将文件指针移动到文件的末尾
int model_len = ftell(fp); // 然后使用 ftell(fp) 获取当前文件指针的位置,即文件的大小
void *model = malloc(model_len); //分配大小为 model_len 字节的内存块,并将内存块的起始地址保存在指针变量 model 中
fseek(fp, 0, SEEK_SET); //是将文件指针重新设置到文件的开头,以便后续读取文件数据或执行其他操作。
if (model_len != fread(model, 1, model_len, fp)) { //model用于存储从文件中读取的数据,model_len这个参数指定要读取的数据的总字节数
printf("fread %s fail!\n", model_path);
free(model);
return -1;
}
/*
定义了一个 rknn_context 类型的变量 ctx,并初始化为0。
rknn_context 是 RKNN 提供的一个上下文对象,用于执行模型推理的各种操作。
通过将其初始化为0,表示暂时没有创建 RKNN 上下文。
*/
rknn_context ctx = 0;
/*
rknn_init 函数用于创建 RKNN 上下文,并将模型数据加载到上下文中。它的参数如下:
ctx: 这是一个指向 rknn_context 的指针的地址,通过传递指针的地址,函数可以在内部分配内存并创建一个新的 RKNN 上下文,
并将其地址存储在 ctx 变量中;
model:这是之前通过 fread 从模型文件中读取的模型数据的指针。它包含了要加载到 RKNN 上下文的模型数据;
rknn_init 函数执行成功后,会返回一个非负值,表示初始化成功,并将 RKNN 上下文的地址存储在 ctx 变量中。
如果初始化失败,返回值将是一个负数,表示初始化失败的错误码。
*/
int ret = rknn_init(&ctx, model, model_len, RKNN_FLAG_COLLECT_PERF_MASK, NULL);
if (ret < 0) {
printf("rknn_init fail! ret=%d\n", ret);
return -1;
}
/* Query sdk version
查询 Rockchip Neural Network Toolkit(RKNN)的 SDK 版本和驱动版本,并输出它们的信息。
*/
rknn_sdk_version version;
ret = rknn_query(ctx, RKNN_QUERY_SDK_VERSION, &version,
sizeof(rknn_sdk_version));
if (ret < 0) {
printf("rknn_init error ret=%d\n", ret);
return -1;
}
printf("sdk version: %s driver version: %s\n", version.api_version,
version.drv_version);
/* Get input,output attr
查询模型的输入和输出数量,并将结果输出到控制台。
io_num,用于存储模型的输入和输出数量信息;rknn_query 函数来查询模型的输入和输出数量
*/
rknn_input_output_num io_num;
ret = rknn_query(ctx, RKNN_QUERY_IN_OUT_NUM, &io_num, sizeof(io_num));
if (ret < 0) {
printf("rknn_init error ret=%d\n", ret);
return -1;
}
printf("model input num: %d, output num: %d\n", io_num.n_input,
io_num.n_output);
/*
查询模型的输入属性,并将输入属性信息打印到控制台,memset 函数将 input_attrs 数组的内存清零,以确保所有属性初始值为0。
*/
rknn_tensor_attr input_attrs[io_num.n_input];
memset(input_attrs, 0, sizeof(input_attrs));
for (int i = 0; i < io_num.n_input; i++) {
input_attrs[i].index = i;
ret = rknn_query(ctx, RKNN_QUERY_INPUT_ATTR, &(input_attrs[i]),
sizeof(rknn_tensor_attr));
if (ret < 0) {
printf("rknn_init error ret=%d\n", ret);
return -1;
}
printRKNNTensor(&(input_attrs[i]));
}
/*
查询模型的输出属性,并将输出属性信息打印到控制台
*/
rknn_tensor_attr output_attrs[io_num.n_output];
memset(output_attrs, 0, sizeof(output_attrs));
for (int i = 0; i < io_num.n_output; i++) {
output_attrs[i].index = i;
ret = rknn_query(ctx, RKNN_QUERY_OUTPUT_ATTR, &(output_attrs[i]),
sizeof(rknn_tensor_attr));
printRKNNTensor(&(output_attrs[i]));
}
/*
确定模型的输入格式(NCHW或NHWC)以及输入的宽度、高度和通道数,并将这些信息输出到控制台。
*/
int input_channel = 3;
int input_width = 0;
int input_height = 0;
if (input_attrs[0].fmt == RKNN_TENSOR_NCHW) {
printf("model is NCHW input fmt\n");
input_width = input_attrs[0].dims[0];
input_height = input_attrs[0].dims[1];
printf("input_width=%d input_height=%d\n", input_width, input_height);
} else {
printf("model is NHWC input fmt\n");
input_width = input_attrs[0].dims[2];
input_height = input_attrs[0].dims[1];
printf("input_width=%d input_height=%d\n", input_width, input_height);
}
printf("model input height=%d, width=%d, channel=%d\n", input_height, input_width,
input_channel);
/* Init input tensor
准备模型推理所需的输入数据
*/
rknn_input inputs[1]; //定义了一个 rknn_input 数组 inputs,其中包含一个元素
memset(inputs, 0, sizeof(inputs)); // 使用 memset 函数将 inputs 数组的内存清零,以确保所有属性初始值为0。
// 设置 inputs[0] 的属性。
inputs[0].index = 0; //将 index 设置为0,表示这是模型的第一个输入。
//将输入数据的指针 img_resize.data 赋值给 inputs[0].buf。这表示输入数据的实际内容存储在 img_resize.data 中,而 inputs[0].buf 指向该数据。
inputs[0].buf = img_resize.data;
inputs[0].type = RKNN_TENSOR_UINT8; //表示输入数据的数据类型为无符号8位整数(uint8)。
// 将 inputs[0].size 设置为输入数据的大小,即输入数据的宽度 (input_width)、高度 (input_height) 和通道数 (input_channel) 的乘积。这表示输入数据的总字节数。
inputs[0].size = input_width * input_height * input_channel;
inputs[0].fmt = RKNN_TENSOR_NHWC; //表示输入数据的格式为 NHWC
inputs[0].pass_through = 0; //表示在输入数据到达模型之前,不对输入数据进行任何处理。
/* Init output tensor
用于进行模型的推理(inference)过程,将输入数据输入到模型中并获取输出结果。
*/
rknn_output outputs[io_num.n_output]; //定义了一个 rknn_output 数组 outputs,用于存储模型的输出数据
memset(outputs, 0, sizeof(outputs));
//want_float 属性表示是否希望输出结果为浮点数(float)。将它设置为1表示希望输出为浮点数。这通常在需要对输出进行后处理时使用。
for (int i = 0; i < io_num.n_output; i++) {
outputs[i].want_float = 1;
}
printf("img.cols: %d, img.rows: %d\n", img_resize.cols, img_resize.rows);
// auto t1=std::chrono::steady_clock::now(); //记录当前时间,用于计算推理时间
//rknn_inputs_set 函数用于将输入数据绑定到 RKNN 上下文,以便进行推理。
gettimeofday(&start_time, NULL);
rknn_inputs_set(ctx, io_num.n_input, inputs);
ret = rknn_run(ctx, NULL); //执行模型的推理过程
if (ret < 0) {
printf("ctx error ret=%d\n", ret);
return -1;
}
ret = rknn_outputs_get(ctx, io_num.n_output, outputs, NULL); //用于从 RKNN 上下文中获取输出数据
//毫秒级
// auto t2=std::chrono::steady_clock::now(); //获取当前时间
// double dr_ms=std::chrono::duration<double,std::milli>(t2-t1).count(); //计算推理时间
gettimeofday(&stop_time, NULL);
printf("once run use %f ms\n", (__get_us(stop_time) - __get_us(start_time)) / 1000);
// printf("%lf ms\n",dr_ms);
if (ret < 0) {
printf("outputs error ret=%d\n", ret);
return -1;
}
// rknn_perf_detail perf_detail;
// ret = rknn_query(ctx, RKNN_QUERY_PERF_DETAIL, &perf_detail, sizeof(perf_detail));
// printf("Perf detail:\n");
// printf("process_detil : %s",perf_detail.perf_data);
// printf(&perf_detail);
/* Post process
后处理(post-process)模型输出;
out_scales 和 out_zps,用于存储输出数据的缩放因子和零点偏移值
*/
std::vector<float> out_scales;
std::vector<uint8_t> out_zps;
for (int i = 0; i < io_num.n_output; ++i) {
out_scales.push_back(output_attrs[i].scale);
out_zps.push_back(output_attrs[i].zp);
}
gettimeofday(&start_time, NULL);
//通过比较 post_process_type 的值是否等于 "fp",来确定是否进行后处理。如果 post_process_type 是 "fp",则调用 post_process_u8 函数进行后处理。
if (strcmp(post_process_type, "fp") == 0) {
post_process_u8((float *) outputs[0].buf,img_resize,
320, 320);
}
gettimeofday(&stop_time, NULL);
printf("process use %f ms\n", (__get_us(stop_time) - __get_us(start_time)) / 1000);
/*
用于释放模型推理过程中获取的输出数据的内存,以便避免内存泄漏。
*/
ret = rknn_outputs_release(ctx, io_num.n_output, outputs);
if (ret < 0) {
printf("rknn_query fail! ret=%d\n", ret);
goto Error;
}
/*
错误处理部分,当在前面的代码执行过程中发生错误时,将会跳转到 Error 标签处进行错误处理。
*/
Error:
if (ctx > 0)
rknn_destroy(ctx);
if (model)
free(model);
if (fp)
fclose(fp);
return 0;
}
4、运行sh文件,开始编译
编译成功!
三、在板端运行
将编译好的可执行文件(install下的文件)全部送入到板端任意位置,执行以下命令即可(根据自己的路经修改):
./rknn_bisenetv2_demo ./model/RK3566_RK3568/.rknn ./image
后续等加入3T的计算棒之后,速度应该会更快。
点个赞呗!