RK3588芯片NPU的使用:yolov8-pose例子图片检测在安卓系统部署与源码深度解析(rknn api)

一、本文的目标

将yolo8-pose例子适配安卓端,提供选择图片后进行姿态识别功能。
通过项目学习源码和rknn api。

二、开发环境说明

主机系统:Windows 11
目标设备:搭载RK3588芯片的安卓开发板
核心工具:Android Studio Koala | 2024.1.1 Patch 2,NDK 27.0

三、适配(迁移)安卓

有了前两次的迁移经验,这次就很顺利了。可以参考之前三篇文章,如果还是遇到问题(或者需要源码),给我留言。
Yolo8-pose C语言例子请参考之前的博文《RK3588芯片NPU的使用:Windows11 Docker中编译YOLOv8-Pose C Demo并在开发板运行实践》。
将C Demo移植到安卓应用端的相关知识,请参考博文《手把手部署YOLOv5到RK3588安卓端:NPU加速与JNI/C/Kotlin接口开发指南》。
上一次移植,请参考《RK3588芯片NPU的使用:PPOCRv4例子在安卓系统部署》,解决图像格式问题,很重要。

四、重要源码解析

4.1 init_yolov8_pose_model方法

本函数主要任务是YOLOv8模型在RKNN框架下的初始化、属性查询和配置保存。
函数源码如下:

