去年下半年我接了一个边缘端人脸识别的小项目,需求很直接:在低功耗设备上离线跑人脸检测,还要能输出人脸关键点,而且关键点不能像老方案那样只有5个或者68个,客户明确要求能支持更细的面部特征定位,最后定下来就是“瑞芯微 + EASY EAI + RV1126B + 人脸98关键点算法识别”这套组合。拿到板子之前我也纠结过要不要直接用RV1106或者RK3568,但对比一圈之后发现RV1126B的性价比和易用性最适合这个场景。这篇文章就是我从拿到开发板、搭SDK环境、转模型到最终在板子上跑通98点识别的完整记录,里面包含我踩过的坑、调过的参数和实测数据,希望对正在做同类项目的朋友有参考价值。
1. 项目背景与方案选型:为什么是RV1126B + 98点
1.1 芯片选型背后的逻辑
人脸关键点识别这个需求,听起来简单,但真正落到边缘端就牵扯到几个硬约束:算力够不够、内存够不够、摄像头接口好不好调、整机功耗能不能压住。我一开始看的是RV1106,这颗芯片便宜是真便宜,但NPU算力只有0.5 TOPS左右,跑一个人脸检测就吃掉大半算力,再叠加98关键点模型,帧率很难看。再看RK3568,算力是够,但芯片面积、外围成本、DDR配置都往上走,对一个做单目视觉的产品来说有点浪费。RV1126B属于中间态,NPU算力大约2 TOPS(INT8),虽然不及更高端的RK3588,但在2~3W这个功耗段里已经能扛住两个模型串行推理。它还带ISP,直接接MIPI摄像头做图像预处理很省事,这点在做人脸类应用时非常重要,因为人脸识别的效果好坏,光线好不好、图像清不清晰占了很大权重。
细看RV1126B这颗芯片,实际上它在RV1126的基础上做了DDR内置整合,部分型号直接片内集成128MB DDR,这样硬件设计的难度降了一截,不需要像RK3568那样外挂DDR颗粒,Layout和成本都友好很多。当然代价是内存有限,跑大模型容易OOM,所以模型必须严格控制参数量和输入分辨率。这个特性决定了我们在选算法模型时,不能用那种动辄上百MB的“大而全”网络,而是要走轻量化路线,用蒸馏出来的人脸检测网络加轻量关键点网络。
1.2 为什么关键点要选98点
行业里比较常见的方案是68点(dlib经典方案)、106点(部分商用SDK习惯)、468点(MediaPipe Face Mesh),以及98点。我这次选98点,主要是基于精度和算力的平衡。68点对眉毛、眼睛轮廓的描述比较粗,遇到大幅度侧脸或者夸张表情时,轮廓贴合度明显不够;468点虽然效果细腻,但参数数量和计算量都上来了,在RV1126B这种2T算力的芯片上,实时性会变得很紧张。98点这个粒度介于两者之间,能比较准确地描述眼睛、眉毛、鼻子、嘴唇和脸部轮廓,同时模型参数量可以控制在几MB级别,INT8量化之后基本不影响实时性。
实际项目中,我参考了公开的WFLW数据集标注习惯,这个数据集本身就是98点标注,训练资源也相对好找。如果你手头有自定义需求,比如要在特定点位(眼角、嘴角、鼻尖)做业务逻辑判断,98点也可以在这几个区域提供足够多的候选点,方便做局部坐标插值。
1.3 EASY EAI开发套件的价值
EASY EAI是围绕瑞芯微方案做开发套件的厂商,我拿到的是EASY EAI推出的RV1126B评估板。这类套件最大的价值不是板子硬件本身,而是它帮你把很多底层的“脏活”提前趟了一遍:DDR初始化配置、MIPI摄像头驱动、WIFI/BT模组适配、SDK版本对齐等等。如果你直接用瑞芯微原厂的公板或者自己画板,光Bring Up外设就要花不少时间。对算法工程师来说,EASY EAI这种套件可以让我们把精力集中在模型和业务代码上,而不是先花两周解决摄像头I2C读不到寄存器的问题。
倒不是说原厂SDK不好,而是原厂SDK默认面向方案商,U-Boot、内核、Buildroot里要改的东西比较多。EASY EAI做的比较贴心的是它会提供一个相对稳定的基线版本,并且把常用外设的DTS和设备树配置都整理清楚,照着示例改即可。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 开发环境搭建与SDK准备
2.1 PC端环境与SDK目录结构
开发这行最怕的就是环境不一致,RKNN工具链尤其吃版本。我最后把环境分成三层:PC端做模型转换和调试、板端做推理运行、两者之间通过网线或者USB共享文件。
先说我PC端的配置:Ubuntu 20.04 x86_64,Python 3.8,conda管理环境。RV1126B对应的RKNN工具链是rknn-toolkit2,我用的版本是1.6.0(不同SDK版本对应的工具链版本不同,建议先确认你手上SDK里的librknnrt版本)。下载rknn-toolkit2仓库后,创建独立conda环境:
bash复制conda create -n rknn python=3.8
conda activate rknn
pip install rknn_toolkit2-1.6.0-cp38-cp38-linux_x86_64.whl
安装完成后可以验证一下导入是否正常:
bash复制python -c "from rknn.api import RKNN; print('RKNN import ok')"
如果这一步报缺库,一般是缺numpy和opencv的依赖,补装一下就好。有一点要特别注意:rknn-toolkit2的PC端版本和板端的librknnrt.so必须配套,跨版本经常会出现模型初始化失败或者推理结果完全不对的情况,最好在README里确认两者的版本对应关系。
2.2 板端SDK编译与烧录
EASY EAI的RV1126B板子拿回来,第一件事不是写算法,而是先编译SDK并烧录固件,确认板子能正常启动。SDK目录一般包含u-boot、kernel、buildroot、app等子目录,编译前先同步环境变量:
bash复制source build/envsetup.sh
lunch
lunch之后会列出一堆产品配置,选择EASY EAI对应的RV1126B配置,然后执行:
bash复制./build.sh
首次编译时间比较久,我机器上大概半小时左右,主要看内核和Buildroot的编译情况。编译完成之后会生成固件目录,里面是打包好的update.img。烧录方式两种:Windows下用瑞芯微开发工具,Linux下用upgrade_tool。我习惯Linux下用upgrade_tool,先把板子进入Loader模式(按住板上的RECOVERY键再上电,或者通过串口命令行输入reboot loader),然后执行:
bash复制./upgrade_tool uf update.img
烧录完成后板子会自动重启。如果一切正常,串口终端会打印到buildroot的登录提示符,默认用户通常是root,密码为空或者123456,具体看EASY EAI的出厂说明。
2.3 MIPI摄像头与ISP的正确姿势
RV1126B自带ISP,但不是说接上摄像头就能用。EASY EAI官方套件一般会配OV5645或GC2053这类Sensor,板级配置里已经把驱动写好了。我这次用的是GC2053,分辨率1920x1080。启动后先用v4l2确认设备节点:
bash复制v4l2-ctl --list-devices
正常情况下会看到类似“gc2053 2-003b: gc2053”的设备,节点通常是 /dev/video0。如果要看实时画面,可以用gst-launch,但RV1126B上更常见的做法是通过瑞芯微的media模块pipeline来做ISP处理。我建议先跑一下官方自带的摄像头测试脚本,确认图像能出来再做算法。很多新手一上来就折腾v4l2,忽略了ISP的三A(AE、AWB、AF)配置,结果画面过暗或者偏色严重,最后还以为是摄像头坏了。
3. 人脸关键点模型转换与量化
3.1 模型结构设计与选型注意点
整个视觉pipeline分为两段:第一阶段是人脸检测,输出人脸框坐标;第二阶段是在人脸框区域内做人脸98关键点回归。检测模型我用了RetinaFace的轻量版,把输入分辨率控制在320x320,后续为了加速也可以降到160x160,但评估下来320x320比较稳。关键点模型结构参考了MobileNetV2的Backbone加全连接输出98*2个坐标,输入分辨率112x112。
这里要强调一下:关键点模型输出的是相对人脸框的归一化坐标,不是绝对图像坐标。这样设计的好处是模型只需要关注人脸区域内的相对位置,泛化性更好,对检测框轻微偏移的容忍度也高一些。后处理的时候再根据检测框的坐标做一次线性映射回到原图,代码实现不复杂,训练和转换时也更稳定。
另一个关键点是模型导出ONNX时,要固定输入shape。RKNN对动态shape的支持虽然也在变好,但为了稳定推理,我建议把batch固定为1,输入尺寸固定,避免在板端出现意外的shape冲突。
3.2 ONNX转RKNN的主要参数
拿到ONNX模型之后,用rknn-toolkit2转换。RV1126B的目标平台标识是rv1126,转换代码大致如下:
python复制from rknn.api import RKNN
rknn = RKNN()
rknn.config(mean_values=[[0, 0, 0]], std_values=[[255, 255, 255]], target_platform='rv1126')
rknn.load_onnx(model='face98.onnx')
rknn.build(do_quantization=True, dataset='dataset.txt')
rknn.export_rknn('face98.rknn')
这里的mean_values和std_values一定要和训练时的预处理一致。我这次用的模型训练时输入是0~255归一化到0~1,所以mean为0、std为255。有的模型mean是103.94之类,那就需要基于你的模型来定,千万不要照抄。dataset.txt里放的是用于量化的校准图片路径列表,建议至少准备50张来自真实场景的图片,内容要尽量覆盖不同光照、姿态和人种。校准集如果太单一,量化后模型精度会崩得很厉害,后面会细说。
3.3 量化后精度验证与精度回退策略
RKNN转完模型之后,强烈建议先用模拟器评估一下量化精度变化,再上板子。rknn-toolkit2提供了精度分析功能,可以逐层输出量化误差,但由于分析过程比较耗时,我一般只用它做关键层的检查。更实用的方式是直接写一个测试脚本,用同一组测试图片对比ONNX模型和RKNN模型的输出差异。人脸关键点任务对位置精度要求很高,如果量化后的平均归一化误差(NME)超过0.02,那画出来的点就会明显偏移。
如果量化精度不理想,有几个可以尝试的补救措施。第一是增加校准集图片数量,我试过从20张加到100张,NME能下降30%左右;第二是开启混合量化,把对精度影响大的层保留为float16甚至float32,RV1126B的NPU支持部分通道回退,代价是推理时间略微增加;第三是检查模型里是否有不支持的算子,比如动态尺寸、复杂的Grid Sample等,遇到这种情况尽量把算子改写成卷积或全连接组合。
4. 板上推理实现与性能优化
4.1 Python接口还是C接口
RV1126B的板端推理有两种常见方式:Python接口(rknn-toolkit-lite)和C接口(librknnrt.so)。如果你只是做功能验证或者快速原型,Python接口很方便,代码短、调试块,我在第一阶段就是用它验证模型输出和后处理的逻辑是否正确。但实际产品化或者需要追求更高帧率时,C接口才是正路,因为它省掉了Python解释器开销,也更容易做内存池复用和多线程流水线。
我的建议是:前期用Python把算法逻辑调通,然后把核心推理和后处理封装成C函数,最后在业务代码里加上线程池做pipeline。如果你对C不熟,也可以先用Python把整个流程跑完,看是否满足业务需求。RV1126B跑Python接口转C接口,单帧推理时间变化最大的还是在图像缩放和颜色转换上,NPU本身的耗时基本差不多。
4.2 Python完整推理流程示例
下面是我在板子上验证用的一套简化流程,主要包括读图、缩放、推理、后处理四个步骤。这里省略了检测模型的细节,假设已经拿到了人脸框。
python复制import cv2
import numpy as np
from rknnlite.api import RKNNLite
rknn = RKNNLite()
ret = rknn.load_rknn('face98.rknn')
rknn.init_runtime()
img = cv2.imread('test.jpg')
rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
# 假设检测得到的人脸框
x1, y1, x2, y2 = 100, 80, 300, 420
face = rgb[y1:y2, x1:x2]
face = cv2.resize(face, (112, 112))
# 推理 输入要求是RGB、HWC、uint8
outputs = rknn.inference(inputs=[face])
pts = outputs[0].reshape(-1, 2) # 98*2 -> 98,2
# 归一化坐标映射回原图
landmarks = np.zeros_like(pts)
landmarks[:, 0] = pts[:, 0] * (x2 - x1) + x1
landmarks[:, 1] = pts[:, 1] * (y2 - y1) + y1
for (px, py) in landmarks:
cv2.circle(img, (int(px), int(py)), 2, (0, 255, 0), -1)
cv2.imwrite('result.jpg', img)
实际跑下来,关键点模型在112x112输入下单次推理耗时大概5~8ms,检测模型320x320输入大约20~35ms,整个pipeline在35毫秒左右,换算过来差不多28帧FPS,已经能满足大部分实时人脸应用的需求。但要注意,Python接口读摄像头帧并进行cv2.resize时,会有额外的CPU开销,经验值是把FPS降到22左右。如果需要更高的帧率,推荐用C接口,或者把resize操作放到NPU的RGA模块做硬件加速。
4.3 性能优化三板斧:多线程、RGA和内存复用
项目做到后期,我总结出三个很实用的优化手段。第一是多线程pipeline:把摄像头采集、ISP处理、NPU推理、显示/上传分别放到不同线程,通过队列传递数据,避免某个环节阻塞整个链路。第二是使用RGA硬件加速做图像缩放和格式转换,RV1126B内部有RGA模块,能在几毫秒内完成1080P到320x320的缩放,大幅减少CPU占用。第三是内存复用:不要在循环里反复申请和释放图像缓冲区,最好在初始化阶段一次性把Buffers Allocate出来,推理完成后原地覆盖。
这三个手段都做了之后,整体帧率从22帧能提升到30帧左右。如果对延迟有更高要求,还可以把检测和关键点两个模型放到同一个RKNN context里,用同一个NPU执行流交替推理,避免重复初始化带来的开销。
5. 常见问题与排查实录
5.1 量化后关键点严重偏移
这是整个项目里让我最头疼的问题。起初我用了20张校准图,量化后跑测试图,眼睛和嘴角点全乱了,NME到了0.06以上。后来排查下来原因有几个:第一是校准图质量太差,里面有大量模糊和过度曝光的样本,导致量化参数算偏了;第二是测试图片和训练集分布差异太大,出现极端角度人脸时会失效。解决办法是把校准图换成贴近真实场景的清晰图片,数量加到100张,另外在dataset.txt里可以添加轻微的随机裁剪样本做数据增强,效果会好很多。
5.2 检测框正常但98点整体飘移
有时候检测框位置是正确的,但输出的98个点整体偏移到框边缘,看起来像是模型输出的坐标没有对齐。这个问题大概率出在后处理的坐标映射上。很多关键点模型训练时输出的不是[0,1]的相对坐标,而是相对于某一固定尺寸(比如112x112)的像素坐标。如果用了错误的分母,位置自然全错。我的建议是先从单张测试图开始,打印出输出的原始值范围,再根据范围反推正确的归一化方式,不要盲目套用公式。
5.3 MIPI摄像头黑屏或者花屏
这个问题两个原因最常见:一是Sensor供电时序不对,二是MIPI时钟频率和驱动不匹配。EASY EAI官方底板一般情况不会出现供电问题,但如果你换过摄像头型号,就要去检查DTS里的regulator配置和GPIO控制。花屏问题多数是lane数或者时钟频率配置错误,检查一下DTS里的mipi dphy设置与Sensor的datasheet是否一致。另外一定要看开机时内核日志里有没有sensor探测失败的信息,如果有,优先查I2C地址是否冲突。
5.4 RV1126B的MIC电路调试注意点
这个跟人脸算法无关,但既然标题里提到了RV1126B,我还是想说一下。如果你用EASY EAI官方底板,音频采集基本不需要操心;但如果你是自己设计底板,MIC电路很容易踩坑,最常见的问题是偏置电压不足和差分走线太长导致底噪明显。RV1126B内置Codec的MIC单端输入需要外部提供偏置电阻,差分输入则要保证两条走线等长且远离数字信号线。我调试的时候出现过录音波形看起来正常但实际播放有强烈电流声的情况,最后把MIC偏置电阻从2.2K改成4.7K后明显好转。
5.5 问题排查速查表
| 现象 | 可能原因 | 排查思路 |
|---|---|---|
| 量化后关键点飘移 | 校准集质量差/数量不足 | 增加真实场景图片,检查NME |
| 输出点整体偏移 | 坐标归一化方式不对 | 打印原始输出范围,反推公式 |
| 摄像头黑屏 | Sensor驱动未加载 | 检查内核日志和I2C地址 |
| 花屏/画面撕裂 | MIPI lane/时钟配置错 | 核对DTS和Sensor datasheet |
| 启动卡死 | 固件版本与板子不匹配 | 重新烧录对应版本固件 |
| MIC底噪大 | 偏置电路/走线问题 | 调整偏置电阻,改差分走线 |
| 推理速度慢 | 使用了Python接口 | 换C接口或启用RGA加速 |
写在最后的实践心得
这套RV1126B + EASY EAI + 人脸98关键点方案做下来,我最深的体会是:用瑞芯微的芯片做视觉应用,真正的难点往往不在NPU算力,而在从“模型在PC上跑通”到“模型在板子上稳定运行”这最后一公里,这中间包含模型转换、量化、后处理坐标系统一、外设调试等一系列细节。98点关键点相比5点和68点,在边缘端是一个比较理想的中间方案,信息量足够、计算负担又不高。我在实际量产准备中还会继续优化的方向包括:把检测模型和关键点模型做通道剪枝、在板端加入人脸质量评估模块、把整个算法封装成标准C API方便上层应用调用。如果你也正在做类似项目,希望这篇记录能帮你少走一些弯路。
