ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

RK3588部署FaceNet完整指南:PyTorch转RKNN的踩坑与优化

2026/10/3 3:34:44 拓冰建站 浏览量
RK3588部署FaceNet完整指南:PyTorch转RKNN的踩坑与优化 去年年中接了一个边缘设备上做人脸识别的项目老板指定要用 RK3588模型用 FaceNet。说实话当时脑子里第一个念头是“这不就是装个环境导个模型跑个推理吗”真正动手之后才发现从 PyTorch 到 RKNN 这条链路上到处都是坑而且每个坑都长得不一样有算子兼容的、有归一化配置的、有量化精度塌方的还有板子端版本对不上导致模型加载失败的。这篇文章就是把我在 RK3588 上从零部署 FaceNet 的完整过程记录下来。FaceNet 的核心思想是“把一张人脸映射到一个向量空间同类人脸的向量距离近不同人脸的距离远”部署时最终落到板子上就是两件事一个能跑出 128 维或 512 维取决于你用的网络结构embedding 的推理程序和一个能算相似度、做比对的后续链路。这篇文章适合正在做边缘端人脸识别、准备把 PyTorch 模型往 Rockchip NPU 上迁移的开发者参考尤其是第一次碰 RKNN 工具链的人。1. 动手之前先理清楚模型结构、部署流程图和 RK3588 的算力边界1.1 确认你手里的是哪种 FaceNetFaceNet 在 PyTorch 生态里最常用的开源实现是timesler/facenet-pytorch它用的是 Inception ResNet V1 作为 backbone输出维度默认是 512也可以用embedding_size128重新定义。我这边用的就是官方预训练权重的 512 维版本因为下游比对逻辑是复用一套现成的向量检索服务512 维对检索库来说压力不大就没动结构。这里提醒一句别只看名字叫 FaceNet 就直接拿来部署先确认三件事。输入尺寸是多少facenet-pytorch 默认是 160x160有些仓库是 112x112比如基于 mobilefacenet 的。预处理方式是什么常见有两种(x / 255 - 0.5) / 0.5和(x - 127.5) / 128这两种在 RKNN 里对应的mean_values和std_values参数配置完全不同。检测和对齐用的是哪套FaceNet 本身不检测人脸只负责特征提取前面必须接 MTCNN 或 RetinaFace 做人脸框检测和关键点对齐。这一部分在 RK3588 上也要一起部署。1.2 部署流程图从 PyTorch 到 RKNN 要走五步整个链路在动手前一定要画清楚在 x86 主机上准备一个干净的 Python 环境装好 PyTorch、onnx、onnxruntime、rknn-toolkit2。把训练好的 FaceNet PyTorch 权重导出为 ONNX。用 onnx-simplifier 对导出的 ONNX 做算子化简并用 onnxruntime 验证输出一致性。用 rknn-toolkit2 把 ONNX 转为 RKNN 格式这一步可以选择是否量化。把生成的.rknn文件部署到 RK3588 板子上通过 RKNN Runtime 调用 NPU 推理完成 embedding 提取。这五步里面第 1 步看似废话实际上最容易出问题的是版本兼容。第 4 步是坑最多的地方第 5 步则是“看起来跑通了但结果就是不对”的高发区。1.3 RK3588 的 NPU 到底适合跑什么RK3588 内置的 NPU 标称算力是 6 TOPSINT8支持 INT4、INT8、INT16、FP16 混合精度这个数字在边缘设备里属于中上水平。实测对 160x160 输入的 Inception ResNet V1 来说纯 NPU 推理单帧耗时大概在 10 到 20 毫秒这个量级具体取决于你的量化方式和主线频率。如果你用 FP16耗时会比 INT8 高一些如果你在板子上用 CPU 跑同一个模型耗时可能要 400 到 500 毫秒。所以结论很直接FaceNet 这种计算量不小的模型在 RK3588 上一定要上 NPU而 NPU 要走通就逃不掉 RKNN 工具链和量化这两个绕不开的坎。提示不要在脑子里默认“NPU 什么算子都能跑”Rockchip 的 NPU 对算子是有一套支持列表的。FaceNet 里面的某些结构比如 Inception ResNet V1 的某些 block可能会在转换时碰到算子映射问题后面章节我会具体展开。2. 环境搭建中最容易翻车的版本对齐问题2.1 x86 主机上安装 rknn-toolkit2rknn-toolkit2 是 Rockchip 提供的模型转换、仿真、量化工具跑在你的开发机上不是跑在板子上。安装方式很简单用 conda 创建一个 Python 3.8 或 3.10 的环境然后pip install rknn-toolkit2-x.x.x-cp38-cp38-linux_x86_64.whl。但这一步有几个暗坑Python 版本必须对得上包名。rknn-toolkit2 的 wheel 包是按 Python 版本发布的cp38 的包装在 3.8 环境cp310 就装在 3.10别用 3.11 强行装大概率依赖冲突。宿主机依赖rknn-toolkit2 依赖onnx1.8.1或相近版本如果你环境里装了一个很新的 onnx 1.14装 rknn-toolkit2 可能会因依赖冲突直接失败。我后来是单独建了一个 conda 环境专门给 RKNN 用不和其他项目混。转换工具版本和板子端 Runtime 版本必须对齐。这是我踩了很久才意识到的问题如果你用 rknn-toolkit2 1.6.0 转换出来的模型拿到板子上的 RKNN Runtime 1.5.0 去加载可能报错也可能不报错但行为异常。我的建议是直接查对应版本的 release note确保 toolkit 和 lite runtime 版本一致。我最终用的组合是rknn-toolkit2 1.6.0 Python 3.8 Ubuntu 20.04 开发机板子上用配套的rknn-toolkit2-litePython 包。2.2 板端环境的准备RK3588 板子上我用的是一块基于 RK3588 的 ARM 开发板烧录 Ubuntu 系统后需要确认两件事系统里是否已经有/usr/lib/librknnmrt.so这就是 NPU 的 runtime 库。/usr/share/rknn-toolkit-lite目录下是否有对应版本的 Python wheel 包。如果没有需要从 Rockchip 官方资料库里下载对应版本的rknn-toolkit2-lite并安装。安装后可以用一段最简单的代码验证板端 NPU 是否可用from rknnlite.api import RKNNLite rknn_lite RKNNLite() ret rknn_lite.load_rknn(/path/to/model.rknn) ret rknn_lite.init_runtime() print(NPU runtime initialized:, ret)这一步如果把整条链路比作盖房子就是打地基。地基没打好后面什么模型转换、精度验证都无从谈起。2.3 为什么不建议在板子上直接做转换有些人图省事直接在 RK3588 上跑 rknn-toolkit2 的完整版做模型转换实际上并不推荐。完整版 rknn-toolkit2 在板子上跑一是依赖库很多容易污染系统环境二是模型量化过程需要跑大量校准数据在板子上跑效率很低。正确的操作是在 x86 主机上转换、量化、生成 .rknn 文件板子上只安装轻量的 rknn-toolkit2-lite 做加载和推理。3. PyTorch 导出 ONNX 时的关键操作和常见坑3.1 导出前先冻结模型结构FaceNet 这类模型在 PyTorch 里通常是以nn.Module形式存在的。导出 ONNX 之前一定要把模型切到 eval 模式并且把model.classify这类分类头关掉。faceNet-pytorch 仓库里的模型有一个classify参数如果为 True输出的就是分类 logits 而不是 embedding导出时一定要确保这个开关是 Falseimport torch from facenet_pytorch import InceptionResnetV1 model InceptionResnetV1(pretrainedvggface2, classifyFalse) model.eval() dummy_input torch.randn(1, 3, 160, 160) torch.onnx.export( model, dummy_input, facenet.onnx, opset_version11, input_names[input], output_names[output], dynamic_axesNone ) print(ONNX exported.)这里有几个参数需要说明opset_version建议用 11rknn-toolkit2 对 opset 11 的支持最成熟。用太高的 opset比如 17、18可能会导致转换时出现未知算子。dynamic_axes我特意设为 None。虽然动态尺寸很诱人但在 RKNPU 上动态 shape 支持有限而且推理速度可能反而变慢。既然 FaceNet 的输入尺寸在设计时就固定为 160x160那就老老实实静态导出。dummy_input的 batch size 设为 1。RKNN 转换时如果你用 batch 1 导出后面推理时也最好用 batch 1不要想着批量推理NPU 上 batch 推理的效率提升不一定明显还会增加内存占用。3.2 用 onnx-simplifier 化简提前拆掉容易出问题的算子PyTorch 导出的 ONNX 往往带有很多冗余的 shape 操作、恒等映射、Transpose 等。这些算子在 ONNX Runtime 上跑没问题但在 RKNN 转换时可能被判定为“不支持的算子”或者会让转换后的模型在 NPU 上多出很多无用的 CPU 算子拖慢速度。我用onnx-simplifier做了一次化简pip install onnx-simplifier python -m onnxsim facenet.onnx facenet_sim.onnx化简之后最好打开 ONNX 文件看一眼算子列表import onnx model onnx.load(facenet_sim.onnx) ops {node.op_type for node in model.graph.node} print(ops)我这边化简后的算子集合大概包括 Conv、Relu、BatchNormalization、MaxPool、AveragePool、AdaptiveAvgPool、Reshape、Gemm、Add、Mul 等。这些都是 RKNN 的熟面孔基本不会出大问题。如果看到像GridSample、TfIdfVectorizer这类冷门算子基本就是模型结构不合适需要提前处理。这一步特别重要一定不要省。因为有相当一部分 ONNX 转 RKNN 报错最后追根究底都是因为原始 ONNX 里残留了一些逻辑上存在但计算上冗余的算子。3.3 导出后用 onnxruntime 验证输出在转 RKNN 之前先用 onnxruntime 跑一遍导出的 ONNX和 PyTorch 的输出做一个对比。这个验证能排除“模型没导出好”的干扰因素让你后面在 RKNN 上遇到精度问题时能明确知道问题出在哪个环节。对比方法很简单同一个输入分别用 PyTorch 和 onnxruntime 推理计算两者输出向量的余弦相似度和欧氏距离。正常情况下因为浮点计算的微小差异余弦相似度应该接近 1.0比如 0.9999 以上欧氏距离应该非常小。如果差异很大一定是导出过程出了问题先解决这个再往下走。import onnxruntime as ort import numpy as np sess ort.InferenceSession(facenet_sim.onnx) ort_output sess.run(None, {input: dummy_input.numpy()})[0] torch_output model(dummy_input).detach().numpy() cos_sim np.dot(ort_output.flatten(), torch_output.flatten()) / ( np.linalg.norm(ort_output) * np.linalg.norm(torch_output) ) print(cosine similarity:, cos_sim)4. ONNX 转 RKNN核心配置项和算子兼容问题4.1 转换前的输入输出梳理ONNX 转 RKNN 不是简单地把文件格式换一下而是要告诉转换工具“这个模型的输入长什么样、预处理怎么算、输出是什么”。这一步配置错了后面所有工作都白做。FaceNet 在 PyTorch 里的预处理是x (x / 255 - 0.5) / 0.5对应到 RKNN 的 config 里mean_values和std_values要理解成input (img / 255 - mean) / std。所以from rknn.api import RKNN rknn RKNN() rknn.config( mean_values[[0.5, 0.5, 0.5]], std_values[[0.5, 0.5, 0.5]], target_platformrk3588, optimization_level3 ) ret rknn.load_onnx(modelfacenet_sim.onnx) ret rknn.build(do_quantizationFalse)这里target_platformrk3588是必须写的。不写的话默认可能是 rk3568 或其他平台生成的模型在 RK3588 上也能加载但算子映射方式和性能调优可能不是最优的。注意 PyTorch 的通道顺序ONNX 里输入一般是 NCHW1, 3, 160, 160而 RKNN 内部处理时会切换成 NHWC这些是工具自动做的不需要你手动转。但如果你在预处理里把 RGB 和 BGR 搞反了后面人脸识别效果会非常差而且不太好排查。整个流程里我建议从图片解码开始就统一用 RGB 顺序在 RKNN 推理传入前保持和 PyTorch 训练时一致。4.2 量化 vs 不量化第一次先跑通我在第一次转换时直接把do_quantization设为 False也就是先导出一个 FP16 精度的 RKNN 模型把整条链路跑通、输出对了再做 INT8 量化。这个顺序是我强烈推荐的先跑通再优化。do_quantizationFalse时RKNN 会生成一个 FP16 精度的模型。FP16 在 RK3588 NPU 上是可以直接跑的精度损失比 INT8 小得多。但代价是模型体积更大、推理速度更慢。这个阶段我们不在乎速度只要结果对。转换完成后导出.rknn文件rknn.export_rknn(facenet_fp16.rknn)同时可以用 RKNN 自带的模拟器模拟器跑在 x86 上模拟 NPU 推理先看一眼输出是否正常。模拟器跑通了再放板子上。4.3 算子不支持怎么办转换过程中最常见的报错是类似这样E LoadONNX: Cannot find a valid rknn for the current op: xxx遇到这种报错我的排查顺序是看报错里的算子名是什么。如果是卷积、池化、激活这一类大概率是参数组合不被支持比如dilation 1的卷积在某些版本上映射得不好。回到 PyTorch 模型结构里找到这个算子对应的代码位置。用 ONNX 的节点名定位或者在导出 ONNX 时把每个 tensor 的 name 打印出来。对模型结构做等效替换。比如某些自定义的AdaptiveAvgPool2d(1)可以手动替换成AvgPool2dReshape某些F.interpolate换成固定size的Upsample。替换后重新导出 ONNX、重新 onnxsim、重新转换。Facenet 的 Inception ResNet V1 整体结构不算复杂一般不会遇到太难的算子问题。我最开始遇到过torch.split导出的Split算子在某些版本上映射效率低的问题后来通过结构上直接改成多个 slice 拼起来解决了但这个属于特例不同模型遇到的情况不同。4.4 模拟器推理和真机推理的差异rknn-toolkit2 提供模拟器可以在 x86 上模拟 NPU 推理。模拟器跑通的模型放在板子上大概率能跑通但不能保证数值完全一致。我遇到过模拟器输出正常、真机输出 NaN 的情况最后发现是板子上的 RKNN Runtime 版本和 toolkit 版本不一致导致的。所以这里有个经验模拟器只是用来验证链路通不通最终精度验证必须放在真机上做。而且最好从最开始就在板子上留好一个“模型输出对比”的测试脚本用同一张测试图对比 ONNX Runtime 的输出、RKNN 模拟器的输出、RKNN 真机的输出这样一旦出问题能立刻定位是哪个环节坏了。5. int8 量化后精度崩塌原因定位和解决办法5.1 “不量化正常int8 量化后数值不动”的现象如果你在rknn.build(do_quantizationTrue)之后发现模型在真机上输出变成了一堆恒定值或者和输入完全无关这说明量化过程出了问题。我这边遇到的情况是FP16 模型输出的 embedding 和 ONNX Runtime 基本一致但 INT8 量化后输出的 512 维向量所有维度都趋近于同一个值。这个问题的本质是量化时激活值范围估算出了问题。RKNN 在做 INT8 量化时需要跑一批校准数据来统计每一层的激活值分布然后根据 min/max 把 float 映射到 int8。如果校准数据不够有代表性或者某些层的激活值分布特别不均匀量化参数就会算偏最终结果就是模型在量化后“表情呆滞”输出失去区分度。5.2 量化校准数据集不是越多越好我在第一次量化时犯了一个典型错误为了省事拿了很多不同场景的图片做校准但没有注意图片的质量和内容分布。结果就是模型在测试集上输出非常差比随机向量还离谱。后来我重新整理了校准数据集遵循这几个原则使用和真实部署场景一致的数据。如果你的摄像头装在室内门禁就多放室内光照下人脸图片如果装在室外闸机就放室外自然光下人脸图片。不要拿了一堆 ImageNet 风格图片来做 FaceNet 的校准。数量不用多但要覆盖人脸姿态、光照的多样性。一般 200 到 500 张就足够了甚至 100 张做得好的效果也明显好于 1000 张乱选的。校准图片要经过同样的预处理。也就是说图片要先经过 MTCNN 检测和对齐裁出 160x160 人脸区域再喂给 RKNN 的量化器。如果你拿原始大图直接校准量化器看到的分布和真实推理时完全不同量化参数肯定不准。校准数据准备的过程我建议单独写一个脚本把“检测人脸-对齐-裁切-缩放-acetime”这一套流程固定下来确保后续无论做什么实验校准数据的处理方式都是统一且可复现的。RKNN 的build接口支持传入一个 dataset 文件rknn.load_onnx(modelfacenet_sim.onnx) ret rknn.build(do_quantizationTrue, datasetdataset.txt)dataset.txt的每一行是一个经过预处理的图片路径或者是你手动提取的 npy 文件路径。我更推荐直接用.npy文件因为这样可以确保传给 RKNN 的数据和你用 PyTorch 验证时的输入完全一致不经过任何图片解码差异的干扰。5.3 诊断量化问题的三板斧如果你做完校准量化后输出的 embedding 和 FP16 还是有明显差距我一般按下面的顺序排查逐层对比RKNN 提供了rknn.accuracy_analysis接口可以对比原始模型和量化模型每一层的输出误差。跑一次这个分析很快就能定位到是哪一个 layer 的量化误差最大。rknn.accuracy_analysis(inputs[test_input], output_dir./accuracy_analysis)检查敏感层清单如果误差集中在某些层看一下这些层的算子类型。卷积层的权重通常对量化不敏感但一些Concat、Add、BatchNormalization层如果处理不好会放大误差。FaceNet 里 Inception 模块有大量的Concat有时候问题就出在 concat 之前某个分支的激活值范围特别大。对应到模型结构决定是否混合量化如果误差来自模型最后几层而对整体识别率影响不大可以直接把敏感的子模块在 RKNN 里设置为不量化保留 FP16。Rockchip 的混合量化配置方式相对简陋一般是在构建时通过custom_quantize_layers来指定某些层走 FP16。rknn.config( custom_quantize_layers[module.last_linear], # ... )这个方法在实际项目中经常用来救急。它的原理很简单把影响最大的几层保留高精度其他层继续 INT8这样既保住了精度模型体积和推理速度的损失又比较小。5.4 量化后的精度验收标准量化完不是看一眼输出“差不多”就算完了。我的验收流程是找 100 到 200 张真实场景下的人脸图跑完全流程检测、对齐、embading。同类人脸两两计算余弦相似度得到类内相似度分布异类人脸两两计算余弦相似度得到类间相似度分布。对比 FP16 模型和 INT8 模型在相同测试集上的相似度分布重叠程度。如果两个分布的重叠面积明显变大说明量化已经影响到了实际识别效果需要回去调整校准数据或混合量化策略。如果只是追求“跑通”量化后也能出数值但识别率掉了 3 到 5 个点很多人可能觉得无所谓。但在真实门禁、闸机场景下3 个点的识别率差异可能就决定了项目验收过不过所以精度验证一定不能省。6. 在 RK3588 板子上跑通完整推理代码细节和调用流程6.1 RKNN Lite 接口的使用板子上用的推理接口是RKNNLite比完整版轻量很多。加载模型、初始化 runtime、推理的代码也不复杂from rknnlite.api import RKNNLite import numpy as np import cv2 rknn_lite RKNNLite() rknn_lite.load_rknn(facenet_int8.rknn) rknn_lite.init_runtime() # 读取图像统一为 RGBresize 到 160x160 img cv2.imread(face.jpg) img cv2.cvtColor(img, cv2.COLOR_BGR2RGB) img cv2.resize(img, (160, 160)).astype(np.float32) # RKNN 的输入默认接受 NHWC同时支持传入未归一化的 0-255 数值 # 归一化由 mean_values/std_values 自动完成 input_data np.expand_dims(img, axis0) outputs rknn_lite.inference(inputs[input_data]) embedding outputs[0].flatten()这个接口非常简洁但有几个细节决定成败输入通道顺序cv2.imread读出来是 BGR要手动转成 RGB或者把 RKNN config 里的 mean_values 顺序调整成 BGR 对应的值。我吃了这个亏第一次在板子上跑出来的 embedding 在比对时效果极差排查了很久才发现是通道顺序搞反了。这里统一建议在代码层直接转成 RGB和 PyTorch 训练时的输入保持一致这是最不容易出错的做法。输入尺寸RKNN 模型导入时自带输入 shape如果你传一个 200x200 的图片进去会被直接 resize 到 160x160这个 resize 是 RKNN runtime 内部做的。但为了减少额外的精度损失最好在喂给 RKNN 之前自己用 OpenCV 处理好尺寸和数据类型。数据类型float32是安全的。如果你传uint8RKNN 也会处理但为了统一建议在外部转好 float32。6.2 前面要接检测和对齐MTCNN 在 CPU 上跑FaceNet 只做特征提取实测在 RK3588 上如果直接用原始摄像头画面喂进去效果会非常差。部署时必须前置一个人脸检测和对齐模块。我当时在板子上用的是 MTCNN通过 facenet-pytorch 自带的实现from facenet_pytorch import MTCNN mtcnn MTCNN(image_size160, margin0, keep_allFalse, thresholds[0.6, 0.7, 0.7], devicecpu) face_tensor mtcnn(img_rgb)MTCNN 这块我在 RK3588 上是用 CPU 跑的没有移植到 NPU 上。实测单帧人脸检测加对齐大概耗时 120 到 180 毫秒在门禁场景下完全够用。如果你对速度要求更高可以考虑用 RetinaFace 的简化版或者把检测模型也转成 RKNN 上 NPU但这就是另一个工程了。有一点要注意MTCNN 返回的对齐后人脸图像内部实现里已经做了归一化(x / 255 - 0.5) / 0.5并转成了 PyTorch Tensor。所以如果你用 MTCNN 的输出直接喂到 RKNN需要先把它转回 0-255 范围的 RGB 图像再让 RKNN 的 mean/std 配置去处理或者你调整 RKNN 的 mean/std 配置让输入匹配 MTCNN 的输出。我的做法是在 MTCNN 前后加一个转换函数统一以“RGB uint8 图像”作为整个识别链路的数据接口。6.3 embedding 比对和后处理拿到 512 维的 embedding 后后续比对逻辑很直接。我用的是余弦相似度def cosine_similarity(a, b): return np.dot(a, b) / (np.linalg.norm(a) * np.linalg.norm(b))阈值的选择没有固定标准需要在自己的数据集上调。我见过有些项目用欧氏距离有些用余弦相似度二者效果差异不大但要注意FACE-NET 开源仓库在训练时做的是 L2 归一化 三元组损失所以理论上用欧氏距离判定更符合原论文的设计。实际项目里怎么选取决于你的 embedding 在库里的分布。我在自己的数据集上跑下来余弦相似度阈值定在 0.68 到 0.72 之间效果不错不同的摄像头和光照条件差异会很大上线前一定要采集真实环境数据做阈值标定。一个完整的比对流程大致是摄像头取帧。MTCNN 检测到人脸并框出来。对齐并裁剪出 160x160 人脸图。RKNN 推理得到 embedding。在本地人脸库 / 特征库里检索最相似的向量。相似度超过阈值判定为同一人否则拒识或进入注册流程。6.4 性能实测int8 和 fp16 的对比我在自己的 RK3588 板子8G 内存、标准散热片、系统负载较低时上跑了几个版本的模型统计了从输入图像到输出 embedding 的单次推理耗时结果如下模型版本推理耗时ms模型体积MB与 PyTorch 输出的余弦相似度FP16不量化32~40约 1100.9992INT8校准良好12~18约 300.9921INT8校准糟糕12~18约 300.47从表里可以清楚看到校准质量对量化模型的影响是断崖式的。校准好了精度损失在 0.7% 以内校准不好输出基本不能用。如果把 MTCNN 的 CPU 耗时加进来整条链路的帧率大概在 4 到 6 FPS 左右对我们这种轻交互的门禁场景来说是够用的。7. RKNN 模型优化和后续可以扩展的方向部署跑通之后还有几个方向可以做我的经验是每一步都能带来实际收益。7.1 第一件事跑一次 accuracy_analysis把精度损失的账算清楚在部署一切之前强烈建议在 toolkit 端跑一次完整的accuracy_analysis它需要你放一组和真实场景一致的输入数据进去然后针对每一层输出一个“量化前后差异”的报告。这个报告是后续判断“要不要上混合量化、哪一层需要保留 FP16”的依据。报告里如果一个模型的平均余弦相似度在 0.99 以上基本可以无脑用 INT8如果掉到 0.95 以下就需要检查是校准数据问题还是模型对该任务特别敏感。7.2 尝试更轻量的骨干网络替代FaceNet 的 Inception ResNet V1 在边缘设备上虽然能跑但算力开销不小。如果在精度允许的范围内可以换成 MobileFaceNet 这类轻量网络甚至直接使用已有人脸识别模型如 MobileFaceNet ArcFace 的组合。RKNN 部署方式完全一样只是导出 ONNX 时输入输出尺寸可能要调整。实测 MobileFaceNet 在 RK3588 上能做到单帧纯 NPU 推理 5 到 10 毫秒而且识别精度并不一定比 FaceNet 差太多特别是在受限场景下。7.3 批量特征库的构建人脸识别项目一般不会只有一个注册用户。随着用户增加embedding 库会变大简单的线性扫描会越来越慢。我这边目前是直接用 numpy 做向量化批量距离计算库容量在 1 万以下时毫无压力。如果库继续膨胀可以试试用 SQLite 存特征 预先对 embedding 做 PCA 降维到 128 维检索效果和速度都能得到改善。7.4 进一步做工程化的方向部署完成后我做的最有价值的一件事是把整个推理封装成一个 HTTP 服务通过局域网把摄像头采集到的每一帧发送到服务端做识别。这样摄像头端只需要负责采集和推流RK3588 板子专注做人脸特征提取和比对架构解耦之后后续升级模型或增加摄像头数量都会方便很多。如果内存允许还可以考虑在 RKNN runtime 初始化时同时加载两个模型一个人脸检测模型跑 NPU一个 FaceNet 特征模型跑 NPU这样整条链路都不依赖 CPU 上的 MTCNN瓶颈会小很多。这也是我下一步打算做的事。说到底RK3588 部署 FaceNet 这条路模型转换只是最前面的 30%后面关于量化、预处理、阈值标定和工程集成的功夫才是决定项目能不能真正落地的关键。希望这篇记录能帮你少走一些弯路。