【深度学习】【RKNN】【C++】应用程序编程接口化处理详细教程

发布于:2024-11-29 ⋅ 阅读:(57) ⋅ 点赞:(0)

【深度学习】【RKNN】【C++】应用程序编程接口化处理详细教程

提示:博主取舍了很多大佬的博文并亲测有效,分享笔记邀大家共同学习讨论


前言

应用程序编程接口(API)是软件工程中的一种常见做法,本博文将RKNPU2推理代码拆分为不同的API,旨在提高代码的可维护性、复用性和模块化。这样做可以让你的应用更加灵活,更容易扩展,并且可以支持并发开发。

这里需要先学习一下【深度学习】【RKNN】【C++】模型转化、环境搭建以及模型部署的详细教程,本博文是其后续补充。

下列是拆分成不同API后的工程结构总览:

 AlexNet_API
   └── 3rdparty
       ├── opencv
       |   ├── opencv-linux-aarch64
   └── librknn_api
           ├── aarch64
           |   ├── vlibrknnrt.so
           ├── include
           |   ├── rknn_api.h
           |   ├── rknn_matmul_api.h
   └── src
       ├── engine
       |   ├── engine.h
       |   ├── rknn_engine.cpp
       |   ├── rknn_engine.h
       ├── process
       |   ├── postprocess.cpp
       |   ├── postprocess.h
       |   ├── preprocess.cpp
       |   ├── preprocess.h
       ├── types
       |   ├── datatype.h
       |   ├── error.h
       ├── utils
       |   ├── engine_helper.h
       |   ├── logging.h
   └── weights
       ├── AlexNet.rknn
   └── CMakeLists.txt

封装RKNN API

NNEngine引擎接口

定义NNEngine抽象类:不能实例化,全部使用纯虚函数,只能作为不同板子的基类使用,使得不同的引擎的接口一致,方便使用,也可以隐藏不同引擎的实现细节,方便维护。
engine.h

// 接口定义
// 预处理器指令 用于防止头文件的重复包含
#ifndef RK3566_DEMO_ENGINE_H
#define RK3566_DEMO_ENGINE_H

#include "types/error.h"
#include "types/datatype.h"

#include <vector>
#include <memory>

class NNEngine
{
public:
    virtual ~NNEngine(){};                                                                                               // 析构函数
    virtual nn_error_e LoadModelFile(const char *model_file) = 0;                                                        // 加载模型文件,=0表示纯虚函数,必须在子类中实现
    virtual const std::vector<tensor_attr_s> &GetInputShapes() = 0;                                                      // 获取输入张量的形状
    virtual const std::vector<tensor_attr_s> &GetOutputShapes() = 0;                                                     // 获取输出张量的形状
    virtual nn_error_e Run(std::vector<tensor_data_s> &inputs, std::vector<tensor_data_s> &outpus, bool want_float) = 0; // 运行模型
};

std::shared_ptr<NNEngine> CreateRKNNEngine(); // 智能指针类避免内存泄漏 创建RKNN引擎

#endif // RK3566_DEMO_ENGINE_H

RKEngine引擎接口以及实现

rknn_engine.h
继承自NNEngine接口:新增了部分私有变量。

#ifndef RK3566_DEMO_RKNN_ENGINE_H
#define RK3566_DEMO_RKNN_ENGINE_H

#include "engine.h"
#include <vector>
#include <rknn_api.h>

// 实现NNEngine的接口
class RKEngine : public NNEngine
{
public:
    RKEngine() : rknn_ctx_(0), ctx_created_(false), input_num_(0), output_num_(0){};    // 构造函数 初始化
    ~RKEngine() override;                                                               // 析构函数

    nn_error_e LoadModelFile(const char *model_file) override;                                                         // 加载模型文件
    const std::vector<tensor_attr_s> &GetInputShapes() override;                                                       // 获取输入张量的形状
    const std::vector<tensor_attr_s> &GetOutputShapes() override;                                                      // 获取输出张量的形状
    nn_error_e Run(std::vector<tensor_data_s> &inputs, std::vector<tensor_data_s> &outputs, bool want_float) override; // 运行模型

private:
    // rknn context
    rknn_context rknn_ctx_; // rknn context
    bool ctx_created_;      // rknn context是否创建

    uint32_t input_num_;  // 输入的数量
    uint32_t output_num_; // 输出的数量

    std::vector<tensor_attr_s> in_shapes_;  // 输入张量的形状
    std::vector<tensor_attr_s> out_shapes_; // 输出张量的形状
};

