RK3588部署MM系列模型全流程
本文是笔者利用RK3588开发板从零开始,踩坑无数,最终成功部署MM系列中的mmpose和mmdet模型的全流程。国产开发板框架仍在发展中,对于许多算子有不支持的地方,所以整个流程中最重要的地方就是对于不支持的算子进行手动的替换。本文以一个完全新手(未接触过任何Linux、开发板基础)的角度编写。希望看完这篇文章,任何一位小白也可以进行自己的部署任务,或者从中找到思路。具体的代码开源到GitHub中:Your Repositories
由于是实验室的产出,所以博客和开源代码不提供模型和测试图片
整体思路:将训练好的模型(.pth)转换为.onnx文件——>onnx转换为rknn文件(使用RKNN-Toolkit2工具)——>连扳推理——>结果正确进行开发板部署(使用RKNN-Toolkit_Lite2工具)
一、准备环境
在用开发板进行模型部署前,需要准备必备的软硬件环境。本章节将详细描述如何进行环境的搭建。包含了开发板系统烧录、虚拟机设置、模型转换工具等。
1.1、 开发板系统
1.1.1、开发板介绍
首先仔细阅读开发板厂商的指导内容:本次使用的是飞凌嵌入式出品的OK3588-C开发板,内核使用的是瑞芯微出品的RK3588。开发板外观如图所示。本文将先介绍最常用的一些接口,对开发板进行一个简单的认识:
DEBUG接口:类型为Type-C接口,该接口是主机与开发板相连,来对开发板进行调试工作的接口,可以认为通过这个接口来操作开发板内部。
HDMI接口:分为RX和TX接口,分别代表了HDMI信号的输入和输出接口。
网口:有两个接口,分别是eth0、eth1,如果像笔者一样没有购买WIFI模块的,可以使用网口来连接路由器或自己的电脑,来进行联网。
Type-C0接口:一般是用来连接外部设备或者连接主机中的虚拟机软件,以此来使用虚拟机进行连板推理、文件传输等操作。

1.1.2、开发板系统烧写
拿到的开发板必须有一个系统才能够进行一系列操作,本文将按照飞凌嵌入式给出的技术手册进行系统的烧写。(如果是其他型号的开发板请根据商家的手册进行系统的烧写)
-
首先安装驱动助手DriverAssitant,这是进行烧写的软件,安装完毕后才可进入下一步。
-
助手安装结束后,使用Type-C连接开发板的TypeC0接口和主机PC,就可以利用主机将系统映像烧录进入开发板中。
-
之后点击“升级固件”,这一步软件应该会自动搜索到这个img文件的。至此系统算是烧写进入了开发板,这意味着开发板将可以进行后续开发操作。

1.2、 虚拟机环境
由于开发板是Ubuntu系统,所以需要一个虚拟机来模拟开发板环境,在虚拟机中进行编写代码、开发软件。再通过TypeC0接口和ADB工具将文件传送给开发板,以完成开发板环境下的开发任务。
1.2.1 VMWare配置
官网:https://www.vmware.com/cn.html 下载 VMware Workstation Pro。
之后进行虚拟机的配置,这里建议直接使用官方给出的映像文件进行虚拟机的配置。映像文件的位置需要自己找对应的开发板系统环境

解压完成之后,使用VMWare打开虚拟机,在目录下找到vmx文件,就完成了虚拟机的配置。


完成了这一步,虚拟机的配置就结束了。虚拟机在整个部署任务中起到的是连板调试(不直接使用开发板硬件资源,而是模拟)、使用ADB等工具进行文件的传输工作。
1.2.2 虚拟机环境
完成了虚拟机的设置,就需要将虚拟机中完成深度学习环境的搭建,环境搭建参考了迅维公司的视频和对应的手册:【AI深度学习推理加速器】——RKNPU2 从入门到实践(基于RK3588和RK3568)_哔哩哔哩_bilibili
1、Miniconda安装:用来构建一个虚拟环境,方便后续进行RKNN转换工具的安装和代码编写,所以选择轻量的 miniconda。官方链接如下: https://docs.conda.io/en/latest/miniconda.html。打开之后进行Linux版本下载。



