YOLO 人体姿态检测在 RK3588(安卓平台)上的端侧部署全流程

免责声明:本文仅作技术探讨,所有技术细节基于公开资料与个人经验,内容已做脱敏处理,与任何特定公司、项目无关。


前言

人体姿态检测是计算机视觉的经典任务之一,广泛应用于安防监控、健身指导、人机交互等场景。本文记录 YOLO 模型在 安卓RK3588 平台上的端侧部署完整流程,涵盖模型转换、Android JNI 桥接、性能优化与常见问题解决。

技术关键词:安卓RK3588 / RKNN Toolkit2 / YOLO / INT8 量化 / Android NPU 部署

说明:本文以 YOLO(COCO 17 点)为示例模型,介绍通用的端侧部署方法。


1 硬件平台选型

1.1 为什么选择 安卓RK3588

RK3588 是瑞芯微旗舰级 SoC,定位边缘计算与智能硬件,核心参数:

指标 安卓RK3588
NPU 算力 6TOPS(INT8),支持 INT8/FP16/FP32
CPU ARM Cortex-A76/A55,8核
视频能力 8K@30fps 解码,4K@120fps
接口 MIPI CSI、PCIe、USB3.0、HDMI2.1
系统支持 Android 14+、Linux、Debian

对比竞品(树莓派4B / Jetson Nano / 算能 SC5H),安卓RK3588 在性价比RKNN工具链成熟度上优势明显,YOLO、SSD 等主流模型均有官方适配支持。

1.2 安卓RK3588 vs 其他平台

平台 NPU 算力 功耗 价格 RKNN 支持
安卓RK3588 6TOPS 5-10W 完善
Jetson Nano 0.5TOPS 5-10W 无官方
树莓派4B - 3-5W
SC5H 2TOPS 3-5W 一般

2 模型转换:PyTorch → ONNX → RKNN

2.1 工具链概述

RKNN 模型转换依赖官方工具 RKNN Toolkit2,支持 PyTorch、ONNX、TensorFlow、TFLite 等主流框架的模型导入与量化。

安装方式:

pip install rknn-toolkit2==1.5.2

建议使用 Anaconda 创建独立 Python 环境,避免依赖冲突。

2.2 YOLO 简介

YOLO 是 YOLO 架构,输出人体关键点坐标与置信度。COCO 标准输出 17 个关键点

0: nose, 1: left_eye, 2: right_eye, 3: left_ear, 4: right_ear,
5: left_shoulder, 6: right_shoulder, 7: left_elbow, 8: right_elbow,
9: left_wrist, 10: right_wrist, 11: left_hip, 12: right_hip,
13: left_knee, 14: right_knee, 15: left_ankle, 16: right_ankle

2.3 ONNX 导出

以 YOLO 为例,从 PyTorch 导出 ONNX:

from ultralytics import YOLO
import torch

# 加载 YOLO 模型
model = YOLO('yolov8n.pt')
model.export(format='onnx', opset=12, simplify=True)

或在 PyTorch 中手动导出:

import torch

model = torch.load("yolov8n.pt", map_location='cpu')
model.eval()

# YOLO 输入 [1, 3, 640, 640],输出 [1, 57, 8400]
# 57 = 17 keypoints * 3 (x, y, conf) + 1 (box)
torch.onnx.export(
    model,
    torch.randn(1, 3, 640, 640),
    "yolov8n.onnx",
    input_names=["images"],
    output_names=["output"],
    opset_version=12,
    dynamic_axes={"images": {0: "batch"}}
)

注意:opset_version 建议选择 11 或 12,避免高版本算子导致转换失败。

2.4 RKNN 模型转换与量化

RKNN Toolkit2 核心流程:加载模型 → 配置量化 → 构建 → 导出:

from rknn.toolkit2 import RKNN

rknn = RKNN(verbose=True)

# 加载 ONNX 模型
rknn.config(
    mean=[0, 0, 0],
    std=[255, 255, 255],
    target_platform="rk3588"
)

print("--> Loading ONNX model")
ret = rknn.load_onnx("yolov8n.onnx")
if ret != 0:
    print("Load ONNX failed!")
    exit(ret)