#endif // RK3566_DEMO_RKNN_ENGINE_H

rknn_engine.cpp
实现NNEngine:主要为加载模型和推理模型俩个函数。加载模型函数主要将核心流程中的初始化RKNN模型、获取模型输入输出信息部分整合在一起;推理模型函数则将设置输入、执行推理以及获取输出部分整合在一起。

// rknn_engine.h的实现

#include "rknn_engine.h"
#include <string.h>
#include "utils/engine_helper.h"
#include "utils/logging.h"

static const int g_max_io_num = 10; // 最大输入输出张量的数量

/**
 * @brief 加载模型文件、初始化rknn context、获取rknn版本信息、获取输入输出张量的信息
 * @param model_file 模型文件路径
 * @return nn_error_e 错误码
 */
nn_error_e RKEngine::LoadModelFile(const char *model_file)
{
    int model_len = 0;                                  // 模型文件大小
    auto model = load_model(model_file, &model_len);    // 加载模型文件
    if (model == nullptr)
    {
        NN_LOG_ERROR("load model file %s fail!", model_file);
        return NN_LOAD_MODEL_FAIL; // 返回错误码:加载模型文件失败
    }
    int ret = rknn_init(&rknn_ctx_, model, model_len, 0, NULL); // 初始化rknn context
    if (ret < 0)
    {
        NN_LOG_ERROR("rknn_init fail! ret=%d", ret);
        return NN_RKNN_INIT_FAIL; // 返回错误码:初始化rknn context失败
    }
    // 打印初始化成功信息
    NN_LOG_INFO("rknn_init success!");
    ctx_created_ = true;

    // 获取rknn版本信息
    rknn_sdk_version version;
    ret = rknn_query(rknn_ctx_, RKNN_QUERY_SDK_VERSION, &version, sizeof(rknn_sdk_version));
    if (ret < 0)
    {
        NN_LOG_ERROR("rknn_query fail! ret=%d", ret);
        return NN_RKNN_QUERY_FAIL;
    }
    // 打印rknn版本信息
    NN_LOG_INFO("RKNN API version: %s", version.api_version);
    NN_LOG_INFO("RKNN Driver version: %s", version.drv_version);

    // 获取输入输出个数
    rknn_input_output_num io_num;
    ret = rknn_query(rknn_ctx_, RKNN_QUERY_IN_OUT_NUM, &io_num, sizeof(io_num));
    if (ret != RKNN_SUCC)
    {
        NN_LOG_ERROR("rknn_query fail! ret=%d", ret);
        return NN_RKNN_QUERY_FAIL;
    }
    NN_LOG_INFO("model input num: %d, output num: %d", io_num.n_input, io_num.n_output);

    // 保存输入输出个数
    input_num_ = io_num.n_input;
    output_num_ = io_num.n_output;

    // 输入属性
    NN_LOG_INFO("input tensors:");
    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(rknn_ctx_, RKNN_QUERY_INPUT_ATTR, &(input_attrs[i]), sizeof(rknn_tensor_attr));
        if (ret != RKNN_SUCC)
        {
            NN_LOG_ERROR("rknn_query fail! ret=%d", ret);
            return NN_RKNN_QUERY_FAIL;
        }
        print_tensor_attr(&(input_attrs[i]));
        // set input_shapes_
        in_shapes_.push_back(rknn_tensor_attr_convert(input_attrs[i]));
    }

    // 输出属性
    NN_LOG_INFO("output tensors:");
    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(rknn_ctx_, RKNN_QUERY_OUTPUT_ATTR, &(output_attrs[i]), sizeof(rknn_tensor_attr));
        if (ret != RKNN_SUCC)
        {
            NN_LOG_ERROR("rknn_query fail! ret=%d", ret);
            return NN_RKNN_QUERY_FAIL;
        }
        print_tensor_attr(&(output_attrs[i]));
        // set output_shapes_
        out_shapes_.push_back(rknn_tensor_attr_convert(output_attrs[i]));
    }

    return NN_SUCCESS;
}

// 获取输入张量的形状
const std::vector<tensor_attr_s> &RKEngine::GetInputShapes()
{
    return in_shapes_;
}

// 获取输出张量的形状
const std::vector<tensor_attr_s> &RKEngine::GetOutputShapes()
{
    return out_shapes_;
}

/**
 * @brief 运行模型,获得推理结果
 * @param inputs 输入张量
 * @param outputs 输出张量
 * @param want_float 是否需要float类型的输出
 * @return nn_error_e 错误码
 */
