
1. 项目概述与整体技术方案1.1 这个项目到底在做什么最近在搞一个边缘端的人脸识别项目硬件选型时纠结了很久最后敲定了RK3588。原因很简单这颗芯片的NPU算力有6 TOPS带6个CPU核心4个A764个A55跑人脸识别这种中等规模模型绰绰有余而且瑞芯微的生态在国产平台里算比较完整的。但真正动手后发现从拿到板子到让模型在NPU上跑起来中间的路比想象中曲折得多——光是模型转换、工具链版本配对、量化精度调试这几个环节就够喝一壶的。这篇文章就是把我踩过的坑和最终的可行方案完整记录下来目标读者是正在或准备在RK3588上做视觉类AI部署的开发者。无论你是要做人脸识别、目标检测还是其他CV任务核心的RKNN-Toolkit2使用流程、NPU推理加速思路、板端环境配置方法都是通用的。认真看完并跟着操作一遍你就能在自己的板子上把Facenet人脸识别模型完整跑起来。1.2 技术选型为什么是FacenetRK3588先说Facenet。人脸识别领域现在方案很多有基于ArcFace的、有基于CosFace的还有各种轻量化的MobileFaceNet。Facenet虽然2015年就提出了但时至今日依然是个非常经典且好用的方案。它最核心的思想是把人脸图像映射到一个512维的欧氏空间中同一个人脸在该空间中的距离近不同人的距离远。这个特性让它特别适合做识别任务——你只需要在数据库中预存每个人的人脸特征向量推理时提取待识别人的特征向量然后比对距离即可。选RK3588也经过了多轮比较。同类产品里树莓派5的算力明显不够Jetson Orin Nano性能强但价格高一大截。RK3588的6 TOPS NPU在2000元档位的开发板上很有竞争力而且支持INT8量化推理实测下来跑Facenet的推理延迟能做到30毫秒左右完全满足实时人脸识别的需求。另外瑞芯微的RKNN工具链对PyTorch、ONNX、TensorFlow等主流框架都有不错的支持这也是我最终选它的重要原因。整个项目的架构其实不复杂分三层PC端负责模型转换和量化使用RKNN-Toolkit2把PyTorch版Facenet转成RKNN格式板端使用RKNN Runtime C/Python API完成NPU推理提取人脸512维特征向量业务层负责摄像头采集、人脸检测对齐、特征比对和结果输出。后面所有章节都是围绕这三层展开的。2. 环境准备与工具链版本配对2.1 RKNN-Toolkit2的安装过程RKNN-Toolkit2是瑞芯微官方提供的模型转换和推理仿真工具跑在PC端Ubuntu系统。它负责把训练好的PyTorch模型转换成NPU能识别的RKNN格式文件。这块最容易被忽视的就是版本配对问题——RKNN-Toolkit2的版本和板端RKNN Runtime的版本必须严格对应否则转换出来的模型在板子上根本加载不了或者推理结果完全错误。我环境使用的是Ubuntu 20.04 LTSPython 3.8。安装命令其实很简单# 创建虚拟环境避免污染系统环境 python3 -m venv rknn_env source rknn_env/bin/activate # 安装RKNN-Toolkit2 pip install rknn-toolkit21.6.0这里有两个重要提醒。第一RKNN-Toolkit2对Python版本要求比较严格1.6.0版本需要Python 3.8到3.10建议直接用官方推荐的Ubuntu 20.04 Python 3.8组合省得折腾依赖。第二安装包比较大包含了很多NPU驱动和调试工具网络不好的时候容易超时。实测用国内镜像源安装会顺利很多。安装完成后用下面的命令验证是否成功python -c from rknn.api import RKNN; print(RKNN-Toolkit2 OK)如果能正常输出说明工具链安装成功了。这里顺便说一下RKNN-Toolkit2除了可以做模型转换还能在PC端做仿真推理也就是不连接板子就能测试模型效果。但我实际使用下来的感受是仿真推理的精度和板端NPU真实推理有差异只能作为参考最终必须放到板子上验证。2.2 板端RKNN Runtime与系统准备板端需要安装的是RKNN Runtime和librknnrt.so动态库。如果你的RK3588开发板使用的是官方提供的Ubuntu固件通常系统里已经预装了Runtime但版本可能和工具箱不匹配。我用的板子是正点原子的RK3588开发板自带Ubuntu 20.04系统。首先确认当前系统Runtime版本strings /usr/librknnrt.so | grep rknn_server如果找不到就需要手动安装。先去瑞芯微的官方文档下载对应版本的RKNN Runtime包然后用adb或者U盘拷到板子上安装。板端环境还有一个容易踩的坑硬件加速依赖。RKNN Runtime在NPU推理时会使用RGA图像处理加速和MPP视频硬编解码模块如果固件里这些模块的固件版本过低可能导致推理时内存出错或者图像处理异常。因此建议先把系统升级到最新版固件sudo apt update sudo apt upgrade如果是从零开始移植Ubuntu还需要注意RK3588的分区布局。官方Ubuntu固件默认使用AB分区方案这意味着系统分区是双份的OTA升级时可以在A/B分区之间切换。手动刷机时要注意别刷错分区表不然系统会起不来。我建议新手直接用官方工具RKDevTool烧录完整固件不要自己改分区先把环境跑通再说。2.3 版本配套关系速查表RKNN-Toolkit2版本RKNN Runtime版本适用芯片支持框架1.5.01.5.0RK3588/RK3568PyTorch 1.x / ONNX / TF1.6.01.6.0RK3588/RK3568PyTorch 2.x / ONNX / TF2.0.02.0.0RK3588/RK3576PyTorch 2.x / ONNX / TF上面这个表是我自己使用过程中的总结官方文档里也有详细的兼容性矩阵。这里必须要强调的是版本严格配对是第一位的。我一开始用的是RKNN-Toolkit2 1.6.0 Runtime 1.4.0的组合结果模型在板子上加载时就报错rknn_init fail, ret -1。所以后来我把工具箱和Runtime都统一到了1.6.0问题才解决。3. Facenet模型转换核心流程3.1 模型的获取与ONNX导出Facenet的PyTorch实现网上有很多我用的这个版本是基于Inception ResNet v1骨干网络的在LFW数据集上准确率能达到99%以上。模型结构不复杂核心是最后一层输出512维的嵌入向量。这里要注意我们做部署时只需要推理部分不需要训练逻辑所以导出模型前要把trainingFalse固定住。拿到PyTorch模型后第一步是转成ONNX格式。为啥要转ONNX因为RKNN-Toolkit2虽然支持直接加载PyTorch模型但兼容性不如ONNX好尤其是一些新版本PyTorch才支持的算子。ONNX作为一个中间格式兼容性和稳定性都要好得多。导出ONNX的脚本比较简单import torch import torch.onnx from facenet_pytorch import InceptionResnetV1 # 加载预训练模型 model InceptionResnetV1(pretrainedvggface2).eval() # 创建示例输入注意归一化范围是[-1,1] dummy_input torch.randn(1, 3, 160, 160) # 导出ONNX torch.onnx.export( model, dummy_input, facenet.onnx, input_names[input], output_names[embedding], dynamic_axes{input: {0: batch}}, opset_version11 )这里几个参数值得仔细说一下。opset_version建议用11或12太高了RKNN-Toolkit2可能不支持。dynamic_axes允许动态batch但如果你是固定batch为1的场景建议别开动态轴转换时处理起来更稳妥。还有一点输入尺寸固定为160x160这是Facenet的标准输入大小后面板端预处理也要按照这个尺寸来。导出后可以用onnxsim工具对模型进行简化去掉一些冗余算子减小模型体积同时也能提升转换成功率pip install onnxsim onnxsim facenet.onnx facenet_sim.onnx我实际对比过简化前后的模型转换成功率差别挺大的。尤其是PyTorch导出的ONNX里经常有一堆Shape、Gather这类不影响计算结果的算子RKNN-Toolkit2遇到某些算子时会直接报错不支持。3.2 使用RKNN-Toolkit2进行模型转换模型转换是整个部署流程的枢纽环节配置直接决定模型能不能在NPU上高效运行。RKNN-Toolkit2提供了Python API来完成转换核心逻辑分四步初始化、加载模型、配置参数、构建RKNN模型。下面是完整的转换脚本from rknn.api import RKNN # 创建RKNN对象 rknn RKNN() # 配置模型输入 rknn.config( mean_values[[127.5, 127.5, 127.5]], std_values[[127.5, 127.5, 127.5]], target_platformrk3588 ) # 加载ONNX模型 ret rknn.load_onnx(modelfacenet_sim.onnx) if ret ! 0: print(模型加载失败) exit(-1) # 构建RKNN模型 ret rknn.build(do_quantizationTrue, datasetdataset.txt) if ret ! 0: print(模型构建失败) exit(-1) # 导出RKNN文件 ret rknn.export_rknn(./facenet.rknn) if ret ! 0: print(导出失败) exit(-1) rknn.release()这段代码里面有两个关键点要重点解释都是容易忽略的。第一个是mean_values和std_values的配置。Facenet的PyTorch模型输入归一化是把每个像素从[0,255]映射到[-1,1]具体计算是(x - 127.5) / 127.5。所以在RKNN中配置mean127.5, std127.5。这个必须和模型训练时的预处理方式完全一致否则特征提取出来就是垃圾数据后面比对距离的时候全乱了。第二个是量化参数。do_quantizationTrue表示启用INT8量化。量化后模型体积会缩小到原来的四分之一推理速度明显提升但精度会有一定损失。Facenet这种输出512维特征向量的模型量化对精度的伤害比较敏感后面会有专门章节讲精度恢复。dataset.txt里面每行是一个图像路径RKNN-Toolkit2会利用这些图像计算每层的激活值范围作为量化的依据。这个数据集最好从你的实际业务数据里挑分布越接近真实场景量化效果越好。3.3 量化配置与精度验证量化是RKNN部署中最让人头疼的部分关键在于理解为什么量化会导致精度掉点。简单来说NPU做INT8推理时把推理过程中的浮点张量变成8位整数-128到127。这就会丢失一部分信息。Facenet的输出是512维向量每个维度是浮点数量化后变成整数特征之间的微小差异可能就会被吞掉。在我实测中量化后的Facenet模型在LFW数据集上的准确率从98.8%降到了97.2%掉了约1.6个百分点。对于实际门禁场景来说这个精度勉强够用但如果你对精度要求很高建议尝试以下方案量化数据集尽量大。我一开始dataset.txt只放了20张图精度掉到95%以下后面扩充到200张恢复了不少精度。使用近期校准RKNN-Toolkit2的build接口中提供了quantized_method参数可以指定按通道量化还是按层量化。实测按通道量化能提升约0.5个百分点。混合量化对某些敏感层不量化其他层量化。这个RKNN-Toolkit2的进阶功能需要先打开一个hybrid_quantization的配置然后指定不想量化的算子名称。精度验证的流程也交代一下。转换完成后先用RKNN-Toolkit2的PC仿真模式做初步精度测试import numpy as np from rknn.api import RKNN rknn RKNN() rknn.load_rknn(./facenet.rknn) ret rknn.init_runtime(targetNone) # None表示PC仿真 # 读取测试图像并预处理 img cv2.imread(test_face.jpg) img cv2.cvtColor(img, cv2.COLOR_BGR2RGB) img cv2.resize(img, (160, 160)) img img.astype(np.float32) img (img - 127.5) / 127.5 img np.expand_dims(img, axis0) # 推理 outputs rknn.inference(inputs[img]) emb outputs[0][0] # 512维特征向量 print(emb[:10]) rknn.release()但注意PC仿真模式用的是x86平台的模拟器所以只能验证推理流程是否正确不能完全代表在RK3588的真机性能。真机推理的性能数据后面的实战章节会给出具体数字。4. 板端部署与推理实现4.1 板端RKNN Runtime API调用模型转换完成只是第一步真正的考验是把模型在板子上跑起来。板端推理用到的是RKNN Runtime的C语言或Python API。考虑到整个项目还涉及到摄像头、图像处理我最终选择的方案是Python API做整体控制C API只留给性能瓶颈部分如果有的话。先在板子上安装RKNN Runtime的Python包pip install rknn-runtime1.6.0板端推理核心代码如下import numpy as np from rknnlite.api import RKNNLite # 使用Lite接口只做推理不带模型转换功能 rknn RKNNLite() ret rknn.load_rknn(./facenet.rknn) if ret ! 0: print(RKNN模型加载失败) exit(-1) ret rknn.init_runtime(core_maskRKNNLite.NPU_CORE_0) if ret ! 0: print(NPU初始化失败) exit(-1) # 推理 emb rknn.inference(inputs[preprocessed_img])[0][0] print(提取到%d维特征向量 % len(emb)) rknn.release()RKNNLite和RKNN不同它是专门为端侧推理设计的轻量接口不包含模型转换能力直接加载RKNN文件进行推理。core_mask参数可以指定使用哪个NPU核心RK3588有三个NPU核心这里NPU_CORE_0表示只用一个。如果你用的板子内存比较紧张可以在初始化后设置NPU的调度策略比如rknn.config(enable_cpu_memTrue, core_maskRKNNLite.NPU_CORE_0_AUTO)。4.2 人脸检测与对齐预处理Facenet本身只做特征提取不做人脸检测和对齐。在实际部署中我们还需要一个人脸检测器来定位人脸位置然后裁剪、缩放、对齐后才能送入Facenet。现在比较流行的方案是使用RetinaFace或者SCRFD检测人脸检测出5个关键点两只眼睛、鼻子、两个嘴角再根据关键点做仿射变换对齐到标准位置。这里我不打算从头讲怎么训练一个人脸检测器而是直接给出最常见的操作流程使用OpenCV或MTCNN轻量方案从摄像头帧中检测人脸区域。提取人脸关键点坐标。根据关键点计算仿射变换矩阵将人脸区域对齐到160x160。归一化到[-1,1]送入Facenet。对齐这一步特别关键。很多新手直接裁剪人脸区域就送进去推理效果非常差。因为Facenet是在对齐好的数据上训练的人脸的位置、角度、尺度必须和训练集分布一致特征向量才有意义。对齐的代码一般长这样import cv2 import numpy as np def align_face(image, landmarks, output_size(160, 160)): # 标准参考关键点坐标 ref_landmarks np.array([ [30.2946, 51.6963], [65.5318, 51.5014], [48.0252, 71.7366], [33.5493, 92.3655], [62.7299, 92.2041] ], dtypenp.float32) # 根据实际关键点和参考关键点计算仿射变换矩阵 transform cv2.estimateAffinePartial2D(landmarks, ref_landmarks, methodcv2.LMEDS) # 应用变换 aligned cv2.warpAffine(image, transform[0], output_size) return aligned这段代码里的ref_landmarks是标准人脸对齐坐标来自DeepFace等相关开源项目的经验值。实际使用中如果检测到的关键点不够准可以考虑用更稳定的检测器但基本的对齐原理是不变的。4.3 特征提取与人脸比对模型推理输出的512维特征向量接下来要进行人脸比对。比对方法通常有两种欧氏距离和余弦相似度。Facenet在训练时用的是三元组损失目标是让同一个人的特征向量欧氏距离近不同人的远。因此欧氏距离是更直接的选择。如果数据库里有N个人的特征向量每张人脸比对就是一个1x512向量和Nx512矩阵求距离的过程def find_match(embedding, db_embeddings, db_names, threshold0.8): # 计算欧氏距离 diffs np.linalg.norm(db_embeddings - embedding, axis1) # 找到距离最小的那个人 best_idx np.argmin(diffs) if diffs[best_idx] threshold: return db_names[best_idx], diffs[best_idx] return None, diffs[best_idx]阈值的选择是系统工程。阈值设小了容易把同一个人识别为陌生人误拒率高设大了容易把不同的人识别为同一个人误纳率高。根据我实测Facenet在RK3588上INT8量化后同一个人的特征距离一般在0.4-0.6之间不同人的距离通常在1.0以上。因此阈值设在0.8左右比较合理。但建议你还是在自己的数据集上重新统计距离分布后再确定阈值不要直接照搬别人的数字。4.4 实战性能测评完成上述编码后我们在实际板子上进行一次完整测试。测试条件RK3588开发板Ubuntu 20.04单核NPUCPU功耗未做额外限制输入分辨率为160x160 RGB图像。测试结果如下表环节平均耗时备注摄像头取帧 人脸检测18.2msMTCNN检测160x160区域人脸对齐仿射变换0.3msOpenCV warpAffineFacenet NPU推理31.5msINT8量化单核NPU特征比对1v10000.1ms1000人库的欧氏距离计算总耗时约51ms约20 FPS如果用上全部三个NPU核心同时做多路推理吞吐量还能提升。实测三核并行处理3个不同人的推理请求总耗时没有增加太多单路推理延迟约35ms但整体吞吐率接近3倍。所以做高并发人脸识别服务的话多核调度是很有效的优化方式。另一个经验是如果你觉得31.5ms的推理延迟还不够快可以考虑更换更轻量级的骨干网络比如MobileFaceNet。相比Inception ResNet v1MobileFaceNet在RK3588上单次推理能做到8ms左右精度在LFW上也有99%以上。不过本文重点还是完整走通Facenet的流程MobileFaceNet的转换和部署过程大同小异。5. 常见问题与排查技巧实录5.1 模型转换阶段的高频报错报错1Load ONNX model failed这个是最常见的原因通常是ONNX模型里包含RKNN-Toolkit2不支持的算子。解决思路是先用onnxsim简化模型如果还不行就要手动修改ONNX模型把不支持的算子替换掉。比如一些模型里的Resize算子版本太高可以在导出时指定opset_version11来规避。从我个人经验看90%以上的模型加载失败都能通过降低opset版本onnxsim解决。报错2AttributeError: NoneType object has no attribute xxxx这个报错一般是模型输入没有正确指定或者输入数据格式不对。在rknn.config()中检查input_size_list是否和实际输入维度一致。Facenet的输入是(1, 3, 160, 160)而不可以是(3, 160, 160)。报错3量化时内存溢出RKNN-Toolkit2在进行量化校准的时候需要加载模型的所有激活值如果你的数据集太大内存可能不够。我们曾经在VMware虚拟机里跑默认2G内存根本不够经常直接崩掉。解决办法是把虚拟机内存调到8G以上或者用物理机操作。5.2 板端推理时常见问题问题1rknn_init fail, ret -1这个大多数情况是RKNN Runtime和RKNN-Toolkit2版本不匹配导致的。记住我之前说的版本必须严格配对。另外确认一下板子上的librknnrt.so权限是否正确有些系统环境会导致权限问题sudo chmod 777 /usr/librknnrt.so问题2推理结果全为NaN这个问题很隐蔽通常不是模型问题而是输入数据有问题。检查输入图像是否为空检查预处理时像素值是否溢出。RKNN的inference接口默认情况下期望输入是uint8或float32类型且值域正确。我用PyTorch时习惯输出float32的[-1,1]但RKNN的预处理配置里已经做了(x-127.5)/127.5所以你喂给模型的数据就应该是[0,255]的int8或float32。这个搞错的话特征向量全乱套。问题3精度还可以但单张推理花了200ms如果你确认模型已经量化成功但推理速度依然很慢先检查NPU是否真的在干活。可以看CPU占用率如果推理时CPU占用率飙高说明模型里的某些算子没有完全迁移到NPU上又退回CPU执行了。这种情况要检查模型转换时是不是有层不兼容被降级到CPU执行了。可以通过打印RKNN转换日志来确认是否出现了CPU字样。5.3 独家避坑技巧预处理对齐这是最重要的。很多同学在PC上跑PyTorch时用的是(img - 127.5)/127.5但到板子上直接用了/255导致特征向量完全对不上。我建议不管在哪个环节都用同一套预处理代码并且在测试阶段用同一张图对比PyTorch输出和RKNN输出的特征向量先确保特征一致再谈后续。先把单张测试跑通再加摄像头。摄像头流是实时数据干扰因素太多。先用一张静态图片把整条链路跑通确认特征提取无误后再把摄像头加进来。每次只改一个变量这样出问题才好排查。不要迷信PC仿真。我在第3章提过PC仿真结果和板端真机推理有差异。比如某个模型在PC仿真时精度很高但放到板子上掉点严重这种情况多半是量化时某些层在校准数据上没有遇到合适的数据分布。别在PC上纠结太久尽早部署到板子上做真机测试才是正解。保存中间结果。在编写板端代码时可以把对齐后的人脸图保存下来发回PC和PyTorch的输入对比看看是不是同一张图、同样的预处理。这能节约大量调试时间。6. 一些额外想补充的经验这个项目从开始动手到最后稳定跑通我前前后后大概用了两周时间。回头看大部分时间都花在了各种莫名其妙的错误排查上真正写代码的时间其实很少。所以如果我再做一次这类部署一定会先把官方文档里兼容性表格、版本配套关系仔细看一遍再参考社区里同类项目的成功经验然后再动手。还有一个小建议RK3588的NPU虽然有6 TOPS算力但实际部署时优化空间还是很大的。如果你的目标是高并发的人脸识别服务可以考虑把模型输出层改变设计、或者用多进程加NPU多核调度这样可以榨干板子的能力。同时也要注意散热跑满三个NPU核心时芯片温度会迅速升高建议加装散热风扇不然高温降频会导致推理延迟上升。目前这个项目我已经在门禁场景实跑了三个多月稳定性很好夜间红外条件下的人脸识别成功率也很理想。在RK3588上部署模型的整个思路不仅仅适用于Facenet你换成其他模型也可以复用。如果这个过程中还有什么问题欢迎在评论区和我交流我会把我知道的细节一五一十分享出来。