一、本文的目标
解析源码,了解(掌握)yolov5在rk3588中的使用,为后续在自己项目中应用该技术打下坚实基础。
对于这个例子的编译和运行,请参考上一篇yolov5的文章。
二、阅读源码前要了解rknn_api头文件
rknn_api.h 是 Rockchip NPU (Neural Processing Unit) 软件开发工具包(SDK)中的核心头文件,它定义了与 RKNN (Rockchip Neural Network) 模型推理相关的接口和数据结构。
以我们的编译目标安卓rk3588为例,它的位置在rknn_model_zoo\3rdparty\rknpu2\include下。
主要作用
- 接口定义:提供了加载、运行和卸载 RKNN 模型所需的所有函数原型
- 数据结构定义:包含了模型输入输出、张量信息、运行配置等数据结构
- 错误码定义:定义了所有可能的错误返回码
- 版本控制:包含 API 版本信息
核心功能
- 模型加载与初始化 (
rknn_init
,rknn_destroy
) - 输入输出设置 (
rknn_inputs_set
,rknn_outputs_get
) - 模型推理执行 (
rknn_run
) - 性能统计 (
rknn_query
) - 内存管理 (
rknn_create_mem
,rknn_destroy_mem
)
本文中要用到的数据结构
- rknn_context
#ifdef __arm__
typedef uint32_t rknn_context;
#else
typedef uint64_t rknn_context;
#endif
定义 RKNN 模型的上下文句柄,用于标识和管理加载的模型实例。
ARM 平台:使用 32 位无符号整数(uint32_t)
非 ARM 平台:使用 64 位无符号整数(uint64_t)
**使用场景:**在调用 rknn_init() 初始化模型后,返回的句柄用于后续所有模型操作(如推理、查询等)。
2. rknn_input_output_num
typedef struct _rknn_input_output_num {
uint32_t n_input; // 输入张量数量
uint32_t n_output; // 输出张量数量
} rknn_input_output_num;
记录模型的输入和输出张量数量。
**使用场景:**通过 rknn_query() 查询模型基本信息时获取,用于预分配输入/输出缓冲区。
- rknn_tensor_attr
typedef struct _rknn_tensor_attr {
uint32_t index; // 输入/输出张量的索引
uint32_t n_dims; // 维度数量
uint32_t dims[RKNN_MAX_DIMS]; // 维度数组(如 [1,3,224,224])
char name[RKNN_MAX_NAME_LEN]; // 张量名称
uint32_t n_elems; // 元素总数(各维度乘积)
uint32_t size; // 数据总大小(字节)
rknn_tensor_format fmt; // 数据布局(如 NCHW/NHWC)
rknn_tensor_type type; // 数据类型(如 FP32/INT8)
rknn_tensor_qnt_type qnt_type; // 量化类型(如非量化/对称量化)
int8_t fl; // 小数位长度(DFP 量化用)
int32_t zp; // 零点值(非对称量化用)
float scale; // 缩放因子(非对称量化用)
uint32_t w_stride; // 宽度方向步长(只读,0=自动对齐)
uint32_t size_with_stride; // 包含步长的总大小(字节)
uint8_t pass_through; // 是否启用直通模式(绕过数据转换)
uint32_t h_stride; // 高度方向步长(可写,0=自动对齐)
} rknn_tensor_attr;
完整描述输入/输出张量的属性和量化信息。
关键字段:
量化相关:
qnt_type:量化类型(如 RKNN_TENSOR_QNT_AFFINE_ASYMMETRIC)
zp 和 scale:INT8 量化模型的零点和缩放因子
内存布局:
w_stride 和 h_stride:控制数据在内存中的步长(优化对齐)
直通模式:
pass_through=1 时,输入数据直接传递给模型,跳过格式转换
**使用场景:**通过 rknn_query() 获取模型输入/输出的详细属性,配置输入数据时需匹配这些属性(如量化参数、布局等)
三、yolov5 c++例子目录结构说明
样例的目录在rknn_model_zoo\examples\yolov5\cpp结构如下:
├── cpp
│ ├── rknpu2 # 使用该版本下的yolov5源文件
│ │ ├── yolov5.cc # 这是我们的重点阅读对象
│ │ └── yolov5_rv1106_1103.cc # RV1106及RV1103芯片专用
│ ├── CMakeLists.txt # CMake 的配置文件,用于定义项目的构建规则、依赖关系、编译选项
│ ├── main.cc # main函数自不必说
│ ├── postprocess.cc # 后处理源文件
│ ├── postprocess.h # 后处理头文件,用于对识别结果进行画框等操作
│ └── yolov5.h # yolov5的头文件
四、yolov5 c++例子源代码分析
从main函数入手看主要逻辑
从引入的头文件看,我们稍后还要了解其他文件:
#include "image_utils.h"
#include "file_utils.h"
#include "image_drawing.h"
该例子的主要逻辑见下图:
下面按照逻辑顺序逐步分析
1.初始化上下文和后处理
rknn_app_context_t rknn_app_ctx;
memset(&rknn_app_ctx, 0, sizeof(rknn_app_context_t));
init_post_process();
了解上下文rknn_app_context_t:
该结构体定义在yolov5.h,主要字段和说明如下:
typedef struct {
rknn_context rknn_ctx; // RKNN 模型的核心句柄,用于管理模型的加载、推理和资源释放。所有与模型交互的操作(如推理、查询输入输出属性)均需此上下文。(rknn_api.h)
rknn_input_output_num io_num; // 存储模型的输入和输出数量。例如,YOLOv5 模型通常有 **1 个输入**和 **3 个输出**。 (rknn_api.h)
rknn_tensor_attr* input_attrs; // 存储输入和输出张量的属性,包括维度(shape)、数据类型(dtype)、量化参数(qnt_type)等。
rknn_tensor_attr* output_attrs;
#if defined(RV1106_1103)
rknn_tensor_mem* input_mems[1]; // 在 RV1106/1103 平台上,输入输出内存需要显式分配为 DMA 缓冲区以提高性能。 input_mems 和 output_mems 是输入输出张量的内存对象指针数组。
rknn_tensor_mem* output_mems[3];
rknn_dma_buf img_dma_buf; // 存储输入图像的 DMA 缓冲区信息,用于在 RV1106 平台上高效传输图像数据到 NPU。
#endif
int model_channel; // 模型的输入尺寸(如 YOLOv5 的 3x640x640)。用于指导输入图像的预处理(缩放、归一化等)。
int model_width;
int model_height;
bool is_quant; // 标记模型是否为量化模型。量化模型(is_quant=true)的输入输出通常为 uint8,需要反量化处理;非量化模型(如 FP16/FP32)则无需。
} rknn_app_context_t;
2.加载Yolov5模型
ret = init_yolov5_model(model_path, &rknn_app_ctx);
init_yolov5_model函数实现在yolov5.cc文件,逻辑示意图如下:
加载模型文件:从 model_path 读取 RKNN 模型数据到内存。
初始化 RKNN 上下文:调用 rknn_init 初始化模型,获取 rknn_context 句柄。并释放模型文件内存。
查询输入输出数量:通过 rknn_query 获取模型的输入和输出张量数量(io_num)。
查询输入张量属性:遍历所有输入张量,查询其属性(维度、数据类型、量化参数等)。打印输入张量信息(dump_tensor_attr)。
查询输出张量属性:遍历所有输出张量,查询其属性。打印输出张量信息。
设置应用上下文(app_ctx):
- 保存 rknn_context 到 app_ctx->rknn_ctx。
- 根据输出张量属性判断模型是否为量化模型(is_quant)。
- 保存输入输出数量(io_num)和属性(input_attrs/output_attrs)。
- 解析输入张量的维度(model_height/width/channel),支持 NCHW 和 NHWC 格式。
**返回状态:**成功返回 0,失败返回 -1。
3.读取图片
image_buffer_t src_image;
memset(&src_image, 0, sizeof(image_buffer_t));
ret = read_image(image_path, &src_image);
read_image的实现在rknn_model_zoo\utils\image_utils.c:
int read_image(const char* path, image_buffer_t* image)
{
const char* _ext = strrchr(path, '.');
if (!_ext) {
// missing extension
return -1;
}
if (strcmp(_ext, ".data") == 0) {
return read_image_raw(path, image);
} else if (strcmp(_ext, ".jpg") == 0 || strcmp(_ext, ".jpeg") == 0 || strcmp(_ext, ".JPG") == 0 ||
strcmp(_ext, ".JPEG") == 0) {
return read_image_jpeg(path, image);
} else {
return read_image_stb(path, image);
}
}
我们主要来看看read_image_jpeg,逻辑示意图如下:
功能概述
读取 JPEG 图像文件,解码为 RGB888 格式,并存储到 image_buffer_t
结构体中。
3.1. 文件读取阶段
- 打开文件
打开 JPEG 文件,获取文件大小 - 内存分配
分配内存并读取文件内容到缓冲区jpegBuf
- 错误处理
若文件操作失败,打印错误并返回
3.2. 解码初始化
- 初始化解码器
调用tjInitDecompress()
初始化解码器句柄handle
- 解析头信息
使用tjDecompressHeader3()
获取:- 原始宽高
- 子采样方式
- 色彩空间
3.3. 图像预处理
- 16字节对齐
对原始宽高进行裁剪(便于后续 RGA 操作) - 打印尺寸信息
输出原始和裁剪后的尺寸
3.4. 图像解码
- 参数确认
再次调用tjDecompressHeader3()
验证参数 - 缓冲区分配
分配输出缓冲区sw_out_buf
(若未预分配) - 执行解码
使用tjDecompress2()
解码为 RGB888 格式
3.5. 结果存储
- 填充结构体
保存到image_buffer_t
:- 宽高
- 格式(RGB888)
- 数据指针
- 数据大小
- 错误处理
若解码失败,跳转到资源释放
6. 资源清理
- 释放解码器
销毁 TurboJPEG 句柄handle
- 释放内存
释放 JPEG 缓冲区jpegBuf
4.执行推理
object_detect_result_list od_results;
ret = inference_yolov5_model(&rknn_app_ctx, &src_image, &od_results);
4.1 先了解几个数据结构
object_detect_result_list结构体用于存储一帧图像中的所有检测结果。在postproess.h可以找到它:
typedef struct {
image_rect_t box; // 目标检测框坐标(通常包含left/top/right/bottom)
float prop; // 检测置信度(0.0~1.0)
int cls_id; // 类别ID(对应coco_classes等分类体系)
} object_detect_result;
typedef struct {
int id; // 帧标识ID(可用于多帧追踪)
int count; // 实际检测到的目标数量
object_detect_result results[OBJ_NUMB_MAX_SIZE]; // 结果数组(固定大小)
} object_detect_result_list;
image_rect_t在common.h中:
typedef struct {
int left;
int top;
int right;
int bottom;
} image_rect_t;
image_buffer_t与摄像头采集、视频解码等模块交互时描述图像数据。
typedef struct {
int width; // 图像宽度(像素)
int height; // 图像高度(像素)
int width_stride; // 每行数据的步长(字节)
int height_stride; // 垂直方向的步长(行数)
image_format_t format;// 图像格式(如 RGB/YUV)
unsigned char* virt_addr; // 虚拟地址指针(图像数据)
int size; // 图像数据总大小(字节)
int fd; // 内存文件描述符(用于零拷贝)
} image_buffer_t;
这是很重要的一个结构体,下面展开说说:
基础属性
字段 | 类型 | 说明 |
---|---|---|
width |
int |
图像的有效宽度(像素数),如 1920 |
height |
int |
图像的有效高度(像素数),如 1080 |
format |
image_format_t |
图像格式(枚举值,如 FORMAT_RGB888 /FORMAT_NV12 ) |
内存布局控制
字段 | 说明 |
---|---|
width_stride |
每行实际占用的字节数(可能包含填充字节),例如 RGB888 图像宽度为 1920 时,width_stride 可能是 1920*3=5760 或 4 字节对齐后的 5824 |
height_stride |
实际存储的行数(可能包含填充行),用于描述内存中图像的连续存储方式 |
数据存储
字段 | 说明 |
---|---|
virt_addr |
指向图像数据的虚拟地址指针,可直接读写像素数据 |
size |
图像数据占用的总内存大小(字节),计算方式通常为:height_stride * width_stride |
fd |
内存文件描述符(DMA-BUF),用于零拷贝操作(如 NPU 硬件加速时共享内存) |
内存结构示意图 | |
以 RGB888 图像 为例(width_stride 含 64 字节对齐填充): |
virt_addr → ┌───────────────┬───────────────┬───────┐
│ Pixel Data │ Padding │ ... │ (height_stride)
│ (RGB pixels) │ (对齐填充) │ │
└───────────────┴───────────────┴───────┘
↑ ↑
width=1920 width_stride=1984 (1920*3 + 64对齐)
letterbox_t在image_utils.h中。
typedef struct {
int x_pad; // X轴方向的填充像素数
int y_pad; // Y轴方向的填充像素数
float scale; // 图像缩放比例
} letterbox_t;
4.2 再来分析inference_yolov5_model函数
该函数的具体实现在yolov5.cc,流程为:图像预处理、模型推理和后处理,最终输出检测结果。
参数说明
参数 | 类型 | 说明 |
---|---|---|
app_ctx |
rknn_app_context_t* |
RKNN模型上下文 |
img |
image_buffer_t* |
输入图像缓冲区 |
od_results |
object_detect_result_list* |
输出检测结果 |
主要逻辑如下图: | ||
![]() |
初始化输出图像缓冲区
为预处理后的图像分配内存空间,确保尺寸与模型输入严格匹配。
memset(&dst_img, 0, sizeof(image_buffer_t)); // 清空结构体
dst_img.width = app_ctx->model_width; // 设置为目标模型宽度(如640)
dst_img.height = app_ctx->model_height; // 设置为目标模型高度(如640)
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预处理
- 保持原图宽高比进行缩放
- 用灰色(bg_color=114)填充边缘至目标尺寸
- 记录填充偏移量(letter_box)用于后续坐标还原
ret = convert_image_with_letterbox(img, &dst_img, &letter_box, bg_color);
**目的:**避免图像变形,同时适配NPU的固定输入尺寸。
配置模型输入张量
inputs[0].index = 0; // 指定输入索引
inputs[0].type = RKNN_TENSOR_UINT8; // 数据类型(8位无符号整型)
inputs[0].fmt = RKNN_TENSOR_NHWC; // 内存布局(Height-Width-Channel)
inputs[0].size = app_ctx->model_width * app_ctx->model_height * app_ctx->model_channel; // 数据大小
inputs[0].buf = dst_img.virt_addr; // 绑定预处理后的图像数据
rknn_inputs_set(app_ctx->rknn_ctx, app_ctx->io_num.n_input, inputs); // 提交给NPU
模型推理
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); // 量化模型需反量化输出
}
获取NPU输出结果
ret = rknn_outputs_get(app_ctx->rknn_ctx, app_ctx->io_num.n_output, outputs, NULL);
if (ret < 0) {
printf("rknn_outputs_get fail! ret=%d\n", ret);
goto out; // 跳转到资源释放
}
后处理与资源释放
post_process(app_ctx, outputs, &letter_box, box_conf_threshold, nms_threshold, od_results);
rknn_outputs_release(app_ctx->rknn_ctx, app_ctx->io_num.n_output, outputs);
后处理流程:
- 置信度过滤(box_conf_threshold)
- NMS非极大值抑制(nms_threshold)
- 将Letterbox坐标还原为原始图像坐标
post_process函数实现在postprocess.cc中,主要完成以下任务: - 解析模型输出:处理不同硬件平台(RV1106/RKNPU1/其他)和数据类型(量化/非量化)的输出
- 过滤与排序:根据置信度阈值过滤检测框,并按置信度排序
- NMS处理:对同类别的检测框进行非极大值抑制
- 坐标转换:将检测框坐标从模型输入尺寸转换回原始图像尺寸
参数说明:
参数 | 类型 | 说明 |
---|---|---|
app_ctx |
rknn_app_context_t* |
模型上下文(包含模型尺寸/量化信息等) |
outputs |
void* |
模型原始输出(平台相关格式) |
letter_box |
letterbox_t* |
Letterbox预处理参数(用于坐标还原) |
conf_threshold |
float |
置信度阈值(默认0.25) |
nms_threshold |
float |
NMS阈值(默认0.45) |
od_results |
object_detect_result_list* |
输出检测结果 |
NMS非极大值抑制:
for (auto c : class_set) {
nms(validCount, filterBoxes, classId, indexArray, c, nms_threshold);
}
- 按类别处理:对每个类别独立执行NMS
- 抑制冗余框:移除IOU超过阈值的重叠框
5.推理结果的处理:画框和置信度并保存图片
// 画框和概率
char text[256];
for (int i = 0; i < od_results.count; i++)
{
object_detect_result *det_result = &(od_results.results[i]);
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;
draw_rectangle(&src_image, x1, y1, x2 - x1, y2 - y1, COLOR_BLUE, 3);
sprintf(text, "%s %.1f%%", coco_cls_to_name(det_result->cls_id), det_result->prop * 100);
draw_text(&src_image, text, x1, y1 - 20, COLOR_RED, 10);
}
write_image("out.png", &src_image);
小结
今天深入分析了rknn_model_zoo中YOLOv5的C++示例代码,包括:
- 模型加载与初始化的完整流程(init_yolov5_model)
- 图像预处理中的Letterbox实现细节
- NPU推理过程的关键API调用(rknn_inputs_set/rknn_run)
- 后处理中的置信度过滤与NMS实现
通过今天的实践,已经建立起RK3588 NPU开发的基础知识框架,为后续的复杂项目开发与端到端落地奠定了坚实基础。