1.前言

下文以泰山派RK3566开发板为例

  • 本文系统使用: Ubuntu 20.04.6 LTS
  • Linux 内核版本为:5.15.0-136-generic
  • 嵌入式Linux学习交流群:1005210698
  • 本文相关源码以及更多免费资料可加群获取

涉及技术栈

  • 软件:RKNN、RGA、DRM、V4L2(LVGL、线程池、视频编解码、推流不久后添加)
  • 硬件:泰山派RK3566、ov5695摄像头(MIPI)、500W摄像头(USB免驱)、6寸720*1280 MIPI 屏幕

概述

相关文章

摄像头相关

【嵌入式Linux应用开发 | V4L2】摄像头V4L2应用程序开发(MIPI摄像头)
【嵌入式Linux应用开发 | V4L2】摄像头V4L2应用程序开发(USB免驱摄像头)

屏幕显示相关

【嵌入式Linux应用开发 | DRM 】在泰山派RK3566使用DRM控制屏幕(超详细)

RGA 图像处理相关

欠着先

RKNN 模型推理相关

【 嵌入式Linux应用开发项目 | yolov8】手把手从零在泰山派RK3566上用C语言部署自己的yolov8(上)
【 嵌入式Linux应用开发项目 | yolov8】手把手从零在泰山派RK3566上用C语言部署自己的yolov8(下)


2.移植流程

1.编译 rknn_model_zoo 库

  1. 在瑞芯微官网获取 rknn_model_zoo
    RKNN Model Zoo 基于 RKNPU SDK 工具链开发,提供了目前主流算法的部署例程,例程包含导出 RKNN 模型,使用 Python API,CAPI 推理 RKNN 模型的流程。

    • 支持 RK3562, RK3566, RK3568, RK3576, RK3588, RV1126B 平台。
    • 部分支持 RV1103, RV1106
    • 支持 RV1109, RV1126, RK1808 平台。
      在这里插入图片描述
  2. 使用 git 获取源代码

git clone https://github.com/airockchip/rknn_model_zoo.git
  1. 得到这样的目录
    在这里插入图片描述

  2. 可以查看官方给出的 README 文档,里面有介绍该库的作用和所支持的模型。
    在这里插入图片描述

  3. 在编译 rknn_model_zoo 前,需要在对应模型示例中下载对应的 rknn 模型。有三种方式获取 rknn 模型(模型示例在 examples 目录下)

    1. 可以使用我提供的 rknn 模型,把它复制到 rknn_model_zoo/examples/yolov8/model 中。
    2. 第二种是使用官方提供的网盘 https://console.zbox.filez.com/l/8ufwtG (提取码: rknn) 下载模型文件,然后复制到 examples/yolov8/model 目录下。
    3. 第三种是在 rknn_model_zoo/examples/yolov8/model 目录下运行脚本下载模型文件。
cd examples/yolov8/model
./download_model.sh

注意:第二三种方式获取到的是 onnx 模型,我们需要对其进行模型转换,转换成 rknn 模型才行。否则在编译时会报错误。
在这里插入图片描述

  1. 最后就是编译这个 rknn_model_zoo 库了。在库的根目录下执行
./build-linux.sh -t <target> -a <arch> -d <build_demo_name> [-b <build_type>] [-m]
    -t : target (rk356x/rk3576/rk3588/rv1106/rv1126b/rv1126/rk1808)
    -a : arch (aarch64/armhf)
    -d : demo name
    -b : build_type(Debug/Release)
    -m : enable address sanitizer, build_type need set to Debug
Note: 'rk356x' represents rk3562/rk3566/rk3568
	  'rv1106' represents rv1103/rv1106
	  'rv1126' represents rv1109/rv1126
	  'rv1126b' is different from 'rv1126'.

# 以编译64位Linux RK3566的yolov8 demo为例:
./build-linux.sh -t rk356x -a aarch64 -d yolov8

在这里插入图片描述

  1. 编译成功后,在在根目录下有个 install 。里面存放编译好的 rga 和 rknn 相关的动态库、 rknn 模型和模型对应的标签集。
    在这里插入图片描述

2.移植 yolov8 模块

  1. 我们把 examples 目录中的 yolov8 目录复制到自己项目的目录中,保留和添加以下内容:

在这里插入图片描述

  1. rknpu2 中,所支持的设备是 RK3566/RK3568、RK3588/RK3588S、RV1103/RV1106、RK3562,而 rknpu1 不支持泰山派 RK3566 的,别移植错误文件。
  2. 其中头文件 rknn_apirknn_model_zoo/3rdparty/rknpu2/include/rknn_api.h
  3. 头文件 turbojpeg.hrknn_model_zoo/3rdparty/jpeg_turbo/include/turbojpeg.h,同时还有对应的库文件 libturbojpeg.arknn_model_zoo/3rdparty/jpeg_turbo/Linux/aarch64/libturbojpeg.a(这个库是编解码 JPEG 图片的,可以用在拍照功能上)。
  4. 同时需要移植 RGA 模块,位于 rknn_model_zoo/3rdparty/librga,动态库选择 Linux/aarch64 下的。
  5. 输入 file 查看该动态库是否符合 ARM64 架构。
