RK3588这块板子我用了大半年前后在它上面折腾过YOLO系列目标检测、车牌识别、人脸关键点检测最近又把Facenet人脸识别模型完整跑通了。说实话RK3588的NPU算力放在端侧设备里确实能打6 TOPS的INT8算力跑Facenet这种轻量级特征提取网络绰绰有余但是真正动手做模型转换、量化、板端部署这一套流程坑还是不少。写这篇保姆级教程就是想把最近一个完整项目的踩坑和实操记录整理出来给准备在RK3588上做人脸识别或者类似embedding模型部署的朋友做个参考。这篇文章不会只贴代码我会把我为什么这么配置、转换脚本里每个参数是干什么的、量化精度掉了怎么排查这些细节都讲清楚。不管你是第一次接触RKNN-Toolkit2还是已经在RK3588上部署过几个模型但被Facenet的特殊预处理搞懵了这篇文章都能给你一个可直接复现的完整路径。全程基于Ubuntu开发机加一块RK3588开发板软件层面用Rockchip官方的RKNN-Toolkit2和板端Runtime库不涉及自研推理框架纯粹是官方工具链的实战记录。1. 整体设计思路一条从PyTorch权重到NPU硬件的完整流水线我们常说的“部署模型”本质上就是让训练好的网络权重在目标硬件上高效前向推理。在RK3588上部署Facenet完整路径其实是这样的先拿到PyTorch或者TensorFlow版本的Facenet预训练权重然后导出成ONNX中间格式再用RKNN-Toolkit2把ONNX转换成RK3588 NPU能直接跑的RKNN格式最后在板端写推理代码调用。每一步都有各自的技术要点但最难踩坑的是第3步到第4步之间的量化和运行时调试。1.1 为什么选择RKNN-Toolkit2而不是ONNX Runtime你可能会有疑问RK3588本身是ARM架构直接在Ubuntu上装个ONNX Runtime跑CPU推理不也能用吗确实能跑但性能天差地别。RK3588的四核Cortex-A76 ARM CPU纯CPU推理一个160x160输入的Facenet单次前向耗时大概在150到300毫秒之间这个速度用来做实时视频流检测明显不够。而RK3588自带6 TOPS算力的NPU专门为INT8量化网络做了硬件加速同样一个Facenet模型跑在NPU上耗时能压到20到40毫秒这个差距就是“能不能商用落地”和“只能演示demo”的分界线。RKNN-Toolkit2是Rockchip官方提供的模型转换工具它工作的目标就是让PyTorch、TensorFlow、ONNX这些框架训练的模型能通过华为风格的工具链那位跨过这一道NPU图编译的门槛。虽然ONNX Runtime也有NPU执行提供程序但对RK3588的支持并不完善官方维护力度有限搞起来难度很高。相比之下RKNN-Toolkit2是芯片原厂自己维护的对自家NPU的算子覆盖、量化策略都有精细调优这才是“官方推荐路线”。1.2 Facenet模型的特殊性为什么这次和前几次部署不太一样我做之前几个检测模型的时候预处理流程都是标准的三通道BGR图直接归一化到0到1就进模型了。但Facenet不一样它是2015年谷歌提出的经典人脸识别模型分为特征提取网络和三元组损失训练两部分部署时只需要导出特征提取网络部分。常见实现中大特征提取器是Inception ResNet v1输入是160x160的RGB图输出是一个128维的归一化特征向量人脸比对依靠这个128维向量的欧氏距离或余弦相似度来完成。Facenet在预处理上有一个固定习惯像素值要先减去127.5再除以128也就是把0到255的像素范围映射到约-1到1之间。这个操作要是用错了归一化参数比如错误地除以255那么提取出来的特征向量会和预期相差很远直接导致后续比对时“同一个人也识别不出来”。所以我们在配置RKNN转换脚本的时候mean_values和std_values的设定一定要和训练时保持一致这一点我会在下面专门展开。2. 环境准备开发机和板端的物料清单这种端侧部署项目环境配置出问题的概率其实比代码本身还大。常见的情况就是版本号不对导致Initialize失败或者是Python环境里RKNN-Toolkit2导入时报错说缺少某个so文件这种问题不看源码很难定位。所以环境准备这一步我建议宁可多花半小时检查也不要跳过细节直接冲代码。2.1 开发机环境配置Ubuntu CondaRKNN-Toolkit2官方推荐的开发机环境是Ubuntu 18.04/20.04/22.04Python版本3.8到3.10之间。我这边用的是Ubuntu 22.04Python 3.10配合Conda管理环境。为什么要用Conda呢因为RKNN-Toolkit2依赖的库版本和系统自带的Python环境经常打架比如numpy版本过高会导致某些接口不兼容用Conda隔离一下出错了直接删掉重建效率最高。安装命令如下conda create -n rknn python3.10 conda activate rknn pip install rknn-toolkit2如果你在官网下载的是whl包安装方式也一样是pip install。装完后执行一下python代码验证能否正常导入python -c from rknn.api import RKNN; print(RKNN import ok)如果没有报错说明核心工具已经就绪。但我还要提醒一件事RKNN-Toolkit2有个依赖是OpenCV和很多图形相关库如果导入时报错libGL.so.1: cannot open shared object file执行sudo apt install libgl1 libglib2.0-0就能解决。这个错误特别常见因为很多服务器版Ubuntu默认不装图形库。2.2 板端系统与连接方式板端我用的是一块瑞芯微RK3588开发板运行Ubuntu 22.04系统。烧录系统这一步各家板卡略有差异但一般都有官方烧录工具和镜像文件按手册来就行。除了系统本身板端必须要准备好的是ADB连接能力因为默认情况下USB ADB调试模式是最方便的调试通道。连接板子时用Type-C数据线一定要是能传数据的那种不是纯电源线连接开发板的OTG口和电脑的USB口。板子开机后执行adb devices如果能看到设备编号并且状态是device说明连接成功。如果显示unauthorized就去板子屏幕上允许USB调试授权即可。我在实际项目中还遇到过no permissions (user in plugdev group)这种权限错误解决办法是把当前用户加入plugdev组然后重新插拔USB线sudo usermod -aG plugdev $USER修改完用户组后重新登录一次ADB就能正常识别了。这一步容易卡住我第一次搞的时候也折腾了十分钟才知道是用户组权限问题。另外板子一定要联网因为后面要从开发机拷贝文件到板子或者板子上也要装一些必要的Python依赖库网络不通会非常麻烦。3. 模型转换从PyTorch权重到RKNN格式的详细拆解环境准备好之后开始进入整个项目最核心的转换环节。这一步做得好不好直接决定了后面板端推理的精度和速度。我把整个转换过程拆成四个阶段导出ONNX、配置转换参数、准备量化数据集、构建RKNN模型。3.1 导出Facenet的ONNX模型Facenet的官方实现有好几个版本我用的最顺手的是timesler的facenet-pytorch它有完整的预训练权重支持Inception ResNet v1结构代码封装也比较友好。在开发机上先装好这个库pip install facenet-pytorch然后用PyTorch的torch.onnx.export导出ONNX。导出前有几个关键点需要处理清楚。第一模型要切到eval模式并且把dropout等训练专用层关掉第二输入张量的shape要固定为(1, 3, 160, 160)因为RKNN转换时对动态shape的支持有限固定shape最稳妥第三输出只需要拿到128维embedding就行不需要它在训练时用于计算loss的那个中间层。下面是我用的导出脚本import torch from facenet_pytorch import InceptionResnetV1 model InceptionResnetV1(pretrainedvggface2).eval() dummy_input torch.randn(1, 3, 160, 160) torch.onnx.export( model, dummy_input, facenet.onnx, input_names[input], output_names[embedding], opset_version11, dynamic_axesNone ) print(ONNX export done)这里把opset_version定在11是因为RKNN-Toolkit2对ONNX算子版本兼容性在opset 11到13区间表现最稳定超过13偶尔会遇到算子不支持的情况。导出完成后可以用onnxruntime先跑一遍推理确认ONNX模型输出和PyTorch原始输出一致pip install onnxruntimeimport onnxruntime as ort import numpy as np sess ort.InferenceSession(facenet.onnx) input_name sess.get_inputs()[0].name output sess.run(None, {input_name: np.random.randn(1, 3, 160, 160).astype(np.float32)}) print(output[0].shape) # 应该是 (1, 128)3.2 量化策略与dataset.txt的准备RKNN模型的默认量化方式是INT8这也是NPU能跑出那么多TOPS算力的前提——INT8向量单元是NPU性能的核心来源。量化的本质是把FP32的权重和激活值映射到INT8的256个离散整数上这个过程不可避免会带来精度损失但通过合理的校准数据集基本能把误差控制在可接受范围内。RKNN-Toolkit2的量化过程需要读取一批代表真实场景的图片称为量化校准集或评估集。这里有个经验值校准图片数量最好在200到500张之间。图片太少网络各层的激活值分布估计不准确量化后精度波动很大图片太多转化时间变长但精度不再明显提升所以200到300张是一个很高效的区间。对于人脸识别这种下游任务校准图片最好直接使用和实际应用场景接近的人脸图片一张图里包含一个清晰正脸即可。可以从LFW数据集、自己的测试集甚至从摄像头采集的视频帧里截取人脸区域后resize到160x160。关键是要把图片路径写进一个文本文件一行一张称为dataset.txt。文件格式如下dataset/face_001.jpg dataset/face_002.jpg dataset/face_003.jpg ...这里需要注意dataset.txt里面写的路径是相对于你执行转换脚本时的当前目录写相对路径最稳妥避免把绝对路径写死在脚本里导致换机器就失效。3.3 编写并运行RKNN转换脚本具备了ONNX模型文件、量化校准图片集、dataset.txt之后就可以写转换脚本了。我会把关键步骤和参数意义讲清楚from rknn.api import RKNN rknn RKNN() # 配置阶段 rknn.config( mean_values[[127.5, 127.5, 127.5]], std_values[[127.5, 127.5, 127.5]], target_platformrk3588, ) print(-- Loading ONNX model) ret rknn.load_onnx(modelfacenet.onnx) assert ret 0, Load ONNX model failed print(-- Building RKNN model) ret rknn.build(do_quantizationTrue, datasetdataset.txt) assert ret 0, Build RKNN model failed ret rknn.export_rknn(facenet.rknn) assert ret 0, Export RKNN model failed print(Done)这里的核心参数有三个mean_values和std_valuesFacenet的预处理是(x - 127.5) / 128我已试过填127.5和128量化和匹配训练一致。你可能会问为什么不是除以128而是127.5其实这是因为人脸图片像素在0-255之间用127.5做中心化和用128做缩放都是常见归一化手法Facenet的官方实现用128而我们这里填写的std_values会作用在原图的归一化流水线里这一点不可混淆。具体来说如果训练时预处理是(pixel - 127.5) / 128那mean_values填127.5std_values填128。如果你填错了127.5和128的比例关系会导致特征空间偏移影响识别。target_platformrk3588如果不填这个参数默认可能是空或者别的平台构建出的RKNN模型在3588上初始化就会失败或者跑在低性能模式。基本所有首次部署的朋友都踩过这个坑。do_quantizationTrue表示启用INT8量化。如果你填False生成的模型是FP16精度精度高但推理速度会慢很多而且RK3588的NPU对FP16的支持不如INT8高效。除非你发现量化后精度暴降到不能用的程度否则我建议直接用INT8性能优势太明显。构建完成后会生成facenet.rknn文件体积大概在几十MB级别视原始权重和量化情况而定。我的vggface2预训练权重转换后大概是86MB左右跑起来没问题。3.4 量化精度验证转换完别急着上板子拿到RKNN模型之后我强烈建议先在开发机上做一次模拟推理用同一张测试图片对比RKNN输出和ONNX输出的128维特征计算余弦相似度。如果相似度大于0.95说明量化精度损失在可接受范围如果低于0.90就要重新审视量化数据集是否覆盖了足够的特征分布。开发机模拟推理我用的脚本大概是这样的from rknn.api import RKNN import numpy as np import cv2 rknn RKNN() rknn.load_rknn(facenet.rknn) ret rknn.init_runtime() assert ret 0 img cv2.imread(test_face.jpg) img cv2.cvtColor(img, cv2.COLOR_BGR2RGB) img cv2.resize(img, (160, 160)) img np.expand_dims(img, 0).astype(np.float32) outputs rknn.inference(inputs[img]) print(RKNN embedding:, outputs[0]) rknn.release()同时用ONNX Runtime跑同一张图的预处理版本得到ONNX的输出比较两者余弦相似度。如果分数很低就回去增加量化数据集或者检查是否填写错了mean/std这是检查和排除量化误差最有效的方法。4. 板端部署写一个最小可运行的推理程序模型转换完成之后真正的挑战是从“开发机上模拟推理”过渡到“板子上实际运行”。这一阶段涉及板端Runtime库安装、推理接口调用、预处理和后处理逻辑以及最终的性能验证。4.1 板端运行时库的安装与拷贝模型RK3588板端运行RKNN模型需要两样东西librknnrt.so运行库和对应的Python接口包rknn-toolkit-lite。在开发机上可以找到Rockchip提供的板端Runtime包包含在RKNN-Toolkit2发布包的rknpu2目录里。把这个目录下的librknnrt.so推送到板子的/usr/lib目录下adb push librknnrt.so /usr/lib/同时把Python推理接口包rknn-toolkit-lite的whl文件拷贝到板子在板子上用pip安装。我板子上的Ubuntu是Python 3.10安装后同样验证一下导入是否成功python3 -c from rknnlite.api import RKNNLite; print(RKNNLite import ok)注意板端用的是RKNNLite类而不是开发机的RKNN类这两个类的方法基本一致但内部实现和适用场景有区别。RKNNLite专门为嵌入式设备做了裁剪依赖更少性能更优所以板端不要用开发机版的rknn-toolkit2去跑。模型文件facenet.rknn同样通过ADB推送到板子adb push facenet.rknn /home/orange/4.2 板端推理代码从图像输入到128维特征向量板端推理代码的核心逻辑和开发机模拟推理很像但有几个细节差别很大。第一是RKNNLite初始化时可以不指定target参数它会自动适配当前设备平台第二是推理前后要手动完成图像预处理和后处理。我会直接写一个完整的可运行脚本然后在下面逐行解释关键点import cv2 import numpy as np from rknnlite.api import RKNNLite class FaceEncoder: def __init__(self, model_path): self.rknn RKNNLite() ret self.rknn.load_rknn(model_path) assert ret 0, Load RKNN model failed ret self.rknn.init_runtime() assert ret 0, Init runtime failed def preprocess(self, img_bgr): img_rgb cv2.cvtColor(img_bgr, cv2.COLOR_BGR2RGB) img_resized cv2.resize(img_rgb, (160, 160)) img_nchw np.expand_dims(img_resized, axis0).astype(np.float32) return img_nchw def embed(self, img_bgr): input_data self.preprocess(img_bgr) outputs self.rknn.inference(inputs[input_data]) embedding outputs[0].flatten() norm np.linalg.norm(embedding) return embedding / norm def release(self): self.rknn.release() if __name__ __main__: encoder FaceEncoder(facenet.rknn) img1 cv2.imread(person1.jpg) img2 cv2.imread(person2.jpg) emb1 encoder.embed(img1) emb2 encoder.embed(img2) dist np.linalg.norm(emb1 - emb2) similarity np.dot(emb1, emb2) print(fEuclidean distance: {dist:.4f}) print(fCosine similarity: {similarity:.4f}) encoder.release()关于预处理有两个细节容易踩坑。第一RKNN模型输入是NCHW布局所以即便图片是HWC格式也要通过expand_dims加到第0维而不是直接用HWC输入否则会被当成一个“宽高错乱”的图处理。第二Facenet在训练时用的是RGB输入而OpenCV默认读出来的是BGR如果不转人脸特征会严重偏移识别准确率直接崩掉。我自己的项目里就因为这个RGB/BGR问题浪费了整整一个下午。关于后处理Facenet输出的128维向量通常在做特征计算前要L2归一化。因为我在embed方法里手动做了一次归一化后续计算欧氏距离和余弦相似度时结果会更稳定。同一人的两张人脸特征欧氏距离一般小于1.0余弦相似度通常大于0.8不同人则欧氏距离在1.2以上余弦相似度在0.5以下。当然这个阈值和具体数据集、模型训练集都有关系建议根据自己测试集先做一个阈值标定。4.3 推理性能测试与NPU占用验证程序跑通以后紧接着要验证到底是不是在NPU上执行的以及性能是否达标。检查方法很简单在板子终端执行sudo cat /sys/kernel/debug/rknpu/load这个文件会显示当前NPU的实时负载百分比。如果在运行推理脚本的同时执行这条命令能看到NPU负载明显升高说明模型确实跑在NPU上了。如果NPU负载一直是0但推理时间却比我预期慢很多那就要警惕是不是模型根本没被NPU接手而是落到CPU上执行了。还有一种情况是模型初始化日志里会出现类似target platform rk3588的字样以及librknnrt version之类信息。我建议推理时开启详细日志模式方便定位问题ret self.rknn.init_runtime(verboseTrue)verboseTrue会打印NPU执行时的详细日志一旦有问题能很快定位。性能测试也不复杂直接对同一张图连续推理100次去掉前10次warmup统计平均耗时。我这边实测数据如下推理模式单次耗时备注RK3588 CPU纯推理约180msONNX RuntimeRK3588 NPU INT8约28msRKNN Runtime这还是在没有启用多线程流水线的情况下。如果后续再接上摄像头帧队列把预处理、推理、后处理做成流水线实际帧率还可以再往上提。4.4 接入人脸检测器形成完整链路严格来说具备人脸识别能力的完整系统前置还需要一个目标检测模型把图片中的人脸框出来然后把裁剪的人脸区域送入Facenet提取特征。这里我简单说一下串联思路。人脸检测我用的比较顺手的是YOLOv5s-face或者RetinaFace的RKNN转换版本检测到人脸框后将区域坐标进行适度扩展避免头发、耳朵边缘被切除影响embedding质量。然后对裁剪后的区域做resize到160x160再输入Facenet。这个串联流程的核心是“检测框的尺度一致性”训练时的人脸区域如果包含完整额头到下巴推理时也尽量保持这个占比。如果你暂时不想自己训练检测模型也可以用Rockchip官方仓库里提供的YOLOv5s模型转换脚本转换流程和Facenet几乎一致。把两个RKNN模型都加载到板端一个负责定位人脸一个负责提取特征就能拼出一个完整的端到端人脸识别应用了。5. 常见问题与排查技巧实录这种嵌入式模型部署项目真正花时间的往往不是主链路而是各种周边问题。我把这次实际遇到过的、以及同行反馈过的高频问题列成速查表方便你快速定位现象可能原因解决办法导入RKNN报libGL.so.1错误系统缺少OpenCV图形库sudo apt install libgl1 libglib2.0-0load_onnx后build失败ONNX版本过高或算子不支持导出时设置opset_version11尝试升级RKNN-Toolkit2build时提示量化数据集找不到dataset.txt路径写错检查相对路径确保文件存在且可读生成的RKNN在开发机推理全0归一化参数或通道顺序错误检查mean/std、RGB/BGR转换板端init_runtime失败模型target_platform不是rk3588重新构建确保config指定target_platformrk3588板端推理速度过慢模型没跑NPU而是CPU检查NPU负载开启verbose日志确认执行设备量化后精度明显下降校准图片数量少或和真实场景不符增加校准图到200-500张覆盖角度、光线变化ADB识别不了板子USB线不支持数据或权限不足换数据线加入plugdev用户组检查板端USB调试状态RKNNLite与RKNN混淆使用板端装了开发机版toolkit板端安装rknn-toolkit-lite使用RKNNLite类同一个人识别距离1.5预处理或归一化不一致对比ONNX输出逐步排查归一化参数和通道顺序5.1 量化精度下降的实际排查过程在调试量化精度时我遇到过一个典型情况。第一次用250张校准图片build出来的RKNN模型在开发机上拿LFW数据测试和FP32 ONNX输出的余弦相似度高达0.97我以为万事大吉结果一到板端跑真实摄像头采集的人脸同一个人两张图的欧氏距离竟然高达1.6完全不可用。排查了半天最终定位到问题根源不在量化而在我的测试图片和校准图片分布差异太大校准图片是从公开数据集里截取的比较端正的人脸摄像头采集则有大量侧脸、遮挡和光照不均。后来我从摄像头采集的真实场景中截选了300帧人脸图片重新做校准集重新build模型后距离直接降到0.7以下效果立竿见影。这个经历说明量化校准集不是“随便找一批图凑数”而是必须尽量贴近真实应用场景的数据分布。如果你做的是人脸考勤机就用考勤机视角下的人脸图片如果做的是安防摄像头就用安防摄像头视角下的人脸图片。数据分布一致带来的精度提升有时候比调参更明显。5.2 备份与工程化小建议最后分享一个工程化习惯。我在转换模型的时候会把ONNX、量化校准集、dataset.txt、转换脚本、RKNN模型统一归档到同一个目录并用README记录转换时间和关键参数。这么做的好处是一旦发现某个RKNN模型有问题不用从头开始逆向分析直接看README就知道当时的配置。另外RKNN-Toolkit2的版本升级比较频繁不同版本转换出来的RKNN文件不完全兼容建议在README里记录工具版本方便复现和排查。板端部署时我一般会把librknnrt.so和rknn-toolkit-lite的版本也记录下来。遇到过芯片微架构相同但固件库版本不同的两块板子跑同一个RKNN模型一个正常一个报错后来核对版本才发现底层Runtime差异。所以版本记录这个习惯关键时刻真的能省一天时间。