news 2026/10/3 3:34:40

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
RK3588部署FaceNet完整指南:PyTorch转RKNN的踩坑与优化

去年年中接了一个边缘设备上做人脸识别的项目,老板指定要用 RK3588,模型用 FaceNet。说实话,当时脑子里第一个念头是“这不就是装个环境,导个模型,跑个推理吗”,真正动手之后才发现,从 PyTorch 到 RKNN 这条链路上到处都是坑,而且每个坑都长得不一样:有算子兼容的、有归一化配置的、有量化精度塌方的,还有板子端版本对不上导致模型加载失败的。

这篇文章就是把我在 RK3588 上从零部署 FaceNet 的完整过程记录下来。FaceNet 的核心思想是“把一张人脸映射到一个向量空间,同类人脸的向量距离近,不同人脸的距离远”,部署时最终落到板子上就是两件事:一个能跑出 128 维(或 512 维,取决于你用的网络结构)embedding 的推理程序,和一个能算相似度、做比对的后续链路。这篇文章适合正在做边缘端人脸识别、准备把 PyTorch 模型往 Rockchip NPU 上迁移的开发者参考,尤其是第一次碰 RKNN 工具链的人。

1. 动手之前先理清楚:模型结构、部署流程图和 RK3588 的算力边界

1.1 确认你手里的是哪种 FaceNet

FaceNet 在 PyTorch 生态里最常用的开源实现是timesler/facenet-pytorch,它用的是 Inception ResNet V1 作为 backbone,输出维度默认是 512,也可以用embedding_size=128重新定义。我这边用的就是官方预训练权重的 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 要走五步

整个链路在动手前一定要画清楚:

  1. 在 x86 主机上准备一个干净的 Python 环境,装好 PyTorch、onnx、onnxruntime、rknn-toolkit2。
  2. 把训练好的 FaceNet PyTorch 权重导出为 ONNX。
  3. 用 onnx-simplifier 对导出的 ONNX 做算子化简,并用 onnxruntime 验证输出一致性。
  4. 用 rknn-toolkit2 把 ONNX 转为 RKNN 格式,这一步可以选择是否量化。
  5. 把生成的.rknn文件部署到 RK3588 板子上,通过 RKNN Runtime 调用 NPU 推理,完成 embedding 提取。

这五步里面,第 1 步看似废话,实际上最容易出问题的是版本兼容。第 4 步是坑最多的地方,第 5 步则是“看起来跑通了但结果就是不对”的高发区。

1.3 RK3588 的 NPU 到底适合跑什么

RK3588 内置的 NPU 标称算力是 6 TOPS(INT8),支持 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-toolkit2

rknn-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 依赖onnx==1.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,导出时一定要确保这个开关是 False:

import torch from facenet_pytorch import InceptionResnetV1 model = InceptionResnetV1(pretrained='vggface2', classify=False) model.eval() dummy_input = torch.randn(1, 3, 160, 160) torch.onnx.export( model, dummy_input, 'facenet.onnx', opset_version=11, input_names=['input'], output_names=['output'], dynamic_axes=None ) print('ONNX exported.')

这里有几个参数需要说明:

  • opset_version建议用 11,rknn-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_platform='rk3588', optimization_level=3 ) ret = rknn.load_onnx(model='facenet_sim.onnx') ret = rknn.build(do_quantization=False)

这里target_platform='rk3588'是必须写的。不写的话默认可能是 rk3568 或其他平台,生成的模型在 RK3588 上也能加载,但算子映射方式和性能调优可能不是最优的。

注意 PyTorch 的通道顺序:ONNX 里输入一般是 NCHW(1, 3, 160, 160),而 RKNN 内部处理时会切换成 NHWC,这些是工具自动做的,不需要你手动转。但如果你在预处理里把 RGB 和 BGR 搞反了,后面人脸识别效果会非常差,而且不太好排查。整个流程里,我建议从图片解码开始就统一用 RGB 顺序,在 RKNN 推理传入前保持和 PyTorch 训练时一致。

4.2 量化 vs 不量化:第一次先跑通

我在第一次转换时直接把do_quantization设为 False,也就是先导出一个 FP16 精度的 RKNN 模型,把整条链路跑通、输出对了,再做 INT8 量化。这个顺序是我强烈推荐的:先跑通,再优化。

do_quantization=False时,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

遇到这种报错,我的排查顺序是:

  1. 看报错里的算子名是什么。如果是卷积、池化、激活这一类,大概率是参数组合不被支持,比如dilation > 1的卷积在某些版本上映射得不好。
  2. 回到 PyTorch 模型结构里,找到这个算子对应的代码位置。用 ONNX 的节点名定位,或者在导出 ONNX 时把每个 tensor 的 name 打印出来。
  3. 对模型结构做等效替换。比如某些自定义的AdaptiveAvgPool2d(1)可以手动替换成AvgPool2d+Reshape,某些F.interpolate换成固定size的Upsample。
  4. 替换后重新导出 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_quantization=True)之后,发现模型在真机上输出变成了一堆恒定值,或者和输入完全无关,这说明量化过程出了问题。我这边遇到的情况是: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(model='facenet_sim.onnx') ret = rknn.build(do_quantization=True, dataset='dataset.txt')

dataset.txt的每一行是一个经过预处理的图片路径,或者是你手动提取的 npy 文件路径。我更推荐直接用.npy文件,因为这样可以确保传给 RKNN 的数据和你用 PyTorch 验证时的输入完全一致,不经过任何图片解码差异的干扰。

