ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

RK3568 NPU实战:RetinaFace人脸检测与五点关键点部署全流程

RK3568 NPU实战:RetinaFace人脸检测与五点关键点部署全流程 简介面向RK3568嵌入式平台的人工智能实战资源聚焦人脸检测与人脸五点关键点检测任务适合有一定深度学习基础、希望在边缘设备上部署视觉AI应用的开发者。资源包共2个文件包含1个RKNN格式模型和1个Python推理脚本压缩包整体仅1.16MB轻量紧凑便于直接部署验证。该方案基于SCRFD模型在RK3568的NPU加速下完成人脸框回归与双眼、鼻尖、嘴角五点定位主程序涵盖了图像读取、预处理、模型推理及检测结果可视化全流程代码结构清晰便于二次开发。通过这份资源读者可以掌握RKNN模型转换方法、嵌入式推理环境搭建思路以及将人脸检测与关键点检测串联落地的完整实践经验。已有378人学习下载适合作为智能家居、安防监控等场景中边缘人脸识别模块的参考实现。1. 在RK3568上跑人脸检测和五点关键点先想清楚算力分配RK3568未必是端侧AI板子里最亮眼的但它把CPU、GPU、NPU放在同一颗SoC上做“人脸检测五点关键点检测”这种任务时NPU利用率才是性能瓶颈。很多人拿到RK3568就先跑YOLO然后发现人脸检测在NPU上只有十几帧问题通常不在模型而在预处理与后处理的来回拷贝、模型没有定点化、以及没有用上NPU多核心调度。这个项目实战的核心是找一个能同时输出人脸框和五点关键点的模型RetinaFace、SCRFD这类把权重转成RKNN跑在RK3568的NPU上再把人脸框和眼睛、鼻子、嘴角坐标映射回原图。适合需要在地铁闸机、门禁、智慧屏或摄像头边缘设备上做实时人脸对齐的读者。读完你能在真实板子上跑起来并知道帧率瓶颈在哪一个函数里。2. RK3568的NPU模型选型用RetinaFace把检测和五点关键点一次跑完2.1 先确认“五点”是哪五点再看单模型和多模型方案先定义清楚这里说的五点关键点通常指左眼中心、右眼中心、鼻尖、左嘴角、右嘴角。五点不是随意标的它服务于人脸对齐将检测到的五点与模板五点做相似变换就能把任意姿态的人脸拉正用于人脸识别、活体检测也用于智慧医疗场景下的皮肤病区域对齐先将人脸框出来再按五点把鼻子、脸颊映射到标准坐标系再做皮损分析。在RK3568上实现有两条路线两段式先用一个人脸检测模型拿框再在框内跑关键点回归网络以及单模型多任务用RetinaFace这类网络同时回归人脸框、置信度和五点坐标。两段式的好处是每个模型都可以独立替换比如检测模型换更好的或关键点模型换成带更多点的版本坏处是多一次NPU调用。RK3568上NPU单次推理往往要几十毫秒模型不大时调度和拷贝的固定开销占比很高两次调用基本就是翻倍。因此我一般推荐单模型多任务一次推理输出层同时有检测框和五点坐标后处理时从同一组输出里拆出两类信息。下表是从RK3568落地角度做的快速对比。常见模型有MTCNN、RetinaFace、SCRFD以及只做关键点的轻量模型模型输出内容关键点形式RK3568落地难度MTCNN三级联人脸框关键点PNet/RNet/ONet逐级精修中三段模型需要分别转RKNNRetinaFace单次输出BoxLandmark5点或更多预测相对坐标偏移低一个ONNX直接转SCRFD单次输出BoxLandmark5点低但部署资料相对少纯Landmark小网络仅关键点输入已裁剪的人脸图需额外配一个检测器MTCNN在很多CPU项目里很经典但在RK3568上并不讨好三级联在极小目标上更稳但每一级都多一次前向整条延迟累加后很难做到实时。如果只是为了把这个项目跑通我的选择是RetinaFace的ONNX版本输出heads固定后处理代码相对统一网上能找到的参考实现也多。2.2 RK3568的NPU与BGR/RGB输入约定RK3568的NPU通过librknnrt驱动rknn模型转换后的.rknn不会像ONNX那样随意接受任意输入张量编译器在build阶段就把算子的数据排布和layout固定下来。RKNN通常支持NCHW和NHWC两种排列具体以你转换时设定的target_platform和模型算子为准。一个容易踩的坑是图像通道顺序训练RetinaFace时常用BGR图而很多模型脚本导出前用的是RGB。如果你在RKNN输入侧喂错通道检测框依然会有输出但左右眼坐标会反置信度也常常偏低。我一般把mean/std写进rknn.config里让NPU在算子层完成减均值除方差板端直接喂OpenCV读到的BGR原图。以下是一段常见的转换配置代码from rknn.api import RKNN rknn RKNN() rknn.config( mean_values[[103.52, 116.28, 123.675]], std_values[[57.375, 57.12, 58.395]], target_platformrk3568 )这三组数据分别对应BGR三个通道的均值与方差。如果你的模型训练时使用RGB预处理就把顺序换成[[123.675, 116.28, 103.52]]同时板端输入改为RGB。很多时候模型在PC上跑得很准板子上突然不准不是NPU算错了而是通道顺序和mean/std两端不一致。注意如果你用的是RGB训练模型mean_values和std_values的通道顺序必须和板端输入一致不能只改mean不改std。除了通道还要确认输入是归一化还是原始像素。rknn.config中的均值/方差描述的是(pixel - mean) / std所以板端输入通常喂0~255的uint8数据千万不要在外部再除以255。有些项目从PC端演示代码里直接把输入除以255转成RKNN后反而让模型失效。单看像素值差异不大但经过量化后特征分布会全拧。2.3 用INT8量化换取更低的访存带宽RK3568的NPU在INT8下可以获得更优的能效比。FP16在部分模型上也可以直接跑但内存占用大一倍某些融合算子对FP16的支持不如INT8完整。项目实战建议先转FP16确认模型能正常输出随后再上INT8量化校准。量化后的五点坐标误差通常在2~3像素对框和关键点完全够用。量化校准数据集不要选背景图。要尽量贴近部署场景收集50~150张人脸图覆盖不同姿态、尺度、光照并把路径写进dataset.txt。转换脚本会自动逐图跑网络统计每层激活分布。如果实际场景是小脸距离远校准集里却全是大头照量化后小脸检出率会明显下降。如果你在板子上要调试OV5695、OV8858这类MIPI sensor需要去看设备树里sensor节点配置确认输出格式是NV12还是YUYV如果输出格式不对RGA转出来的RGB通道顺序很容易整体偏移。这个链路看起来和模型无关但会直接影响整条管线上看到的色彩空间是否与训练数据一致。3. 用rknn-toolkit2把ONNX转成rknn关键参数与量化校准3.1 在PC上准备转换环境不要直接在RK3568板子上装rknn-toolkit2转换工具跑在x86 PC上代码生成与模拟器更完整。常见做法是clone rknn-toolkit2仓库在conda环境里安装。安装时注意Python版本要与工具依赖匹配否则onnx导入阶段就会报错。git clone https://github.com/airockchip/rknn-toolkit2.git cd rknn-toolkit2/rknn-toolkit2 pip install -r requirements_cp310.txt -i https://pypi.tuna.tsinghua.edu.cn/simple安装完成后验证导入from rknn.api import RKNN print(RKNN.__version__)如果python版本不对或者numpy版本冲突导入时会出现libstdc.so.6或GLIBCXX相关错误。建议用conda新建一个3.10环境再装。如果提示缺少库先确认conda环境是x86_64不要装进arm环境。rknn-toolkit2的二进制依赖较新的GLIBCXX老旧Ubuntu 18.04上直接加载失败时不是代码问题先换Ubuntu 20.04或22.04环境再试。3.2 固定输入shape并简化ONNXRetinaFace在训练代码里经常使用动态shape导出后输入维度是-1。RKNN转换工具对动态shape支持有限即使支持也会增加板端调度延迟。所以在转换前要先把模型固定到实际使用的分辨率。以640x640为例import onnx from onnxsim import simplify model onnx.load(retinaface_dynamic.onnx) model_sim, check simplify( model, dynamic_input_shapeFalse, input_shapes{input.1: [1, 3, 640, 640]} ) onnx.save(model_sim, retinaface_fixed.onnx)固定后检查一下输出节点是否只剩卷积输出。很多公开的RetinaFace代码会在ONNX里包含NMS或decode层这些层在RKNN上不一定支持。建议导出时把后处理全去掉只保留原始回归张量NMS放板端CPU做。这样模型更干净权重量化时也少一些怪异的算子边界。这里需要做好检查用onnx.checker.check_model验证模型结构再看各个输出name。不同版本RetinaFace输出名差异很大千万别凭记忆写死。我一般会把输出name打印出来再与后处理代码一一对应。3.3 调用rknn.config和build完成转换转换脚本写在PC上运行时需要读一张或多张校准图片。下面代码支持直接传入dataset.txtfrom rknn.api import RKNN rknn RKNN() rknn.config( mean_values[[103.52, 116.28, 123.675]], std_values[[57.375, 57.12, 58.395]], target_platformrk3568, optimization_level3, quantize_input_nodeFalse ) if rknn.load_onnx(modelretinaface_fixed.onnx) ! 0: print(load onnx failed) exit(1) if rknn.build(do_quantizationTrue, datasetdataset.txt) ! 0: print(build failed) exit(1) rknn.export_rknn(retinaface_fixed_640x640.rknn)说明optimization_level3会让工具做更多算子融合但如果后处理时需要访问某个中间层太激进的融合会让输出更少。可以先从2开始调试确认精度后再开3。quantize_input_nodeFalse是让输入层保持浮点使量化对输入色彩空间的损伤降到最低。target_platformrk3568决定了算子选择的硬件特性RK3566的模型可以用同一种平台配置但运行时主频和算力不同实际帧率会差20%左右。3.4 用模拟器验证输出shape与数值范围转换完不要急着烧板先用rknn的模拟器在PC上推理一张小图确认输出shape是否和onnx一致。import cv2 import numpy as np from rknn.api import RKNN rknn RKNN() rknn.config(target_platformrk3568) rknn.load_rknn(retinaface_fixed_640x640.rknn) rknn.init_runtime() img cv2.imread(test_face.jpg) img_rgb cv2.cvtColor(img, cv2.COLOR_BGR2RGB) img_resized cv2.resize(img_rgb, (640, 640)) outputs rknn.inference(inputs[img_resized.astype(np.float32)]) for i, out in enumerate(outputs): print(output, i, out.shape, out.min(), out.max())模拟器上输出的浮点范围如果和ONNX相差太大说明量化的scale信息设置有问题。最常见的是mean/std顺序错误。把原始图片和预处理后的图片分别显示一下确认输出坐标在特征图坐标下是否仍然落在人脸位置。模拟器验证通过后同一份rknn文件可以直接放到板子。要注意模拟器和板端结果是有点差异的但坐标差不应超过几个像素如果差异过大检查板端librknnrt版本是否与PC端rknn-toolkit2配套。4. 在RK3568板端部署C/C推理与五点后处理实现4.1 用RKNN C API完成加载、推理、取输出板端部署优先用C API。Python在RK3568上启动慢而且在GStreamer/DRM显示链路里嵌入容易让线程模型变复杂。登录板子后把rknn模型放在可执行文件目录写一个最小main函数先跑通加载和推理。#include rknn_api.h #include stdio.h #include stdlib.h #include string.h static uint8_t input_buf[640 * 640 * 3]; static void* load_file(const char* path, size_t* size) { FILE* fp fopen(path, rb); if (!fp) return NULL; fseek(fp, 0, SEEK_END); *size ftell(fp); fseek(fp, 0, SEEK_SET); void* data malloc(*size); if (fread(data, 1, *size, fp) ! *size) { free(data); fclose(fp); return NULL; } fclose(fp); return data; } int main() { rknn_context ctx 0; size_t model_size 0; void* model_data load_file(retinaface_fixed_640x640.rknn, model_size); if (rknn_init(ctx, model_data, model_size, 0, NULL) 0) { printf(rknn_init failed\n); return -1; } rknn_input inputs[1]; memset(inputs, 0, sizeof(inputs)); inputs[0].index 0; inputs[0].type RKNN_TENSOR_UINT8; inputs[0].size 640 * 640 * 3; inputs[0].fmt RKNN_TENSOR_NHWC; inputs[0].buf input_buf; rknn_output outputs[4]; memset(outputs, 0, sizeof(outputs)); outputs[0].want_float 1; outputs[1].want_float 1; outputs[2].want_float 1; outputs[3].want_float 1; rknn_inputs_set(ctx, 1, inputs); rknn_run(ctx, NULL); rknn_outputs_get(ctx, 1, outputs, NULL); // 后处理在这里解码 outputs rknn_outputs_release(ctx, 1, outputs); rknn_destroy(ctx); return 0; }这里的input_buf是一帧640x640x3的BGR数据。RKNN_TENSOR_UINT8表示喂入原始像素不再做归一化fmt要与模型转换时的输入layout一致如果转换时工具自动选择了NCHW那么这里可能要改。我建议在板端写一个rknn_query查询模型输入属性再决定fmt不要写死。4.2 解码RetinaFace输出坐标回归和NMS拿到输出后不能直接拿出来当坐标。RetinaFace的框和五点是在anchor基础上的偏移量必须和预先固化的anchor表一起解码。下面代码是后处理的核心typedef struct { float x1, y1, x2, y2; float score; float pts[10]; } BoxInfo; void decode_and_nms(float* bbox_out, float* scores, float* landm_out, float* anchors, int num_anchor, float conf_thresh, BoxInfo* result, int* result_count) { int cnt 0; for (int i 0; i num_anchor; i) { float score scores[i]; if (score conf_thresh) continue; float x1 anchors[i*40] bbox_out[i*40] * 0.1f * anchors[i*42]; float y1 anchors[i*41] bbox_out[i*41] * 0.1f * anchors[i*43]; float x2 anchors[i*42] bbox_out[i*42] * 0.1f * anchors[i*42]; float y2 anchors[i*43] bbox_out[i*43] * 0.1f * anchors[i*43]; result[cnt].x1 x1; result[cnt].y1 y1; result[cnt].x2 x2; result[cnt].y2 y2; result[cnt].score score; for (int j 0; j 5; j) { result[cnt].pts[j*20] anchors[i*40] landm_out[i*10j*20] * 0.1f * anchors[i*42]; result[cnt].pts[j*21] anchors[i*41] landm_out[i*10j*21] * 0.1f * anchors[i*43]; } cnt; } *result_count cnt; }说明0.1f是RetinaFace回归权重SCRFD这类模型则不一定是0.1做之前先查原始模型repo。anchors需要与ONNX导出时保持一致。输入尺寸变了anchor表就变了不能复用。NMS实现可以采用标准循环或者为了速度用简单的降序访问。关键是在做NMS之前把所有足阈值的框先收集起来再按score排序不要边解码边删因为同一个位置可能同时出现多个anchor的输出直接删除会导致剩下的框漏检。4.3 五点坐标映射回原图注意letterbox padding如果采集到的原图是1080p直接resize成640x640会破坏宽高比人脸会变形。常见做法是letterbox也就是等比缩放后用灰色填充。推理得到的是640坐标系下的坐标映射回原图必须先减掉padding再除以缩放系数。函数如下void map_to_original(float* box, float* pts, int orig_w, int orig_h) { float scale fmin(orig_w / 640.0f, orig_h / 640.0f); float pad_x (orig_w - 640.0f * scale) * 0.5f; float pad_y (orig_h - 640.0f * scale) * 0.5f; box[0] (box[0] - pad_x) / scale; box[1] (box[1] - pad_y) / scale; box[2] (box[2] - pad_x) / scale; box[3] (box[3] - pad_y) / scale; for (int i 0; i 5; i) { pts[i*20] (pts[i*20] - pad_x) / scale; pts[i*21] (pts[i*21] - pad_y) / scale; } }注意这里的letterbox填充值必须与训练一致。很多模型用0填充有些用114。在RKNN上如果填充值不同靠近边缘的人脸关键点会偏差很大。调试OV5695这类摄像头时如果想省掉CPU缩放可以先用RGA做resize和NV12转RGBRGA硬件支持缩放letterbox填充值可以用寄存器配置。设备树里sensor节点一般只控制输出格式resize和pad要放在RGA的src/dst rect参数里做。5. 帧率不够时的三个优化技巧NPU核心掩码、双线程流水线和精度自查5.1 用NPU核心掩码隔离多路任务在RK3568上同时跑检测和关键点对齐或者同时跑多路视频NPU内多个核心的调度会影响整体延迟。librknnrt提供rknn_set_core_mask接口可以把当前上下文绑定到指定核心rknn_set_core_mask(ctx, RKNN_NPU_CORE_0);不同固件对core mask的支持程度不同可以在板子上查看/sys/kernel/debug/rknpu/version或运行时查询输入/输出属性确认。RK3566和RK3568在这一层的差异更明显3566的NPU频率较低核心数相同的情况下合并任务比分区任务更稳。做项目时建议先不加掩码跑默认调度再按任务数量逐个绑定测试。5.2 双线程隐藏CPU后处理延迟NPU推理20ms但取帧、resize、NMS、关键点解析这些CPU操作加到一起可能超过30ms。如果串行做帧率很难看。常见做法是双线程一个线程负责取帧和预处理另一个线程做推理和后处理中间用3帧环形缓冲区传递帧指针。伪代码如下#define QUEUE_SIZE 3 void* producer(void* arg) { while (running) { int next (head 1) % QUEUE_SIZE; if (next tail) { usleep(1000); continue; } camera_read(queue[head].buf, 640 * 640 * 3); head next; } return NULL; } void* consumer(void* arg) { while (running) { if (head tail) { usleep(1000); continue; } rknn_inputs_set(ctx, 1, inputs); rknn_run(ctx, NULL); rknn_outputs_get(ctx, outputs_count, outputs, NULL); decode_and_nms(outputs); map_to_original(box, pts, orig_w, orig_h); tail (tail 1) % QUEUE_SIZE; } return NULL; }这里没有用锁保护head和tail单生产者单消费者场景下可以用原子变量或简单的严格交替访问。如果改成多消费者就需要加锁。注意不要在consumer线程里做显示或保存图片磁盘IO和DRM提交都可能阻塞几毫秒。5.3 精度偏移时的四项自查五点关键点最容易出现“PC上准、板子上偏”的现象我按固定顺序排查输入通道顺序BGR/RGB互换会导致左右眼点反letterbox填充值训练用0而推理用114人脸靠近边缘时偏移输出反量化的处理want_float1拿到的是浮点再次除以255会让坐标整体缩小anchor表是否与输入尺寸匹配640模型用了416的anchor坐标全部乱。做完这四项基本能覆盖项目实战中大部分精度问题。如果还不行就在模拟器上用同一张图把每一层输出的数据保存出来和onnx对比。这样定位慢但可靠。在板端还可以调用rknn_query(ctx, RKNN_QUERY_PERF_DETAIL, perf, size);拿到每层耗时找到到底是卷积层还是中间转换层占了时间。最后在多人脸场景下把置信度阈值从0.5降到0.3会明显增加NMS候选框数量如果帧率下降先看NMS函数能否在每次调用前预留足够大的堆栈。也可以对ROI区域做两阶段先用低分辨率快速检测小脸再对检测到的人脸区域做一次高分辨率关键点精修这样在RK3568上仍能保持较高整体帧率。本文还有配套的精品资源点击获取
返回列表