系列导读:前两篇搞定了硬件认知和开发环境。这一篇正式进入 RKNN 推理开发——用 MobileNetV2 图像分类任务,完整走一遍"PyTorch模型 → RKNN格式转换 → 板端C++推理"的全流程。跑通这篇,你就掌握了 RK3588S NPU 推理开发的核心骨架,后续所有任务都是在这个骨架上扩展。


一、RKNN推理开发的整体流程

在写任何代码之前,先建立全局认知。RKNN 推理开发分为两个阶段,运行在两台不同的机器上:

【PC 端(Ubuntu 开发机)】
  PyTorch/ONNX 模型
       ↓ RKNN-Toolkit2(Python)
  .rknn 模型文件(含量化参数)
       ↓ adb push / scp
【板端(RK3588S)】
  .rknn 模型文件
       ↓ librknnrt.so(C API)
  推理结果

PC 端:负责模型转换和量化,使用 Python API(rknn-toolkit2),只在开发阶段运行一次。

板端:负责实际推理,使用 C/C++ API(rknn_api.h + librknnrt.so),是最终产品运行的代码。

这两套 API 完全独立,初学者经常混淆——记住:Python API 是转换工具,C API 是推理引擎


二、准备模型:获取 MobileNetV2

我们用 MobileNetV2 做图像分类,这是入门推理开发的经典选择:模型小(14MB)、推理快、结果直观。

2.1 导出 ONNX 格式

在 PC 开发机上执行:

# export_mobilenetv2.py
import torch
import torchvision.models as models

# 加载预训练模型
model = models.mobilenet_v2(pretrained=True)
model.eval()

# 创建示例输入(batch=1, RGB, 224×224)
dummy_input = torch.randn(1, 3, 224, 224)

# 导出 ONNX
torch.onnx.export(
    model,
    dummy_input,
    "mobilenetv2.onnx",
    opset_version=12,          # RKNN-Toolkit2 推荐 opset 11-13
    input_names=["input"],
    output_names=["output"],
    dynamic_axes=None          # 边缘端固定 batch=1,不用动态 shape
)
print("导出成功:mobilenetv2.onnx")
source ~/rknn-env/bin/activate
pip install torch torchvision --index-url https://download.pytorch.org/whl/cpu
python3 export_mobilenetv2.py

💡 也可以直接用 RKNN Model Zoo

ls ~/rknn-toolkit2/rknn_model_zoo/models/
# 里面有预置的 mobilenet_v2.onnx,可以跳过导出步骤直接用

三、PC端:模型转换(ONNX → RKNN)

3.1 准备量化校准数据集

INT8 量化需要少量真实图片做校准,100张足够:

# 从 ImageNet 验证集随机取100张,或直接用网上的样例图
mkdir -p ~/rknn_demo/dataset
# 把100张 jpg 图片放入该目录,文件名任意