至此,深度学习的虚拟环境已经搭建完毕,之后就是深度学习常见的,创建conda create 虚拟环境,创建自己项目合适的python版本的pytorch框架等等那经典的一套。
2、Pycharm安装:其实这部分并不严格制定Pycharm,而是任意一个可以编译python代码的编译器均可,pycharm官网:“https://www.jetbrains.com/pycharm/”.与平时在主机上安装不同的是,选择Linux选项下载。后续放到虚拟机中使用以下命令进行解压。
tar -vxf pycharm-community-2023.1.tar.gz
整个过程较为简单,值得注意的点是,安装结束后,将Pycharm显示到桌面上需要在第一次启动的时候选择创建快捷环境,否则就需要使用命令打开Pycharm

1.3、 转换工具
本次需要针对模型进行转换并部署到开发板中,为了提升效率,肯定不能只使用开发板的CPU进行加速。而RK3588的NPU如果要进行计算的加速,需要将模型转换成.rknn的格式,这就需要Rockchip公司所提供的转换工具——RKNN-Toolkit2和搭配的部署工具——RKNN-Toolkit_Lite2。
官方工具包下载连接如下:airockchip/rknn-toolkit2 注意要安装最新版本,可能许多不支持的算子新版本都会被支持。这里面包含了全套工具:RKNN-Toolkit2、RKNN-Toolkit_Lite2、rknpu2。以下统一称为工具包。
1.3.1 RKNN-Toolkit2安装
1、在虚拟机中创建rknn虚拟环境,指定python环境,然后激活环境
conda create -n pytorch38 python=3.8
conda conda activate pytorch38
2、从GitHub中下载完成工具包后,进入路径:rknn-toolkit2-master\rknn-toolkit2-master\rknn-toolkit2\packages\x86_64,找到和自己虚拟环境中python版本对应的whl安装包和requirements,分别代表了转换工具本体和版本对应的其他所需要的库。例如本次使用python版本为3.8