nn_error_e RKEngine::Run(std::vector<tensor_data_s> &inputs, std::vector<tensor_data_s> &outputs, bool want_float)
{
    // 检查输入输出张量的数量是否匹配
    if (inputs.size() != input_num_)
    {
        NN_LOG_ERROR("inputs num not match! inputs.size()=%ld, input_num_=%d", inputs.size(), input_num_);
        return NN_IO_NUM_NOT_MATCH;
    }
    if (outputs.size() != output_num_)
    {
        NN_LOG_ERROR("outputs num not match! outputs.size()=%ld, output_num_=%d", outputs.size(), output_num_);
        return NN_IO_NUM_NOT_MATCH;
    }

    // 设置rknn inputs
    rknn_input rknn_inputs[g_max_io_num];
    for (int i = 0; i < inputs.size(); i++)
    {
        // 将自定义的tensor_data_s转换为rknn_input
        rknn_inputs[i] = tensor_data_to_rknn_input(inputs[i]);
    }
    int ret = rknn_inputs_set(rknn_ctx_, (uint32_t)inputs.size(), rknn_inputs);
    if (ret < 0)
    {
        NN_LOG_ERROR("rknn_inputs_set fail! ret=%d", ret);
        return NN_RKNN_INPUT_SET_FAIL;
    }

    // 推理
    NN_LOG_DEBUG("rknn running...");
    ret = rknn_run(rknn_ctx_, nullptr);
    if (ret < 0)
    {
        NN_LOG_ERROR("rknn_run fail! ret=%d", ret);
        return NN_RKNN_RUNTIME_ERROR;
    }

    // 获得输出
    rknn_output rknn_outputs[g_max_io_num];
    memset(rknn_outputs, 0, sizeof(rknn_outputs));
    for (int i = 0; i < output_num_; ++i)
    {
        rknn_outputs[i].want_float = want_float ? 1 : 0;
    }
    ret = rknn_outputs_get(rknn_ctx_, output_num_, rknn_outputs, NULL);
    if (ret < 0)
    {
        printf("rknn_outputs_get fail! ret=%d\n", ret);
        NN_LOG_ERROR("rknn_outputs_get fail! ret=%d", ret);
        return NN_RKNN_OUTPUT_GET_FAIL;
    }

    NN_LOG_DEBUG("output num: %d", output_num_);
    // copy rknn outputs to tensor_data_s
    for (int i = 0; i < output_num_; ++i)
    {
        // 将rknn_output转换为自定义的tensor_data_s
        rknn_output_to_tensor_data(rknn_outputs[i], outputs[i]);
        NN_LOG_DEBUG("output[%d] size=%d", i, outputs[i].attr.size);
    }
    return NN_SUCCESS;
}

// 析构函数
RKEngine::~RKEngine()
{
    if (ctx_created_)
    {
        rknn_destroy(rknn_ctx_);
        NN_LOG_INFO("rknn context destroyed!");
    }
}

// 创建RKNN引擎
std::shared_ptr<NNEngine> CreateRKNNEngine()
{
    return std::make_shared<RKEngine>();
}

封装前后处理

前处理

preprocess.h
核心流程中的预处理输入数据:对输入数据进行颜色空间转换,尺寸缩放操作。

// 前处理
#ifndef RK3566_DEMO_PREPROCESS_H
#define RK3566_DEMO_PREPROCESS_H
#include <opencv2/opencv.hpp>
#include "types/datatype.h"

void imgPreprocess(const cv::Mat &img, cv::Mat &img_resized, uint32_t width, uint32_t height);
#endif //RK3566_DEMO_PREPROCESS_H

preprocess.cpp

// 前处理
#include "preprocess.h"
#include "utils/logging.h"

void imgPreprocess(const cv::Mat &img, cv::Mat &img_resized, uint32_t width, uint32_t height)
{
    // img has to be 3 channels
    if (img.channels() != 3)
    {
        NN_LOG_ERROR("img has to be 3 channels");
        exit(-1);
    }

    cv::Mat img_rgb;
    // bggr to rgb
    cv::cvtColor(img, img_rgb, cv::COLOR_BGR2RGB);
    // resize img
    cv::resize(img_rgb, img_resized, cv::Size(width, height), 0, 0, cv::INTER_LINEAR);
}

后处理

postprocess.h
核心流程中的后处理推理结果:推理完成后,从输出张量中获取结果数据,根据需要对结果进行后处理,以获得最终的预测结果。