file librga.so
# librga.so: ELF 64-bit LSB shared object, ARM aarch64, version 1 (SYSV), dynamically linked, stripped
  1. 最后移植的目录是这个样子在这里插入图片描述

2.模型推理流程

1.API 流程说明

  • 目前在 RK356X/RK3588 上有两组 API 可以使用,分别是通用 API 接口零拷贝流程的 API 接口RV1106/RV1103 只支持零拷贝流程的 API 接口(目前不涉及零拷贝流程)。
  • 两组 API 的主要区别在于,通用接口每次 更新帧数据,需要将外部模块分配的数据拷贝到 NPU 运行时的输入内存,而零拷贝流程的接口会直接使用预先分配的内存(包括 NPU 运行时创建的或外部其他框架创建的,比如 DRM 框架),减少了内存拷贝的花销。
  • 当用户输入数据只有虚拟地址时,只能使用通用 API 接口;
  • 当用户输入数据有物理地址或 fd 时,两组接口都可以使用。
    在这里插入图片描述

2.函数调用结构图

RKNN 的整体的函数调用结构图如下图所示。
在这里插入图片描述

单击查看大图


3.相关结构体

1.RKNN主要结构体

rknn_input_output_num
  • ​模型输入输出元数据的核心描述单元​​,用于定义模型推理时需要处理的 ​​输入数据组数​​ 和 ​​输出结果份数​​。
    1. ​输入规格说明书​​:声明模型期望接收的输入数据组数(如单张图像或多帧视频流)
    2. ​输出结果计数器​​:记录模型生成的结果份数(如多尺度检测框或分类概率)
typedef struct _rknn_input_output_num {
    uint32_t n_input;     // 输入张量数量(如:1张RGB图像)
    uint32_t n_output;    // 输出张量数量(如:3个检测结果层)
} rknn_input_output_num;
  • 用于保存通过 RKNN_QUERY_IN_OUT_NUM 查询操作获取的模型输入输出张量数量信息。
  • 通常在初始化 RKNN 上下文或加载模型后调用相关接口获取,为后续内存分配、数据预处理提供依据。
rknn_tensor_attr
  • ​张量元数据描述的核心结构体​​,用于定义模型输入/输出张量的 ​​存储格式​​、​​数据类型​​、​​量化参数​​ 和 ​​内存布局​​。
    1. ​数据说明书​​:明确张量的维度、类型和量化方式
    2. ​内存布局规范​​:指导数据在内存中的排列方式
    3. ​量化参数载体​​:存储反量化所需的缩放因子和零点
typedef struct _rknn_tensor_attr {
    uint32_t index;                      // 张量索引(输入0号,输出0-8号)
    uint32_t n_dims;                     // 维度数(输入4维[1,640,640,3])
    uint32_t dims[RKNN_MAX_DIMS];        // 各维度值(dims[0]=1, dims[1]=640等)
    char name[RKNN_MAX_NAME_LEN];        // 张量名称(如"input"或"output_0")
    
    uint32_t n_elems;                    // 元素总数(1 * 640 * 640 * 3=1,152,000)
    uint32_t size;                       // 数据总大小(1,152,000 * 1字节=1,152,000字节)
    
    rknn_tensor_format fmt;              // 数据排列(NHWC格式)
    rknn_tensor_type type;               // 数据类型(INT8量化)
    rknn_tensor_qnt_type qnt_type;       // 量化类型(仿射量化AFFINE)
    int8_t fl;                           // 小数位长度(DFP量化用,YOLOv8n不用)
    int32_t zp;                          // 量化零点(INT8反量化用,如-128)
    float scale;                         // 量化比例(如0.003922=1/255)
    
    uint32_t w_stride;                   // 宽度步长(优化对齐,通常设0)
    uint32_t size_with_stride;           // 含步长的总大小
    uint8_t pass_through;                // 直通模式(0=需转换,1=直传)
    uint32_t h_stride;                   // 高度步长(自动对齐)
} rknn_tensor_attr;
  • 用于保存通过 RKNN_QUERY_INPUT_ATTR / RKNN_QUERY_OUTPUT_ATTR 查询操作获取的模型输入输出信息。