int init_yolov8_pose_model(const char *model_path, rknn_app_context_t *app_ctx)
{
            
    int ret;
    // 1.初始化RKNN上下文
    rknn_context ctx = 0;
    ret = rknn_init(&ctx, (char *)model_path, 0, 0, NULL);
    if (ret < 0)
    {
            
        printf("rknn_init fail! ret=%d
", ret);
        return -1;
    }

    // 2.查询模型的输入输出数量
    rknn_input_output_num io_num;
    ret = rknn_query(ctx, RKNN_QUERY_IN_OUT_NUM, &io_num, sizeof(io_num));
    if (ret != RKNN_SUCC)
    {
            
        printf("rknn_query fail! ret=%d
", ret);
        return -1;
    }
    printf("model input num: %d, output num: %d
", io_num.n_input, io_num.n_output);

    // 3.获取输入张量属性
    printf("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(ctx, RKNN_QUERY_INPUT_ATTR, &(input_attrs[i]), sizeof(rknn_tensor_attr));
        if (ret != RKNN_SUCC)
        {
            
            printf("rknn_query fail! ret=%d
", ret);
            return -1;
        }
        dump_tensor_attr(&(input_attrs[i]));
    }

    // 4.获取输出张量属性
    printf("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(ctx, RKNN_QUERY_OUTPUT_ATTR, &(output_attrs[i]), sizeof(rknn_tensor_attr));
        if (ret != RKNN_SUCC)
        {
            
            printf("rknn_query fail! ret=%d
", ret);
            return -1;
        }
        dump_tensor_attr(&(output_attrs[i]));
    }

    // 5.保存配置到应用上下文
    app_ctx->rknn_ctx = ctx;
    // 6.判断模型是否量化:检查第一个输出张量是否是非FP16的仿射量化类型,设置is_quant标志,用于后续反量化处理。
    if (output_attrs[0].qnt_type == RKNN_TENSOR_QNT_AFFINE_ASYMMETRIC && output_attrs[0].type != RKNN_TENSOR_FLOAT16)
    {
            
        app_ctx->is_quant = true;
    }
    else
    {
            
        app_ctx->is_quant = false;
    }
    // 7. 复制输入输出属性到应用上下文:动态分配内存并拷贝输入输出属性,保存到app_ctx以便后续访问。
    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));
    // 8. 解析输入张量维度
    if (input_attrs[0].fmt == RKNN_TENSOR_NCHW)
    {
            
        printf("model is NCHW input fmt
");
        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
    {
            
        printf("model is NHWC input fmt
");
        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];
    }
    printf("model input height=%d, width=%d, channel=%d
",
           app_ctx->model_height, app_ctx->model_width, app_ctx->model_channel);

    return 0;
}

4.1.1 rknn_init初始化

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

int rknn_init(
    rknn_context* context,      // 输出参数:返回的 RKNN 上下文句柄
    void* model,                // 输入参数:模型数据或模型文件路径
    uint32_t size,              // 输入参数:模型数据的大小(字节数)
    uint32_t flag,              // 输入参数:初始化标志位(扩展选项)
    rknn_init_extend* extend    // 输入参数:扩展初始化信息(可选)
);

本例中使用前两个参数:

rknn_context ctx = 0;
ret = rknn_init(&ctx, (char *)model_path, 0, 0, NULL);

4.1.2 查询模型的输入输出数量、输入张量属性和输出张量属性

rknn_query函数能够查询获取到模型输入输出信息、逐层运行时间、模型推理的总时间、SDK版本、内存占用信息、用户自定义字符串等信息。
函数原型:

int rknn_query(
    rknn_context context,       // 输入参数:RKNN 上下文句柄(由 rknn_init 返回)
    rknn_query_cmd cmd,         // 输入参数:查询命令类型(枚举值)
    void* info,                 // 输出参数:存储查询结果的缓冲区指针
    uint32_t size               // 输入参数:info 缓冲区的大小(字节数)
);

SDK支持的查询命令很多,具体参考官方文档:04_Rockchip_RKNPU_API_Reference_RKNNRT_V2.3.0_CN.pdf。本函数中涉及三个查询命令:

查询命令 返回结果结构体 功能
RKNN_QUERY_IN_OUT_NUM rknn_input_output_num 查询输入输出 tensor 个数
RKNN_QUERY_INPUT_ATTR rknn_tensor_attr 查询输入 tensor 属性
RKNN_QUERY_OUTPUT_ATTR rknn_tensor_attr 查询输出 tensor 属性

两个重要的结构体说明:
rknn_input_output_num:用于 RKNN_QUERY_IN_OUT_NUM 命令的返回结果,存储模型的输入/输出张量数量。

typedef struct _rknn_input_output_num {
            
    uint32_t n_input;   // 输入张量数量
    uint32_t n_output;  // 输出张量数量
} rknn_input_output_num;

rknn_tensor_attr:用于 RKNN_QUERY_INPUT_ATTR 和 RKNN_QUERY_OUTPUT_ATTR 命令,描述张量属性。

#define RKNN_MAX_DIMS                           16      /* maximum dimension of tensor. */
#define RKNN_MAX_NAME_LEN                       256     /* maximum name lenth of tensor. */
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;              // 动态定点的小数位长度(RKNN_TENSOR_QNT_DFP 时有效)
    int32_t zp;             // 零点偏移(非对称量化时有效)
    float scale;            // 缩放系数(非对称量化时有效)

    /* 内存布局 */
    uint32_t w_stride;      // 宽度方向步长(只读,0 表示等于宽度)
    uint32_t size_with_stride; // 包含步长的总字节大小
    uint8_t pass_through;   // 直通模式(为 TRUE 时数据不转换直接输入模型)
    uint32_t h_stride;      // 高度方向步长(可写,0 表示等于高度)
} rknn_tensor_attr;

rknn_tensor_qnt_type :定义张量(Tensor)的量化类型,用于描述模型中的数据存储和计算方式。见上述源码注释,6.判断模型是否量化。

枚举值 名称 解释
RKNN_TENSOR_QNT_NONE 非量化 数据未经量化,通常是浮点格式(如 FP32、FP16)。
RKNN_TENSOR_QNT_DFP 动态定点量化 使用动态小数位(fl 参数)表示数据,适用于低精度定点计算。
RKNN_TENSOR_QNT_AFFINE_ASYMMETRIC 非对称仿射量化 常见的 INT8 量化方式,使用 scale(缩放系数)和 zp(零点偏移)进行线性映射。
RKNN_TENSOR_QNT_MAX 枚举边界 仅用于标记枚举范围,无实际用途。

4.1.3 解析输入张量维度

根据输入张量的布局(NCHW/NHWC),提取模型的高度、宽度、通道数,用于图像预处理。

    // 8. 解析输入张量维度
    if (input_attrs[0].fmt == RKNN_TENSOR_NCHW)
    {
            
        printf("model is NCHW input fmt
");
        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 {
            
        printf("model is NHWC input fmt
");
        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];
    }

rknn_tensor_format fmt说明:定义张量(Tensor)的内存布局格式,影响数据在内存中的存储顺序和计算效率。

typedef enum _rknn_tensor_format {
            
    RKNN_TENSOR_NCHW = 0,                               /* data format is NCHW. */
    RKNN_TENSOR_NHWC,                                   /* data format is NHWC. */
    RKNN_TENSOR_NC1HWC2,                                /* data format is NC1HWC2. */
    RKNN_TENSOR_UNDEFINED,

    RKNN_TENSOR_FORMAT_MAX
} rknn_tensor_format;
枚举值 名称 解释 适用场景
RKNN_TENSOR_NCHW NCHW 格式 数据按 [Batch, Channel, Height, Width] 顺序存储 传统 CNN 框架(如 PyTorch)
RKNN_TENSOR_NHWC NHWC 格式 数据按 [Batch, Height, Width, Channel] 顺序存储 TensorFlow、TFLite
RKNN_TENSOR_NC1HWC2 NC1HWC2 格式 特殊分块格式,用于 NPU 硬件加速 华为昇腾、Rockchip NPU
RKNN_TENSOR_UNDEFINED 未定义格式 格式未指定,可能由系统自动推断 兼容性保留选项
RKNN_TENSOR_FORMAT_MAX 枚举边界 仅用于标记枚举范围 无实际用途

4.2 inference_yolov8_pose_model方法

该方法执行YOLOv8姿态检测模型的推理流程,包括输入预处理、模型推理、后处理(解码/坐标转换/NMS)及结果保存,最终输出检测到的人体姿态信息(包含关键点)。

int inference_yolov8_pose_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;
    rknn_input inputs[app_ctx->io_num.n_input];
    rknn_output outputs[app_ctx->io_num.n_output];
    const float nms_threshold = NMS_THRESH;      // Default NMS threshold
    const float box_conf_threshold = BOX_THRESH; // Default box threshold
    int bg_color = 114;
    // 1. 输入校验与初始化
    if ((!app_ctx) || !(img) || (!od_results))
    {
            
        return -1;
    }

    memset(od_results, 0x00, sizeof(*od_results));
    memset(&letter_box, 0, sizeof(letterbox_t));
    memset(&dst_img, 0, sizeof(image_buffer_t));
    memset(inputs, 0, sizeof(inputs));
    memset(outputs, 0, sizeof(outputs));

    // 2. 图像预处理(Letterbox)
    dst_img.width = app_ctx->model_width;
    dst_img.height = app_ctx->model_height;
    dst_img.format = IMAGE_FORMAT_RGB888;
    dst_img.size = get_image_size(&dst_img);
    dst_img.virt_addr = (unsigned char *)malloc(dst_img.size);
    if (dst_img.virt_addr == NULL)
    {
            
        printf("malloc buffer size:%d fail!
", dst_img.size);
        goto out;
    }

    // 3.执行 Letterbox 缩放填充
    ret = convert_image_with_letterbox(img, &dst_img, &letter_box, bg_color);
    if (ret < 0)
    {
            
        printf("convert_image_with_letterbox fail! ret=%d
", ret);
        goto out;
    }
    // 4.模型输入设置:RKNN模型推理前数据灌入的关键环节,确保输入数据以正确的格式、尺寸和类型传递给NPU硬件,直接影响推理结果的准确性和性能。
    inputs[0].index = 0;
    inputs[0].type = RKNN_TENSOR_UINT8;
    inputs[0].fmt = RKNN_TENSOR_NHWC;
    inputs[0].size = app_ctx->model_width * app_ctx->model_height * app_ctx->model_channel;
    inputs[0].buf = dst_img.virt_addr;
    ret = rknn_inputs_set(app_ctx->rknn_ctx, app_ctx->io_num.n_input, inputs);
    if (ret < 0)
    {
            
        printf("rknn_input_set fail! ret=%d
", ret);
        goto out;
    }

    // 5.模型推理与耗时统计
    printf("rknn_run
");
    int start_us,end_us;
    start_us = getCurrentTimeUs();
    ret = rknn_run(app_ctx->rknn_ctx, nullptr);
    end_us = getCurrentTimeUs() - start_us;
    printf("rknn_run time=%.2fms, FPS = %.2f
",end_us / 1000.f, 
            1000.f * 1000.f / end_us);

    if (ret < 0)
    {
            
        printf("rknn_run fail! ret=%d
", ret);
        goto out;
    }

    // 6.输出数据获取
    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);
    }
    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
", ret);
        goto out;
    }
    // 7.后处理
    start_us = getCurrentTimeUs();
    post_process(app_ctx, outputs, &letter_box, box_conf_threshold, nms_threshold, od_results);
    end_us = getCurrentTimeUs() - start_us;
    printf("post_process time=%.2fms, FPS = %.2f
",end_us / 1000.f, 
            1000.f * 1000.f / end_us);
    // 8.资源释放
    rknn_outputs_release(app_ctx->rknn_ctx, app_ctx->io_num.n_output, outputs);
out:
    if (dst_img.virt_addr != NULL)
    {
            
        free(dst_img.virt_addr);
    }

    return ret;
}

4.2.1 rknn_inputs_set模型输入设置

通过rknn_inputs_set函数可以设置模型的输入数据。该函数能够支持多个输入,其中每个输入是rknn_input结构体对象。
函数原型:

int rknn_inputs_set(
    rknn_context context,       // 输入参数:RKNN 上下文句柄(由 rknn_init 返回)
    uint32_t n_inputs,          // 输入参数:需要设置的输入数量
    rknn_input inputs[]         // 输入参数:输入数据信息数组(每个元素对应一个输入)
);

rknn_input结构体:描述输入张量的数据信息和传输方式。

typedef struct _rknn_input {
            
    /* 必须设置的参数 */
    uint32_t index;         // 输入参数:指定设置的输入张量索引(从0开始)
    void* buf;              // 输入参数:输入数据缓冲区指针
    uint32_t size;          // 输入参数:缓冲区字节大小

    /* 数据转换控制 */
    uint8_t pass_through;   // 输入参数:直通模式开关
                            // TRUE:数据直接传输,不转换格式(需确保格式完全匹配模型)
                            // FALSE:根据下方type和fmt自动转换(默认推荐)

    /* 当pass_through=FALSE时需设置 */
    rknn_tensor_type type;  // 输入参数:输入数据类型(如RKNN_TENSOR_FLOAT32)
    rknn_tensor_format fmt; // 输入参数:数据内存布局(如RKNN_TENSOR_NCHW)
} rknn_input;

4.2.2 rknn_run模型推理

执行 RKNN 模型推理(前向计算),触发NPU对已设置的输入数据进行处理,并生成输出结果。
没什么好说的,输入设置完毕后,执行模型推理。

4.2.3 rknn_outputs_get

获取模型推理的输出数据。
函数原型:

int rknn_outputs_get(
    rknn_context context,           // 输入参数:RKNN上下文句柄(由rknn_init初始化得到)
    uint32_t n_outputs,             // 输入参数:需要获取的输出张量数量
    rknn_output outputs[],          // 输出参数:输出结果缓冲区数组
    rknn_output_extend* extend      // 输入参数:输出扩展信息(可选,通常为NULL)
);

rknn_output结构体:用于配置和接收RKNN模型的输出数据。

typedef struct _rknn_output {
            
    /* 必须设置的参数 */
    uint32_t index;         // 输入参数:指定获取的输出张量索引(从0开始)
    
    /* 输出数据控制 */
    uint8_t want_float;     // 输入参数:输出格式控制
                            // TRUE:强制转换为浮点数(FP32)
                            // FALSE:保持原始输出格式(如INT8)
    
    /* 内存管理模式 */
    uint8_t is_prealloc;    // 输入参数:内存分配方式
                            // TRUE:用户预分配内存(需设置下方buf和size)
                            // FALSE:由SDK自动分配内存(默认推荐)
    
    /* 当is_prealloc=TRUE时需设置 */
    void* buf;              // 输入参数:输出数据缓冲区指针(用户预分配)
    uint32_t size;          // 输入参数:缓冲区字节大小
} rknn_output;

对于输出数据的buffer存放可以采用两种方式:一种是用户自行申请和释放,此时rknn_output对象的is_prealloc需要设置为1,并且将buf指针指向用户申请的buffer;另一种是由rknn来进行分配,此时rknn_output对象的is_prealloc设置为0即可,函数执行之后buf将指向输出数据。

4.3 post_process后处理

该方法对检测模型的输出进行后处理,包括 检测框解码、置信度过滤、非极大值抑制(NMS)以及关键点坐标映射,最终输出人体检测框和对应的关键点信息,封装在object_detect_result_list中。以下为源码,已尽量加注释。

int post_process(rknn_app_context_t *app_ctx, void *outputs, letterbox_t *letter_box, float conf_threshold, float nms_threshold,
                 object_detect_result_list *od_results) {
            

    rknn_output *_outputs = (rknn_output *)outputs;

    std::vector<float> filterBoxes;
    std::vector<float> objProbs;
    std::vector<int> classId;
    int validCount = 0;
    int stride = 0;
    int grid_h = 0;
    int grid_w = 0;
    int model_in_w = app_ctx->model_width;
    int model_in_h = app_ctx->model_height;
    memset(od_results, 0, sizeof(object_detect_result_list));
    int index = 0;
    // 多尺度输出层处理
    for (int i = 0; i < 3; i++) {
             // 遍历 3 个输出层(YOLO 多尺度检测)
        // 获取特征图维度 
        grid_h = app_ctx->output_attrs[i].dims[2];
        grid_w = app_ctx->output_attrs[i].dims[3];
        stride = model_in_h / grid_h; // 计算步长(下采样倍数)
        // 量化模型处理(UINT8/INT8)
        if (app_ctx->is_quant) {
            
            validCount += process_i8((int8_t *)_outputs[i].buf, grid_h, grid_w, stride, filterBoxes, objProbs,
                                     classId, conf_threshold, app_ctx->output_attrs[i].zp, app_ctx->output_attrs[i].scale,index);
        }
        else
        {
            
            validCount += process_fp32((float *)_outputs[i].buf, grid_h, grid_w, stride, filterBoxes, objProbs,
                                     classId, conf_threshold, app_ctx->output_attrs[i].zp, app_ctx->output_attrs[i].scale, index);
        }
        index += grid_h * grid_w;
    }

    // no object detect
    if (validCount <= 0) {
            
        return 0;
    }
    std::vector<int> indexArray;
    for (int i = 0; i < validCount; ++i) {
            
        indexArray.push_back(i);
    }
    // 按置信度降序排序(快速排序)
    quick_sort_indice_inverse(objProbs, 0, validCount - 1, indexArray);
    // 按类别分组进行 NMS
    std::set<int> class_set(std::begin(classId), std::end(classId));
    for (auto c : class_set) {
            
        nms(validCount, filterBoxes, classId, indexArray, c, nms_threshold);
    }

    int last_count = 0;
    od_results->count = 0;
    // 关键点坐标映射,遍历所有有效检测结果
    for (int i = 0; i < validCount; ++i) {
            
        // 跳过无效结果或超过最大数量限制
        if (indexArray[i] == -1 || last_count >= OBJ_NUMB_MAX_SIZE) {
            
            continue;
        }
        int n = indexArray[i];
        // 框坐标映射回原图(Letterbox 逆变换)
        float x1 = filterBoxes[n * 5 + 0] - letter_box->x_pad;
        float y1 = filterBoxes[n * 5 + 1] - letter_box->y_pad;
        float w = filterBoxes[n * 5 + 2];
        float h = filterBoxes[n * 5 + 3];
        int keypoints_index = (int)filterBoxes[n * 5 + 4];
        // 关键点处理(17 个关键点)
        for (int j = 0; j < 17; ++j) {
            
            // 量化模型反量化
            if (app_ctx->is_quant) {
            
                od_results->results[last_count].keypoints[j][0] = ((float)((rknpu2::float16 *)_outputs[3].buf)[j*3*8400+0*8400+keypoints_index] 
                                                                - letter_box->x_pad)/ letter_box->scale;
                od_results->results[last_count].keypoints[j][1] = ((float)((rknpu2::float16 *)_outputs[3].buf)[j*3*8400+1*8400+keypoints_index] 
                                                                    - letter_box->y_pad)/ letter_box->scale;
                od_results->results[last_count].keypoints[j][2] = (float)((rknpu2::float16 *)_outputs[3].buf)[j*3*8400+2*8400+keypoints_index];
            }
            else
            {
            
                // 浮点模型直接转换
                od_results->results[last_count].keypoints[j][0] = (((float *)_outputs[3].buf)[j*3*8400+0*8400+keypoints_index] 
                                                                - letter_box->x_pad)/ letter_box->scale;
                od_results->results[last_count].keypoints[j][1] = (((float *)_outputs[3].buf)[j*3*8400+1*8400+keypoints_index] 
                                                                    - letter_box->y_pad)/ letter_box->scale;
                od_results->results[last_count].keypoints[j][2] = ((float *)_outputs[3].buf)[j*3*8400+2*8400+keypoints_index];
            }
        }

        int id = classId[n];
        float obj_conf = objProbs[i];
        // 结果保存
        od_results->results[last_count].box.left = (int)(clamp(x1, 0, model_in_w) / letter_box->scale);
        od_results->results[last_count].box.top = (int)(clamp(y1, 0, model_in_h) / letter_box->scale);
        od_results->results[last_count].box.right = (int)(clamp(x1+w, 0, model_in_w) / letter_box->scale);
        od_results->results[last_count].box.bottom = (int)(clamp(y1+h, 0, model_in_h) / letter_box->scale);
        od_results->results[last_count].prop = obj_conf; // 置信度
        od_results->results[last_count].cls_id = id; // 类别
        last_count++;
    }
    od_results->count = last_count;
    return 0;
}

参数说明

参数名称 类型 说明
app_ctx 结构体 模型上下文,包含输出张量属性(维度、量化参数等)
outputs 数组/张量 模型原始输出数据(多尺度特征图)
letter_box 结构体 预处理阶段生成的Letterbox参数(缩放比例、填充大小)
conf_threshold float 置信度过滤阈值(用于筛选检测框)
nms_threshold float NMS(非极大值抑制)重叠阈值
od_results 结构体数组 输出参数,存储最终检测结果(包含类别、置信度、坐标等信息)

4.4 拿到od_results(结果列表)后,绘制框、姿态和文字

4.4.1 先了解几个结构体

#define OBJ_NUMB_MAX_SIZE 128
typedef struct {
            
    int left; // 矩形框左上角的 x 坐标(单位:像素)
    int top;
    int right;
    int bottom;
} image_rect_t;

typedef struct {
            
    image_rect_t box; // 目标的边界框坐标
    float keypoints[17][3];//17个关键点的坐标和置信度,每行格式为 [x, y, confidence],关键点通常对应COCO数据集的关节点(如鼻子、左眼、右肩等)。
    float prop; // 检测结果的置信度
    int cls_id; // 类别ID
} object_detect_result;

typedef struct {
            
    int id; // 帧或场景的标识符
    int count; // 实际检测到的目标数量
    object_detect_result results[OBJ_NUMB_MAX_SIZE]; // 存储所有检测结果的数组,最大容量128
} object_detect_result_list;

源码如下,解读在注释中。

// 画框和概率
char text[256];
// 遍历所有检测结果
for (int i = 0; i < od_results.count; i++)
{
            
    object_detect_result *det_result = &(od_results.results[i]);
    LOGI("%s @ (%d %d %d %d) %.3f
", 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(&dst_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(&dst_image, text, x1, y1 - 20, COLOR_RED, 10);
    // 绘制骨架连线(姿态估计)
    for (int j = 0; j < 38/2; ++j) {
            
        draw_line(&dst_image, (int)(det_result->keypoints[skeleton[2*j]-1][0]),(int)(det_result->keypoints[skeleton[2*j]-1][1]),
                    (int)(det_result->keypoints[skeleton[2*j+1]-1][0]),(int)(det_result->keypoints[skeleton[2*j+1]-1][1]),COLOR_ORANGE,3);
    }
    // 绘制关节点圆点
    for (int j = 0; j < 17; ++j) {
            
        draw_circle(&dst_image, (int)(det_result->keypoints[j][0]),(int)(det_result->keypoints[j][1]),1, COLOR_YELLOW,1);
    }
}

五、写在后面

实际项目中学习rknn api是比较高效的,用的越多,掌握的越多。菜鸟终将熬成老鹰。

© 版权声明
THE END
如果内容对您有所帮助,就支持一下吧!
点赞0 分享
评论 抢沙发

请登录后发表评论

    暂无评论内容