# 生成数据集列表文件(RKNN-Toolkit2 需要)
ls ~/rknn_demo/dataset/*.jpg > ~/rknn_demo/dataset.txt

3.2 转换脚本

# convert_mobilenetv2.py
from rknn.api import RKNN

ONNX_MODEL = "mobilenetv2.onnx"
RKNN_MODEL = "mobilenetv2.rknn"
DATASET    = "./dataset.txt"

# ── 初始化 ──────────────────────────────────────
rknn = RKNN(verbose=False)

# ── 配置转换参数 ─────────────────────────────────
rknn.config(
    mean_values=[[123.675, 116.28, 103.53]],   # ImageNet 均值(RGB)
    std_values=[[58.395, 57.12, 57.375]],       # ImageNet 标准差
    target_platform="rk3588",                   # 目标芯片
    quantized_dtype="asymmetric_quantized-8",   # INT8 非对称量化
    quantized_algorithm="normal",               # 普通校准,速度快
    optimization_level=3                        # 最高优化等级
)

# ── 加载 ONNX 模型 ───────────────────────────────
print(">>> 加载 ONNX 模型...")
ret = rknn.load_onnx(model=ONNX_MODEL)
assert ret == 0, "加载失败"

# ── 构建 RKNN 模型(含量化)────────────────────────
print(">>> 构建并量化模型...")
ret = rknn.build(do_quantization=True, dataset=DATASET)
assert ret == 0, "构建失败"

# ── 导出 .rknn 文件 ──────────────────────────────
print(">>> 导出 RKNN 模型...")
ret = rknn.export_rknn(RKNN_MODEL)
assert ret == 0, "导出失败"

print(f"✅ 转换完成:{RKNN_MODEL}")
rknn.release()
python3 convert_mobilenetv2.py
# 耗时约 2-5 分钟(取决于校准数据集大小)

3.3 PC端模拟推理验证(可选但推荐)

转换完之后先在 PC 上模拟推理,确认模型没问题,不用频繁推到板子:

# pc_infer_test.py
from rknn.api import RKNN
from PIL import Image
import numpy as np

rknn = RKNN()
rknn.load_rknn("mobilenetv2.rknn")
rknn.init_runtime()   # PC 模拟运行,不需要连板子

# 准备输入:加载图片,归一化
img = Image.open("test.jpg").resize((224, 224))
img = np.array(img, dtype=np.float32)
img = np.expand_dims(img, axis=0)  # 增加 batch 维度

# 推理
outputs = rknn.inference(inputs=[img])
top5 = np.argsort(outputs[0][0])[::-1][:5]
print("Top-5 类别索引:", top5)

rknn.release()

⚠️ 常见问题:PC 端模拟推理的精度与板端结果可能有细微差异(<1%),这是正常的,由量化误差引起,不影响实际使用。


四、板端:C++推理程序

这是本篇最核心的部分。我们用 C++ 调用 RKNN C API 写一个完整的推理程序。

4.1 RKNN C API 核心概念

在写代码之前,先理解 5 个核心 API:

API 作用
rknn_init() 加载 .rknn 模型,初始化推理上下文
rknn_inputs_set() 设置输入张量数据
rknn_run() 执行一次推理
rknn_outputs_get() 获取推理输出结果
rknn_destroy() 释放资源

整个推理流程就是这 5 步,非常清晰。

4.2 完整推理程序

// main.cpp
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#include <algorithm>
#include <vector>

#include "rknn_api.h"

#define STB_IMAGE_IMPLEMENTATION
#include "stb_image.h"          // 轻量图片加载库,无需 OpenCV

// ── 工具函数:加载文件到内存 ────────────────────────
static unsigned char* load_model(const char* path, int* size) {
    FILE* fp = fopen(path, "rb");
    if (!fp) { fprintf(stderr, "无法打开模型文件: %s\n", path); return nullptr; }
    fseek(fp, 0, SEEK_END);
    *size = ftell(fp);
    fseek(fp, 0, SEEK_SET);
    unsigned char* data = (unsigned char*)malloc(*size);
    fread(data, 1, *size, fp);
    fclose(fp);
    return data;
}

// ── Top-K 结果 ──────────────────────────────────
void print_topk(float* probs, int num_classes, int k = 5) {
    std::vector<std::pair<float, int>> scores;
    for (int i = 0; i < num_classes; i++)
        scores.push_back({probs[i], i});
    std::sort(scores.begin(), scores.end(), std::greater<>());
    printf("\n===== Top-%d 推理结果 =====\n", k);
    for (int i = 0; i < k; i++)
        printf("  [%d] 类别 %4d  置信度: %.4f\n", i+1, scores[i].second, scores[i].first);
}

int main(int argc, char* argv[]) {
    if (argc != 3) {
        printf("用法: %s <model.rknn> <image.jpg>\n", argv[0]);
        return -1;
    }
    const char* model_path = argv[1];
    const char* image_path = argv[2];

    // ── 1. 加载模型 ──────────────────────────────
    int model_size = 0;
    unsigned char* model_data = load_model(model_path, &model_size);

    rknn_context ctx;
    int ret = rknn_init(&ctx, model_data, model_size, 0, NULL);
    if (ret < 0) { fprintf(stderr, "rknn_init 失败: %d\n", ret); return -1; }
    free(model_data);   // 模型已加载到 NPU,原始数据可释放

    // ── 2. 查询模型输入输出信息 ───────────────────
    rknn_input_output_num io_num;
    rknn_query(ctx, RKNN_QUERY_IN_OUT_NUM, &io_num, sizeof(io_num));
    printf("输入张量数: %d  输出张量数: %d\n", io_num.n_input, io_num.n_output);

    rknn_tensor_attr input_attr = {0};
    input_attr.index = 0;
    rknn_query(ctx, RKNN_QUERY_INPUT_ATTR, &input_attr, sizeof(input_attr));
    printf("输入尺寸: %dx%dx%d\n", input_attr.dims[2], input_attr.dims[1], input_attr.dims[3]);

    // ── 3. 加载并预处理图片 ──────────────────────
    int img_w, img_h, img_c;
    unsigned char* img_data = stbi_load(image_path, &img_w, &img_h, &img_c, 3);
    if (!img_data) { fprintf(stderr, "图片加载失败: %s\n", image_path); return -1; }

    // 简单 resize(生产环境用 RGA,见第8篇)
    int in_h = input_attr.dims[1];   // 224
    int in_w = input_attr.dims[2];   // 224
    // 此处省略 resize 代码,假设输入图片已是 224×224

    // ── 4. 设置输入 ──────────────────────────────
    rknn_input inputs[1] = {0};
    inputs[0].index = 0;
    inputs[0].type  = RKNN_TENSOR_UINT8;    // 输入 uint8,SDK 内部做归一化
    inputs[0].size  = in_w * in_h * 3;
    inputs[0].fmt   = RKNN_TENSOR_NHWC;
    inputs[0].buf   = img_data;
    inputs[0].pass_through = 0;             // 0 = SDK 自动做均值/方差归一化

    ret = rknn_inputs_set(ctx, 1, inputs);
    if (ret < 0) { fprintf(stderr, "rknn_inputs_set 失败: %d\n", ret); return -1; }
    stbi_image_free(img_data);

    // ── 5. 执行推理 ──────────────────────────────
    struct timespec t0, t1;
    clock_gettime(CLOCK_MONOTONIC, &t0);

    ret = rknn_run(ctx, NULL);

    clock_gettime(CLOCK_MONOTONIC, &t1);
    float elapsed_ms = (t1.tv_sec - t0.tv_sec) * 1000.0f +
                       (t1.tv_nsec - t0.tv_nsec) / 1e6f;
    printf("推理耗时: %.2f ms\n", elapsed_ms);

    if (ret < 0) { fprintf(stderr, "rknn_run 失败: %d\n", ret); return -1; }

    // ── 6. 获取输出 ──────────────────────────────
    rknn_output outputs[1] = {0};
    outputs[0].index = 0;
    outputs[0].want_float = 1;   // 自动反量化为 float32
    ret = rknn_outputs_get(ctx, 1, outputs, NULL);

    float* probs = (float*)outputs[0].buf;
    int num_classes = outputs[0].size / sizeof(float);   // MobileNetV2 = 1000

    print_topk(probs, num_classes, 5);

    // ── 7. 释放资源 ──────────────────────────────
    rknn_outputs_release(ctx, 1, outputs);
    rknn_destroy(ctx);

    return 0;
}

4.3 CMakeLists.txt

cmake_minimum_required(VERSION 3.16)
project(rknn_classify)

set(CMAKE_C_COMPILER   aarch64-none-linux-gnu-gcc)
set(CMAKE_CXX_COMPILER aarch64-none-linux-gnu-g++)
set(CMAKE_CXX_STANDARD 14)

set(RKNN_API_PATH "$ENV{HOME}/rknn-toolkit2/rknpu2/runtime/Linux/librknn_api")

add_executable(rknn_classify main.cpp)

target_include_directories(rknn_classify PRIVATE
    ${RKNN_API_PATH}/include
)
target_link_directories(rknn_classify PRIVATE
    ${RKNN_API_PATH}/aarch64
)
target_link_libraries(rknn_classify
    rknnrt pthread dl m
)

4.4 编译与部署

mkdir build && cd build
cmake .. -DCMAKE_BUILD_TYPE=Release
make -j$(nproc)

# 推到板子
adb push rknn_classify /tmp/
adb push mobilenetv2.rknn /tmp/
adb push test.jpg /tmp/
adb shell "chmod +x /tmp/rknn_classify"

# 在板子上运行
adb shell "/tmp/rknn_classify /tmp/mobilenetv2.rknn /tmp/test.jpg"

预期输出:

输入张量数: 1  输出张量数: 1
输入尺寸: 224x224x3
推理耗时: 3.87 ms

===== Top-5 推理结果 =====
  [1] 类别  285  置信度: 0.8921
  [2] 类别  281  置信度: 0.0634
  [3] 类别  282  置信度: 0.0241
  ...

MobileNetV2 的推理耗时约 3-5ms,这就是 NPU 单核 2 TOPS 的实际表现。使用三核并发可降至约 2ms(后续第7篇详述)。


五、关键参数详解

5.1 pass_through 参数

这是初学者最容易踩的坑:

inputs[0].pass_through = 0;  // SDK 自动用 mean/std 做归一化(推荐)
inputs[0].pass_through = 1;  // 原始数据直接送入,你自己负责归一化

pass_through = 0 时,SDK 会用转换时配置的 mean_valuesstd_values 自动做归一化,输入 uint8 图像数据即可,最省事。

pass_through = 1 时,你需要自己把图片归一化到模型要求的数值范围,适合有特殊前处理需求的场景。

初学阶段统一用 pass_through = 0

5.2 want_float 参数

outputs[0].want_float = 1;  // 自动反量化,输出 float32(方便调试)
outputs[0].want_float = 0;  // 输出原始 int8,后处理需要手动反量化(性能略好)

调试阶段用 want_float = 1,省去手动反量化的麻烦。量产代码可以改成 0 并自己处理,减少一次类型转换的开销。

5.3 NPU core mask 设置

// 默认:SDK 自动分配一个核心
rknn_init(&ctx, model_data, model_size, 0, NULL);

// 指定使用 Core 0
rknn_set_core_mask(ctx, RKNN_NPU_CORE_0);

// 单模型使用三核(适合大模型降延迟)
rknn_set_core_mask(ctx, RKNN_NPU_CORE_0_1_2);

MobileNetV2 这种小模型用单核就够,三核并发反而因调度开销导致延迟增大。


六、常见报错与解决方案

报错信息 原因 解决方案
rknn_init error ret=-1 模型文件路径错误或文件损坏 检查路径,重新转换模型
librknnrt.so: cannot open shared object file 运行时库未部署到板子 adb push librknnrt.so /usr/lib/ && adb shell ldconfig
rknn_run error ret=-9 NPU 驱动版本与 SDK 不匹配 确保板子内核驱动版本与 librknnrt.so 版本一致
推理结果全为 0 或乱码 mean/std 配置与模型不匹配 检查转换时的 mean_values/std_values 是否正确
推理耗时异常高(>50ms) NPU 核心没有激活 检查 /sys/kernel/debug/rknpu/load,确认 NPU 在工作

七、完整项目结构

至此,一个最小可用的 RKNN 推理项目结构如下:

rknn_classify/
├── CMakeLists.txt
├── main.cpp
├── stb_image.h          # 单头文件图片库,从 GitHub 下载
├── mobilenetv2.rknn     # 转换好的模型(由 PC 端生成)
└── build/
    └── rknn_classify    # 编译产物(aarch64 可执行文件)

这个骨架是后续所有推理项目的模板,换一个 .rknn 模型文件,修改输入输出处理逻辑,就能适配任意任务。


八、总结与下篇预告

本篇完成的工作:

  • 理解了 PC 端(转换)和板端(推理)两套 API 的分工
  • 用 RKNN-Toolkit2 完成了 ONNX → RKNN 的 INT8 量化转换
  • 用 C++ RKNN API 写出了完整的板端推理程序
  • 掌握了 pass_throughwant_floatcore_mask 三个关键参数

下一篇(系列第 4 篇)深入讲模型转换全流程,覆盖 PyTorch / TFLite / PaddlePaddle 三种框架的转换方式,以及常见算子不支持时的解决策略。


本系列文章列表(持续更新)

Logo

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

更多推荐