rknn_input
  • ​输出数据解析的核心控制单元​​,负责描述模型推理结果的存储方式、数据类型和内存管理规则。
    1. ​输出数据说明书​​:定义每个输出张量的存储格式(浮点/量化)
    2. ​内存分配控制器​​:管理输出缓冲区的生命周期(自动释放或用户预分配)
    3. ​数据转换开关​​:控制是否将量化数据反转为浮点型
typedef struct _rknn_input {
    uint32_t index;                     // 输入张量索引(0-based)
    void* buf;                          // 输入数据缓冲区指针
    uint32_t size;                      // 输入数据总字节数
    uint8_t pass_through;               /* 直通模式标志
                                           * 0:需按type/fmt转换数据(默认)
                                           * 1:跳过转换,直接传递原始数据(需预处理对齐) */
    rknn_tensor_type type;              /* 输入数据类型
                                           * 常用值:RKNN_TENSOR_UINT8(8位无符号整数)
                                           *        RKNN_TENSOR_FLOAT32(32位浮点) */
    rknn_tensor_format fmt;             /* 输入数据布局
                                           * 常用值:RKNN_TENSOR_NHWC(默认)
                                           *        RKNN_TENSOR_NCHW(需与模型输入格式匹配) */
} rknn_input;
rknn_output
  • ​输出数据控制的核心结构体​​,用于定义模型推理结果的 ​​存储格式​​、​​数据类型​​ 和 ​​内存管理规则​​。
    1. ​数据类型转换控制​​:决定输出数据是否转换为浮点型
    2. ​内存生命周期管理​​:区分 SDK 自动分配与用户预分配模式
    3. ​输出索引标识​​:关联模型定义的输出张量顺序
typedef struct _rknn_output {
    uint8_t want_float;       // 数据类型转换开关
    uint8_t is_prealloc;      // 内存管理模式
    uint32_t index;           // 输出张量唯一标识
    void* buf;                // 数据存储地址
    uint32_t size;            // 数据总字节数
} rknn_output;

4.相关函数API

1.RKNN主要函数API

rknn_init()
  • 初始化函数将创建 rknn_context 对象、加载 RKNN 模型以及根据 flag 和 rknn_init_extend 结构体执行特定的初始化行为。

函数原型:

int rknn_init(rknn_context* context, void* model, uint32_t size, 
			 uint32_t flag, rknn_init_extend* extend);

参数说明:

  • rknn_context *context: 初始化后的上下文句柄。
  • const void *model:模型数据指针或文件路径
  • unsigned int size:模型数据大小(若为路径则设为0)
  • unsigned int flag:特定的初始化标志。目前,仅支持如下标志:
    • RKNN_FLAG_COLLECT_PERF_MASK:用于运行时查询网络各层时间。
    • RKNN_FLAG_MEM_ALLOC_OUTSIDE:用于表示模型输入、输出、权重、中间 tensor 内存全部由用户分配。
    • RKNN_FLAG_SHARE_WEIGHT_MEM:用于表示共享另个模型的 weight 权重
  • const rknn_init_extend *extend: 扩展参数(通常设为NULL)
rknn_destroy()
  • 将释放传入的 rknn_context 及其相关资源。

函数原型:

int rknn_destroy(rknn_context context);

参数说明:

  • rknn_context context: 销毁 rknn_context 对象及其相关资源。
rknn_query()
  • 查询获取到模型输入输出信息、逐层运行时间、模型推理的总时间、SDK 版本、内存占用信息、用户自定义字符串等信息。

函数原型:

int rknn_query(rknn_context context, rknn_query_cmd cmd, void* info, uint32_t size);

参数说明:

  • rknn_context context: rknn_context 对象。
  • rknn_query_cmd cmd:查询的类型,指定要获取的信息类别,不同的类型对应不同的查询内容。
  • void* info:用于存储查询结果的数据缓冲区,其类型和内容根据查询类型 type 确定。
  • uint32_t size:数据缓冲区 data 的大小,以字节为单位。

当前 SDK 支持的查询命令如下表所示:

查询命令 返回结果结构体 功能
RKNN_QUERY_IN_OUT_NUM rknn_input_output_num 查询输入输出 tensor 个数
RKNN_QUERY_INPUT_ATTR rknn tensor_attr 使用通用 API 接口时,查询输入tensor属性
RKNN_QUERY_OUTPUT_ATTR rknn tensor attr 使用通用API接口时,查询输出tensor属性
RKNN_QUERY_PERF_DETAIL rknn perf detail 查询网络各层运行时间,需要调用rknn_init接口时,设置RKNN_FLAG_COLLECT_PERF_MASK标志才能生效
RKNN_QUERY_PERF_RUN rknn_perf_run 查询推理模型(不包含设置输入/输出)的耗时,单位是微秒
RKNN_QUERY_SDK_VERSION rknn sdk version 查询SDK版本
RKNN_QUERY_MEM_SIZE rknn mem size 查询分配给权重和网络中间tensor的内存大小
RKNN_QUERY_CUSTOM_STRING rknn custom string 查询RKNN模型里面的用户自定义字符串信息
RKNN_QUERY_NATIVE_INPUT_ATTR rknn_tensor_attr 使用零拷贝API接口时,查询原生输入tensor居性,它是NPU直接读取的模型输入属性
RKNN_QUERY_NATIVE_OUTPUT_ATTR rknn_tensor_attr 使用零拷贝API接口时,查询原生输出tensor属性,它是NPU直接输出的模型输出属性
RKNN_QUERY_NATIVE_NC1HWC2_INPUT_ATTR rknn_tensor_attr 使用等拷贝API接口时,查询原生输入tensor属性,它是NPU直接读取的模型输入属性与RKNN_QUERY_NATIVE_INPUT_ATTR查询结果一致
RKNN_QUERY NATIVE_NC1HWC2_OUTPUT_ATTR rknn_tensor_attr 使用零拷贝 API 接口时,查询原生输出 tensor属性,它是NPU直接输出的模型输出属与RKNN_QUERY NATIVE_OUTPUT_ATTR查询结果一致性
RKNN_QUERY_NATIVE_NHWC_INPUT_ATTR rknn_tensor_attr 使用零拷贝API接口时,查询原生输入tensor属性与RKNN_QUERY_NATIVE_INPUT_ATTR查询结果一致
RKNN_QUERYNATIVENHWCOUTPUT_ATTR rknn tensor attr 使用等拷贝API接口时,查询原生输出NHWCtensor属性,

其中重要的就三个:

  • RKNN_QUERY_IN_OUT_NUM:查询模型输入输出 tensor 的个数。其中需要先创建 rknn_input_output_num 结构体对象
  • RKNN_QUERY_INPUT_ATTR:查询模型输入 tensor 的属性。其中需要先创建 rknn_tensor_attr 结构体对象。
  • RKNN_QUERY_OUTPUT_ATTR:查询模型输出 tensor 的属性。其中需要先创建 rknn_tensor_attr 结构体对象
rknn_inputs_set()
  • 设置模型的输入数据。
  • 该函数能够支持多个输入,其中每个输入是 rknn_input 结构体对象,在传入之前用户需要设置该对象。(注:RV1106/RV1103 不支持这个接口)

函数原型:

int rknn_inputs_set(rknn_context context, uint32_t n_inputs, rknn_input inputs[]);

参数说明:

  • rknn_context context: 上下文的句柄。
  • uint32_t n_inputs:输入数据个数。
  • rknn_input inputs[]:输入数据数组,数组每个元素是 rknn_input 结构体对象。
rknn_run()
  • 执行一次模型推理,调用之前需要先通过 rknn_inputs_set 函数或者零拷贝的接口设置输入数据。

函数原型:

int rknn_run(rknn_context context, rknn_run_extend* extend);

参数说明:

  • rknn_context context: rknn_context 对象。
  • rknn_run_extend* extend:保留扩展,当前没有使用,传入 NULL 即可。
rknn_outputs_get()
  • 获取模型推理的输出数据。该函数能够一次获取多个输出数据。其中每个输出是 rknn_output 结构体对象,在函数调用之前需要依次创建并设置每个 rknn_output 对象。
  • 对于输出数据的 buffer 存放可以采用两种方式:
    • 一种是用户自行申请和释放,此时 rknn_output 对象的 is_prealloc 需要设置为 1,并且将 buf 指针指向用户申请的 buffer。
    • 另一种是由 rknn 来进行分配,此时 rknn_output 对象的 is_prealloc 设置为 0 即可,函数执行之后 buf 将指向输出数据。

函数原型:

int rknn_outputs_get(rknn_context context, uint32_t n_outputs, rknn_output outputs[], rknn_output_extend* extend);

参数说明:

  • rknn_context context: rknn_context 对象。
  • uint32_t n_outputs:输出数据个数。
  • rknn_output outputs[]:输出数据的数组,其中数组每个元素为 rknn_output 结构体对象,代表模型的一个输出。
  • rknn_output_extend* extend:保留扩展,当前没有使用,传入 NULL 即可。
rknn_outputs_release()
  • 释放 rknn_outputs_get 函数得到的输出的相关资源。

函数原型:

int rknn_outputs_release(rknn_context context, uint32_t n_ouputs, rknn_output outputs[]);

参数说明:

  • rknn_context context: rknn_context 对象。
  • uint32_t n_ouputs:输出数据个数。
  • rknn_output outputs[]:要销毁的 rknn_output 数组。