// 后处理
#ifndef RK3566_DEMO_POSTPROCESS_H
#define RK3566_DEMO_POSTPROCESS_H

#include <stdint.h>
#include <vector>
#include "opencv2/core/core.hpp"
#include "opencv2/imgcodecs.hpp"
#include "opencv2/imgproc.hpp"

int get_classification(float* pfProb, uint32_t out_w, uint32_t out_h);

#endif //RK3566_DEMO_POSTPROCESS_H

postprocess.cpp

// 后处理
#include <string.h>
#include <stdlib.h>
#include <algorithm>
#include <iostream>
#include "postprocess.h"
#include "utils/logging.h"

int get_classification(float* pfProb, uint32_t out_w, uint32_t out_h) {
     // 1x5 获取输出数据并包装成一个cv::Mat对象,为了方便后处理
      cv::Mat prob(out_w, out_h, CV_32F, (float*) pfProb);
      std::cout << "prob : " << prob << std::endl;
      // 后处理推理结果
      cv::Point maxL, minL;		// 用于存储图像分类中的得分最小值索引和最大值索引(坐标)
      double maxv, minv;			// 用于存储图像分类中的得分最小值和最大值
      cv::minMaxLoc(prob, &minv, &maxv, &minL, &maxL); 
      int max_index = maxL.x;		// 获得最大值的索引,只有一行所以列坐标既为索引
    return max_index;
}

封装自定义类型

定义的NNEngine接口,其子类实现了RKNN API封装,同样也可以对未来新增的不同框架API进行封装,为了NNEngine与包括RKNN API在内的不同官方类型进行转化,提升兼容性和可扩展性,因此需要自定义类型。

RKNN API官方类型librknn_api/include/rknn_api.h中。

自定义数据类型

datatype.h

// 自定义定义数据类型
#ifndef RK3566_DEMO_DATATYPE_H
#define RK3566_DEMO_DATATYPE_H

#include <stdint.h>
#include <stdlib.h>

#include "utils/logging.h"
#include "types/error.h"

// 输入数据(张量)的不同通道布局
typedef enum _tensor_layout
{   
    // N批次batch,C通道数channel,H高度height,W宽度width
    NN_TENSORT_LAYOUT_UNKNOWN = 0,
    NN_TENSOR_NCHW = 1,
    NN_TENSOR_NHWC = 2,
    NN_TENSOR_OTHER = 3,
} tensor_layout_e;

// 输入数据(张量)的不同数据类型
typedef enum _tensor_datatype
{
    NN_TENSOR_INT8 = 1,
    NN_TENSOR_UINT8 = 2,
    NN_TENSOR_FLOAT = 3,
    NN_TENSOR_FLOAT16 = 4,
} tensor_datatype_e;

// 输入数据(张量)的维度
static const int g_max_num_dims = 4;

// 输入数据(张量)的基本信息
typedef struct
{
    uint32_t index;                 // 存在多个输入数据时的索引
    uint32_t n_dims;                // 输入数据维度的数量
    uint32_t dims[g_max_num_dims];  // 不同维度的大小
    uint32_t n_elems;               // 输入数据元素的个数 
    uint32_t size;                  // 元素的占用空间(字节):元素的个数×每个元素的占用空间
    tensor_datatype_e type;         // 元素的数据类型,不同类型就和不同占用空间有关
    tensor_layout_e layout;         // 输入数据的通道布局
    int32_t zp;                     // 非对称仿射量化方法中的零点值
    float scale;                    // 非对称仿射量化方法中的缩放值
} tensor_attr_s;

// 输入数据(张量)的完整内容:输入数据基本信息+输入数据本身
typedef struct
{
    tensor_attr_s attr;
    void *data;
} tensor_data_s;


// 不同数据类型的占用空间
static size_t nn_tensor_type_to_size(tensor_datatype_e type)
{
    switch (type)
    {
    case NN_TENSOR_INT8:
        return sizeof(int8_t);
    case NN_TENSOR_UINT8:
        return sizeof(uint8_t);
    case NN_TENSOR_FLOAT:
        return sizeof(float);
    case NN_TENSOR_FLOAT16:
        return sizeof(uint16_t);
    default:
        NN_LOG_ERROR("unsupported tensor type");
        exit(-1);
    }
}