将这两个文件放到虚拟机中,然后进行对应版本的安装:
pip install -r requirements_XXX.txt -i https://pypi.tuna.tsinghua.edu.cn/simple
pip install rknn_toolkit2-XXX.whl -i https://pypi.tuna.tsinghua.edu.cn/simple
其中有可能出现tf-estimator-nightly==2.8.0.dev2021122109找不到的问题,解决方案参考博主:Ubuntu 20.04安装RKNN-Toolkit2出现tf-estimator-nightly==2.8.0.dev2021122109找不到的问题_no matching distribution found for tf-estimator-ni-优快云博客
至此转换工具便安装完成。
1.3.2 RKNPU安装
RKNPU的安装目的是开发板的服务,也是进行连板推理和部署所必须的工具,版本需要和转换工具RKNN-Toolkit2一致。这里是参考的飞凌嵌入式发布的技术手册:技术帖来啦——飞凌嵌入式RK3588开发板推理模型转换及测试 - 知乎
1、安装adb工具,这个工具是为了完成虚拟机和开发板之间的互通,使用USB-typeC线连接到板子的TypeC0接口,PC端识别到虚拟机中。(左侧出现手机图标)
pip install adb
adb devices # 如果安装完成了那么这个命令会显示设备号
2、从GitHub中下载的工具包找到如代码块中展示的路径的两个文件传给虚拟机,然后使用adb工具将文件推到开发板中。具体路径书写按照自己的实际路径来。
adb push XXX/rknpu2/runtime/Linux/rknn_server/aarch64/usr/bin/rknn_server /usr/bin/
adb push XXX/rknpu2/runtime/Linux/librknn_api/aarch64/librknnrt.so /usr/lib
至此完成了rknpu2开发板服务工具的安装
1.3.3 RKNN-Toolkit_Lite2安装
这个工具是用来进行具体的开发板部署任务的。当rknn模型转换好,并且连板调试没有问题之后,便可以使用该工具调用开发板的硬件设施进行真正的运行了。
要在开发板中进行代码的运行,首先需要给开发板将深度学习的环境搭建好,具体的操作与之间虚拟机的配置类似,也是安装Miniconda、安装虚拟环境等。搭建好之后也是一样,将工具包中的rknn-toolkit_lite2找到和自己python版本对应的文件放到开发板中进行安装。注意这一步都是在开发板中操作。
pip install rknn_toolkit_lite2-XXX.whl -i https://pypi.tuna.tsinghua.edu.cn/simple
至此,全部的环境终于都安装完成了,后面将介绍具体的部署过程,以及在部署中碰到问题的处理思路。
二、部署流程
2.1、 pth转换onnx
当使用mm系列的框架训练得到模型之后,利用mmdet和mmpose自带的apis进行模型的转换。在这里值得注意的一点,最好在转换onnx的时候能够转出来框架所使用的最原始的模型,也就是返璞归真,这样可以有许多后处理资料,用别人的模型可能不一定合适。
2.1.1 mmdet转换
1、主体转换:
from mmdet.apis import init_detector, inference_detector
import torch
# 加载模型
config_file = '.py配置文件路径'
checkpoint_file = '.pth模型文件路径'
model = init_detector(config_file, checkpoint_file, device='cuda:0')
print(model)
# 推理图片
img_path = '图像路径'
result = inference_detector(model, img_path)
# 可视化
# show_result_pyplot(model, img_path, result, score_thr=0.5)
# 导出为 ONNX
dummy_input = torch.randn(1, 3, 640, 640).cuda()
print(len(model(dummy_input)))
torch.onnx.export(
model,
dummy_input,
"detection_model.onnx",
input_names=["input"],
output_names=["rtm_cls_1", "rtm_cls_2", "rtm_cls_3","rtm_reg_1", "rtm_reg_2", "rtm_reg_3"],
dynamic_axes=None,
opset_version=11
)
print("模型转换完成!")
2.1.2 mmpose转换
1、主体转换
from mmpose.apis import init_model
import torch
# 加载模型
config_file = '.DDH/pose/rtmpose-m_DDH_test_dataset_coco.py'
checkpoint_file = '.DDH/pose/rtmpose_new.pth'
model = init_model(config_file, checkpoint_file, device='cuda:0')
# 推理图片
img_path = '.DDH/DDH.bmp'
# result = inference_detector(model, img_path)
# 可视化
# show_result_pyplot(model, img_path, result, score_thr=0.5)
# 导出为 ONNX
dummy_input = torch.randn(1, 3, 512, 512).cuda()
#print(len(model(dummy_input)))
torch.onnx.export(
model,
dummy_input,
"pose_model.onnx",
input_names=["input"],
output_names=["output"],
dynamic_axes=None,
opset_version=11
)
print("模型转换完成!")
2.2、 onnx转换rknn
利用rknn-toolkit2转换工具进行转换,代码是通用的,det和pose模型换路径即可。在转换过程中,由于mmdet是做关键框检测的,这一步并不太需要很高的精度,所以直接使用了int8量化进行转换。而mmpose是关键点检测,需要较高的精度,不能进行量化。
from rknn.api import RKNN
import cv2
import numpy as np
if __name__ == '__main__':
rknn = RKNN(verbose=True,verbose_file="log.txt") # 创建RKNN对象,为了后面的操作。parm1表示打印详细日志;parm2表示保存到对应路径
# 具体的应用:配置RKNN config 用于设置转换的参数设置(一般只修改mean_values和std_values和platform平台即可)
rknn.config(
mean_values= [[0,0,0]], # 表示预处理减去的均值化参数
std_values= [[255,255,255]], # 表示预处理除的标准化参数
quantized_dtype="asymmetric_quantized-8", # 表示量化类型
quantized_algorithm='normal', # 表示量化算法
quantized_method='channel', # 表示量化方式
quant_img_RGB2BGR= False, # 是否转换格式
target_platform="rk3588", # 运行的平台
float_dtype="float16", # RKNN默认的副点数类型
optimization_level=3, # 表示模型优化等级
custom_string="this is my rknn model for 3588", # 添加的自定义信息
remove_weight=False, # 去权重的从模型
compress_weight=False, # 压缩模型权重,减小模型体积
inputs_yuv_fmt=None, # 输入数据的YUV格式
single_core_mode=False, # 表示构建RKNN模型在单核心模式,只用于RK3588
)
# 加载onnx模型
rknn.load_onnx(
model="onnx路径", # 表示加载模型的路径
input_size_list=[[1,3,512,512]], # 表示模型输入图片的个数、尺寸和通道数(分别是batch、channel、img_size)
)
# 使用build接口构建rknn模型
rknn.build(
do_quantization=False, # 是否做量化操作(量化——降低模型复杂性)
#dataset="./res/dataset.txt", # dataset 表示要量化的图片集(去找对应输入图片的)
)
# 导出rknn模型
rknn.export_rknn(
export_path="rknn路径" # 表示导出rknn的保存路径
)
# 初始化运行环境(指定运行环境)
rknn.init_runtime(
target=None,
device_id=None,
perf_debug=False, # 设置为true可以打开性能评估的debug模式
eval_mem=False, # 设置为True,表示打开性能内存评估模式
async_mode=False, # 表示是否打开异步模式
core_mask=RKNN.NPU_CORE_AUTO, # 设置运行的NPU核心
)
# 使用opencv打开要推理的图片
img_2 = cv2.imread("box.jpg")
# 转换编码格式。由于cv2读图片会转换成rgb,所以需要转化
cv2.cvtColor(src=img_2, code=cv2.COLOR_BGR2RGB)
img_2 = cv2.resize(img_2,(512,512))
# 使用inference进行推理测试
outputs=rknn.inference(
inputs=[img_2],
data_format="nhwc",
)
print(len(outputs))
rknn.release()
如果模型效果可以接受,那么就可以认为rknn模型是有效的。由于笔者做的是像素级的任务,所以对精度有较高的要求。那么就不能够使用量化操作,否则会丢掉大量的精度。所以这一步特别注意,给出来的日志信息,很多算子不支持或者模型出现的问题都可以在这里溯源得到具体的问题点。同样,对于量化操作,由于rk3588使用的是float16,所以容易出现溢出的问题,也可以从转换这一步得到溯源信息。
例如本次转换的模型就会出现上溢的问题,然后对日志信息进行溯源,找到算子位置,然后通过onnx模型进行修改,例如添加clip算子等,来限制位数
2.3、 模型修改
由于mmpose模型中算子有不支持的地方以及溢出的问题,所以需要对模型进行修改。而mmdet因为使用了量化操作,所以无虞担心。
笔者在转换过程中出现了溢出的问题,相信有许多人都会遇到这个问题:使用量化操作,不会出现溢出,但是会掉精度。而不适用量化,精度可以控制,但是可能出现溢出问题,那么去解决这个溢出问题,就需要使用上文转换模型中提到的,溯源日志信息。
例如在笔者初次进行模型转换时,遇到了对应Reducel2算子的溢出问题,如下图所示。经过查询了解到ReduceL2算子(即计算L2范数),会沿着指定的维度(例如,沿着某个轴进行求和)计算输入张量每个元素的L2范数。
之后返回日志信息进行查找,发现RK3588会将ReduceL2算子进行自动拆分,删除源算子,并且补上三个算子由此完成算子的替换。而在这个过程中,由于平方操作的存在,所以非常容易出现溢出的问题。这个时候就可以确定修改方案:将整个算子在onnx模型中进行删除,并且自己进行替换,并且在每一次运算时进行clip进行数位的限制,来完成算子的修改。
使用如下代码进行算子的替换和clip操作
import onnx
from onnx import helper, TensorProto
# 加载 ONNX 模型
model = onnx.load('./ONNX/pose_model.onnx')
# 找到 /mlp/mlp.0/ReduceL2 节点的位置
reduce_l2_node = None
reduce_l2_index = -1
next_node = None
next_node_index = -1
# 遍历图中的所有节点,查找 ReduceL2 节点以及其下一个节点
for i, node in enumerate(model.graph.node):
if node.name == '/mlp/mlp.0/ReduceL2':
reduce_l2_node = node
reduce_l2_index = i # 记录 ReduceL2 节点的位置
# 查找下一个节点(在模型中紧随其后的节点)
if i + 2 < len(model.graph.node):
next_node = model.graph.node[i + 2]
next_node_index = i + 2
break
# 如果没有找到 ReduceL2 节点,抛出错误
if reduce_l2_node is None:
raise ValueError("Cannot find '/mlp/mlp.0/ReduceL2' node.")
# 创建 Constant 节点,表示常量 2.0
constant_node = helper.make_node(
'Constant',
inputs=[], # Constant 不需要输入
outputs=['const_2_output'], # 常量输出名称
value=helper.make_tensor(
name='const_2', # Tensor 名称
data_type=TensorProto.FLOAT, # 数据类型
dims=[], # 标量值,无需维度
vals=[2.0] # 常量值 2.0
)
)
# 创建 Constant 节点,表示 float16 的 min 和 max 值
min_value_node = helper.make_node(
'Constant',
inputs=[], # Constant 不需要输入
outputs=['min_value_output'], # 常量输出名称
value=helper.make_tensor(
name='min_value', # Tensor 名称
data_type=TensorProto.FLOAT16, # 数据类型
dims=[], # 标量值,无需维度
vals=[-65504.0] # float16 最小值
)
)
max_value_node = helper.make_node(
'Constant',
inputs=[], # Constant 不需要输入
outputs=['max_value_output'], # 常量输出名称
value=helper.make_tensor(
name='max_value', # Tensor 名称
data_type=TensorProto.FLOAT16, # 数据类型
dims=[], # 标量值,无需维度
vals=[65504.0] # float16 最大值
)
)
# 创建 Clip 节点,限制输入到 [-65504, 65504]
clip_node = helper.make_node(
'Clip',
inputs=['pow_output', 'min_value_output', 'max_value_output'], # 输入是 pow_output 和常量的 min/max
outputs=['clip_output']
)
# 创建 Pow 节点,用于每个元素平方
pow_node = helper.make_node(
'Pow',
inputs=[reduce_l2_node.input[0], 'const_2_output'], # 使用 constant 节点的输出作为第二个输入
outputs=['pow_output']
)
# 创建 ReduceSum 节点,计算平方后的和
reduce_sum_node = helper.make_node(
'ReduceSum',
inputs=['clip_output'], # 对 clip_output 求和
outputs=['sum_output'],
axes=[-1], # 如果是按行求和,根据需要调整轴
)
# 创建 Sqrt 节点,计算平方根
sqrt_node = helper.make_node(
'Sqrt',
inputs=['sum_output'], # 对求和结果取平方根
outputs=['l2_output']
)
# 更新原始 ReduceL2 节点的输出为 l2_output
reduce_l2_node.output[0] = 'l2_output'
# 删除 ReduceL2 节点
del model.graph.node[reduce_l2_index]
# 将 Constant, Pow, Clip, ReduceSum, 和 Sqrt 节点插入到模型中
model.graph.node.insert(reduce_l2_index, constant_node) # 在 ReduceL2 前插入 Constant 节点
model.graph.node.insert(reduce_l2_index + 1, pow_node) # 插入 Pow 节点
model.graph.node.insert(reduce_l2_index + 2, min_value_node) # 插入 min_value 节点
model.graph.node.insert(reduce_l2_index + 3, max_value_node) # 插入 max_value 节点
model.graph.node.insert(reduce_l2_index + 4, clip_node) # 插入 Clip 节点
model.graph.node.insert(reduce_l2_index + 5, reduce_sum_node) # 插入 ReduceSum 节点
model.graph.node.insert(reduce_l2_index + 6, sqrt_node) # 插入 Sqrt 节点
# 更新连接:确保新的节点输出连接到下一个节点的输入
if next_node:
next_node.input[0] = 'l2_output' # 将下一个节点的输入连接到 'l2_output'
# 验证模型
onnx.checker.check_model(model)
# 保存修改后的模型
onnx.save(model, './ONNX/pose_model_pow.onnx')
print("ReduceL2 node replaced with equivalent operations successfully.")
可以从上面的对比图看出来ReduceL2算子被修改成了三个单独算子——Pow、ReduceSum、Sqrt组合的情况。这一点和RKNN自动转换的思想相同,但是不同的是,可以更好的对每个算子进行控制操作。
例如本次因为Pow算子容易造成数位的溢出问题,添加了Clip算子对其进行了限制。其中ReduceSum算子需要指明连和的维度,这一点在笔者第一次进行算子修改的时候疏忽,先在原始模型中确定所有元素按照哪个维度进行相加,再进行ReduceSum算子的编写。这就比直接RKNN转换的黑箱模式要方便控制一些。
2.4、 模型推理
这一步是使用有效的rknn模型,将输入数据给到模型,然后进行模型的推理,得到输出结果。也是只需要使用rknn转换工具提供的api即可完成。与转换rknn模型的步骤基本类似,只需要修改模型路径为rknn模型、指定运行环境即可。
from rknn.api import RKNN
import cv2
import numpy as np
if __name__ == '__main__':
rknn = RKNN(verbose=True,verbose_file="log.txt") # 创建RKNN对象,为了后面的操作。parm1表示打印详细日志;parm2表示保存到对应路径
# 加载rknn模型
rknn.load_rknn(Model_Path)
# 初始化运行环境(指定运行环境)
rknn.init_runtime(
target='rk3588',
core_mask=RKNN.NPU_CORE_AUTO, # 设置运行的NPU核心
)
# 使用inference进行推理测试
outputs = rknn.inference(
inputs=[IMAGE],
data_format="nhwc",
)
print(outputs)
rknn.release()
2.5、 预处理、后处理操作
仅仅有了有效的rknn模型是不够的,最终的应用需要可视化或者利用模型输出做二级操作等,所以需要补上后处理、预处理操作。因为篇幅问题,仅贴出关键代码,完整代码已开源至GitHub。
2.5.1 mmdet预处理操作
mmdet预处理操作较为简单,不涉及复杂的转换,仅需要进行resize和letter_box操作到模型对应的尺寸即可
def bbox_preprocess(IMG_PATH,INPUT_SIZE=640):
# 参数设置
input_size = (INPUT_SIZE, INPUT_SIZE) # [H, W]
image = IMG_PATH
# 读取图像
image = cv2.imread(image) # 直接输入BGR图片,也不需要归一化
# 转换编码格式。由于cv2读图片会转换成rgb,所以需要转化
cv2.cvtColor(src=image, code=cv2.COLOR_BGR2RGB)
image_pad, ratio = letter_box(image, input_size[1], input_size[0]) # 填充到指定大小
return image_pad,ratio
2.5.2 mmdet后处理操作
这里后处理操作需要提一点mmdet的框架设计,最终输出是3对类别、检测框坐标(待解码)的loss,所以后处理的一个重要操作是对坐标进行解码操作。
rtmdet和yolo等基于锚框的框架不同,在深度眸大佬中rtmdet解密:MMYOLO 社区倾情贡献,RTMDet 原理社区开发者解读来啦! - 知乎中可以看到,rtmdet是将 bboxes (x1, y1, x2, y2) 编码为 (top, bottom, left, right),并且解码至原图像上。

