分类模型部署(LeNet/ResNet)
目标平台: RK3576 | NPU 算力: 6 TOPS | 文档版本: V1.0
适用板型: C-3576BQ 核心板 + M4-R2 底板
最后更新: 2026-10-23
1. 概述
1.1 功能简介
图像分类是最基础的 AI 视觉任务,其目标是将输入图像归入预定义的类别中。RK3576 内置 NPU 算力达 6 TOPS(INT8),可高效运行各类分类模型。
本文档以两个典型模型为例,介绍在 RK3576 上完成分类模型部署的完整流程:
| 模型 | 数据集 | 输入尺寸 | 类别数 | 用途 |
|---|---|---|---|---|
| LeNet | MNIST | 28x28x1 | 10 | 手写数字识别(入门示例) |
| ResNet-50 | ImageNet | 224x224x3 | 1000 | 通用图像分类(工程级参考) |
部署流程包含以下环节:
原始模型 (ONNX) → RKNN 模型转换 → INT8 量化 → 板端推理 → 分类结果1.2 应用场景
- 手写数字识别:银行票据、快递单号、邮政编码等场景中的数字自动录入。
- 通用图像分类:农产品分级、工业缺陷分类、场景识别等需要多类别判断的场景。
- 物体识别预处理:作为目标检测管线的前置阶段,快速判断图像中是否存在感兴趣的目标类别。
- 质量检测:产线上对产品外观进行分类判定(合格/不合格,或细粒度缺陷分类)。
1.3 工作原理
分类模型的核心计算流程如下:
图像输入
│
▼
预处理(resize → 归一化 → 格式转换)
│
▼
RKNN 模型推理 ────── NPU 6 TOPS 加速
│
▼
后处理(Softmax → Top-K 排序)
│
▼
分类结果输出RK3576 NPU 对分类模型的关键加速点:
- Conv2d 卷积层:占据模型 90% 以上的计算量,NPU 通过专用卷积引擎硬件加速。
- BatchNorm / ReLU:与卷积融合执行,减少访存开销。
- 全连接层(FC):对较薄的特征图有良好的调度效率。
- INT8 量化推理:充分利用 6 TOPS INT8 算力,相比 FP16 理论性能翻倍。
2. 硬件准备
2.1 所需硬件清单
| 序号 | 硬件名称 | 规格 | 数量 | 说明 |
|---|---|---|---|---|
| 1 | C-3576BQ 核心板 | V1.0 | 1 | RK3576 主芯片,含 NPU |
| 2 | M4-R2 底板 | V1.0 | 1 | 提供外设接口 |
| 3 | USB to TTL 调试串口线 | 3.3V 电平 | 1 | 连接 DEBUG UART |
| 4 | 电源适配器 | 5V/2A | 1 | 底板供电(Type-C 或 6pin,空载最小 1000mA) |
| 5 | TF 卡 | 32GB 以上 | 1 | 烧录系统镜像 |
| 6 | 网线 | -- | 1 | SSH 连接或文件传输 |
2.2 硬件连接
┌─────────────────────────────────┐
│ M4-R2 底板 │
│ │
│ ┌─────────────────────┐ │
│ │ C-3576BQ 核心板 │ │
│ │ RK3576 (6 TOPS NPU)│ │
│ └─────────────────────┘ │
│ │
│ DEBUG UART ──── USB to TTL ──→ PC (串口终端) │
│ ETH0 ──── 网线 ────────→ 路由器/PC │
│ DC 5V ──── 电源适配器 │
│ TF Card ──── TF 卡 │
└─────────────────────────────────┘2.3 串口连接参数
通过 USB to TTL 线连接核心板 DEBUG 口后,在 PC 端使用串口终端工具(如 minicom、PuTTY)连接:
| 参数 | 值 |
|---|---|
| 波特率 | 1500000 |
| 数据位 | 8 |
| 停止位 | 1 |
| 校验位 | 无 |
| 流控 | 无 |
2.4 上电启动
- 插入已烧录系统镜像的 TF 卡。
- 连接 USB to TTL 串口线至 PC。
- 连接网线(可选,便于文件传输)。
- 接通 5V 电源,系统开始启动。
- 在串口终端中观察启动日志,等待登录提示符出现。
3. 软件准备
3.1 开发环境(PC 端)
在 PC 的 Linux 环境(推荐 Ubuntu 20.04/22.04)中安装 RKNN 工具链:
# RKNN Toolkit2 —— PC 端模型转换与量化
pip3 install rknn-toolkit2
# 依赖库
pip3 install numpy opencv-python pillow如需使用 RKNN Toolkit2 的高级功能(如量化分析、性能评估),可安装完整版:
pip3 install rknn-toolkit2[all]3.2 运行环境(板端)
板端运行镜像中已预装 rknn-lite2 运行时库。如需手动安装:
# 板端运行时(轻量版,仅含推理 API)
pip3 install rknn-lite2
# 板端依赖
pip3 install numpy opencv-python3.3 工具版本对照
| 工具 | 推荐版本 | 说明 |
|---|---|---|
| rknn-toolkit2 | >= 2.0.0 | PC 端模型转换 |
| rknn-lite2 | >= 2.0.0 | 板端推理运行时 |
| numpy | >= 1.21.0 | 数值计算 |
| opencv-python | >= 4.5.0 | 图像读取与预处理 |
| onnx | >= 1.12.0 | ONNX 模型加载(可选) |
以上版本为通用推荐,具体以实际发布的 RK3576 SDK 为准。
4. 操作步骤
4.1 LeNet 模型转换
LeNet 是经典的卷积神经网络,适用于 MNIST 手写数字识别。以下脚本在 PC 端将 ONNX 格式的 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='rk3576',
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='rk3576',
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}')4.3 板端推理 —— ResNet-50 图像分类
将 RKNN 模型部署到 RK3576 板端执行推理:
"""
ResNet-50 板端推理 —— Top-5 分类结果
运行环境:RK3576 板端 (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()4.4 板端推理 —— LeNet 手写数字识别
"""
LeNet 板端推理 —— 手写数字识别
运行环境:RK3576 板端 (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 批量推理性能测试
以下脚本在 RK3576 上测试 ResNet-50 的实际推理性能,使用随机输入数据模拟真实场景。
"""
RK3576 NPU 分类模型性能基准测试
运行环境:RK3576 板端 (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()实测 FPS 需在 C-3576BQ + M4-R2 V10 板卡上实际测量(待板上验证)。
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 核心模式对比
RK3576 内置 3 个 NPU 核心,合计算力 6 TOPS。通过 core_mask 参数可选择使用的核心组合:
| 模式 | core_mask | 有效核心数 | 理论算力 | 适用场景 |
|---|---|---|---|---|
| 单核 | NPU_CORE_0 | 1 | ~2 TOPS | 低功耗场景、调试 |
| 双核 | NPU_CORE_0_1 | 2 | ~4 TOPS | 功耗与性能平衡 |
| 三核 | NPU_CORE_0_1_2 | 3 | 6 TOPS | 最高性能 |
说明: 核心数量与算力分配为 RK 平台通用结构描述,具体 FPS 以实际板卡测量为准(待板上验证)。
核心选择建议:
- 调试阶段使用单核(
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 | 目标平台 | rk3576 | rk3576 |
quantized_dtype | 量化类型 | asymmetric_quantized-8 | asymmetric_quantized-8 |
do_quantization | 是否量化 | True | True |
dataset | 校准数据列表路径 | mnist_samples.txt | imagenet_samples.txt |
提示:
target_platform='rk3576'确保转换器生成针对 RK3576 NPU 优化的算子调度策略。不要使用其他平台参数。
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 远低于参考值。
排查步骤:
# 1. 确认 NPU 频率是否正常
cat /sys/kernel/debug/clk/clk_npu/clk_rate
# 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='rk3576'转换。 - RKNN Toolkit2 版本与板端 rknn-lite2 版本不匹配,需保持一致。
- 模型文件损坏或传输不完整,需重新拷贝。
6.5 内存不足 (OOM)
现象: 推理时程序崩溃,串口输出 out of memory 相关错误。
解决方案:
# 检查系统可用内存
free -h
# 检查 NPU 已分配内存
cat /sys/kernel/debug/rknpu/version
# 关闭不必要的进程释放内存
# 使用单核模式减少内存占用相关文档
| 文档名称 | 路径 | 说明 |
|---|---|---|
| NPU 架构与性能概览 | NPU架构与性能概览 | RK3576 NPU 架构介绍 |
| RKNN Toolkit2 安装与使用 | RKNN Toolkit2安装与使用 | 工具链安装与基本使用 |
| 模型转换与量化 | 模型转换与量化 | 通用模型转换流程与量化策略 |
| NPU 性能调优与调试 | NPU性能调优与调试 | 性能分析与调优技巧 |