3.示例代码分析

  • 因篇幅有限,故删减了一些非泰山派RK3566的代码,以及返回值判断

主函数入口

int main(int argc, char **argv) {
    if (argc != 3) {
        printf("%s <model_path> <image_path>\n", argv[0]);
        return -1;
    }

    // 从命令行参数中提取模型路径和图像路径
    const char *model_path = argv[1];
    const char *image_path = argv[2];

    int ret;
    rknn_app_context_t rknn_app_ctx;  // 存储RKNN应用上下文的结构体
    memset(&rknn_app_ctx, 0, sizeof(rknn_app_context_t));

    // 初始化后处理模块(如加载标签文件等)
    init_post_process();

    // 初始化YOLOv8模型,传入模型路径和上下文指针
    ret = init_yolov8_model(model_path, &rknn_app_ctx);

    image_buffer_t src_image;  // 存储输入图像数据的结构体
    memset(&src_image, 0, sizeof(image_buffer_t));
    // 读取图像文件到src_image结构体
    ret = read_image(image_path, &src_image);

    object_detect_result_list od_results;  // 存储目标检测结果的列表

    // 执行YOLOv8模型推理,传入上下文、输入图像和结果列表指针
    ret = inference_yolov8_model(&rknn_app_ctx, &src_image, &od_results);

    // 对检测结果进行可视化:在图像上画框并标注类别和概率
    char text[256];  // 用于存储标注文本(类别+概率)
    // 遍历所有检测结果
    for (int i = 0; i < od_results.count; i++) {
        object_detect_result *det_result = &(od_results.results[i]);  // 当前检测结果
        // 打印检测结果:类别名称、坐标(左上角x,y,右下角x,y)、概率
        printf("%s @ (%d %d %d %d) %.3f\n", coco_cls_to_name(det_result->cls_id), det_result->box.left,
            det_result->box.top, det_result->box.right, det_result->box.bottom, det_result->prop);
        // 提取检测框坐标
        int x1 = det_result->box.left;
        int y1 = det_result->box.top;
        int x2 = det_result->box.right;
        int y2 = det_result->box.bottom;

        // 在图像上绘制检测框(蓝色,线宽3)
        draw_rectangle(&src_image, x1, y1, x2 - x1, y2 - y1, COLOR_BLUE, 3);

        // 格式化标注文本:类别名称 + 概率(百分比,保留1位小数)
        sprintf(text, "%s %.1f%%", coco_cls_to_name(det_result->cls_id), det_result->prop * 100);
        // 在图像上绘制文本(红色,字体大小10,位置在检测框上方)
        draw_text(&src_image, text, x1, y1 - 20, COLOR_RED, 10);
    }

    // 将标注后的图像保存为"out.png"
    write_image("out.png", &src_image);

    return 0;
}

1.模型初始化

1.1 加载标签数据集
// 完成模型推理后处理所需的标签数据加载与初始化
// 为后续的结果解析(如目标类别名称匹配)提供基础数据支持
init_post_process();

int init_post_process(){
    // 加载的文件是 model/coco_80_labels_list.txt
    // 内部调用流程
	// loadLabelName() -->readLines() --> readLine()
    ret = loadLabelName(LABEL_NALE_TXT_PATH, labels);

    return 0;
}
1.2 初始化模型及相应操作
/**
 * 初始化 YOLOv8 模型,完成模型加载、RKNN 上下文创建、输入输出信息解析等操作
 * @param model_path YOLOv8 模型文件(RKNN 格式)的路径
 * @param app_ctx 用于存储模型初始化后上下文信息的结构体指针
 * @return 0 表示初始化成功,-1 表示失败
 */