# 构建 RKNN 模型(启用量化)
print("--> Building RKNN model")
ret = rknn.build(do_quantization=True, quantize_steps=10)
if ret != 0:
    print("Build RKNN failed!")
    exit(ret)

# 导出 RKNN 模型
print("--> Export RKNN model")
ret = rknn.export_rknn("yolov8n.rknn")
if ret != 0:
    print("Export RKNN failed!")
    exit(ret)

rknn.release()
print("Done")

2.5 量化方式对比

量化模式 模型体积 推理速度 精度损失 推荐场景
FP32 100% 1x 0% 精度优先
FP16 50% ~1.5x <2% 平衡方案
INT8 25% ~2-3x 3-8% 速度优先

量化建议:首次部署建议先跑 FP16 验证功能正常,再切换 INT8 追求性能。INT8 量化时建议使用真实场景校准集(50-200 张图片),可显著降低精度损失。

2.6 PC 端仿真验证

转换完成后,可先用 rknn_toolkit2 在 PC 上仿真推理,验证模型正确性:

from rknn.toolkit2 import RKNN

rknn = RKNN(verbose=True)
rknn.load_rknn("yolov8n.rknn")

# 设置输入
img = preprocess_image("test.jpg", 640)  # 自己实现预处理
rknn.inputs[0].data = img

# 仿真推理
rknn.run()

# 获取输出
outputs = rknn.outputs
# 解析 17 个关键点 ...

rknn.release()

3 Android JNI 桥接设计

Android 应用无法直接调用 RKNN Runtime,需要通过 JNI 构建桥接层。这是端侧 AI 部署到 Android 的核心环节。

3.1 RKNN Android SDK 集成

RKNN Toolkit2 仓库 下载 Android SDK,包含 librknn_runtime.so 动态库。

app/build.gradle 中引入:

android {
    sourceSets {
        main {
            jniLibs.srcDirs = ['libs']
        }
    }
}

dependencies {
    implementation files('libs/rknnpu-android-sdk-1.5.2/runtime/libs/arm64-v8a/librknn_runtime.so')
}

3.2 JNI 接口封装

核心是封装模型加载、推理执行、结果解析三个接口。以下为通用设计模板:

// PoseEngine.h
#include <rknn_api.h>
#include <vector>

struct KeyPoint {
    float x, y;       // 归一化坐标 [0, 1]
    float confidence; // 置信度
    int id;           // 关键点编号 (0-16 for COCO)
};

class PoseEngine {
public:
    int init(const char* model_path);
    int inference(const uint8_t* input_data, int width, int height,
                std::vector<KeyPoint>& keypoints);
    int release();

private:
    rknn_context ctx_;
    rknn_input_output_num io_num_;
    int model_input_width_;
    int model_input_height_;
};
// PoseEngine.cpp
#include "PoseEngine.h"
#include <android/log.h>
#include <cstring>

#define LOG_TAG "PoseEngine"
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__)
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__)

int PoseEngine::init(const char* model_path) {
    int ret = rknn_init(&ctx_, model_path, 0, 0);
    if (ret < 0) {
        LOGE("rknn_init failed: %d", ret);
        return -1;
    }

    // 查询模型输入输出信息
    rknn_query(ctx_, RKNN_QUERY_IN_OUT_NUM, &io_num_, sizeof(io_num_));
    LOGI("model input/output num: %d / %d", io_num_.n_input, io_num_.n_output);

    rknn_tensor_attr input_attr;
    input_attr.index = 0;
    rknn_query(ctx_, RKNN_QUERY_INPUT_ATTR, &input_attr, sizeof(input_attr));
    model_input_width_ = input_attr.dims[2];
    model_input_height_ = input_attr.dims[1];

    return 0;
}