// 设置输入数据的完整内容:整个过程就是将基本信息复制到完整内容中
static void nn_tensor_attr_to_cvimg_input_data(const tensor_attr_s &attr, tensor_data_s &data)
{   
    if (attr.n_dims != 4)
    {
        NN_LOG_ERROR("unsupported input dims");
        exit(-1);
    }
    data.attr.n_dims = attr.n_dims;
    data.attr.index = 0;
    data.attr.type = NN_TENSOR_UINT8;               // 在转成rknn模型时候通常会选择量化,因此输入数据类型通常都是UINT8
    data.attr.layout = NN_TENSOR_NHWC;              // 在转成rknn模型时候通常是NHWC格式
    if (attr.layout == NN_TENSOR_NCHW)
    {
        data.attr.dims[0] = attr.dims[0];
        data.attr.dims[1] = attr.dims[2];
        data.attr.dims[2] = attr.dims[3];
        data.attr.dims[3] = attr.dims[1];
    }
    else if (attr.layout == NN_TENSOR_NHWC)
    {
        data.attr.dims[0] = attr.dims[0];
        data.attr.dims[1] = attr.dims[1];
        data.attr.dims[2] = attr.dims[2];
        data.attr.dims[3] = attr.dims[3];
    }
    else
    {
        NN_LOG_ERROR("unsupported input layout");
        exit(-1);
    }
    // 计算输入元素个数
    data.attr.n_elems = data.attr.dims[0] * data.attr.dims[1] *
                        data.attr.dims[2] * data.attr.dims[3];
    // 计算输入元素的占用空间
    data.attr.size = data.attr.n_elems * sizeof(uint8_t);
}

#endif // RK3566_DEMO_DATATYPE_H

自定义错误类型

error.h

// 自定义错误码
#ifndef RK3566_DEMO_ERROR_H
#define RK3566_DEMO_ERROR_H
typedef enum
{
    NN_SUCCESS = 0,                 // 成功
    NN_LOAD_MODEL_FAIL = -1,        // 加载模型失败
    NN_RKNN_INIT_FAIL = -2,         // rknn初始化失败
    NN_RKNN_QUERY_FAIL = -3,        // rknn查询失败
    NN_RKNN_INPUT_SET_FAIL = -4,    // rknn设置输入数据失败
    NN_RKNN_RUNTIME_ERROR = -5,     // rknn运行时错误
    NN_IO_NUM_NOT_MATCH = -6,       // 输入输出数量不匹配
    NN_RKNN_OUTPUT_GET_FAIL = -7,   // rknn获取输出数据失败
    NN_RKNN_INPUT_ATTR_ERROR = -8,  // rknn输入数据属性错误
    NN_RKNN_OUTPUT_ATTR_ERROR = -9, // rknn输出数据属性错误
    NN_RKNN_MODEL_NOT_LOAD = -10,   // rknn模型未加载
} nn_error_e;

#endif // RK3566_DEMO_ERROR_H

封装辅助功能

日志包装器

logging.h
宏定义来实现日志记录是一种强大且灵活的方法,它可以帮助开发者更好地控制日志输出,实现代码的灵活性,同时提高代码的性能和可维护性。

rknn_engine.cpp很多信息输出都是用日志记录的形式。

// 日志包装器,便于更换
#ifndef RK3566_DEMO_LOGGING_H
#define RK3566_DEMO_LOGGING_H

#include <stdio.h>

// 日志等级:从低到高
// 0: no log
// 1: error
// 2: error, warning
// 3: error, warning, info
// 4: error, warning, info, debug
static int32_t g_log_level = 4;

// 打印信息日志信息
// #defin 宏定义配合 do...while(0) 结构是常见的编程技巧,用于确保宏定义的行为类似于一个单独的语句
#define NN_LOG_ERROR(...)          \
    do                             \
    {                              \
        if (g_log_level >= 1)      \
        {                          \
            printf("[NN_ERROR] "); \
            printf(__VA_ARGS__);   \
            printf("\n");          \
        }                          \
    } while (0)

#define NN_LOG_WARNING(...)          \
    do                               \
    {                                \
        if (g_log_level >= 2)        \
        {                            \
            printf("[NN_WARNING] "); \
            printf(__VA_ARGS__);     \
            printf("\n");            \
        }                            \
    } while (0)

#define NN_LOG_INFO(...)          \
    do                            \
    {                             \
        if (g_log_level >= 3)     \
        {                         \
            printf("[NN_INFO] "); \
            printf(__VA_ARGS__);  \
            printf("\n");         \
        }                         \
    } while (0)

