分类模型部署(LeNet/ResNet)
目标平台: RK3588 | NPU 算力: 6 TOPS | 文档版本: V1.0
适用板型: C-3588LQ 核心板 + SHIMETAPI-M5-R2A 底板
最后更新: 2026-08-19
1. 概述
1.1 功能简介
图像分类是最基础的 AI 视觉任务,目标是把输入图像归到预定义的类别里。RK3588 内置 NPU 算力达 6 TOPS(INT8),跑各类分类模型都很轻松。
本文档以两个典型模型为例,走完在 RK3588 上部署分类模型的完整流程:
| 模型 | 数据集 | 输入尺寸 | 类别数 | 用途 |
|---|---|---|---|---|
| LeNet | MNIST | 28x28x1 | 10 | 手写数字识别(入门示例) |
| ResNet-50 | ImageNet | 224x224x3 | 1000 | 通用图像分类(工程级参考) |
整个部署流程一句话概括:
原始模型 (ONNX) → RKNN 模型转换 → INT8 量化 → 板端推理 → 分类结果1.2 应用场景
- 手写数字识别:银行票据、快递单号、邮政编码等场景中的数字自动录入。
- 通用图像分类:农产品分级、工业缺陷分类、场景识别等需要多类别判断的场景。
- 物体识别预处理:作为目标检测管线的第一阶段,快速判断图像中是否存在感兴趣的目标类别。
- 质量检测:产线上对产品外观进行分类判定(合格/不合格,或细粒度缺陷分类)。
1.3 工作原理
分类模型的核心计算流程:
图像输入
│
▼
预处理(resize → 归一化 → 格式转换)
│
▼
RKNN 模型推理 ────── NPU 6 TOPS 加速
│
▼
后处理(Softmax → Top-K 排序)
│
▼
分类结果输出RK3588 NPU 对分类模型的加速点主要在这几处:
- Conv2d 卷积层:占据模型 90% 以上的计算量,NPU 通过专用卷积引擎硬件加速。
- BatchNorm / ReLU:与卷积融合执行,减少访存开销。
- 全连接层(FC):对较薄的特征图有良好的调度效率。
- INT8 量化推理:充分利用 6 TOPS INT8 算力,相比 FP16 理论性能翻倍。
2. 硬件准备
2.1 所需硬件清单
| 序号 | 硬件名称 | 规格 | 数量 | 说明 |
|---|---|---|---|---|
| 1 | C-3588LQ 核心板 | V1.0 | 1 | RK3588 主芯片,含 NPU |
| 2 | SHIMETAPI-M5-R2A 底板 | V1.0 | 1 | 提供外设接口 |
| 3 | USB to TTL 调试串口线 | 3.3V 电平 | 1 | 连接 DEBUG UART |
| 4 | 电源适配器 | 12V/2A DC | 1 | 底板供电 |
| 5 | MicroSD 卡 | 32GB 以上 | 1 | 烧录系统镜像 |
| 6 | 网线或 USB 网卡 | -- | 1 | SSH 连接或文件传输 |
2.2 硬件连接
各接口的连接关系如下:
┌─────────────────────────────────┐
│ SHIMETAPI-M5-R2A 底板 │
│ │
│ ┌─────────────────────┐ │
│ │ C-3588LQ 核心板 │ │
│ │ RK3588 (6 TOPS NPU)│ │
│ └─────────────────────┘ │
│ │
│ DEBUG UART ──── USB to TTL ──→ PC (串口终端) │
│ ETH0 ──── 网线 ────────→ 路由器/PC │
│ DC 12V ──── 电源适配器 │
│ SD Card ──── MicroSD 卡 │
└─────────────────────────────────┘2.3 串口连接参数
用 USB to TTL 线接好核心板 DEBUG 口后,在 PC 端用串口终端工具(如 minicom、PuTTY)按下面的参数连接:
| 参数 | 值 |
|---|---|
| 波特率 | 1500000 |
| 数据位 | 8 |
| 停止位 | 1 |
| 校验位 | 无 |
| 流控 | 无 |
2.4 上电启动
- 插入已烧录系统镜像的 MicroSD 卡。
- 连接 USB to TTL 串口线至 PC。
- 连接网线(可选,便于文件传输)。
- 接通 12V 电源,系统开始启动。
- 在串口终端中观察启动日志,等待登录提示符出现。
3. 软件准备
3.1 开发环境(PC 端)
在 PC 的 Linux 环境(推荐 Ubuntu 20.04/22.04)中安装 RKNN 工具链:
# RKNN Toolkit2 —— PC 端模型转换与量化(版本与板端 librknnrt 匹配)
pip3 install rknn-toolkit2==2.3.2 -i https://pypi.org/simple
# 依赖库
pip3 install numpy opencv-python pillow补充三点:
- PyPI 无 Windows wheel,PC 需 Linux(x86_64/aarch64 均有)。
- aarch64 板上直装需
--no-deps绕过 onnxoptimizer(详见 ch02 3.1)。rknn-toolkit2[all]的 extras 写法不被该包支持,装完整依赖直接 pip 装对应包即可。
3.2 运行环境(板端)
板端(出厂 Debian 12):librknnrt.so 2.3.2 已预装在 /usr/lib/,但 Python 的 rknnlite 包没有预装,需要自己装:
# 板端推理包(正确包名 rknn-toolkit-lite2,版本与 librknnrt 一致)
sudo pip3 install --break-system-packages rknn-toolkit-lite2==2.3.2
# 板端依赖(numpy 已预装 1.24.2;opencv 出厂未装,需要时再装)
pip3 list 2>/dev/null | grep -iE "numpy|opencv|pillow"
# numpy 1.24.2(预装)、Pillow 12.3.0(预装)、opencv 无3.3 工具版本对照
本机的工具版本汇总,供排错时对照:
| 工具 | 版本 | 说明 |
|---|---|---|
| rknn-toolkit2 | 2.3.2 | 转换(PC 或板上) |
| rknn-toolkit-lite2 | 2.3.2 | 板端推理 |
| librknnrt | 2.3.2 (出厂预装) | 板端 C 运行时 |
| rknpu 驱动 | 0.9.8 (出厂内建) | DRM 方式,renderD129 |
| numpy | 1.24.2 (预装) | 数值计算 |
| Pillow | 12.3.0 (预装) | 图像读取(opencv 未预装) |
| onnx | 1.16.1 | ONNX 处理(板上 pip 装) |
4. 操作步骤
4.1 LeNet 模型转换
LeNet 是经典的卷积神经网络,适用于 MNIST 手写数字识别。下面的脚本在 PC 端把 ONNX 格式的 LeNet 模型转成 RKNN 格式(Rockchip NPU 专用格式),跑完后输出 lenet.rknn 即为成功:
"""
LeNet ONNX → RKNN 转换脚本
运行环境:PC 端 (rknn-toolkit2)
"""
from rknn.api import RKNN
# 模型路径配置
ONNX_MODEL = 'lenet.onnx'
RKNN_MODEL = 'lenet.rknn'
DATASET = 'mnist_samples.txt' # 量化校准数据列表
# 初始化 RKNN
rknn = RKNN()
# 配置模型参数
# LeNet 输入为灰度图 28x28x1,归一化到 [-1, 1]
rknn.config(
mean_values=[[128]],
std_values=[[128]],
target_platform='rk3588',
quantized_dtype='asymmetric_quantized-8'
)
# 加载 ONNX 模型
ret = rknn.load_onnx(model=ONNX_MODEL)
if ret != 0:
print('加载模型失败')
exit(ret)
# 构建 RKNN 模型并进行 INT8 量化
ret = rknn.build(do_quantization=True, dataset=DATASET)
if ret != 0:
print('构建模型失败')
exit(ret)
# 导出 RKNN 模型
ret = rknn.export_rknn(RKNN_MODEL)
if ret != 0:
print('导出模型失败')
exit(ret)
# 释放资源
rknn.release()
print(f'模型转换完成: {RKNN_MODEL}')校准数据文件 mnist_samples.txt 的格式:每行一个图片路径,共 20-50 张即可:
./calibration_data/0_1.png
./calibration_data/1_3.png
./calibration_data/2_5.png
...4.2 ResNet-50 模型转换
ResNet-50 是 ImageNet 上的经典分类网络,1000 类输出,是工程中最常用的分类基线模型。
"""
ResNet-50 ONNX → RKNN 转换脚本
运行环境:PC 端 (rknn-toolkit2)
"""
from rknn.api import RKNN
ONNX_MODEL = 'resnet50.onnx'
RKNN_MODEL = 'resnet50.rknn'
DATASET = 'imagenet_samples.txt'
rknn = RKNN()
# ResNet-50 输入为 RGB 图 224x224x3
# ImageNet 标准归一化参数
rknn.config(
mean_values=[[123.675, 116.28, 103.53]],
std_values=[[58.395, 57.12, 57.375]],
target_platform='rk3588',
quantized_dtype='asymmetric_quantized-8'
)
ret = rknn.load_onnx(model=ONNX_MODEL)
if ret != 0:
print('加载模型失败')
exit(ret)
ret = rknn.build(do_quantization=True, dataset=DATASET)
if ret != 0:
print('构建模型失败')
exit(ret)
ret = rknn.export_rknn(RKNN_MODEL)
if ret != 0:
print('导出模型失败')
exit(ret)
rknn.release()
print(f'模型转换完成: {RKNN_MODEL}')下面的输出取自实机(resnet50-v2-7.onnx 97.7MB,板上转换,32 张校准图):
原 shape: ['N', 3, 224, 224] ← 又是动态 batch,先固化(见下方坑2)
saved static: /var/tmp/resnet50_static.onnx
[fp16] load_onnx ret=0 114.5s
[fp16] build ret=0 130.5s
[fp16] export ret=0 size=49.92MB 130.6s
[i8] build ret=0 279.5s
[i8] export size=25.46MB踩过两个坑:
- 内存不足会被 OOM-kill:resnet50 转换峰值 RSS 3.34GB,4GB 板(跑桌面)直接被内核杀掉,日志只剩 toolkit banner。先加 swap 再转:
加 4G swap 后 fp16/INT8 转换全部成功(INT8 279.5s)。sudo fallocate -l 4G /userdata/swapfile && sudo chmod 600 /userdata/swapfile sudo mkswap /userdata/swapfile && sudo swapon /userdata/swapfile- 动态 batch 维(
'N'):load 前用 onnx 库把 dim[0] 固定为 1,否则报The input shape ... is not support!。
4.3 板端推理:ResNet-50 图像分类
模型转好后传到板上,用下面的脚本执行推理,会打印 Top-5 分类结果:
"""
ResNet-50 板端推理 —— Top-5 分类结果
运行环境:RK3588 板端 (rknn-lite2)
"""
from rknnlite.api import RKNNLite
import cv2
import numpy as np
# ── 加载 ImageNet 标签 ──
LABELS = [line.strip() for line in open('imagenet_labels.txt')]
# ── 初始化 RKNN 运行时 ──
rknn = RKNNLite()
rknn.load_rknn('resnet50.rknn')
# 使用三核模式,获得最高推理性能
rknn.init_runtime(core_mask=RKNNLite.NPU_CORE_0_1_2)
# ── 读取并预处理图像 ──
img = cv2.imread('test.jpg')
img = cv2.resize(img, (224, 224))
img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
# ── 推理 ──
outputs = rknn.inference(inputs=[img])
# ── 后处理:Softmax + Top-5 ──
output = outputs[0][0] # shape: (1, 1000)
# 数值稳定的 softmax
output = output - np.max(output)
probs = np.exp(output) / np.sum(np.exp(output))
# 取概率最高的 5 个类别
top5_idx = np.argsort(probs)[-5:][::-1]
print('Top-5 预测结果:')
for i, idx in enumerate(top5_idx):
print(f' {i+1}. {LABELS[idx]}: {probs[idx]*100:.2f}%')
# ── 释放资源 ──
rknn.release()实机测得的速度(resnet50 fp16,随机输入,30 次平均):
| core_mask | 耗时/帧 | 帧率 |
|---|---|---|
| NPU_CORE_0 | 25.67 ms | 39 FPS |
| NPU_CORE_0_1_2 | 21.19 ms | 47 FPS |
INT8 版本(同一测试条件):
| core_mask | 耗时/帧 | 帧率 |
|---|---|---|
| NPU_CORE_0 | 10.75 ms | 93 FPS |
| NPU_CORE_0_1_2 | 8.54 ms | 117 FPS |
INT8 量化后吞吐约 2.4x;ResNet-50 这类重模型多核收益(1.2~1.3x)明显小于 mobilenetv2(1.5x)。
4.4 板端推理:LeNet 手写数字识别
"""
LeNet 板端推理 —— 手写数字识别
运行环境:RK3588 板端 (rknn-lite2)
"""
from rknnlite.api import RKNNLite
import cv2
import numpy as np
# ── 初始化 ──
rknn = RKNNLite()
rknn.load_rknn('lenet.rknn')
rknn.init_runtime(core_mask=RKNNLite.NPU_CORE_0)
# ── 读取灰度图并预处理 ──
img = cv2.imread('digit.png', cv2.IMREAD_GRAYSCALE)
img = cv2.resize(img, (28, 28))
img = img.reshape(1, 28, 28).astype(np.float32)
img = (img - 128) / 128 # 归一化到 [-1, 1]
# ── 推理 ──
outputs = rknn.inference(inputs=[img])
# ── 后处理 ──
pred = np.argmax(outputs[0])
confidence = np.max(np.exp(outputs[0][0]) / np.sum(np.exp(outputs[0][0])))
print(f'预测数字: {pred}, 置信度: {confidence*100:.2f}%')
rknn.release()4.5 批量推理性能测试
想知道模型在板上的真实帧率,可以用下面的脚本测,它用随机输入模拟真实场景:
"""
RK3588 NPU 分类模型性能基准测试
运行环境:RK3588 板端 (rknn-lite2)
"""
import time
from rknnlite.api import RKNNLite
import numpy as np
RKNN_MODEL = 'resnet50.rknn'
# ── 初始化 ──
rknn = RKNNLite()
rknn.load_rknn(RKNN_MODEL)
rknn.init_runtime(core_mask=RKNNLite.NPU_CORE_0_1_2)
# 生成随机输入数据(模拟 224x224 RGB 图像)
dummy = np.random.randint(0, 255, (1, 224, 224, 3), dtype=np.uint8)
# ── 预热(消除首次推理的额外开销) ──
WARMUP_ROUNDS = 10
for _ in range(WARMUP_ROUNDS):
rknn.inference(inputs=[dummy])
print(f'预热完成 ({WARMUP_ROUNDS} 轮)')
# ── 正式测试 ──
TEST_ROUNDS = 100
times = []
for i in range(TEST_ROUNDS):
start = time.time()
outputs = rknn.inference(inputs=[dummy])
elapsed = time.time() - start
times.append(elapsed)
# ── 统计结果 ──
times_ms = np.array(times) * 1000
avg_ms = np.mean(times_ms)
min_ms = np.min(times_ms)
max_ms = np.max(times_ms)
fps = 1000 / avg_ms
print(f'测试轮数: {TEST_ROUNDS}')
print(f'平均推理时间: {avg_ms:.2f} ms')
print(f'最快: {min_ms:.2f} ms | 最慢: {max_ms:.2f} ms')
print(f'平均 FPS: {fps:.1f}')
# ── 释放 ──
rknn.release()参考值(本机,50 次平均):resnet50 INT8 三核约 8.5 ms / 117 FPS;mobilenetv2 INT8 三核约 1.9 ms / 523 FPS。把脚本里的 RKNN_MODEL 换成对应文件即可复现。
5. 代码详解
5.1 预处理流程
分类模型对输入数据的预处理非常敏感,预处理不当是分类精度异常的首要原因。
ResNet-50 预处理链路:
原始图像 (BGR, uint8, HxWxC)
│
├─ cv2.resize(img, (224, 224)) # 缩放到模型输入尺寸
├─ cv2.cvtColor(img, cv2.COLOR_BGR2RGB) # BGR → RGB(训练时使用 RGB)
│
├─ normalize:
│ img = (img - mean) / std
│ mean = [123.675, 116.28, 103.53] # ImageNet 通道均值
│ std = [58.395, 57.12, 57.375] # ImageNet 通道标准差
│
└─ 输出格式: NHWC (1, 224, 224, 3) uint8LeNet 预处理链路:
原始图像 (灰度, uint8, HxW)
│
├─ cv2.resize(img, (28, 28)) # 缩放到 28x28
├─ reshape → (1, 28, 28) # 添加 batch 维度
├─ astype(np.float32) # 转浮点
│
├─ normalize:
│ img = (img - 128) / 128 # 归一化到 [-1, 1]
│
└─ 输出格式: NCHW (1, 1, 28, 28) float32注意:LeNet 输入为单通道灰度图,使用 NCHW 格式;ResNet-50 输入为三通道 RGB 图,RKNN 默认使用 NHWC 格式。预处理时务必确认
mean_values和std_values的维度与输入通道数一致。
5.2 后处理(Softmax)
Softmax 函数把模型原始输出(logits,一组未归一化的分数)转换成概率分布:
def softmax(x):
"""
数值稳定的 Softmax 实现。
通过减去最大值防止 exp 溢出。
"""
x = x - np.max(x) # 平移,防止 exp 溢出
exp_x = np.exp(x)
return exp_x / np.sum(exp_x)Top-K 取前 K 个最高概率的类别索引:
# Top-1
pred = np.argmax(output)
# Top-5
top5_idx = np.argsort(output)[-5:][::-1]5.3 NPU 核心模式对比
RK3588 内置 3 个 NPU 核心,每个核心约 2 TOPS。通过 core_mask 参数可选择使用的核心组合:
| 模式 | core_mask | 有效核心数 | 理论算力 | ResNet-50 实际 FPS(INT8) | 适用场景 |
|---|---|---|---|---|---|
| 单核 | NPU_CORE_0 | 1 | 2 TOPS | 93(fp16: 39) | 低功耗场景、调试 |
| 三核 | NPU_CORE_0_1_2 | 3 | 6 TOPS | 117(fp16: 47) | 最高性能 |
怎么选:调试阶段用单核(NPU_CORE_0),方便定位问题;正式部署用三核(NPU_CORE_0_1_2),把 6 TOPS 算力用满;多模型并行部署时,给每个模型分配不同的核心,避免资源竞争。
5.4 模型转换参数详解
| 参数 | 含义 | LeNet 取值 | ResNet-50 取值 |
|---|---|---|---|
mean_values | 输入图像均值(用于归一化) | [[128]] | [[123.675, 116.28, 103.53]] |
std_values | 输入图像标准差 | [[128]] | [[58.395, 57.12, 57.375]] |
target_platform | 目标平台 | rk3588 | rk3588 |
quantized_dtype | 量化类型 | asymmetric_quantized-8 | asymmetric_quantized-8 |
do_quantization | 是否量化 | True | True |
dataset | 校准数据列表路径 | mnist_samples.txt | imagenet_samples.txt |
注意:
target_platform='rk3588'确保转换器生成针对 RK3588 NPU 优化的算子调度策略。不要使用rk3568等其他平台参数。
6. 常见问题
6.1 分类结果全为同一类
现象:所有输入图像都返回相同的预测类别,置信度接近 100%。
先按下表排查,多数情况是预处理没对齐:
| 原因 | 排查方法 | 解决方案 |
|---|---|---|
| mean/std 与训练不一致 | 打印模型转换时的配置 | 确认 mean_values 和 std_values 与训练代码一致 |
| 输入格式错误(NHWC vs NCHW) | 打印输入 tensor 的 shape | 确认 RKNN config 中的输入格式与模型要求匹配 |
| 通道顺序错误(RGB vs BGR) | 打印预处理后的像素值 | 确认 cv2.cvtColor 转换方向正确 |
| 量化校准数据质量差 | 检查校准数据集 | 使用多样化、有代表性的校准图片 |
怀疑输入有问题时,用这段代码把推理前后的数据打印出来看:
# 在推理前检查输入
print(f'输入 shape: {img.shape}') # 应为 (1, 224, 224, 3)
print(f'输入 dtype: {img.dtype}') # 应为 uint8
print(f'输入值范围: [{img.min()}, {img.max()}]') # 应在 [0, 255] 范围
# 在推理后检查输出
output = outputs[0][0]
print(f'输出 shape: {output.shape}') # ResNet-50 应为 (1, 1000)
print(f'输出值范围: [{output.min():.2f}, {output.max():.2f}]')6.2 推理速度远低于预期
现象:FPS 远低于参考值(如 ResNet-50 低于 300 FPS)。
按顺序排查:
# 1. 确认 NPU 频率是否正常
cat /sys/class/devfreq/fdab0000.npu/cur_freq # devfreq 接口(clk_npu 在本机不存在)
# 正常值应为 800MHz 或更高
# 2. 确认是否使用了多核模式
# 检查推理代码中的 core_mask 参数
# 3. 确认输入尺寸是否合理
# ResNet-50 标准输入为 224x224
# 4. 检查系统 CPU 负载
top -d 1
# 5. 检查内存使用
free -h对应常见的优化方向:确保使用 NPU_CORE_0_1_2 三核模式;检查 init_runtime 的 core_mask 参数是否正确;去掉多余的内存拷贝或数据格式转换;预处理放在 NPU 推理之外完成,避免阻塞 NPU 调度。
6.3 量化后精度下降严重
现象:INT8 量化后 Top-1 精度下降超过 2 个百分点。
三个方案按代价从小到大:
# 方案一:使用 MMSE 量化算法(精度更高,转换时间更长)
rknn.build(
do_quantization=True,
dataset='calibration.txt',
quantized_algorithm='mmse'
)
# 方案二:增加校准数据量(推荐 200-500 张)
# 将 calibration.txt 中的样本数量从 20 张增加到 200+ 张
# 方案三:混合精度量化(对敏感层保留 FP16)
rknn.config(
mixed_precision_ctrl_cfg={
'core_mask': 0, # 不启用混合精度
'plugin_layer': [], # 可指定保留 FP16 的层
}
)6.4 模型加载失败
现象:板端 load_rknn 返回错误码。
常见原因有三个:RKNN 模型是针对其他平台(如 RK3568)转换的,需重新以 target_platform='rk3588' 转换;RKNN Toolkit2 版本与板端 rknn-lite2 版本不匹配,需保持一致;模型文件损坏或传输不完整,需重新拷贝。
6.5 内存不足 (OOM)
现象:推理时程序崩溃,串口输出 out of memory 相关错误。
排查思路:
# 检查系统可用内存
free -h
# 检查 NPU 已分配内存
cat /sys/kernel/debug/rknpu/version
# 关闭不必要的进程释放内存
# 使用单核模式减少内存占用7. 相关文档
| 文档名称 | 路径 | 说明 |
|---|---|---|
| NPU 架构与性能概览 | ch01-npu-architecture-and-performance.md | RK3588 NPU 架构介绍 |
| RKNN Toolkit2 安装与使用 | ch02-rknn-toolkit2-setup-and-usage.md | 工具链安装与基本使用 |
| 模型转换与量化 | ch03-model-conversion-and-quantization.md | 通用模型转换流程与量化策略 |
| NPU 性能调优与调试 | ch09-npu-performance-and-debugging.md | 性能分析与调优技巧 |