RTMDet 的预测是基于 anchor-free 的方式,输出的是 [l, t, r, b]
,即特征点到目标边界的距离。在后处理时,需要将这些距离转化为绝对坐标形式,即
然后进行的操作都较为常规,例如nms等操作来确定最终锚框的坐标。
ef decoded_bbox(bbox_preds, priors):
# 确保输入为二维数组
bbox_preds = bbox_preds.reshape(-1, 4)
priors = priors.reshape(-1, 4)
# 分离先验框的中心点和宽高
prior_centers = priors[:, :2] # (cx, cy)
# 分离预测的距离值
l, r, t, b = bbox_preds[:, 0], bbox_preds[:, 1], bbox_preds[:, 2], bbox_preds[:, 3]
# 计算左上角和右下角坐标
x1 = prior_centers[:, 0] - l
y1 = prior_centers[:, 1] - t
x2 = prior_centers[:, 0] + r
y2 = prior_centers[:, 1] + b
# 拼接解码后的边界框
decoded_bboxes = np.stack([x1, y1, x2, y2], axis=-1)
return decoded_bboxes
2.5.3 mmpose预处理操作
具体参考了镜子大佬发布的rtmlib中的预处理和后处理。Tau-J/rtmlib: RTMPose series (RTMPose, DWPose, RTMO, RTMW) without mmcv, mmpose, mmdet etc.
在预处理的时候,与mmdet或者其他框架不同的点在于,mmdet的预处理可以直接通过resize,但是这里需要进行仿射变换,将mmdet处理出来的图像进行放大到目标尺寸。
def pose_preprocess(
img: np.ndarray, input_size: Tuple[int, int] = (512, 512)
) -> Tuple[np.ndarray, np.ndarray, np.ndarray]:
"""Do preprocessing for RTMPose model inference.
Args:
img (np.ndarray): Input image in shape.
input_size (tuple): Input image size in shape (w, h).
Returns:
tuple:
- resized_img (np.ndarray): Preprocessed image.
- center (np.ndarray): Center of image.
- scale (np.ndarray): Scale of image.
"""
# get shape of image
img_shape = img.shape[:2]
print(img_shape)
bbox = np.array([0, 0, img_shape[1], img_shape[0]])
# get center and scale
center, scale = bbox_xyxy2cs(bbox, padding=1.25)
# do affine transformation
resized_img, scale = top_down_affine(input_size, scale, center, img)
return resized_img, center, scale
2.5.4 mmpose后处理操作
简单的来说,需要对mmpose模型的输出进行解码,其输出是x和y两个方向上的预测向量,只需要取两个向量的最大值的index即可获得关键点的坐标。而关键点的置信度是取 max(max(x), max(y))。代码如下:
def pose_postprocess(
outputs: List[np.ndarray],
model_input_size:(512,512),
center: Tuple[int, int],
scale: Tuple[int, int],
simcc_split_ratio: float = 2.0) -> Tuple[np.ndarray, np.ndarray]:
# decode simcc
simcc_x, simcc_y = outputs
locs, scores = get_simcc_maximum(simcc_x, simcc_y)
keypoints = locs / simcc_split_ratio
# rescale keypoints
keypoints = keypoints / model_input_size * scale
keypoints = keypoints + center - scale / 2
return keypoints, scores
2.6、 连板调试
有了模型、有了预处理、后处理操作,就可以尝试将开发板与虚拟机相连来测试最终的效果如何。这部分的代码其实就是将前面的代码做结合,包括了模型的推理和前后处理。
from rknn.api import RKNN
import cv2
from utils import preprocess,postprocess
IMG_PATH = "./res/IMG/DDH.bmp"
BOX_PATH = "./RKNN/box_test.rknn"
POSE_PATH = "./RKNN/pose_test_without_quan.rknn"
def load_model(rknn,Model_Path,IMAGE):
# 加载rknn模型
rknn.load_rknn(Model_Path)
# 初始化运行环境(指定运行环境)
rknn.init_runtime(
#target=None
target='rk3588',
core_mask=RKNN.NPU_CORE_AUTO, # 设置运行的NPU核心
)
# 使用inference进行推理测试
outputs = rknn.inference(
inputs=[IMAGE],
data_format="nhwc",
)
print(len(outputs))
return outputs
if __name__ == '__main__':
box_rknn = RKNN() # 创建RKNN对象
pose_rknn = RKNN() # 创建RKNN对象
# 使用opencv打开要推理的图片
img = cv2.imread(IMG_PATH)
image_pad, ratio = preprocess.bbox_preprocess(IMG_PATH, 640)
box_result = load_model(box_rknn,BOX_PATH,image_pad)
# 推理预测得到结果进行显示
box = postprocess.bbox_postprocess(img, ratio, box_result, 640)
x, y = [], [] # 记录点坐标的列表
for bbox in box:
x1,y1,x2,y2 = bbox
det_image = img[int(y1):int(y2),int(x1):int(x2)]
#cv2.imwrite("./res/test/box.jpg", det_image)
pose_image, center, scale = preprocess.pose_preprocess(det_image,(512,512))
#pose_image = [pose_image.transpose(2, 0, 1)]
pose_result = load_model(pose_rknn, POSE_PATH, pose_image)
kpts,score = postprocess.pose_postprocess(pose_result,(512,512),center,scale)
print(kpts)
print("jjjj",len(kpts[0]))
# 画点操作(找5个点),只有5个点才可以处理
if len(kpts[0]) == 5:
for i in range(len(kpts[0])):
px = kpts[0][i][0]
py = kpts[0][i][1]
print(px)
cv2.circle(img, (int(px+x1), int(py+ y1)), 2, (0, 0, 255), cv2.FILLED)
x.append(int(px + x1))
y.append(int(py + y1))
else: # 一个框里并没有五个点,证明该检测框识别失败
print("error!!")
break
cv2.imwrite("", img)
box_rknn.release()
pose_rknn.release()
三、最终效果
然后是部署模型到开发板上,这一步可以参考b站迅维公司介绍的视频08_RKNN Toolkit lite2部署RKNN模型_哔哩哔哩_bilibili,整体来说与连板调试类似,只是需要将RKNN-Toolkit2换成RKNN-Toolkit_Lite2,然后将程序通过adb传送到开发板中即可。这里是开发板的运行结果
NPU并没有拉满,所以速度上有所欠缺。

四、后续展望
可以看到,目前还有两个问题:分别是精度和速度问题,可以后续研究将NPU的核心跑满,来加速。精度方面仍有待测试,但是整个流程已经完成,希望看到的读者的科研、工作顺利!!