#define NN_LOG_DEBUG(...)          \
    do                             \
    {                              \
        if (g_log_level >= 4)      \
        {                          \
            printf("[NN_DEBUG] "); \
            printf(__VA_ARGS__);   \
            printf("\n");          \
        }                          \
    } while (0)

#endif // RK3566_DEMO_LOGGING_H

辅助互换函数

engine_helper.h
实现了加载标签和模型文件的辅助函数,以及实现了nn自定义类型和rknn官方类型的互换函数,未来封装更多API后,可扩展nn自定义类型和其他官方类型的互换函数。

// 辅助互换函数
#ifndef RK3566_DEMO_ENGINE_HELPER_H
#define RK3566_DEMO_ENGINE_HELPER_H

#include <fstream>
#include <string.h>
#include <vector>
#include <rknn_api.h>
#include "utils/logging.h"
#include "types/datatype.h"

/**
 * @brief 加载标签文件
 * @param labels_txt_file 标签文件路径
 * @return std::vector<std::string> 输出标签名称
 */
std::vector<std::string> readClassNames(std::string labels_txt_file)
{
	std::vector<std::string> classNames;

	std::ifstream fp(labels_txt_file);
	if (!fp.is_open())
	{
		printf("could not open file...\n");
		exit(-1);
	}
	std::string name;
	while (!fp.eof())
	{
		std::getline(fp, name);
		if (name.length())
			classNames.push_back(name);
	}
	fp.close();
	return classNames;
}
/**
 * @brief 加载模型文件
 * @param filename 模型文件路径
 * @param model_size 模型文件大小,会在函数内部赋值
 * @return unsigned char* 模型文件数据
 */
static unsigned char *load_model(const char *filename, int *model_size)
{
    FILE *fp = fopen(filename, "rb");
    if (fp == nullptr)
    {
        NN_LOG_ERROR("fopen %s fail!", filename);
        return nullptr;
    }
    fseek(fp, 0, SEEK_END);
    int model_len = ftell(fp);
    unsigned char *model = (unsigned char *)malloc(model_len);
    fseek(fp, 0, SEEK_SET);
    if (model_len != fread(model, 1, model_len, fp))
    {
        NN_LOG_ERROR("fread %s fail!", filename);
        free(model);
        return nullptr;
    }
    *model_size = model_len;
    if (fp)
    {
        fclose(fp);
    }
    return model;
}

// 打印模型的输入输出的基本信息
static void print_tensor_attr(rknn_tensor_attr *attr)
{
    NN_LOG_INFO("  index=%d, name=%s, n_dims=%d, dims=[%d, %d, %d, %d], n_elems=%d, size=%d, fmt=%s, type=%s, qnt_type=%s, "
                "zp=%d, scale=%f",
                attr->index, attr->name, attr->n_dims, attr->dims[0], attr->dims[1], attr->dims[2], attr->dims[3],
                attr->n_elems, attr->size, get_format_string(attr->fmt), get_type_string(attr->type),
                get_qnt_type_string(attr->qnt_type), attr->zp, attr->scale);
}


// 通道布局:nn转换成对应的rknn
static rknn_tensor_format rknn_layout_convert(tensor_layout_e fmt)
{
    switch (fmt)
    {
    case NN_TENSOR_NCHW:
        return RKNN_TENSOR_NCHW;
    case NN_TENSOR_NHWC:
        return RKNN_TENSOR_NHWC;
    default:
        NN_LOG_ERROR("unsupported nn layout: %d\n", fmt);
        // exit program
        exit(1);
    }
}

// 数据类型:nn转换成对应的rknn
static rknn_tensor_type rknn_type_convert(tensor_datatype_e type)
{
    switch (type)
    {
    case NN_TENSOR_UINT8:
        return RKNN_TENSOR_UINT8;
    case NN_TENSOR_FLOAT:
        return RKNN_TENSOR_FLOAT32;
    default:
        NN_LOG_ERROR("unsupported nn type: %d\n", type);
        // exit program
        exit(1);
    }
}

// 通道布局:rkknn转化成对应的nn
static tensor_layout_e rknn_layout_convert(rknn_tensor_format fmt)
{
    switch (fmt)
    {
    case RKNN_TENSOR_NCHW:
        return NN_TENSOR_NCHW;
    case RKNN_TENSOR_NHWC:
        return NN_TENSOR_NHWC;
    default:
        return NN_TENSOR_OTHER;
    }
}

