
简介面向RK3568嵌入式平台的人脸检测与人脸五点关键点检测实战代码包适合具备一定AI基础、希望将深度学习模型落地到边缘设备的开发者能帮助打通从模型部署到实时推理的完整链路并减少环境配置与踩坑时间。压缩包共2个文件体积仅1.16MB其中RKNN模型基于SCRFD轻量级检测算法转换而来已针对RK3568 NPU优化可同时输出人脸框以及左右眼中心、鼻尖、左右嘴角5个关键点Python主程序集成图像预处理、模型推理与结果绘制无需额外训练即可直接运行。目前已有378人学习/下载项目直接对应真实人工智能项目实战场景便于对照环境复现。通过源码与例程可快速掌握RK3568上RKNN模型的调用方法、NPU加速推理配置和关键点坐标解析流程也可迁移到智能安防、门禁打卡、智慧零售等嵌入式视觉应用中是一套轻量实用的工程参考。1. RK3568做端侧人脸检测NPU虽然小但够用如果让我在华硕Tinker Board 2S、香橙派5和RK3568开发板之间选一块做实时人脸检测我会选RK3568。不是因为它的A55核心比RK3566快多少而是片上带了独立NPU能直接跑INT8量化模型CPU占用率还能留出一大截给UI和业务逻辑。项目遇到的第一个认知问题是没有GPU的板子是不是就跑不动深度学习实际上RK3568的NPU部署ResNet类模型或RetinaFace这类轻量检测模型完全够用瓶颈通常不在算力而在模型算子能不能转成RKNN。我这次用的方案是RetinaFace MobileNet0.25把“人脸检测”和“人脸五点关键点检测”放在同一个模型里一串推理同时拿到框、置信度和两只眼睛、鼻尖、左右嘴角的坐标。对人工智能大作业来说这个选题能跑在真实嵌入式Linux上比单纯跑GPU demo更有说服力对产品原型来说五点关键点可以直接喂给后续人脸对齐、活体判别或美颜贴纸模块。2. 模型选型与RKNN转换ONNX到RKNN的量化陷阱2.1 为什么选RetinaFace而不是单独再挂一个关键点网络人脸检测的常见选项有MTCNN、RetinaFace、SCRFD、YuNet。MTCNN的检测头很轻但五点关键点输出精度一般在RK3568的NPU上跑还依赖多级级联延迟不稳定。RetinaFace的工作方式是在检测头里同时回归边界框和五个人脸关键点共享主干网络的特征因此NPU只需要对同一张特征图做三次轻量卷积输出不会因为“检测关键点”两个任务各自推理一次。SCRFD精度更高但需要更复杂的anchor或自定义算子转RKNN时容易卡在算子兼容性上。所以这里我优先选RetinaFace MobileNet0.25导出后的ONNX算子以Conv和Concat为主NPU支持度高量化效果也好。RK3568与RK3566的NPU能力在同一代产品上基本一致这个模型在RK3568上调通之后放到RK3566的板子上基本不需要重新做算子适配。但要注意RKNN-Toolkit2的版本跟固件里的RKNPU驱动有对应关系单独从pip拉最新版并不意味着板端能正常运行。为了省掉后续来回倒腾我建议直接用瑞芯微SDK里带的那一版rknn-toolkit2或者以官方文档给出的版本匹配表为准。2.2 转换脚本把ONNX变成RKNN在PC上准备一个Python虚拟环境安装rknn-toolkit2。具体wheel包在SDK的packages目录里安装命令通常是cd rknn-toolkit2/packages pip install rknn_toolkit2-*.whl pip install onnx onnxruntime opencv-python安装后先验证from rknn.api import RKNN能正常导入。接下来写转换脚本下面这段是稳定可用的最小流程from rknn.api import RKNN rknn RKNN(verboseTrue) rknn.config( mean_values[[127.5, 127.5, 127.5]], std_values[[127.5, 127.5, 127.5]], target_platformrk3568 ) ret rknn.load_onnx( modelface_det.onnx, outputs[face_rpn_cls_prob_reshape, face_bbox_pred, face_landmark_pred] ) assert ret 0, load onnx failed ret rknn.build(do_quantizationTrue, datasetdataset.txt) assert ret 0, build rknn failed rknn.export_rknn(face_det.rknn) rknn.release()mean_values和std_values必须和训练时的数据预处理一致RetinaFace原仓库最常用的参数是127.5和127.5等价于把像素值从[0,255]归一化到[-1,1]。如果你用的是自己训练的版本这两个参数必须改掉否则推理结果会整体偏移表现为检测框全部偏大或偏小。outputs参数用于指定导出模型输出节点的顺序但部分rknn-toolkit2版本不支持这个参数这时需要在ONNX导出阶段就固定输出名称板端后处理按转换前的输出顺序写就好。do_quantizationTrue打开INT8量化此时需要提供一个dataset.txt格式如下./imgs/001.jpg ./imgs/002.jpg ./imgs/003.jpg每行一张典型RGB图片不要用黑白图也不要只放一张。量化样本建议20到50张横竖比例不同的真实场景照片样本太少时最后几个卷积层的量化scale会卡在异常值上导致置信度飙升但框全乱跳。rknn.build完成之后会打印各层的量化误差我一般只关注误差排在前面那几层是不是集中在最后的head层如果是说明量化掉点主要在关键点回归任务需要回原模型用FP32在板端先跑一遍确认基线。2.3 转换阶段最容易踩的三个问题现象常见原因处理方式load_onnx返回-1ONNX版本里有RKNN不支持的自定义算子在导出时关闭NMS融合或把自定义算子替换成标准卷积build时内存暴涨dataset图片分辨率过大或数量过多将图片缩放到模型输入尺寸建议RGB888并居中裁剪量化后五点关键点偏移量化样本没有覆盖不同角度的人脸在dataset里增加侧脸、俯仰、暗光样本重点覆盖肤色差异这张表其实就是我排错时先看的顺序先看算子是否支持再看输入尺寸最后怀疑量化样本。rknn.build过程中如果出现AttributeError多半是rknn-toolkit2和protobuf版本冲突建议固定到requirements.txt里指定的protobuf版本再试。转换成功的face_det.rknn一般只有几百KB到几MB具体大小取决于输入分辨率不要用ONNX模型大小去判断RKNN内部还会做算子融合。3. 人脸检测的RKNN推理实现anchor生成、解码和NMS3.1 板端初始化RKNNLite比RKNN更合适RKNN接口在板端也能跑但每次初始化会带上下载模型时的调试信息内存占用偏高。更常见的是使用rknn-toolkit2-lite提供的RKNNLite接口它没有PC端的训练和调试功能只负责加载模型和推理运行库更小。我的初始化代码如下import numpy as np import cv2 from rknnlite.api import RKNNLite rknn_lite RKNNLite() ret rknn_lite.load_rknn(face_det.rknn) assert ret 0, load rknn failed ret rknn_lite.init_runtime(core_maskRKNNLite.NPU_CORE_AUTO) assert ret 0, init runtime failedload_rknn从文件载入RKNN模型init_runtime的core_mask默认值是NPU_CORE_AUTO表示让NPU调度器自动分配到三个核心之一。单路摄像头场景我用AU TO多路视频流场景我会显式指定NPU_CORE_0和NPU_CORE_1让不同通道的推理分散到不同核心避免调度抖动。如果用C语言跑这套流程rknn_init对应上面的初始化rknn_run后通过rknn_outputs_get拿三个输出张量官方C示例里都有不重复贴。3.2 anchor生成与解码不要跳过这一步RetinaFace的输出是特征图上的预测值需要结合anchor转成实际坐标。不同仓库导出的ONNX输出结构可能不同这里以后处理阶段能直接读到的三个输出为例分类分支(1, 2, H, W)、框回归(1, 4, H, W)、关键点回归(1, 10, H, W)。下面这段以单层特征图输出为例如果你导出的是三个尺度就在外层加一个循环每组特征图对应自己的anchor表和输出索引。def generate_anchors(feature_map_sizes, min_sizes, steps): anchors [] for fmp_size, min_size, step in zip(feature_map_sizes, min_sizes, steps): fmp_h, fmp_w fmp_size for i in range(fmp_h): for j in range(fmp_w): cx (j 0.5) * step cy (i 0.5) * step for s in min_size: anchors.append([cx, cy, s, s]) return np.array(anchors, dtypenp.float32)这里feature_map_sizes是三个输出特征图的高和宽steps是相对输入图像的步长min_sizes是每个位置预置正方形的边长。cx和cy用(j0.5)*step是为了把anchor中心定在每个特征像素的中心而不是左上角。这一段生成的是简化版anchor如果你的模型来自官方RetinaFace仓库建议直接沿用原代码里从prior_box.py生成的anchor文本表原理是一样的。解码时需要对conf、bbox、kpt三个分支同时操作。我给出一个直接可运行的decode函数def decode(retina_out, anchors, conf_thresh0.5): cls_prob retina_out[0].reshape(2, -1).transpose(1, 0) bbox_pred retina_out[1].reshape(4, -1).transpose(1, 0) kpt_pred retina_out[2].reshape(10, -1).transpose(1, 0) scores cls_prob[:, 1] variance np.array([0.1, 0.2], dtypenp.float32) boxes np.zeros_like(bbox_pred, dtypenp.float32) boxes[:, 0] anchors[:, 0] bbox_pred[:, 0] * variance[0] * anchors[:, 2] boxes[:, 1] anchors[:, 1] bbox_pred[:, 1] * variance[0] * anchors[:, 3] boxes[:, 2] anchors[:, 2] * np.exp(bbox_pred[:, 2] * variance[1]) boxes[:, 3] anchors[:, 3] * np.exp(bbox_pred[:, 3] * variance[1]) keep scores conf_thresh return boxes[keep], scores[keep], kpt_pred[keep], keepvariance[0]用于中心偏移variance[1]用于宽高缩放这两个数来自训练时的prior box定义不能随便改。框解码后boxes[:,0:2]是中心点坐标boxes[:,2:3]是宽高后续需要自己转成x1y1x2y2。keep索引后续还要用来取关键点所以要单独返回不能只返回过滤后的数组。3.3 NMS和画框过滤到少量候选框之后人脸检测一般还会再做NMS避免同一张脸被多个anchor框出来。OpenCV自带的cv2.dnn.NMSBoxes可以直接用但不同OpenCV版本返回的shape不一样我这里做一层兼容def nms(boxes, scores, iou_thresh0.4): boxes_xyxy np.zeros_like(boxes) boxes_xyxy[:, 0] boxes[:, 0] - boxes[:, 2] / 2 boxes_xyxy[:, 1] boxes[:, 1] - boxes[:, 3] / 2 boxes_xyxy[:, 2] boxes[:, 0] boxes[:, 2] / 2 boxes_xyxy[:, 3] boxes[:, 1] boxes[:, 3] / 2 idx cv2.dnn.NMSBoxes( boxes_xyxy.tolist(), scores.tolist(), 0.0, iou_thresh ) if isinstance(idx, tuple): idx idx[0] return np.array(idx).reshape(-1)NMS的score阈值设为0.0因为进入NMS前已经用conf_thresh过滤过重复过滤只会让中间参数更混乱。iou_thresh0.4表示两个框重叠超过40%时保留分数更高的那个。人脸之间重叠少门槛可以放到0.45如果是密集场景比如多人聚会照片我会把IoU阈值降到0.3。注意NMS输入框必须是xyxy格式不能直接填原模型的中心宽高格式否则重叠判断会错误。最终把框画到图像上for i in idx: x1, y1, x2, y2 boxes_xyxy[i].astype(int) cv2.rectangle(frame, (x1, y1), (x2, y2), (0, 255, 0), 2)到这里人脸检测已经能出框了后面的关键点检测才能有意义。别急着把这些代码合并到完整流程里建议先用单张图片跑通再看摄像头实时帧。4. 五点关键点检测坐标顺序、归一化与人脸对齐4.1 关键点解码从回归输出到像素坐标RetinaFace把五点的偏移量放在同一个分支里顺序是左眼x、左眼y、右眼x、右眼y、鼻尖x、鼻尖y、左嘴角x、左嘴角y、右嘴角x、右嘴角y。这个顺序来自Wider Facial Landmark in the Wild (WFLW)不同训练代码也可能交换左右眼顺序先仔细看模型训练时的标注文件。解码原理和框回归一样先用anchor中心加上偏移量再乘scaledef decode_kpts(kpt_pred, anchors, keep): kpts np.zeros((kpt_pred.shape[0], 5, 2), dtypenp.float32) for i in range(5): kpts[:, i, 0] anchors[keep, 0] kpt_pred[:, i*2] * 0.1 * anchors[keep, 2] kpts[:, i, 1] anchors[keep, 1] kpt_pred[:, i*21] * 0.1 * anchors[keep, 3] return kptskeep来自上一节decode出的有效索引这一步必须在NMS之后做因为NMS会再次减少候选框数量。关键点坐标和框一样是相对于模型输入分辨率如果输入图像是640x640输出坐标就是640坐标系下的值不需要额外乘缩放系数如果输入是任意尺寸需要在预处理时记录缩放比例把坐标映射回原图。实际画点时还要做边界裁剪否则关键点在图像边缘时cv2.circle会因为坐标为负数直接报错。4.2 用五官对齐做人脸预处理五点关键点最直接的用处不是画点而是人脸对齐。直接把检测到的带角度人脸送进识别模型识别率会明显下降用两眼连线做旋转可以让后续模型看到正脸。我常用最简单的角度对齐import math def align_face(img, kpts, output_size(112, 112)): (lx, ly) kpts[0] (rx, ry) kpts[1] angle math.degrees(math.atan2(ry - ly, rx - lx)) M cv2.getRotationMatrix2D((lx, ly), angle, 1) aligned cv2.warpAffine(img, M, (img.shape[1], img.shape[0])) # 以左眼为参考点重新裁剪 x1, y1 int(lx - 30), int(ly - 30) x2, y2 int(lx 100), int(ly 100) face_crop aligned[y1:y2, x1:x2] return cv2.resize(face_crop, output_size)cv2.getRotationMatrix2D以左眼为旋转中心角度取两眼连线相对于水平轴的角度。warpAffine之后整张图被旋转再以左眼为基准裁剪固定大小的区域。这里裁剪范围的30和100是我根据常见模型输入定的经验值实际使用时需要按你的数据集重新标定。如果追求更高精度可以用三点相似变换代替旋转变换很多开源人脸识别仓库里都有similarTransform函数。在智慧医疗类项目中这个对齐步骤尤其重要。比如做人脸皮肤病检测如果不先对齐额头和下巴的样本位置会漂移分类器很容易学到背景噪声。先把人脸旋转到统一姿态再切出额头、两颊、下巴等多个局部区域作为训练样本数据集的稳定性会好很多。这个思路同样适用于戴口罩检测、疲劳驾驶检测这类需要固定区域特征的场景关键点检测不是终点而是给后续AI模块做坐标系规整的第一步。4.3 关键点精度验证NME五点关键点模型调优时不能只靠肉眼判断画点准不准一般在验证集上计算NMENormalized Mean Error归一化距离用两只眼睛瞳孔的距离def nme(pred_kpts, gt_kpts, eye_dist): diff pred_kpts - gt_kpts point_err np.sqrt(np.sum(diff ** 2, axis1)) return np.mean(point_err) / eye_disteye_dist可以是左眼到右眼的欧氏距离。NME小于0.05通常意味着关键点平均偏差在眼距的5%以内这个水平用于人脸对齐是够的如果NME大于0.1多半是量化掉点或分辨率不足先回到FP32模型对比再用量化样本修正。我就是用这个指标来确认RKNN INT8后的模型到底能不能上线而不是看几个测试视频就下结论。5. 部署调优与常见坑NPU调度、设备树和稳定性验证5.1 NPU多核调度与异步模式RK3568的NPU有三个核心RKNNLite的core_mask可以把模型固定在某个核心上。单路推理时用NPU_CORE_AUTO即可多路摄像头时我会给每个通道创建独立的RKNNLite实例并指定NPU_CORE_0或NPU_CORE_1实测能降低峰值延迟。还有一个容易被忽略的async_mode参数它让inference不再阻塞主线程适合视频流里做双缓冲rknn_lite.init_runtime(async_modeTrue)开启后inference会立刻返回一个job主线程继续抓下一帧等下一帧需要推理时再调用get_outputs(job)取结果。这里要注意get_outputs之后必须马上处理输出或者复制数据因为内部缓冲区在下一轮推理时可能被复用。5.2 设备树与摄像头时序问题人脸检测的输入如果来自MIPI摄像头Linux端的设备树必须正确描述sensor节点。常见的RK3568底板用OV5695或OV8858设备树里要把对应的I2C地址和电源时序配好否则摄像头反复启动失败。我调试OV5695时遇到最多的问题是reset脚和pwdn脚顺序反了表现形式是sensor not found。这时先用i2cdetect -y 0确认sensor地址是不是能找到再检查pinctrl-0里的GPIO定义。如果需要把触摸屏竖屏改成横屏则要修设备树的touchscreen节点把touchscreen-inverted-y和touchscreen-inverted-x按实际面板方向打开这属于独立的调试项和NPU推理无关但经常会被混在一起排查。5.3 稳定性验证打卡长时间跑人脸检测项目最怕的不是单帧跑多慢而是连续跑一小时之后内存越用越多。我习惯在板端加一个每1000帧打印一次统计信息的脚本import time start time.perf_counter() for i in range(1000): outputs rknn_lite.inference(inputs[input_blob]) if i % 100 0: elapsed time.perf_counter() - start print(f{i} frames, avg {(elapsed/(i1))*1000:.1f} ms/frame)如果发现延迟逐渐变大优先看是不是inference返回的输出被引用没有释放或者cv2.imshow在无显示器环境下等待。还有一点NPU负载可以通过debugfs查看cat /sys/kernel/debug/rknpu/load。这个接口会输出三个核心的使用率当只有单路推理时如果三个核心的使用率都很高说明当前模型可能被调度器拆成了多核并行如果只想单核跑固定NPU_CORE_0即可。cat报文件不存在时先执行mount -t debugfs none /sys/kernel/debug再看负载。本文还有配套的精品资源点击获取