一、本文的目标
将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是比较高效的,用的越多,掌握的越多。菜鸟终将熬成老鹰。






















暂无评论内容