// 数据类型:rknn转换成对应的nn
static tensor_datatype_e rknn_type_convert(rknn_tensor_type type)
{
    switch (type)
    {
    case RKNN_TENSOR_UINT8:
        return NN_TENSOR_UINT8;
    case RKNN_TENSOR_FLOAT32:
        return NN_TENSOR_FLOAT;
    case RKNN_TENSOR_INT8:
        return NN_TENSOR_INT8;
    case RKNN_TENSOR_FLOAT16:
        return NN_TENSOR_FLOAT16;
    default:
        NN_LOG_ERROR("unsupported rknn type: %d\n", type);
        // exit program
        exit(1);
    }
}

// 输入数据:rknn官方类型转nn自定义类型
static tensor_attr_s rknn_tensor_attr_convert(const rknn_tensor_attr &attr)
{
    tensor_attr_s shape;
    shape.n_dims = attr.n_dims;
    shape.index = attr.index;
    for (int i = 0; i < attr.n_dims; ++i)
    {
        shape.dims[i] = attr.dims[i];
    }
    shape.size = attr.size;
    shape.n_elems = attr.n_elems;   
    shape.layout = rknn_layout_convert(attr.fmt);
    shape.type = rknn_type_convert(attr.type);
    shape.zp = attr.zp;
    shape.scale = attr.scale;
    return shape;
}

// 输入数据:nn自定义类型转rknn官方类型,为了输入到rknn模型
static rknn_input tensor_data_to_rknn_input(const tensor_data_s &data)
{
    rknn_input input;
    memset(&input, 0, sizeof(input));
    // set default not passthrough
    input.index = data.attr.index;
    input.type = rknn_type_convert(data.attr.type);
    input.size = data.attr.size;
    input.fmt = rknn_layout_convert(data.attr.layout);
    input.buf = data.data;
    return input;
}

// 输出数据:rknn官方类型转nn自定义类型,方便后处理
static void rknn_output_to_tensor_data(const rknn_output &output, tensor_data_s &data)
{
    data.attr.index = output.index;
    data.attr.size = output.size;
    NN_LOG_DEBUG("output size: %d", output.size);
    NN_LOG_DEBUG("output want_float: %d", output.want_float);
    memcpy(data.data, output.buf, output.size);
}

#endif // RK3566_DEMO_ENGINE_HELPER_H

主函数和CMake配置文件

AlexNet_API.cpp

// 导入相关库
#include <iostream>
#include <opencv2/opencv.hpp>
#include "engine/engine.h"
#include "process/preprocess.h"
#include "process/postprocess.h"
#include "utils/logging.h"
#include "utils/engine_helper.h"

int main(int argc, char **argv)
{
    // 模型文件路径
    const char *model_file = argv[1];
    // 图片文件路径
    const char *img_file = argv[2];
    std::string labels_txt_file = "src/flower_classes.txt";
    std::vector<std::string> labels = readClassNames(labels_txt_file); 

    // ==================== 1. 加载引擎 ==================== 
    // 使用shared_ptr智能指针创建引擎,类型是接口中定义的NNEngine,但是实际上是RKEngine
    std::shared_ptr<NNEngine> engine = CreateRKNNEngine();
    
    // 加载模型文件、初始化rknn context、获取rknn版本信息、获取输入输出张量的信息
    engine->LoadModelFile(model_file);

    // 获取输入输出张量的形状
    auto input_attrs = engine->GetInputShapes();
    auto output_attrs = engine->GetOutputShapes();

    NN_LOG_INFO("processing input data");
    uint32_t n_dims = input_attrs[0].n_dims;  
    uint32_t batch = input_attrs[0].dims[0];           // 根据输入属性查询输入张量的批次(参考:dims=[1, 224, 224, 3])
    uint32_t in_h = input_attrs[0].dims[1];            // 高度
    uint32_t in_w = input_attrs[0].dims[2];            // 宽度
    uint32_t channel = input_attrs[0].dims[3];         // 通道
    tensor_datatype_e type = input_attrs[0].type;      // 数据类型
    tensor_layout_e layout = input_attrs[0].layout;    // 通道布局
    uint32_t bytes = nn_tensor_type_to_size(type);     // 数据类型的占用空间

    // ==================== 2. 读取、预处理图片 ====================
    // 加载图片
    cv::Mat orig_img = cv::imread(img_file);
    // 预处理 bgr->rgb, resize
    cv::Mat img_resized;
    imgPreprocess(orig_img, img_resized, in_w, in_h);

    // ==================== 3. 设置输入、输出属性 ====================
    // 输入数据
    tensor_data_s input;
    input.attr.n_dims = n_dims;
    input.attr.dims[0] = batch;
    input.attr.dims[1] = in_h;
    input.attr.dims[2] = in_w;
    input.attr.dims[3] = channel;
    input.attr.size = channel * in_h * in_w;
    // input.attr.type = type;
    input.attr.type = NN_TENSOR_UINT8;
    input.attr.layout = layout;
    input.data = malloc(input.attr.size * bytes);
    // 将图片数据拷贝到input.data中
    memcpy(input.data, img_resized.data, input.attr.size);

    // 将input追加到inputs中
    std::vector<tensor_data_s> inputs;
    inputs.push_back(input);
    NN_LOG_DEBUG("done preprocessing input data");

    // 输出数据
    std::vector<tensor_data_s> outputs;
    tensor_data_s output;
    output.data = malloc(output_attrs[0].n_elems * sizeof(float));
    outputs.push_back(output);

    // ==================== 4. 推理 ====================
    NN_LOG_INFO("running...");
    engine->Run(inputs, outputs, true);

    // ==================== 5. 后处理 ====================
    uint32_t out_h = output_attrs[0].dims[0];  // 高度
    uint32_t out_w = output_attrs[0].dims[1];  // 宽度
    float *buffer = (float *)output.data;
    int max_index = get_classification(buffer, out_h, out_w);
    std::cout << "label id: " << max_index << std::endl;
    cv::putText(orig_img, labels[max_index], cv::Point(50, 50), cv::FONT_HERSHEY_SIMPLEX, 1.0, cv::Scalar(0, 0, 255), 2, 8);
    cv::imwrite("./output.jpg", orig_img);
    return 0;
}

