RK3588芯片NPU的使用及编程入门:rknn_model_zoo的yolov5 c++ example源码解析

发布于:2025-04-12 ⋅ 阅读:(41) ⋅ 点赞:(0)

一、本文的目标

解析源码,了解(掌握)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下。

主要作用

  1. 接口定义:提供了加载、运行和卸载 RKNN 模型所需的所有函数原型
  2. 数据结构定义:包含了模型输入输出、张量信息、运行配置等数据结构
  3. 错误码定义:定义了所有可能的错误返回码
  4. 版本控制:包含 API 版本信息

核心功能

  • 模型加载与初始化 (rknn_init, rknn_destroy)
  • 输入输出设置 (rknn_inputs_set, rknn_outputs_get)
  • 模型推理执行 (rknn_run)
  • 性能统计 (rknn_query)
  • 内存管理 (rknn_create_mem, rknn_destroy_mem)

本文中要用到的数据结构

  1. 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() 查询模型基本信息时获取,用于预分配输入/输出缓冲区。

  1. 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开发的基础知识框架,为后续的复杂项目开发与端到端落地奠定了坚实基础。