说来也巧,香橙派5(RK3588)刚拿到手那阵子,我第一个想跑的项目就是人脸识别。原因很实在,市面上的方案要么挂在云端,要么绑在笨重的X86盒子上,真正能塞进口袋、功耗只有几瓦、还带本地NPU算力的板子,RK3588几乎就是当下的标准答案。这篇文章就围绕“香橙派RK3588 + RetinaFace + rec”这套组合,把我从模型转换、NPU推理到后处理联调的完整过程写出来,代码直接可抄,希望对想在边缘设备上做AI落地的人有点帮助。
这套系统的分工很清晰:RetinaFace负责在画面中找到人脸,输出人脸框和五个关键点;rec模型(insightface训练的人脸识别模型)负责从对齐后的人脸区域提取512维特征向量,后续靠向量距离判断是不是同一个人。两个模型我都转成了RKNN格式,跑在香橙派的NPU上,单帧检测加识别的耗时能控制在40毫秒级别,应付门禁闸机、考勤打卡、课堂点名这类场景绰绰有余。
1. 项目整体设计与方案选型
1.1 为什么选香橙派RK3588:算力、接口与生态
人脸识别系统在边缘端最怕的就是算力不够。树莓派5的CPU虽然不弱,但用CPU跑一个稍微像样的检测模型,基本就是个位数帧率,更别提再接一个识别模型。RK3588这颗芯片就完全不一样了,8核CPU(4个A76大核加4个A55小核),集成Mali-G610 GPU,最关键是自带一个6 TOPS算力的NPU。
6 TOPS是什么概念?它对标的是英伟达的Jetson Nano约0.5 TOPS的十几倍,足够跑INT8量化的检测和识别模型了。我在实际测试中,RetinaFace的MobileNet0.25版本量化后,在NPU上单次推理大约15到25毫秒,rec模型大约5到10毫秒,这个速度放在门禁场景完全够用。而且RK3588的内存带宽和视频编解码能力都很强,后续要接多路摄像头也有余量。
香橙派5系列的另一个优势是接口齐全。USB 3.0可以接普通USB摄像头,MIPI CSI可以接官方摄像头模组,HDMI可以直接接显示器调试,GPIO还能顺便控制电锁或者继电器。板子自带的Rockchip官方系统镜像里已经包含了NPU驱动和硬件编解码库,省去了很多编译驱动的痛苦。
1.2 算法选型:RetinaFace做什么,rec模型做什么
人脸识别跟一般的目标检测不太一样,它实际上包含两个阶段:先检测,再识别。检测阶段要在画面里把人脸找出来,框出位置,最好还能给出眼睛、鼻子、嘴巴这五个关键点,因为后面做人脸对齐要用。识别阶段则把对齐后的人脸区域转成特征向量,用特征向量的相似度来判断身份。
我选RetinaFace做检测,主要看中三点。一是检测加关键点的一体化输出,框架里天然带了五个关键点的回归,不需要额外再训练人脸关键点模型。二是模型体量小,MobileNet0.25骨干的版本只有几兆字节,非常适合边缘设备。三是它跟insightface生态是同一家出品,前处理、后处理、关键点模板的坐标系约定完全一致,后面串rec模型几乎没有对接成本。
rec模型方面,我直接用了insightface开源的人脸识别模型,典型代表是w600k_r50,输入112x112的RGB人脸图,输出512维归一化特征向量。这套模型在光照变化、角度偏移、表情变化下都保持了不错的鲁棒性。识别判断的逻辑很简单,把人脸特征与库中已注册的特征做余弦相似度计算,超过阈值就认为是同一个人。
1.3 整体数据流架构
整个系统的数据流向是:摄像头采集一帧图像,先把图像缩放成RetinaFace期望的输入尺寸(我用的640x640),送到NPU做检测推理,后处理得到若干个人脸框和关键点;随后对每个人脸框裁剪出人脸区域,利用五个关键点做仿射变换,对齐到112x112;对齐后的图像再送入rec模型提取512维特征;最后与本地特征库逐条比对相似度,输出识别结果。
在代码结构上,我把检测和识别封装成两个独立的类,方便单独调试。实际联调的时候,我建议先让检测单独跑起来,在画面上看到人脸框和关键点,再逐步加入识别部分。这样一旦出问题,能快速定位是检测环节还是识别环节的锅。
2. 环境准备与RKNN工具链
2.1 板端系统与基础依赖
板端我刷的是香橙派官方提供的Ubuntu 22.04镜像(Debian也可以用),Rockchip官方系统镜像的优点是把NPU驱动、GPU驱动、VPU编解码固件都预装好了。上电后用SSH连进板子,第一步先确认NPU设备节点存在:
ls /dev/rknpu cat /proc/rknpu/version正常情况下能看到rknpu设备节点和版本信息。如果看不到,说明系统镜像没带驱动,或者需要在启动参数里打开。之后安装Python依赖,我习惯用系统自带的Python 3.10,直接pip安装就够:
pip install numpy opencv-python-headless pip install rknn-toolkit-lite2rknn-toolkit-lite2就是我们板端的推理库,它会依赖系统的librknnrt.so运行时,这个在官方镜像里已经带上了。实测opencv-python-headless版本就够了,毕竟板子上不需要GUI。如果后续要显示画面调试,再单独装完整版opencv和imshow的依赖。
2.2 PC端模型转换环境
模型转换必须在PC上进行,因为RKNN-Toolkit2的完整版工具链在板端跑不流畅,而且转换时内存和CPU占用都比较高。建议在Ubuntu 20.04或22.04的PC上,用Python 3.8或3.10建一个干净的虚拟环境,直接安装:
pip install rknn-toolkit2安装过程中最容易踩的坑是依赖版本冲突,尤其是onnx、onnxruntime和protobuf。我的建议是安装完后先跑一下自带demo,确认环境OK再开始转换自己的模型。另外注意,PC上装的rknn-toolkit2和板上的rknn-toolkit-lite2是两个东西,前者负责转换模型、仿真推理,后者只负责在板端加载rknn模型做推理,不要搞混了。
2.3 模型文件准备与目录规划
我习惯把所有东西放在一个固定目录,方便管理和反复折腾:
rk3588-face/ ├── models/ │ ├── onnx/ │ │ ├── retinaface_mobile0.25.onnx │ │ └── w600k_r50.onnx │ ├── rknn/ │ │ ├── retinaface.rknn │ │ └── w600k_r50.rknn │ └── dataset.txt ├── convert_retinaface.py ├── convert_rec.py ├── test_recognition.py └── libs/ ├── detector.py ├── recognizer.py └── align.py模型文件的来源要说明一下。RetinaFace的onnx可以从insightface官方仓库拉取,也有不少社区fork了带导出的版本。rec模型一般从insightface model zoo下载,文件名通常叫w600k_r50.onnx。下载的时候注意看一下模型许可证,如果是商用场景,最好换成合规授权的模型,部署逻辑是完全一样的。
3. 模型转换与关键参数配置
3.1 RetinaFace的ONNX导出与固定输出
RetinaFace的原版PyTorch模型,导成ONNX时有个关键点:默认输入是动态的,我建议在导出时直接固定batch为1,输入尺寸固定为640x640,这样转换RKNN时省去动态shape的麻烦,推理性能也更稳定。
onnx导出部分,我用的导出代码大致是:
import torch from retinaface import RetinaFace model = RetinaFace('mobilenet0.25') model.load_state_dict(torch.load('resnet50_softmax.pth', map_location='cpu')) model.eval() dummy_input = torch.randn(1, 3, 640, 640) torch.onnx.export( model, dummy_input, 'retinaface_mobile0.25.onnx', opset_version=11, input_names=['input'], output_names=['face_rpn_cls_prob', 'face_rpn_bbox_pred', 'face_rpn_landmark_pred'], dynamic_axes=None )这里输出的三个tensor分别是对类别概率、边界框偏移、关键点偏移的原始预测。不同版本的RetinaFace输出节点名会有差异,没关系,转换的时候要记下来就行,后处理的代码会用到。有些repo还会输出六个tensor,那是把不同特征层的输出单独列出来了,转换时要注意匹配。
3.2 RetinaFace的RKNN转换实操
拿到onnx之后,转换脚本就很有讲究了。下面是我实际在用的转换脚本:
from rknn.api import RKNN rknn = RKNN() rknn.config( mean_values=[[0, 0, 0]], std_values=[[255, 255, 255]], target_platform='rk3588' ) print('--> Loading onnx') ret = rknn.load_onnx(model='models/onnx/retinaface_mobile0.25.onnx') assert ret == 0, 'load onnx failed' print('--> Building model') ret = rknn.build(do_quantization=True, dataset='models/dataset.txt') assert ret == 0, 'build failed' rknn.export_rknn('models/rknn/retinaface.rknn')两个参数值得重点解释。mean_values和std_values对应的是原始模型训练时的预处理。RetinaFace的MobileNet版本用的是图像直接除以255做归一化,也就是等价于mean为0、std为255,转换成数值就是 (img - 0) / 255,所以配置里写[[0,0,0]]和[[255,255,255]]。如果填错,最典型的症状就是检测框大面积偏移或者干脆检不到人。
dataset.txt是量化校准用的图片路径列表。量化不是随便给几张图就行,选图很影响最终效果。我的建议是挑20到50张真实场景图,覆盖不同的光照、角度和人物数量,最好是目标设备将要部署的环境里拍的图。如果用的是纯背景图或者统一自拍图,量化出来的模型在现场效果会很差。
3.3 rec模型与RetinaFace模型的预处理差异
rec模型的转换逻辑跟RetinaFace类似,但有一个非常大的坑:预处理参数完全不一样。insightface训练rec模型时,输入图像做了这样几步:resize到112x112,RGB三通道,然后做 (img / 255 - 0.5) / 0.5,也就是归一化到-1到1。换算成RKNN的mean和std写法,就是mean填127.5,std也填127.5。
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_platform='rk3588' ) ret = rknn.load_onnx(model='models/onnx/w600k_r50.onnx') assert ret == 0, 'load onnx failed' ret = rknn.build(do_quantization=True, dataset='models/dataset.txt') assert ret == 0, 'build failed' rknn.export_rknn('models/rknn/w600k_r50.rknn')很多新手在移植rec模型的时候,会下意识把mean和std照抄RetinaFace的,结果识别出来的特征向量全都不对,相似度一片混乱。我建议把两个模型的预处理参数做成一张表贴在代码注释里,防止自己以后再踩一遍:
| 模型 | 输入尺寸 | 通道顺序 | RKNN mean_values | RKNN std_values | 数值范围 |
|---|---|---|---|---|---|
| RetinaFace检测 | 640x640 | RGB | 0, 0, 0 | 255, 255, 255 | 0~1 |
| rec识别 | 112x112 | RGB | 127.5, 127.5, 127.5 | 127.5, 127.5, 127.5 | -1~1 |
3.4 量化精度相关:int8、float16与混合量化
RK3588的NPU对int8量化支持最好,速度和吞吐都是最优的。但量化一定会带来精度损失,rec模型尤其敏感。我的实测经验是,RetinaFace量化后掉点不明显,最多就是置信度稍微浮动,而rec模型量化后,特征向量的区分度会明显下降,相似度阈值要重新调整。
如果你的应用对精度要求高,可以尝试两档方案。第一档是do_quantization=False,转成float16的rknn模型,精度基本无损,但推理速度大约是int8的一半到三分之二。第二档是量化数据集里加入大量人脸样本,把量化校准做得更充分,再配合int8跑。如果还不行,就用RKNN-Toolkit2的混合量化功能,把rec模型最后的全连接层或特定层设定为float16,其他层保持int8。后续章节我会把量化对识别的具体影响再展开讲。
4. 推理代码与后处理实现
4.1 RKNN推理类设计:一个类管一个模型
板端的推理代码我习惯封装成类。每个模型对应一个类,加载rknn模型、初始化运行时、执行推理都放在类里面,调用方只需要传图像进去,拿结果出来就行。下面是一个检测类的骨架:
import cv2 import numpy as np from rknnlite.api import RKNNLite class FaceDetector: def __init__(self, model_path, input_size=(640, 640), core_mask=RKNNLite.NPU_CORE_0_1_2): self.input_size = input_size self.rknn = RKNNLite() assert self.rknn.load_rknn(model_path) == 0, 'load rknn failed' assert self.rknn.init_runtime(core_mask=core_mask) == 0, 'init runtime failed' def preprocess(self, bgr_img): img = cv2.cvtColor(bgr_img, cv2.COLOR_BGR2RGB) img = cv2.resize(img, self.input_size) img = img.astype(np.float32) img = img / 255.0 img = img.transpose(2, 0, 1) return np.expand_dims(img, axis=0) def infer(self, bgr_img): input_data = self.preprocess(bgr_img) outputs = self.rknn.inference(inputs=[input_data]) return outputs这里有个容易忽略的点:rknnlite的inference默认输入是NCHW格式,也就是通道在前。我们传入的numpy数组形状需要是(1, 3, H, W)。如果你的模型是NHWC训练的,可以通过rknn.config的input_format配置调整,我这边统一用NCHW,跟PyTorch导出的onnx保持一致。
core_mask这个参数很实用,它决定用几个NPU核心跑。RK3588的NPU内部有三个核心,默认可能是单核,我是直接传NPU_CORE_0_1_2全开,推理速度能提升不少。
4.2 RetinaFace后处理:解码框、关键点与NMS
RetinaFace的原始输出不是直接可用的坐标,需要先解码。解码依赖anchor,anchor是模型在训练时就预设好的一组框。不同特征层对应不同尺寸的anchor,每个anchor会预测类别、框偏移和关键点偏移。
后处理代码的核心逻辑是先为每个特征层生成anchor网格,然后跟模型的偏移预测相加得到坐标,最后合并所有层的候选框做NMS。我用的关键代码大致如下:
def decode_boxes(preds, anchors): # preds: 模型输出 [batch, num_anchors*4, h, w] # anchors: 预设 anchor 网格 boxes = np.zeros_like(anchors) boxes[:, 0] = anchors[:, 0] + preds[:, 0] * 0.1 * anchors[:, 2] boxes[:, 1] = anchors[:, 1] + preds[:, 1] * 0.1 * anchors[:, 3] boxes[:, 2] = anchors[:, 2] * np.exp(preds[:, 2] * 0.2) boxes[:, 3] = anchors[:, 3] * np.exp(preds[:, 3] * 0.2) return boxes不同版本的RetinaFace在解码时的方差系数可能不同,有的用0.1和0.2,有的用1.0,这个要看训练源码,不能乱改。我的建议是把检测模型的锚点解码逻辑单独抽函数,先用公开测试图片验证坐标是否正确,再进入识别环节。
NMS我用的是numpy手写版,因为轻量且不依赖额外库。阈值方面,分类置信度我习惯取0.5左右,NMS的IoU阈值取0.4到0.5。如果你的场景人脸又小又多,置信度阈值可以适当降低。
关键的坐标还原也很容易踩坑。因为推理前我把原图resize到640x640,所以解码出来的坐标都在640坐标系下,要还原到原图,必须记下resize的比例:
scale_x = orig_w / 640 scale_y = orig_h / 640 x1 = int(box[0] * scale_x) y1 = int(box[1] * scale_y) x2 = int(box[2] * scale_x) y2 = int(box[3] * scale_y)如果用了letterbox(等比缩放加填充),还原逻辑还要再加上偏移量。我当时图省事直接拉伸resize,牺牲了一点宽高比,但检测效果影响不大。
4.3 关键点对齐与rec特征提取
检测到人脸框之后,不能直接把裁剪框扔给rec模型。因为人的头部会有倾斜和旋转,直接裁出来的人脸角度不对,识别精度会大幅下降。正确做法是利用检测出来的五个关键点,做一次仿射变换,把脸对齐到标准位置。
标准模板用的是arcface的112x112对齐模板,五个点的坐标如下:
REFERENCE_FACIAL_POINTS = [ [38.2946, 51.6963], [73.5318, 51.5014], [56.0252, 71.7366], [41.5493, 92.3655], [70.7299, 92.2041] ]对齐代码我封装成下面的函数:
def align_face(img, landmarks, output_size=(112, 112)): import cv2 import numpy as np dst = np.array(REFERENCE_FACIAL_POINTS, dtype=np.float32) src = np.array(landmarks, dtype=np.float32) M, _ = cv2.estimateAffinePartial2D(src, dst, method=cv2.LMEDS) aligned = cv2.warpAffine(img, M, output_size, flags=cv2.INTER_LINEAR) return aligned注意这里landmarks的顺序要跟训练模板一致,通常是右眼、左眼、鼻尖、右嘴角、左嘴角。如果你的检测模型输出的关键点顺序不是这个,需要先做索引重排,否则对齐就错位了。对齐好的人脸图再经过RGB转换,送进rec模型即可。
rec模型推理代码同样封装成类,输入之前记得做和转换时相同的预处理:
class FaceRecognizer: def __init__(self, model_path, core_mask=RKNNLite.NPU_CORE_0_1_2): self.rknn = RKNNLite() assert self.rknn.load_rknn(model_path) == 0 assert self.rknn.init_runtime(core_mask=core_mask) == 0 def get_embedding(self, aligned_bgr): rgb = cv2.cvtColor(aligned_bgr, cv2.COLOR_BGR2RGB) rgb = rgb.astype(np.float32) rgb = (rgb - 127.5) / 127.5 rgb = np.transpose(rgb, (2, 0, 1)) rgb = np.expand_dims(rgb, axis=0) outputs = self.rknn.inference(inputs=[rgb]) feat = outputs[0].flatten() norm = np.linalg.norm(feat) if norm > 0: feat = feat / norm return feat输出特征一定要做归一化。因为rec模型训练时就是用归一化后的特征向量计算余弦相似度的,代码里顺手把归一化做了,后面比对就只剩一个点积。
4.4 5分钟跑通的最小可用系统
把上面的零件串起来,就得到了一套最小可用的识别系统。这里给一个完整的单文件示例,我已经把注释写得比较细:
import cv2 import numpy as np import pickle from libs.detector import FaceDetector from libs.recognizer import FaceRecognizer from libs.align import align_face # 1. 加载模型 detector = FaceDetector('models/rknn/retinaface.rknn') recognizer = FaceRecognizer('models/rknn/w600k_r50.rknn') # 2. 加载特征库(预先用 register.py 生成) with open('face_db.pkl', 'rb') as f: face_db = pickle.load(f) # list of (name, feature) def recognize_name(feat, db, threshold=0.45): best_name = 'unknown' best_score = -1.0 for name, db_feat in db: score = float(np.dot(feat, db_feat)) if score > best_score: best_score = score best_name = name if best_score < threshold: return 'unknown', best_score return best_name, best_score cap = cv2.VideoCapture(0) while True: ret, frame = cap.read() if not ret: break # 3. 检测人脸 faces = detector.detect(frame) # list of (box, landmarks) for box, landmarks in faces: x1, y1, x2, y2 = [int(v) for v in box] # 4. 对齐并提取特征 aligned = align_face(frame, landmarks) feat = recognizer.get_embedding(aligned) # 5. 识别 name, score = recognize_name(feat, face_db) # 6. 画框标名 cv2.rectangle(frame, (x1, y1), (x2, y2), (0, 255, 0), 2) cv2.putText(frame, f'{name} {score:.2f}', (x1, y1 - 10), cv2.FONT_HERSHEY_SIMPLEX, 0.8, (0, 255, 0), 2) cv2.imshow('face recognition', frame) if cv2.waitKey(1) & 0xFF == ord('q'): break cap.release() cv2.destroyAllWindows()这整套流程跑通,如果仅仅是把代码运行起来,在模型和特征库都准备好的前提下,5分钟确实够了。不过首次从零开始,要经历模型导出、转换、调参、建库这一套流程,建议预留半天到一天的时间。我写这篇文章的目的一是给一个可以直接参考的完整示例,二是帮大家把那几个最容易浪费时间的暗坑提前趟平。
5. 性能实测与踩坑实录
5.1 实测性能数据与瓶颈分析
以下是我在香橙派5上跑这套系统的实测数据,环境是Ubuntu 22.04、Python 3.10、RKNN Toolkit版本为1.6,输入分辨率为640x640检测、112x112识别,模型均为int8量化:
| 环节 | 平均耗时(毫秒) | 说明 |
|---|---|---|
| 图像预处理(resize + 归一化) | 3~5 | numpy开销 |
| RetinaFace NPU推理 | 15~25 | 三核全开 |
| 检测后处理(解码+NMS) | 4~8 | 纯numpy,有优化空间 |
| 单个人脸对齐+rec推理 | 8~15 | 只在检测到人脸时才执行 |
| 特征比对(100人底库内) | <1 | 纯numpy点积 |
整体来看,画面中没有人脸时,单帧耗时约25到35毫秒;有人脸时,加上识别环节约40到50毫秒,大约20到28帧每秒。这个性能对门禁、考勤类应用已经足够,但如果要跑实时视频监控,就需要做一些优化了。
瓶颈其实不在NPU,而在Python侧的后处理层。RetinaFace的anchor数量比较多,如果我用纯for循环去解码,CPU占用会飙升。后来我把循环全部改成numpy矩阵运算,耗时直接降到原来的三分之一。
5.2 三板斧优化:分辨率、多核、异步
第一板斧是降低输入分辨率。检测模型的输入从640x640降到480x480,NPU推理耗时能降低30%到40%,但小尺寸人脸的检测率也会跟着下降。我的建议是先按640跑通,确认系统稳定后再尝试降分辨率,看效果能否接受。
第二板斧是NPU多核全开。RK3588的NPU有三个核心,默认可能只用了其中一个。初始化时通过core_mask参数可以指定使用哪几个核心,我直接使用NPU_CORE_0_1_2。这一步几乎零成本,推理速度能有明显提升,尤其对多路并行任务友好。
第三板斧是异步流水线。如果你需要同时处理摄像头采集、检测、识别,可以用两个线程加两个queue,一个线程只负责读帧和预处理,另一个线程做推理和后处理,把耗时隐藏起来。板子有四颗A76大核,Python线程可以并行,实测整体吞吐量能提高30%以上。
另外一个小技巧是,检测模型和rec模型可以同时加载在内存里,但每次推理时尽量复用numpy缓冲区,避免频繁分配大数组。这个优化对Python端的垃圾回收压力是个不小的缓解。
5.3 常见问题速查表
我在折腾这套系统的过程中,踩了几个大坑,周围朋友照着跑也踩了同样的坑,整理成一个速查表:
| 现象 | 可能原因 | 解决办法 |
|---|---|---|
| load_rknn失败或初始化报错 | 板端NPU驱动版本与rknn-toolkit-lite2不匹配 | 用官方镜像自带的rknpu驱动,并升级lite版本 |
| 模型转换时报op不支持的错误 | onnx算子版本太高,或RKNN-Toolkit2版本太旧 | 升级RKNN-Toolkit2,或尝试降低onnx opset版本 |
| 推理输出全为0 | 输入预处理mean/std配置错误,或输入尺寸与模型不一致 | 对应模型实际训练预处理,检查输入shape |
| 检测框位置偏得离谱 | letterbox/缩放还原坐标没有换算正确 | 检查坐标缩放比例和offset |
| 识别相似度普遍很低 | rec模型是float16,被量化成int8导致掉点 | 转float16或混合量化,或者增加量化数据集 |
| 识别误识别明显增多 | 特征库质量参差,或阈值设置过低 | 提高余弦相似度阈值,底库照片重新采集 |
| NPU推理速度比预期慢 | 只用了单核,或者输入分辨率过高 | 初始化时开三核,尝试降低输入分辨率 |
| 摄像头调用失败 | 权限问题或设备号错误 | 检查/dev/video*,并把用户加入video组 |
5.4 量化掉点与阈值调参的独家经验
量化掉点这个问题,我单独拎出来说。rec模型对量化真的很敏感,我第一次转int8版本时,底库识别率从99%掉到80%出头,肉眼可见的变差。后来我在量化数据集里加入了各种角度、各种光照下的人脸图片,大概100张,识别率回到了95%左右。
如果你发现量化后精度还是不满意,可以考虑转float16版本。RK3588的NPU对float16的支持也还可以,推理速度虽然比int8慢一些,但比CPU快得多,精度几乎无损。我的建议是在需要严格保证识别准确率的场景里,rec模型用float16,检测模型用int8,这样性能和精度能同时兼顾。
阈值调整也要跟着模型精度走。int8版本下,我把余弦相似度阈值从0.5降到0.42,误识率和拒识率勉强平衡;float16版本下阈值回到0.5也没问题。这个参数没有统一答案,需要拿你自己的底库和现场图去试。
6. 从工程落地到后续扩展
6.1 从单帧推理到实时视频流
前面演示的是最简单的单线程循环,实际工程里不会这么干。接入实时视频流时,我最常用的方案是双线程加队列:采集线程持续读摄像头、做基础预处理,把帧丢进一个有限长度的队列;推理线程从队列里取帧,做检测、识别和结果显示。如果帧率因为推理慢而掉帧,队列能起到缓冲作用,不至于把系统拖死。
摄像头选型方面,普通USB摄像头接上就能用,cv2.VideoCapture(0)直接搞定。如果接MIPI CS摄像头,就不能用OpenCV直接读了,需要走Rockchip的rkmpp或rkcam接口,代码复杂度会高一个台阶。首次调试建议先用USB摄像头把逻辑调通。
6.2 特征库管理与检索
特征库的存储方式取决于规模。如果就几十个人,一个pkl文件存字符串加numpy数组就够了,加载进内存后直接线性比对。如果扩展到几百上千人,线性扫描的耗时虽然还在可接受范围,但代码里依然建议按ID建立索引,避免反复遍历整个列表。
我实际用下来,识别环节“注册新用户”的流程一定要单独写脚本,不要跟识别主程序耦合。注册时用同一套检测、对齐、提特征逻辑,保证跟识别时的特征分布一致。如果注册时用的是自拍大头贴,识别时现场摄像头角度不同,特征差异会非常大,这是我踩过的一个很典型的坑。
特征库更新也需要考虑。人员离职、新增、照片重拍这些操作,都应该只改库文件,不用重启主程序。我用的是pickle加一个小的更新函数,每次保存时写临时文件再rename,避免写一半断电导致整个库损坏。
6.3 个人踩坑体会与最后建议
玩这套系统最深刻的体会是,边缘AI部署真正花时间的从来不是模型训练,而是模型转换、后处理对齐和量化调试这一连串工程问题。RetinaFace加rec这套组合本身很稳定,只要预处理参数对齐、anchor解码对路,RK3588的NPU是完全能扛得住的。
我建议所有想上手的同学,拿到板子后先不要一上来就搞识别,老老实实先把检测跑通,能够稳定框出人脸并输出关键点,再往下走半段路。检测阶段如果都能看到关键点稳稳搭在眼睛鼻子上,后面的对齐和识别就是顺理成章的事了。要是跳步图快,最后检出的框不对、特征提取结果一团乱,排查起来反而更浪费时间。
最后分享一个小技巧:转换模型之前,先用RKNN-Toolkit2自带的simulator在原PC上跑一遍onnx的推理结果,跟PyTorch的原始输出对比一下,确认预处理和模型本身没毛病,再烧到板子上验证。这个习惯帮我省掉了大量在板端来回调试的折腾。香橙派这类国产开发板上手初期总会让人血压升高,但一旦摸清了工具链的脾气,后面跑什么模型都会顺手很多。