int PoseEngine::inference(const uint8_t* input_data, int width, int height,
                        std::vector<KeyPoint>& keypoints) {
    // 设置输入
    rknn_input inputs[1];
    inputs[0].index = 0;
    inputs[0].type = RKNN_TENSOR_UINT8;
    inputs[0].size = width * height * 3;
    inputs[0].fmt = RKNN_TENSOR_NHWC;
    inputs[0].buf = (void*)input_data;

    int ret = rknn_inputs_set(ctx_, 1, inputs);
    if (ret < 0) {
        LOGE("rknn_inputs_set failed: %d", ret);
        return -1;
    }

    // 执行推理
    ret = rknn_run(ctx_, nullptr);
    if (ret < 0) {
        LOGE("rknn_run failed: %d", ret);
        return -1;
    }

    // 获取输出
    rknn_output outputs[1];
    outputs[0].want_float = true;
    ret = rknn_outputs_get(ctx_, 1, outputs, nullptr);
    if (ret < 0) {
        LOGE("rknn_outputs_get failed: %d", ret);
        return -1;
    }

    // 后处理:解析关键点(根据实际模型输出格式)
    postprocess_keypoints(outputs[0].buf, keypoints);

    rknn_outputs_release(ctx_, 1, outputs);
    return 0;
}

int PoseEngine::release() {
    if (ctx_) {
        rknn_destroy(ctx_);
        ctx_ = 0;
    }
    return 0;
}

3.3 Java 层封装

在 Java 层提供简洁调用接口:

public class PoseDetector {

    static {
        System.loadLibrary("pose_engine");
    }

    private long handle;

    public native boolean init(String modelPath);
    public native KeyPoint[] detect(long handle, byte[] imageData,
                                    int imgWidth, int imgHeight);
    public native void release(long handle);

    // 使用示例
    public void detectPose(byte[] nv21Data, int width, int height) {
        if (handle == 0) {
            init("/data/local/tmp/yolov8-pose.rknn");
        }
        KeyPoint[] points = detect(handle, nv21Data, width, height);
        for (KeyPoint kp : points) {
            if (kp.confidence > 0.5f) {
                // 绘制关键点
                drawKeyPoint(kp.x * width, kp.y * height);
            }
        }
    }
}

3.4 关键优化点

  1. 模型预加载:在 Application 或 Activity 启动时调用 init(),避免每次推理重复加载模型
  2. 输入格式:优先使用 RKNN_TENSOR_NHWC + UINT8 输入,跳过 normalize 步骤
  3. 内存管理:推理完成后立即释放输出 buffer,避免内存积累
  4. 线程安全:多个线程不要共享同一个 rknn_context,建议每个线程独立创建

4 预处理与后处理优化

4.1 图像预处理

模型输入通常为 640x640 RGB/BGR 数据,预处理包含格式转换、缩放、归一化。优化方向是 减少 CPU 占用、减少内存拷贝

// NEON 加速的 NV21 → RGB 转换(示例)
void nv21_to_rgb_neon(const uint8_t* nv21, uint8_t* rgb,
                    int width, int height) {
    // Y 平面直接使用,UV 平面 2x2 亚采样
    // 使用 NEON 指令集加速 SIMD 并行操作
}

// 双线性插值缩放(示例)
void resize_bilinear(const uint8_t* src, int src_w, int src_h,
                    uint8_t* dst, int dst_w, int dst_h) {
    // 计算缩放比例,逐行/逐列插值
    // 可使用 NEON 加速
}

提示:如果 NPU 支持 UINT8 输入,预处理只需做格式转换+缩放,不需要归一化,可显著降低耗时。

4.2 后处理:关键点解析

YOLO 输出格式参考:

[batch, 57, num_boxes]  # 57 = 4(box) + 17*3(keypoints) + 1(conf)

解析时需根据实际模型输出顺序提取关键点坐标:

void postprocess_keypoints(void* output_data, std::vector<KeyPoint>& keypoints) {
    float* out = static_cast<float*>(output_data);

    // 具体解析逻辑根据模型输出格式实现
    // 一般包含:box(4) + keypoints(17*3) + conf(1)

    const float conf_threshold = 0.5f;
    for (int i = 0; i < 17; i++) {
        float x = out[i * 3 + 0];
        float y = out[i * 3 + 1];
        float conf = out[i * 3 + 2];

        if (conf > conf_threshold) {
            keypoints.push_back({x, y, conf, i});
        }
    }
}

4.3 端到端性能优化效果