5.3 诊断量化问题的三板斧

如果你做完校准,量化后输出的 embedding 和 FP16 还是有明显差距,我一般按下面的顺序排查:

  1. 逐层对比:RKNN 提供了rknn.accuracy_analysis接口,可以对比原始模型和量化模型每一层的输出误差。跑一次这个分析,很快就能定位到是哪一个 layer 的量化误差最大。
rknn.accuracy_analysis(inputs=[test_input], output_dir='./accuracy_analysis')
  1. 检查敏感层清单:如果误差集中在某些层,看一下这些层的算子类型。卷积层的权重通常对量化不敏感,但一些Concat、Add、BatchNormalization层如果处理不好,会放大误差。FaceNet 里 Inception 模块有大量的Concat,有时候问题就出在 concat 之前某个分支的激活值范围特别大。

  2. 对应到模型结构,决定是否混合量化:如果误差来自模型最后几层,而对整体识别率影响不大,可以直接把敏感的子模块在 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() # 读取图像,统一为 RGB,resize 到 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, axis=0) 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是安全的。如果你传uint8,RKNN 也会处理,但为了统一,建议在外部转好 float32。

6.2 前面要接检测和对齐:MTCNN 在 CPU 上跑

FaceNet 只做特征提取,实测在 RK3588 上如果直接用原始摄像头画面喂进去,效果会非常差。部署时必须前置一个人脸检测和对齐模块。

我当时在板子上用的是 MTCNN,通过 facenet-pytorch 自带的实现:

from facenet_pytorch import MTCNN mtcnn = MTCNN(image_size=160, margin=0, keep_all=False, thresholds=[0.6, 0.7, 0.7], device='cpu') 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 之间效果不错,不同的摄像头和光照条件差异会很大,上线前一定要采集真实环境数据做阈值标定。

一个完整的比对流程大致是:

  1. 摄像头取帧。
  2. MTCNN 检测到人脸并框出来。
  3. 对齐并裁剪出 160x160 人脸图。
  4. RKNN 推理得到 embedding。
  5. 在本地人脸库 / 特征库里检索最相似的向量。
  6. 相似度超过阈值,判定为同一人;否则拒识或进入注册流程。

6.4 性能实测:int8 和 fp16 的对比

我在自己的 RK3588 板子(8G 内存、标准散热片、系统负载较低时)上跑了几个版本的模型,统计了从输入图像到输出 embedding 的单次推理耗时,结果如下:

模型版本推理耗时(ms)模型体积(MB)与 PyTorch 输出的余弦相似度
FP16(不量化)32~40约 1100.9992
INT8(校准良好)12~18约 300.9921
INT8(校准糟糕)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%,后面关于量化、预处理、阈值标定和工程集成的功夫才是决定项目能不能真正落地的关键。希望这篇记录能帮你少走一些弯路。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/3 3:34:38

PostgreSQL流复制协议:从WAL到排障,彻底搞懂主从同步机制

1. 流复制协议不是"配置项",是你排障的最后一层眼睛如果你只把PostgreSQL流复制当成primary_conninfo加max_wal_senders这样的配置项,那你会错过一整个层次的排障能力。我见过太多DBA,主从能跑起来就觉得万事大吉,一遇到…

作者头像 李华
网站建设 2026/10/3 3:34:36

基于DAG区块链的联邦学习框架:去中心化聚合与个性化模型实战

简介:这份资源是一套基于DAG区块链的联邦学习框架Python实现,面向计算机、数学、电子信息等专业的学生与研究人员,适合用作课程设计、期末大作业或毕业设计参考,也适合想深入理解去中心化联邦学习与个性化建模的开发者。项目将DAG…

作者头像 李华
网站建设 2026/10/3 3:34:17

AVO正演从理论到实践:Zoeppritz方程、Aki-Richards近似与Python实现

简介:这份资源面向石油物探方向的研究生及地震数据处理初学者,聚焦AVO正演模型实验与地震数据正演这一核心课题。包内共4个cpp源码文件,压缩包约12KB,均为C实现的正演程序,涵盖加噪音条件下的AVO正演模型实验、角度区域…

作者头像 李华
网站建设 2026/10/3 3:34:15

Lumerical farfieldpolar3d复电场远场分析全解析

1. 这不是个“命令”,而是一把打开远场光学世界的三维标尺如果你刚在Lumerical FDTD Script里敲下farfieldpolar3d,却只看到一串报错或空数组,别急着翻文档——这根本不是个孤立的函数调用,而是整套远场建模逻辑的终点站。我第一次…

作者头像 李华
网站建设 2026/10/3 3:33:46

PostgreSQL v19 新特性解读:INSERT ON CONFLICT DO SELECT 与 UPSERT 语义补全

最近在跟进 PostgreSQL 新版本动态时,我用 DeepSeek 把社区里零零散散的讨论梳理了一遍,最值得展开聊的一条是 v19 的 INSERT ... ON CONFLICT ... DO SELECT。刚开始我也以为这只是 UPSERT 语法多了一个分支,后来把邮件列表、commitfest 议题…

作者头像 李华
网站建设 2026/10/3 3:33:17

宽带GSC波束形成实战:麦克风阵列语音增强Python实现

1. 为什么宽带GSC波束形成是智能音箱落地的“咽喉要道”你拆开市面上任何一款中高端智能音箱,比如某米、某度、某为的主力型号,十有八九会看到一块印着4~8个麦克风的小PCB板。它不发声,却决定着整台设备的“听觉智商”。很多人以为…

作者头像 李华