
先说明一下实际操作的背景这玩意儿很多人第一步就卡在环境上然后第二步卡在模型转换第三步卡在量化最后一步卡在板端运行每一步都有莫名其妙的坑。我自己在RK3588上折腾FaceNet部署前前后后花了三四天时间才跑通网上资料零散官方文档写得也不算友好。这篇把整个流程从零开始走一遍尽量做到你照着敲命令就能跑起来。1. 为什么选RK3588跑FaceNet硬件底子和方案选型先说清楚一个核心问题人脸识别这件事在边缘设备上做和在服务器上做完全是两个逻辑。服务器上你随便上GPU、上PyTorch甚至直接用开源的DeepFace库调预训练模型就行但到了RK3588这种嵌入式平台就得考虑NPU怎么用、模型怎么量化、内存带宽够不够、实时性能不能满足。这也是我这次选RK3588的原因——它内置的NPU算力有6 TOPSINT8配合四核Cortex-A76加四核Cortex-A55的CPU架构做实时人脸识别是够用的而且功耗比x86那一套低太多。再明确一下FaceNet在这条链路里的位置。FaceNet解决的是人脸特征提取这一步输入一张对齐好的人脸图像通常是160x160或112x112输出一个128维或者512维的特征向量。有了这个向量之后你可以拿它做1:1比对人脸验证、也可以做1:N检索人脸识别/搜索甚至做聚类。它和检测模型比如MTCNN、RetinaFace是两个独立模块检测负责从画面里框出人脸FaceNet负责把框出来的人脸变成向量。FaceNet在网络结构上有很多变体常见的是Inception-ResNet v1或MobileFaceNet。前者精度高但参数量大后者轻量、更适合边缘部署。我这次用的是基于Inception-ResNet v1的版本输出512维特征量化成INT8之后在RK3588上跑大约20毫秒左右一张完全能满足实时性要求。如果你要追求更高帧率MobileFaceNet可以压到10毫秒以内但很多参考实现需要自己调整对齐。另外人脸识别不能只看提取网络的耗时整个pipeline还包含人脸检测、对齐关键点仿射变换、特征归一化和比对这些都要算进去。还有一个细节FaceNet原始论文使用的是Triplet Loss模型的输出特征是经过L2归一化的所以最后比对时用余弦相似度或欧氏距离都行。在部署到RK3588上的时候这个L2归一化一定要保留不然后端比对的距离阈值全部失效。这一点在我后面踩坑的时候会细说。2. 开发机环境搭建RKNN-Toolkit2的版本坑和虚拟环境隔离部署的第一步是准备一个宿主环境用来跑模型转换工具这个环境一般是x86的PC不需要是RK3588板子。你用Windows也可以但强烈建议用Ubuntu 20.04或22.04因为后续很多依赖尤其是onnx、torch、opencv在Linux上装起来更顺而且转模型的过程本身要跑模拟器Linux下的性能更好。RKNN-Toolkit2的安装过程有几个容易踩的坑我一个个说。2.1 Python版本与虚拟环境RKNN-Toolkit2官方支持Python 3.6到3.12但不同小版本之间兼容性有差异。我实测下来最稳的是Python 3.8或3.10配合Ubuntu 20.04/22.04。我个人推荐Python 3.8 Ubuntu 20.04的组合因为后续很多模型转换过程中需要调用TensorFlow或PyTorch的库这两个框架对3.8的支持最成熟。直接用系统的Python环境装RKNN-Toolkit2很容易搞乱依赖而且这工具依赖的包很多numpy、onnx、tensorflow、torch、opencv-python、ruamel.yaml等版本稍微一冲突就是各种奇怪的报错。所以务必先用venv或conda创建一个干净的虚拟环境。# 创建并激活虚拟环境 python3.8 -m venv rknn_env source rknn_env/bin/activate # 升级pip pip install --upgrade pip # 安装RKNN-Toolkit2从GitHub Releases下载对应版本的whl包 # 这里以rknn-toolkit2-1.6.0为例注意版本号要和板端runtime匹配 pip install rknn_toolkit2-1.6.0-cp38-cp38-linux_x86_64.whl # 验证安装 python -c from rknn.api import RKNN; print(rknn.__version__ if hasattr(rknn, __version__) else import ok)RKNN-Toolkit2的whl包从1.4.0之后都是Python 3.6通用格式的不会出现很老的版本只支持py3.6的问题。老版本1.2.0以下的API和现在的差异很大不建议用。如果你在import期间报了GLIBCXX not found之类的错通常是系统gcc版本或libstdc.so.6太老升级一下build-essential就能解决。2.2 板端Runtime库与工具链版本匹配这一步是最容易被忽略的。RKNN-Toolkit2是用来在开发机上完成模型转换和模拟推理的而真正在RK3588板子上跑模型需要在板上安装另一套东西RKNN RuntimeLite Runtime或普通Runtime。这两者的版本必须严格匹配否则在板端加载.rknn模型时会报类似version mismatch或直接运行崩溃。具体怎么装RK3588的板子如果刷的是瑞芯微官方提供的Ubuntu固件一般已经预装了RKNN Runtime但版本未必和你的rknn-toolkit2一致。建议在开发机上查看你的rknn-toolkit2版本然后在板子上执行以下命令确认runtime版本# 在RK3588板子上运行 python3 -c from rknnlite.api import RKNNLite; print(runtime ok) # 或者直接查看安装包版本 pip show rknn-toolkit-lite2 # 如果在虚拟环境中安装过lite版本如果版本不匹配去瑞芯微的GitHub仓库下载对应版本的rknn-toolkit-lite2安装包在板子上pip安装即可。注意板上有个坑板端安装Lite Runtime时不要在rknn-toolkit2的开发机虚拟环境里pip install rknn-toolkit-lite2否则两个RKNN包冲突import的时候会报attribute error。正确做法是开发机只装rknn-toolkit2板子只装rknn-toolkit-lite2。2.3 ONNX与PyTorch的环境准备FaceNet这个模型我这次是先从PyTorch导出的ONNX再用RKNN-Toolkit2去转。所以环境里还需要一套PyTorch、ONNX、onnx-simplifier。这里有个经验ONNX opset版本和RKNN的兼容性直接决定你后续能否转换成功。经过实测FaceNet用到的主要算子是Conv、BatchNorm、Pooling、Concat、Reshape这些其实没有太新的算子opset 11或12都能过。但如果你导出时用了opset 13以上某些版本的RKNN-Toolkit2会在解析时出错所以我统一用opset 12。pip install torch torchvision onnx onnx-simplifier onnxruntime装好之后我们就可以进入模型准备了。3. FaceNet模型导出ONNX固定输入尺寸与算子化简FaceNet的PyTorch实现网上有好多版本我这次用的是开源实现但是做了改造。很多开源实现默认输入是160x160x3输出是512维的特征向量。先把PyTorch模型结构固定下来很重要因为RKNN在转换时不允许动态输入尺寸必须固定batch size和图像尺寸。3.1 修改模型前向输出与输入尺寸在导出之前先要在PyTorch侧把模型的mode设置为eval并且确保输入维度为(1, 3, 160, 160)。有些开源FaceNet实现默认输出的是分类logits如果你用的是带分类头的版本需要手动去掉最后的FC层让它输出embedding向量。import torch import torch.nn as nn from facenet_model import FaceNet # 假设你自己的实现 model FaceNet(num_classes1000, modeeval) # 具体构造参数看你的模型定义 checkpoint torch.load(facenet_inres_v1.pth, map_locationcpu) model.load_state_dict(checkpoint[state_dict]) model.eval() # 包装一个只输出embedding的模块 class EmbeddingModel(nn.Module): def __init__(self, base): super().__init__() self.base base def forward(self, x): return self.base.forward_feature(x) # 假设这个函数直接返回embedding if hasattr(model, forward_feature): embed_model EmbeddingModel(model) else: # 如果没有直接返回embedding的方法手动dump中间层输出 embed_model model # 固定batch1 dummy_input torch.randn(1, 3, 160, 160) torch.onnx.export( embed_model, dummy_input, facenet.onnx, input_names[input], output_names[embedding], opset_version12, do_constant_foldingTrue, dynamic_axesNone # 一定要设置None保持全静态 )这里几个容易踩的坑输出节点必须是embedding不是scores。如果你把分类头也导出进去RKNN转出来的模型推理结果就会是一堆分类概率而不是特征向量。不要使用dynamic_axes。RKNN-Toolkit2对动态轴的支持非常有限尤其FaceNet这种结构用动态轴会导致转出来的RKNN模型在NPU上跑不了或者只能CPU模拟。所以固定batch1如果未来需要多batch直接改静态batch数量重新导出后面我会讲多batch的处理。导出后一定要用onnx-simplifier简化一次。PyTorch导出的ONNX往往包含很多多余的Identity、Shape、Gather算子这些算子虽然在onnxruntime里能跑但RKNN的算子解析对它们不够友好。用简化之后的模型转RKNN的成功率会高很多。python -m onnxsim facenet.onnx facenet_sim.onnx跑完这一步再用onnxruntime跑一遍确认输入输出名称、形状都对。import onnxruntime as ort import numpy as np sess ort.InferenceSession(facenet_sim.onnx) inputs sess.get_inputs() outputs sess.get_outputs() print(inputs[0].name, inputs[0].shape) print(outputs[0].name, outputs[0].shape) # 期望输出类似input (1,3,160,160) embedding (1,512)3.2 检查算子支持情况如果你图省事有可能在转换阶段才暴露算子问题。在这里提前检查一下也好用RKNN-Toolkit2自带的model_detect接口或直接尝试转换就能发现问题。常见的Unsupported op XXX里FaceNet如果实现干净的话大概率不会触发但如果你导出的模型里混入了AdaptiveAvgPool2d的某些变体或者用了torch.nn.functional.normalize且以F.normalize的方式导出可能会多出一些Reduction算子。这种情况建议用nn.functional.normalize导出的格式导出后在onnx里看是L2Normalization算子RKNN是支持的。4. 使用RKNN-Toolkit2转换FaceNet模型配置参数逐项说明转换过程就是写一个Python脚本调用RKNN类完成从ONNX到RKNN的转换、量化、模拟推理。先把脚本贴出来然后逐行解释每个参数的意义。from rknn.api import RKNN # 创建RKNN对象 rknn RKNN(verboseTrue) # 1. 配置参数 ckpt facenet_sim.onnx quant_data quant_dataset_20.txt target rk3588 ret rknn.config( mean_values[[127.5, 127.5, 127.5]], std_values[[127.5, 127.5, 127.5]], target_platformrk3588, quantized_dtypew8a8, quantized_algorithmnormal, quantized_methodlayer, optimization_level2, reorder_channel0 1 2, batch_size1, ) if ret ! 0: print(config fail) exit(1) # 2. 加载ONNX模型 ret rknn.load_onnx(modelckpt, outputs[embedding]) if ret ! 0: print(load_onnx fail) exit(1) # 3. 构建RKNN模型这一步会做量化 ret rknn.build( do_quantizationTrue, datasetquant_dataset_20.txt, pre_compileFalse, ) if ret ! 0: print(build fail) exit(1) # 4. 导出RKNN文件 ret rknn.export_rknn(facenet_rk3588.rknn) if ret ! 0: print(export fail) exit(1) # 可选在开发机上做模拟推理验证输出 rknn.release()4.1 mean_values与std_valuesFaceNet在训练时一般会把图像像素从[0,255]归一化到[-1,1]即像素值先除以255再乘以2再减1等价于mean127.5、std127.5。这个和很多ImageNet分类模型的归一化方式一样。如果你的FaceNet用的是另一套归一化方式比如ImageNet的mean[0.485,0.456,0.406], std[0.229,0.224,0.225]那就要按实际训练时的值填。这一点非常关键因为RKNN的量化过程会在转换成RKNN模型时把归一化一起融合进网络前面的层里如果你填错了输入的图像虽然看起来正常但特征向量完全错乱。4.2 target_platform参数这个参数指定目标NPU平台这里就是rk3588。如果你写错了比如写成rk3399pro或者rk3568生成的模型在RK3588上可能也能运行但NPU的算子调度可能不是最优的甚至某些算子要走CPU fallback性能大打折扣。务必确认你的开发机rknn-toolkit2版本支持npu架构为rk3588。从1.4.0开始才正式支持老版本比如1.3.x压根没有这个目标平台。4.3 量化配置量化是边缘部署的必经之路也是问题最多的地方。quantized_dtypew8a8权重和激活都量化为INT8这是最常用的组合也是NPU上最快的方式。如果你的模型对精度敏感比如特征向量质量直接决定识别率可以改用w8a16即权重INT8、激活FP16精度损失更小但NPU的推理速度会慢一些。对你FaceNet这种512维embedding的场景来说w8a8通常够了关键看你的训练数据和比对阈值怎么调。quantized_algorithmnormal瑞芯微支持normal和kl_divergence两种量化算法。KLDKL散度通常对分布不均匀的层更友好能减少精度损失。但KLD转换时间更长而且某些层会退化成float16进行混合精度反而增加部署复杂度。我实测FaceNet用normal其实就够了建议先用normal试精度不达标再换KLD。quantized_methodlayer按层量化也可以按通道channel。按通道量化精度更好一些但会导致模型体积稍大载入时间变长。人脸识别这种任务对精度敏感建议用channel试试。这里要给新手一个经验量化后的模型和浮点模型在行为上会有差异FaceNet输出的embedding并不是完全相同只是近似。你之前训练或者测试时用的比对阈值比如cosine距离0.7在量化后可能变成0.72或0.68必须重新校准一遍阈值否则会出现大量误识别。4.4 dataset说明和量化图片选择上面脚本里写了一行datasetquant_dataset_20.txt这个文本文件里每一行放一张图片的绝对路径。这些图片用来做什么呢RKNN在做激活值量化时需要统计每一层激活值的数值分布然后选择最优的量化scale和zero point。这个统计过程需要你提供一些有代表性的输入图片。这一步有两个常见错误图片数量太少比如只有3、5张会导致激活值范围估计偏差很大量化后模型效果暴跌。图片内容太单一全是同一张脸特征分布没有代表性量化后的模型在真实场景下泛化很差。我的建议是至少准备20到50张不同的人脸图片或者不限制是人脸只要有照片特征即可内容尽量覆盖不同亮度、不同人种、不同姿态。这个数据集不需要标注就是纯图片路径列表。另外不需要手工把所有图片预先做resizeRKNN的预处理会按模型的输入尺寸自动resize但如果你用了mean_values和std_values要注意图片默认读入方式是从文件路径读取你只需要确保图片内容本身清晰。4.5 转换结果验证构建完成并导出rknn后开发机上可以用rknn的inference接口跑一个模拟推理对比onnxruntime的浮点输出和RKNN量化模型的输出看看特征向量的余弦相似度是否还保持着通常在0.98以上就能用。from rknn.api import RKNN rknn RKNN() rknn.load_rknn(facenet_rk3588.rknn) rknn.init_runtime(targetNone) # targetNone在开发机上跑模拟 import cv2 import numpy as np 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.uint8) outputs rknn.inference(inputs[img]) # 注意这里输入不需要归一化因为mean/std在rknn里配置过了 print(outputs[0].shape)这里有个大坑rknn.inference的输入是原始0-255的uint8图像不需要自己预先做/255或减均值归一化。因为RKNN在build时已经把mean_values和std_values融合到模型前向里了。如果你在喂数据前手动做了归一化比如把图像变成0-1浮点数那相当于做了两遍归一化输出的embedding完全错误。5. RK3588板端推理实现RKNNLite运行流程与踩坑点模型转换完成开发机上模拟推理通过接下来就是板端了。5.1 板端运行环境准备板端RK3588上需要装有RKNN RuntimeLite版我上文已经提过版本必须匹配。如果你是直接刷的官方Ubuntu固件一般自带了runtime在/usr/lib/python3/dist-packages/rknnlite/目录下能看到。但自带的版本通常比较旧对应较早的rknn-toolkit2。如果开发机的rknn-toolkit2是1.6.0而板子runtime是1.4.0加载.rknn模型时会直接报E_RKNN_INVALID_MODEL。推荐做法开发机和板子都升级到相同版本。按我之前的经验rknn-toolkit2的1.6.0版本配合板端rknn-toolkit-lite2的1.6.0版本是稳定的组合。板端安装方法# 将开发机上下载好的rknn_toolkit_lite2-1.6.0-cp38-cp38-linux_aarch64.whl拷贝到板子 pip install rknn_toolkit_lite2-1.6.0-cp38-cp38-linux_aarch64.whl如果你板端系统安装了多个Python版本比如系统自带了3.10你又装了3.8pip安装时要特别注意装到了哪个解释器里。建议直接用一个venv避免污染系统环境。5.2 板端推理代码构建完整pipeline板端推理和开发机的逻辑基本一致用RKNNLite而不是RKNN类。FaceNet板端代码大框架如下from rknnlite.api import RKNNLite import cv2 import numpy as np import time class FaceNetRKNN: def __init__(self, model_path, target_platformrk3588): self.rknn RKNNLite() ret self.rknn.load_rknn(model_path) if ret ! 0: raise RuntimeError(load rknn model failed) ret self.rknn.init_runtime() if ret ! 0: raise RuntimeError(init runtime failed) def preprocess(self, face_img): # face_img是从检测模块截出来的原图BGR img cv2.cvtColor(face_img, cv2.COLOR_BGR2RGB) img cv2.resize(img, (160, 160)) img np.expand_dims(img, 0).astype(np.uint8) return img def get_embedding(self, face_img): input_data self.preprocess(face_img) outputs self.rknn.inference(inputs[input_data]) embedding outputs[0].flatten() # L2归一化 norm np.linalg.norm(embedding) if norm 0: embedding embedding / norm return embedding def release(self): self.rknn.release() # 使用示例 if __name__ __main__: facenet FaceNetRKNN(facenet_rk3588.rknn) img cv2.imread(face1.jpg) emb1 facenet.get_embedding(img) img2 cv2.imread(face2.jpg) emb2 facenet.get_embedding(img2) def cosine_similarity(a, b): return np.dot(a, b) / (np.linalg.norm(a) * np.linalg.norm(b)) sim cosine_similarity(emb1, emb2) print(cosine similarity:, sim) facenet.release()这段代码里有几个值得说明的点preprocess中只做了cvtColor和resize没有做归一化原因上文说了。代码中对embedding做了L2归一化。虽然FaceNet训练时输出就是L2归一化的但量化后可能稍有偏移再做一次更稳。在RKNNLite的inference里inputs是一个list里面每个元素是一个numpy数组。如果输入shape是(1,160,160,3)还是(1,3,160,160)取决于rknn.config里是否设置了reorder_channel。一般RKNN-Toolkit2默认接受的NHWC格式即height,width,channel但是FaceNet的onnx是NCHW格式。如果你在load_onnx时没有指定reorder_channel0 1 2RKNN会自动把NCHW转成NHWC那inference时喂入的数据就要是NHWC也就是(1,160,160,3)。代码中opencv读图得到HWC加一个batch维度就是NHWC直接喂即可。5.3 init_runtime的常用参数RKNNLite的init_runtime有这几个参数需要关注targetNone在板端运行时一般不需要手动指定target用了默认的NPU设备。如果你有多个NPU设备或希望指定NPU和CPU分配可以用target参数指定一下但正常人用不到。perf_debugFalse如果设为True可以拿到每一层的推理耗时方便排查性能瓶颈。发布时记得关掉否则有额外的性能开销。asyncioFalse这个是异步推理的开关。如果你做的是视频流实时识别后面可以考虑开异步但级异步推理通常不适合FaceNet这种单输入单输出的场景多路并发可以用python多线程配合RKNNLite的多实例。板端推理还有一个坑就是单个RKNNLite实例是否线程安全。实测下来同一个RKNNLite实例并发调用inference是不安全的往往导致未定义行为或直接崩溃。正确的并发方式是每个线程单独创建RKNNLite实例加载同一个.rknn文件每个线程各跑各的。RK3588的NPU支持多实例并发所以这样做不会有问题。6. 性能优化与调优从模型结构到运行参数如果只是把模型跑通那这篇教程也就到头了。但真实项目中性能往往才是你关心的核心毕竟是边缘设备。RK3588上的NPU虽然提供了6 TOPS算力但能不能用满取决于你的代码和模型。这里我分享几个实测中效果明显的优化手段。6.1 端到端pipeline拆分与耗时统计先说一个整体观念FaceNet只负责特征提取但你最终的人脸识别应用里从摄像头取帧到返回识别结果中间通常还隔着人脸检测和对齐。我建议在最初阶段就把每个模块的独立耗时摸清楚然后用profile工具去查瓶颈。我在RK3588上顺手用MTCNN做检测整个pipeline耗时分布大致如下模块耗时MTCNN检测P-Net R-Net O-Net约120msFaceNet特征提取INT8量化后约20msL2归一化与比对不到1ms从这张表能看出检测模块反而是短板FaceNet自身很快。如果你要做实时视频流优先得优化检测。比如换用轻量级检测模型RetinaFace-Mobile或YuNet可以把检测耗时压到30ms以内。很多新手只盯着FaceNet优化忽略了整体链路最后帧率还是上不去。6.2 RKCNN与NPU调度减少CPU和NPU的拷贝如果特征提取模块的20ms让你不满意可以从两个方向入手。第一尽可能减少预处理开销。cv2.cvtColor本身在CPU上跑是很快的但如果图像分辨率是1080p把整帧做cvtColor和resize再送进NPUCPU耗时可能达到5-8ms。更好的方式是在检测模块中直接截取人脸区域几百像素的ROI再对这个小图做预处理避免整帧缩放。第二检查是否走了CPU fallback。RKNN Runtime有一个debug开关可以打印出模型的算子使用情况。如果不小心包含了不支持的算子NPU会回退到CPU速度会一落千丈。你可以跑一下推理然后统计推理耗时如果超过100ms大概率是有算子fallback了。# 在init_runtime时打开perf_debug然后跑一次inference rknn_lite RKNNLite() rknn_lite.load_rknn(facenet_rk3588.rknn) rknn_lite.init_runtime(perf_debugTrue) # 然后运行一次inference outputs rknn_lite.inference(inputs[input_data])跑完之后调用rknn_lite.eval_perf()就能看到每个算子的耗时和使用的核心NPU还是CPU。6.3 batch输入与多线程并发如果你的业务是需要同时对多个人脸做特征提取比如画面里有多个人一次性把多张人脸拼成一个大batch送进去通常比循环单张要快。在RKNN里input batch size是静态的你在PyTorch导出ONNX时就要决定batch是多少。如果你不确定最大并发数建议导出batch4或batch8然后用固定大小的batch填充不足的可以重复填充一张图然后丢弃多余结果。比如导出batch4的模型后单次推理处理4张人脸整体耗时大约35ms比单张循环4次4*20ms80ms快了一倍多。原理是NPU在计算时可以有效利用并行算力batch1时的算子调度开销反而大。多线程并发也是一条路。RK3588的NPU支持同时跑多个模型实例建议为每个线程创建一个RKNNLite实例实测4个线程同时推理FaceNet每个线程平均延迟只增加5-8ms总吞吐量大幅提升。6.4 将预处理合并进模型如果你用的是ONNX格式导入还有一个trick在PyTorch导出的模型前面补上resize层和通道变换层用Interpolate Permute实现这样模型输入直接就是来自摄像头的原始BGR帧省掉了cvtColor和resize。不过我在实际使用中并不推荐这么做因为模型里的Resize算子虽然RKNN支持但它走NPU的耗时反而可能比CPU手写resize还高而且整帧数据进入NPU会占用更多带宽。你只要把ROI区域取小图就够了。7. 常见报错对照表与调试经验部署过程中遇到各种报错是必然的下面把这些坑集中列出来按排查顺序做一个对照表我实际踩过的都标注了。报错或现象可能原因解决办法E RKNN: Load rknn model failed at ...开发机与板端rknn版本不匹配升级或降级使两端版本一致特别是runtime和toolkit2版本保持一致E RKNN: output shape not match转换时outputs名称填错或者模型输出节点名不对在onnx里查看输出名用脚本里的outputs[...]明确指定inference输出全是0或NaN输入归一化重复或者preprocess时做了resize但未转为RGB保证只用opencv读取BGR且resize后直接feed uint8不做mean/std处理颜色通道顺序也要一致推理速度极慢200ms有算子走了CPU fallbackeval_perf查看各算子耗时定位fallback层用onnx-simplifier化简或改网络实现MTCNN检测出人脸但FaceNet识别率极低检测框未对齐或者训练图像与实际输入分辨率差异过大确保送入FaceNet前对人脸做了关键点对齐5点仿射变换不要直接把原始检测框resize成160x160板端模拟器通过但NPU推理失败target_platform没设为rk3588或runtime不支持该目标用rknn.config中target_platformrk3588重新构建确认板端runtime支持RK3588pip install rknn-toolkit-lite2时与现有包冲突开发机上混装toolkit2和lite2用venv隔离开发机只装toolkit2板子只装lite2import rknn时报GLIBCXX_3.4.29 not found系统gcc版本过旧安装新版gcc和libstdc或换Ubuntu20.04以上系统cv2.cvtColor耗时高整帧做颜色转换只对ROI区域做cvtColor和resize不做全图处理在这里想重点聊聊对齐问题。很多人都忽略了FaceNet在训练时用的数据比如LFW、VGGFace2都是经过人脸对齐的也就是人脸的关键点眼睛、鼻子、嘴角被规范到一个固定的位置然后才裁剪成160x160。如果你的检测框只是把MTCNN的输出直接塞进去没有做仿射变换对齐识别的效果会大打折扣。这不是RKNN的问题而是整个人脸识别pipeline的问题但往往被归结为量化导致精度下降。所以如果你在板端测试时发现识别率突然暴跌先别急着调量化参数先检查对齐代码是否在板端版本中被移除了。8. 量化精度校准让INT8模型的识别率回到正轨FaceNet这类度量学习模型对量化的敏感度比一般分类模型要高因为它的最终输出是距离度量很小的数值偏差经过L2归一化后可能影响阈值判断。我在实测中发现如果量化校准数据集选得不好模型输出的特征向量和浮点模型的余弦相似度只能到0.90左右在人脸识别上已经能明显感觉到误识别增多。如果校准数据集选得足够好余弦相似度可以到0.98以上。如果你遇到量化后效果明显下降的情况按这个顺序排查校准集数量够不够至少20张以上最好覆盖不同人脸、不同光照。校准集和处理pipeline是否一致校准集的图片最好也是经过检测对齐得到的160x160人脸而不是随便裁剪的风景图。如果量化统计的数据分布和你实际推理时输入的数据分布不一致量化误差会被放大。尝试混精度量化如果层量化quantized_methodlayer效果不理想试试通道量化channel精度更高。敏感层保留浮点RKNN-Toolkit2的量化配置支持指定某些层不量化你可以通过分析每个层量化前后的输出差异找出敏感层然后设置custom_quantize_layers参数把这一层保留为FP16。但这需要你熟悉模型结构属于进阶调优。量化精度调试往往是整个部署过程中最耗时的一环。不要指望一次就能调到最优准备一个包含5-10对同一人/不同人的测试图片集在每次修改量化策略后都跑一遍记录识别准确率和特征余弦相似度用数据说话而不是靠感觉。9. 端侧完整项目工程结构建议教程最后给一个能够直接落地的工程目录结构参考。真实项目中你不会只有一个人脸识别模型通常还会搭配检测模型、数据库和通信模块。下面这个结构是我在实际项目中使用的直接抄也可以rk3588_facenet_demo/ ├── detection/ │ ├── mtcnn_rknn.py # MTCNN在RKNN上的推理封装 │ └── align_trans.py # 5点关键点仿射变换对齐 ├── facenet/ │ ├── facenet_rknn.py # FaceNet的RKNN封装 │ └── facenet_rk3588.rknn # 转换好的量化模型 ├── utils/ │ ├── preprocess.py # 图像预处理工具 │ ├── postprocess.py # 特征比对、阈值判断 │ └── config.py # 全局配置模型路径、阈值、camera ID ├── database/ │ ├── face_db.py # 特征数据库管理注册/删除 │ └── features/ # 存npy特征文件 ├── main.py # 摄像头实时识别主程序 └── README.md这个结构的好处是检测、对齐、特征提取、比对、数据库完全解耦方便替换任何一部分比如日后把MTCNN换成RetinaFace。在工程实现上有个比较反直觉的经验人脸特征库不建议直接存图片而是注册时提取embedding并存储为npy文件。在识别阶段每帧画面检测到人脸后提取embedding再用向量检索的方式搜索特征库比对距离最小且小于阈值的就认定为同一人。如果特征库人数少于一两千直接暴力遍历比对就够快了如果超过这个量级再考虑faiss这类向量检索引擎把index文件放到板端CPU上用faiss-CPU版本差不多也能在几毫秒内完成检索。对比阈值的选择也值得说明一下。FaceNet训练时一般用欧氏距离或余弦距离但阈值不是模型给定的需要你在你的数据上实际标定。建议在板端部署完成后采集一批你自己设备的真实图像因为摄像头的镜头、视角、光线和你训练数据差异可能很大统计同一人的类内距离和不同人的类间距离分布选一个让等错误率最低的阈值。这个阈值可以写入utils/config.py中方便调试时修改。另外摄像头选择这块也顺便提一嘴RK3588的ISP在接入USB摄像头时能自动优化曝光和降噪但人脸识别对动态模糊很敏感如果预算允许优先用全局快门global shutter摄像头。默认的卷帘快门在夜间灯光下容易导致运动模糊严重拖累人脸对齐和特征提取。10. 从模型到业务的延伸注册、识别与活体检测如果你只是做Demo跑到上面的代码基本就可以收工了。但如果要做业务级的人脸识别系统还有两个事情必须提前规划人脸注册流程和活体检测。人脸注册一般是在受控环境下拍一张或多张人脸图片不同角度走同样的预处理和特征提取流程把得到的embedding存入数据库。注册照片的质量直接决定后续识别效果所以在前端UI需要做质量检测模糊度、人脸大小、姿态角度不达标的直接不让注册。我在RK3588上是用MTCNN的ONet输出的人脸关键点和置信度做质量判断的逻辑简单但很有效。活体检测则是在人脸识别越来越普遍的当下绕不开的话题。大部分嵌入式人脸识别方案目前依赖静默活体silent liveness即通过摄像头采集正常画面用一个小模型判断画面中的人脸是不是真实的人脸而不是照片或屏幕视频。这个模型也可以部署在RK3588上算力消耗很小大约2-3ms。如果你做的是门禁、支付这类高安全场景建议从一开始就把活体检测模块纳入架构而不是等识别调通之后再外挂。否则到后期你会发现识别功能和活体功能的数据流、并发控制、模块通信全都纠缠在一起改起来很痛苦。我自己在做这个项目时实际产线里还遇到过一种情况识别阈值定得很理想但现场光线变化导致误识别突然增多。最后发现是曝光补偿导致图像的亮度分布变化进而引起特征向量的分布偏移。解决方式不是调阈值而是引入简单的直方图均衡化预处理。这些都是在实验室里不会预见、但真实部署中非常常见的工程问题。回到最初的问题通过这个保姆级教程你应该已经能在RK3588上把Facenet从PyTorch一路部署到NPU上跑起来了。整个过程概括下来就是开发机装好匹配版本的rknn-toolkit2模型导出ONNX并简化配置量化参数并生成rknn模型板端用rknnlite接入pipeline最后调优和校准。最大的几个坑分别是版本匹配、输入归一化、数据排布和对齐预处理——按这篇文章的顺序走每一步都确认无误剩下的就是细水长流的调阈值、调性能了。