环节 优化前 优化手段 优化后
格式转换 ~8ms NEON SIMD ~2ms
图像缩放 ~12ms 双线性插值 ~3ms
模型推理 ~35ms INT8 + NPU ~10ms
后处理 ~3ms 定点化 ~1ms
端到端 ~58ms - ~16ms

具体数值因模型、数据、硬件而异,以上为典型参考值。实际优化需用 profiler 定位瓶颈。


5 常见问题与解决方案

5.1 模型转换失败

现象:RKNN Toolkit2 报错 “not supported operator xxx”

排查步骤

  1. 检查 PyTorch/ONNX 版本兼容性
  2. Netron 可视化 ONNX 模型,定位问题算子
  3. 尝试简化模型(如去掉自定义算子)

解决方案

  • 替换不支持的算子为等价操作
  • 降级 opset_version(11→10)
  • 查看 RKNN 官方支持的算子列表

5.2 INT8 量化精度下降

现象:量化后关键点位置明显偏移

解决方案

  1. 使用校准集:准备 50-200 张真实场景图片,避免纯白噪声图
  2. 调整量化参数:增加 quantize_steps,或对关键层使用 FP16
  3. 混合量化:敏感层保持 FP16,others 用 INT8
rknn.config(
    mean=[0, 0, 0],
    std=[255, 255, 255],
    quant_img_RGB2BGR=False,
    optimization_level=3  # 优化级别
)

5.3 NPU 推理结果全零/NaN

排查步骤

  1. 确认 RKNN 模型与芯片平台匹配(rk3588 / rv1126 等)
  2. 检查 NPU 驱动版本:cat /sys/class/npu/npu0/version
  3. 验证 RKNN Toolkit2 与驱动版本兼容性

解决方案

  • 更新 NPU 驱动到与 Toolkit2 匹配的版本
  • 使用 rknn_toolkit2perf_debug 工具分析推理过程

5.4 Android 应用闪退

常见原因

  1. JNI 线程调用 RKNN Context 未做同步
  2. rknn_destroy 在推理进行中调用
  3. 模型文件路径错误或权限不足
  4. 内存不足(NPU 推理占用较大内存)

解决方案

  • 添加异常捕获,完善生命周期管理
  • 模型文件放 /data/local/tmp/chmod 644
  • 检查 /proc meminfo 确认内存充足

6 完整项目结构参考

PoseDeployment/
├── android/
│   └── app/
│       └── src/main/
│           ├── java/com/example/pose/
│           │   ├── PoseDetector.java     # Java 层接口
│           │   └── PoseOverlayView.java   # 可视化绘制
│           ├── jni/
│           │   ├── CMakeLists.txt
│           │   ├── PoseEngine.cpp        # RKNN 推理封装
│           │   ├── preprocess.cpp        # 预处理
│           │   └── postprocess.cpp       # 后处理
│           └── libs/
│               └── arm64-v8a/librknn_runtime.so
├── model/
│   ├── yolov8-pose.onnx
│   └── yolov8-pose.rknn
├── scripts/
│   ├── export_onnx.py
│   ├── convert_rknn.py
│   └── calibrate.py                      # 量化校准
├── docs/
│   └── deployment_guide.md
└── README.md

总结

本文记录了 YOLOv8 人体姿态检测模型在 RK3588 平台上的端侧部署完整流程,核心要点:

  1. 模型转换:PyTorch → ONNX → RKNN,注意算子兼容性与量化配置
  2. JNI 桥接:封装通用的 RKNN 推理引擎,通过 JNI 连接 Android 与 NPU
  3. 性能优化:NEON 加速预处理、INT8 量化加速推理,可达实时(25+ FPS)
  4. 避坑指南:转换失败、量化精度下降、NPU 推理异常等问题均有对应解决方案

端侧 AI 部署是边缘计算的核心能力,「模型转换 + 嵌入式优化 + Android 集成」的复合技能组合,在当前市场上具有显著的竞争力。


相关资源

  • RKNN Toolkit2:https://github.com/ai-rockchip/rknn-toolkit2
  • YOLOv8:https://github.com/ultralytics/ultralytics
  • Netron(模型可视化):https://netron.app/

本文仅作技术探讨,内容基于公开资料与个人经验,与任何特定公司、项目无关。如有疏漏,欢迎指正。

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