【 嵌入式Linux应用开发项目 | yolov8】手把手从零在泰山派RK3566上用C语言部署自己的yolov8(上)
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 屏幕
概述
- 该文章主要介绍如何移植官方的 yolov8 示例到自己的项目中,以及讲解相关的函数API和结构体,并解析官方给出推理 yolov8 主要代码。然后将输入源(官方给出的是输入一张图片)修改成从摄像头获取图像数据,通过 RGA 进行图像格式转换后进行模型推理,最后通过 DRM 实时显示到屏幕上。
- 该文章分为两篇来讲解。
- 本文主要介绍如何移植官方的 yolov8 示例到自己的项目中,以及讲解相关的函数API和结构体,并解析官方给出推理 yolov8 主要代码。
相关文章
摄像头相关
【嵌入式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 库
-
在瑞芯微官网获取 rknn_model_zoo
RKNN Model Zoo基于RKNPU SDK工具链开发,提供了目前主流算法的部署例程,例程包含导出RKNN模型,使用Python API,CAPI推理RKNN模型的流程。- 支持
RK3562,RK3566,RK3568,RK3576,RK3588,RV1126B平台。 - 部分支持
RV1103,RV1106。 - 支持
RV1109,RV1126,RK1808平台。
- 支持
-
使用 git 获取源代码
git clone https://github.com/airockchip/rknn_model_zoo.git
-
得到这样的目录

-
可以查看官方给出的
README文档,里面有介绍该库的作用和所支持的模型。
-
在编译
rknn_model_zoo前,需要在对应模型示例中下载对应的rknn模型。有三种方式获取rknn模型(模型示例在 examples 目录下)- 可以使用我提供的 rknn 模型,把它复制到
rknn_model_zoo/examples/yolov8/model中。 - 第二种是使用官方提供的网盘 https://console.zbox.filez.com/l/8ufwtG (提取码: rknn) 下载模型文件,然后复制到
examples/yolov8/model目录下。 - 第三种是在
rknn_model_zoo/examples/yolov8/model目录下运行脚本下载模型文件。
- 可以使用我提供的 rknn 模型,把它复制到
cd examples/yolov8/model
./download_model.sh
注意:第二三种方式获取到的是 onnx 模型,我们需要对其进行模型转换,转换成 rknn 模型才行。否则在编译时会报错误。
-
在
rknn_model_zoo/examples/yolov8/python下有convert.py转换脚本,但直接运行会出问题。原因是缺少一些环境,所以不建议使用这个转换。
-
转换模型使用 rknn-toolkit2 。这个库可以将自定义训练的模型
pt转rknn模型(pt -> onnx -> rknn)。这里可以参考《 【YOLOv8部署至RK3588】模型训练→转换RKNN→开发板部署》
- 最后就是编译这个
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

- 编译成功后,在在根目录下有个
install。里面存放编译好的 rga 和 rknn 相关的动态库、 rknn 模型和模型对应的标签集。
2.移植 yolov8 模块
- 我们把
examples目录中的yolov8目录复制到自己项目的目录中,保留和添加以下内容:

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

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
- 模型输入输出元数据的核心描述单元,用于定义模型推理时需要处理的 输入数据组数 和 输出结果份数。
- 输入规格说明书:声明模型期望接收的输入数据组数(如单张图像或多帧视频流)
- 输出结果计数器:记录模型生成的结果份数(如多尺度检测框或分类概率)
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
- 张量元数据描述的核心结构体,用于定义模型输入/输出张量的 存储格式、数据类型、量化参数 和 内存布局。
- 数据说明书:明确张量的维度、类型和量化方式
- 内存布局规范:指导数据在内存中的排列方式
- 量化参数载体:存储反量化所需的缩放因子和零点
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
- 输出数据解析的核心控制单元,负责描述模型推理结果的存储方式、数据类型和内存管理规则。
- 输出数据说明书:定义每个输出张量的存储格式(浮点/量化)
- 内存分配控制器:管理输出缓冲区的生命周期(自动释放或用户预分配)
- 数据转换开关:控制是否将量化数据反转为浮点型
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
- 输出数据控制的核心结构体,用于定义模型推理结果的 存储格式、数据类型 和 内存管理规则。
- 数据类型转换控制:决定输出数据是否转换为浮点型
- 内存生命周期管理:区分 SDK 自动分配与用户预分配模式
- 输出索引标识:关联模型定义的输出张量顺序
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);
未完待续…
魔乐社区(Modelers.cn) 是一个中立、公益的人工智能社区,提供人工智能工具、模型、数据的托管、展示与应用协同服务,为人工智能开发及爱好者搭建开放的学习交流平台。社区通过理事会方式运作,由全产业链共同建设、共同运营、共同享有,推动国产AI生态繁荣发展。
更多推荐



所有评论(0)