int init_yolov8_model(const char *model_path, rknn_app_context_t *app_ctx) {
    int ret;
    int model_len = 0;           // 模型文件的字节长度
    char *model;                 // 用于存放从文件加载的模型数据
    rknn_context ctx = 0;        // RKNN上下文句柄,标识一个RKNN模型实例

    // 加载RKNN模型文件到内存
    model_len = read_data_from_file(model_path, &model);

    // 初始化RKNN上下文,将模型数据传入RKNN引擎
    ret = rknn_init(&ctx, model, model_len, 0, NULL);
    free(model);                 // 模型数据已加载到RKNN,释放内存

    // 获取模型输入输出数量
    rknn_input_output_num io_num;  // 存储输入输出数量的结构体
    // 调用rknn_query查询输入输出数量,查询类型为RKNN_QUERY_IN_OUT_NUM
    ret = rknn_query(ctx, 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);

    // 获取模型输入张量属性
    printf("input tensors:\n");
    rknn_tensor_attr input_attrs[io_num.n_input];  // 存储输入张量属性的数组
    memset(input_attrs, 0, sizeof(input_attrs));   // 初始化数组为0
    for (int i = 0; i < io_num.n_input; i++) {
        input_attrs[i].index = i;                  // 设置当前输入张量的索引
        // 调用rknn_query查询第i个输入张量的属性,查询类型为RKNN_QUERY_INPUT_ATTR
        ret = rknn_query(ctx, RKNN_QUERY_INPUT_ATTR, &(input_attrs[i]), sizeof(rknn_tensor_attr));
        dump_tensor_attr(&(input_attrs[i]));       // 打印输入张量的属性(如数据类型、格式、尺寸等)
    }

    // 获取模型输出张量属性
    printf("output tensors:\n");
    rknn_tensor_attr output_attrs[io_num.n_output];  // 存储输出张量属性的数组
    memset(output_attrs, 0, sizeof(output_attrs));   // 初始化数组为0
    for (int i = 0; i < io_num.n_output; i++) {
        output_attrs[i].index = i;                   // 设置当前输出张量的索引
        // 调用rknn_query查询第i个输出张量的属性,查询类型为RKNN_QUERY_OUTPUT_ATTR
        ret = rknn_query(ctx, RKNN_QUERY_OUTPUT_ATTR, &(output_attrs[i]), sizeof(rknn_tensor_attr));
        dump_tensor_attr(&(output_attrs[i]));        // 打印输出张量的属性
    }

    // 将RKNN上下文句柄保存到应用上下文中
    app_ctx->rknn_ctx = ctx;

    // 判断模型是否为量化模型
    // 若为不对称 affine 量化且数据类型为INT8,则标记为量化模型
    if (output_attrs[0].qnt_type ` RKNN_TENSOR_QNT_AFFINE_ASYMMETRIC && output_attrs[0].type ` RKNN_TENSOR_INT8)
        app_ctx->is_quant = true;
    else
        app_ctx->is_quant = false;

    app_ctx->io_num      = io_num;
    
    app_ctx->input_attrs = (rknn_tensor_attr *)malloc(io_num.n_input * sizeof(rknn_tensor_attr));
    memcpy(app_ctx->input_attrs, input_attrs, io_num.n_input * sizeof(rknn_tensor_attr));

    app_ctx->output_attrs = (rknn_tensor_attr *)malloc(io_num.n_output * sizeof(rknn_tensor_attr));
    memcpy(app_ctx->output_attrs, output_attrs, io_num.n_output * sizeof(rknn_tensor_attr));

    // 根据输入张量的格式(NCHW/NHWC)解析模型输入的通道数、高度和宽度
    if (input_attrs[0].fmt ` RKNN_TENSOR_NCHW) {  // NCHW格式:[批次, 通道, 高度, 宽度]
        printf("model is NCHW input fmt\n");
        app_ctx->model_channel = input_attrs[0].dims[1];  // 通道数
        app_ctx->model_height  = input_attrs[0].dims[2];  // 高度
        app_ctx->model_width   = input_attrs[0].dims[3];  // 宽度
    } else {  // NHWC格式:[批次, 高度, 宽度, 通道]
        printf("model is NHWC input fmt\n");
        app_ctx->model_height  = input_attrs[0].dims[1];  // 高度
        app_ctx->model_width   = input_attrs[0].dims[2];  // 宽度
        app_ctx->model_channel = input_attrs[0].dims[3];  // 通道数
    }

    return 0;
}

2.模型推理

2.1 读取图片
// 读取一张图片
image_buffer_t src_image;
memset(&src_image, 0, sizeof(image_buffer_t));
ret = read_image(image_path, &src_image);
2.2 模型推理
/**
 * @brief 使用YOLOv8模型进行目标检测推理
 * 
 * @param app_ctx RKNN应用上下文指针,包含模型相关配置信息
 * @param img 输入图像缓冲区指针,存储原始图像数据
 * @param od_results 输出参数,用于存储目标检测结果列表
 * @return int 函数执行结果,0表示成功,负数表示失败
 */
int inference_yolov8_model(rknn_app_context_t *app_ctx, image_buffer_t *img, object_detect_result_list *od_results) {
    int ret;
    image_buffer_t dst_img;                   // 预处理后的目标图像缓冲区
    letterbox_t letter_box;                   // 存储letterbox预处理的相关参数(如缩放比例、填充信息等)
    rknn_input inputs[app_ctx->io_num.n_input];  // RKNN模型输入数组,大小为模型输入数量
    rknn_output outputs[app_ctx->io_num.n_output];// RKNN模型输出数组,大小为模型输出数量
    const float nms_threshold      = NMS_THRESH;    // NMS(非极大值抑制)阈值,用于过滤重叠检测框
    const float box_conf_threshold = BOX_THRESH;    // 置信度阈值,用于过滤低置信度检测框
    int bg_color                   = 114;           // letterbox预处理时填充的背景色值

    // 预处理:准备目标图像缓冲区(用于存储letterbox处理后的图像)
    dst_img.width     = app_ctx->model_width;    // 目标图像宽度设为模型要求的宽度
    dst_img.height    = app_ctx->model_height;   // 目标图像高度设为模型要求的高度
    // 这里提一嘴,官方 yolov8 训练的模型是 640*640,RGB888 的
    // 如果提前转换了图像,信息不一样会导致模型不能正常推理
    // 官方的过于复杂且兼容,我自己是转换好再传入的,不走 convert_image_with_letterbox()
    dst_img.format    = IMAGE_FORMAT_RGB888;     // 目标图像格式设为RGB888
    dst_img.size      = get_image_size(&dst_img); // 计算目标图像数据大小
    // 为目标图像分配内存
    dst_img.virt_addr = (unsigned char *)malloc(dst_img.size);

    // 对原始图像进行letterbox预处理(保持长宽比缩放并填充至模型输入尺寸)
    // 详细见 2.2.1 图像处理
    ret = convert_image_with_letterbox(img, &dst_img, &letter_box, bg_color);

    // 设置RKNN模型输入数据
    inputs[0].index = 0;  // 输入索引为0(对应模型的第一个输入)
    inputs[0].type  = RKNN_TENSOR_UINT8;  // 输入数据类型为UINT8
    inputs[0].fmt   = RKNN_TENSOR_NHWC;   // 输入数据格式为NHWC(通道最后)
    inputs[0].size  = app_ctx->model_width * app_ctx->model_height * app_ctx->model_channel;
    inputs[0].buf   = dst_img.virt_addr;  // 输入数据指针指向预处理后的图像数据

    // 将输入数据设置到RKNN上下文
    ret = rknn_inputs_set(app_ctx->rknn_ctx, app_ctx->io_num.n_input, inputs);

    // 执行RKNN模型推理
    printf("rknn_run\n");
    ret = rknn_run(app_ctx->rknn_ctx, nullptr);

    // 准备获取模型输出结果
    memset(outputs, 0, sizeof(outputs));
    for (int i = 0; i < app_ctx->io_num.n_output; i++) {
        outputs[i].index      = i;
        outputs[i].want_float = (!app_ctx->is_quant);  // 根据模型是否量化决定是否需要浮点输出
    }
    // 从RKNN上下文获取输出结果
    ret = rknn_outputs_get(app_ctx->rknn_ctx, app_ctx->io_num.n_output, outputs, NULL);

	// 后处理:解析模型输出,进行置信度过滤和NMS,生成最终检测结果(过于复杂,就不解析了)
    post_process(app_ctx, outputs, &letter_box, box_conf_threshold, nms_threshold, od_results);

    // 释放RKNN输出结果占用的内存
    rknn_outputs_release(app_ctx->rknn_ctx, app_ctx->io_num.n_output, outputs);

    return ret;
}
2.2.1 图像处理
/**
 * @brief 对图像进行letterbox预处理(保持宽高比缩放并填充至目标尺寸)
 *
 * @param src_image 源图像缓冲区指针,包含原始图像信息
 * @param dst_image 目标图像缓冲区指针,用于存储处理后的图像(输出)
 * @param letterbox 用于存储letterbox处理参数(缩放比例、填充量等)
 * @param color 填充区域的颜色值
 * @return int 函数执行结果,0表示成功,负数表示失败
 */
int convert_image_with_letterbox(image_buffer_t *src_image, image_buffer_t *dst_image, letterbox_t *letterbox, char color) {
    int ret                 = 0;
    int allow_slight_change = 1;                    // 是否允许微调缩放后的尺寸以满足对齐要求(如4的倍数)
    int src_w               = src_image->width;     // 源图像宽度
    int src_h               = src_image->height;    // 源图像高度
    int dst_w               = dst_image->width;     // 目标图像宽度(模型输入宽度)
    int dst_h               = dst_image->height;    // 目标图像高度(模型输入高度)
    int resize_w            = dst_w;                // 缩放后的宽度(初始设为目标宽度)
    int resize_h            = dst_h;                // 缩放后的高度(初始设为目标高度)

    int padding_w = 0;    // 水平方向填充量(目标宽度 - 缩放后宽度)
    int padding_h = 0;    // 垂直方向填充量(目标高度 - 缩放后高度)

    int _left_offset = 0;      // 水平方向填充的左偏移(使缩放后图像居中)
    int _top_offset  = 0;      // 垂直方向填充的上偏移(使缩放后图像居中)
    float scale      = 1.0;    // 缩放比例(源图像到目标图像的缩放因子)

    // 源图像的矩形区域(整个源图像)
    image_rect_t src_box;
    src_box.left   = 0;
    src_box.top    = 0;
    src_box.right  = src_image->width - 1;     // 源图像右下角x坐标
    src_box.bottom = src_image->height - 1;    // 源图像右下角y坐标

    // 目标图像的矩形区域(初始为整个目标图像)
    image_rect_t dst_box;
    dst_box.left   = 0;
    dst_box.top    = 0;
    dst_box.right  = dst_image->width - 1;     // 目标图像右下角x坐标
    dst_box.bottom = dst_image->height - 1;    // 目标图像右下角y坐标

    // 计算宽高方向的缩放比例
    float _scale_w = (float)dst_w / src_w;    // 宽度方向缩放比例(目标宽/源宽)
    float _scale_h = (float)dst_h / src_h;    // 高度方向缩放比例(目标高/源高)

    // 选择较小的缩放比例,确保源图像完全放入目标尺寸(避免裁剪)
    if (_scale_w < _scale_h) {
        scale    = _scale_w;                // 以宽度缩放比例为准
        resize_h = (int)(src_h * scale);    // 按比例计算缩放后的高度
    } else {
        scale    = _scale_h;                // 以高度缩放比例为准
        resize_w = (int)(src_w * scale);    // 按比例计算缩放后的宽度
    }

    // 微调缩放后的尺寸,使其满足对齐要求(优化硬件加速处理)
    if (allow_slight_change == 1 && (resize_w % 4 != 0))
        resize_w -= resize_w % 4;    // 宽度调整为4的倍数
    if (allow_slight_change == 1 && (resize_h % 2 != 0))
        resize_h -= resize_h % 2;    // 高度调整为2的倍数

    // 计算填充量(目标尺寸与缩放后尺寸的差值)
    padding_h = dst_h - resize_h;
    padding_w = dst_w - resize_w;

    // 计算填充偏移,使缩放后的图像在目标图像中居中
    if (_scale_w < _scale_h) {
        // 宽度方向缩放为主,垂直方向需要填充
        dst_box.top = padding_h / 2;    // 上填充量(总填充的一半)
        // 确保上偏移为偶数(优化对齐)
        if (dst_box.top % 2 != 0) {
            dst_box.top -= dst_box.top % 2;
            if (dst_box.top < 0) {
                dst_box.top = 0;
            }
        }
        dst_box.bottom = dst_box.top + resize_h - 1;    // 缩放后图像的底部坐标
        _top_offset    = dst_box.top;                   // 记录垂直方向填充偏移
    } else {
        // 高度方向缩放为主,水平方向需要填充
        dst_box.left = padding_w / 2;    // 左填充量(总填充的一半)
        // 确保左偏移为偶数(优化对齐)
        if (dst_box.left % 2 != 0) {
            dst_box.left -= dst_box.left % 2;
            if (dst_box.left < 0) {
                dst_box.left = 0;
            }
        }
        dst_box.right = dst_box.left + resize_w - 1;    // 缩放后图像的右侧坐标
        _left_offset  = dst_box.left;                   // 记录水平方向填充偏移
    }

    // 记录letterbox参数(用于后续坐标映射)
    if (letterbox != NULL) {
        letterbox->scale = scale;           // 缩放比例
        letterbox->x_pad = _left_offset;    // 水平填充偏移
        letterbox->y_pad = _top_offset;     // 垂直填充偏移
    }

    if (dst_image->virt_addr == NULL && dst_image->fd <= 0) {
        int dst_size         = get_image_size(dst_image);
        dst_image->virt_addr = (uint8_t *)malloc(dst_size);
    }

    // 执行图像转换(缩放源图像到目标区域,并填充背景色)
    // 内部会优先使用 RGA 进行图像转换,当不符合条件和 RGA 转换失败时,就会启用 CPU 软件转换
  	// 内部调用流程:convert_image() ————> convert_image_rga() ————> improcess()
  	//							   |————> convert_image_cpu() ————> crop_and_scale_image_c()
    ret = convert_image(src_image, dst_image, &src_box, &dst_box, color);
    return ret;
}

画框和写标签

  • 根据 object_detect_result_list od_results 中的信息在图片上画框和写标签了。
  • 感兴趣可以去看看。

3.结束推理

// 后处理模块的资源释放,主要负责释放存储类别标签字符串的内存,避免内存泄漏。
deinit_post_process();
// 释放 YOLOv8 模型相关的资源,包括模型输入输出属性的内存、RKNN 上下文等,是模型使用完毕后的清理函数。
ret = release_yolov8_model(&rknn_app_ctx);

未完待续…

Logo

腾讯云面向开发者汇聚海量精品云计算使用和开发经验,营造开放的云计算技术生态圈。

更多推荐