CMakeLists.txt

# 设置最低版本号
cmake_minimum_required(VERSION 3.11 FATAL_ERROR)
# 设置项目名称
project(rk3566-demo VERSION 0.0.1 LANGUAGES CXX)

# 输出系统信息
message(STATUS "System: ${CMAKE_SYSTEM_NAME} ${CMAKE_SYSTEM_VERSION}")

# 设置编译器
set(CMAKE_CXX_STANDARD 14)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

# 设置库架构
#  rknn_api 文件夹路径
set(RKNN_API_PATH ${CMAKE_CURRENT_SOURCE_DIR}/librknn_api)
#  rknn_api include 路径
set(RKNN_API_INCLUDE_PATH ${RKNN_API_PATH}/include)
#  rknn_api lib 路径
set(RKNN_API_LIB_PATH ${RKNN_API_PATH}/aarch64/librknnrt.so)

# 寻找OpenCV库,使用自定义的OpenCV_DIR
set(3RDPARTY_PATH ${CMAKE_CURRENT_SOURCE_DIR}/3rdparty)
set(OpenCV_DIR ${3RDPARTY_PATH}/opencv/opencv-linux-aarch64/share/OpenCV)
find_package(OpenCV 3.4.5 REQUIRED)
# 输出OpenCV信息
message(STATUS "    include path: ${OpenCV_INCLUDE_DIRS}")


# 用来搜索头文件的目录
include_directories(
    ${OpenCV_INCLUDE_DIRS}
    ${RKNN_API_INCLUDE_PATH}
    ${CMAKE_CURRENT_SOURCE_DIR}/src
)

# 构建预处理和后处理库
add_library(nn_process SHARED
            src/process/preprocess.cpp
            src/process/postprocess.cpp
)
# 链接库
target_link_libraries(nn_process
    ${OpenCV_LIBS}
)

# 构建自定义封装API库
add_library(rknn_engine SHARED 
            src/engine/rknn_engine.cpp) 
# 链接库
target_link_libraries(rknn_engine
    ${RKNN_API_LIB_PATH}
)

# 测试自定义封装API
add_executable(alexnet_api src/AlexNet_API.cpp)

# 链接库
target_link_libraries(alexnet_api
        rknn_engine
        nn_process
)

编译和链接,完成推理,查看结果:

# 用于配置 CMake 项目的命令 
# -S .: 指定了源代码目录,.当前目录
# -B build: 指定了构建目录,当前目录下创建build子目录
cmake -S . -B build

# 使用先前配置好的构建系统来编译和链接项目
cmake --build build

# 执行推理
 ./build/alexnet_api ./weights/AlexNet.rknn ./images/sunflowers.jpg 

图片依旧正确预测为向日葵:


总结

尽可能简单、详细的介绍了RKNN C++ 应用程序编程接口化处理详细教程。


网站公告

今日签到

点亮在社区的每一天
去签到