【教程】在RK3568上部署(C++)语义分割算法BiSeNetv1/v2

引言

在本篇教程中,博主将记录国庆假期前在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的例子,如下图:

【教程】在RK3568上部署(C++)语义分割算法BiSeNetv1/v2_第1张图片

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 
#include 
#include 
#include 
#include "rknn_api.h"
#include "opencv2/core/core.hpp"
#include "opencv2/imgproc/imgproc.hpp"
#include "opencv2/highgui/highgui.hpp"
#include 
#include 
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 vec_host_scores;
    for(int i=0;i 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(r, c)[0] = color_map[idx * 3];
            pseudo_img.at(r, c)[1] = color_map[idx * 3 + 1];
            pseudo_img.at(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   \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(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 out_scales;
    std::vector 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文件,开始编译

编译成功!

【教程】在RK3568上部署(C++)语义分割算法BiSeNetv1/v2_第2张图片

【教程】在RK3568上部署(C++)语义分割算法BiSeNetv1/v2_第3张图片

三、在板端运行

将编译好的可执行文件(install下的文件)全部送入到板端任意位置,执行以下命令即可(根据自己的路经修改):

./rknn_bisenetv2_demo ./model/RK3566_RK3568/.rknn ./image

【教程】在RK3568上部署(C++)语义分割算法BiSeNetv1/v2_第4张图片

后续等加入3T的计算棒之后,速度应该会更快。

点个赞呗!

你可能感兴趣的:(RK开发板,算法,深度学习,linux,人工智能,windows,ubuntu,嵌入式硬件)