Rockchip RK3566 SoC 内置的 NPU算力达到 0.8 TOPS,对于人脸检测与识别这类任务而言,是替代 CPU 进行通用推理的绝佳选择。
基于 Qt 5 (GUI & 线程管理) + OpenCV (图像预处理) + RKNN (NPU推理) 的高效人脸识别系统。
1. 系统架构设计
为了保证 1080P 视频流的流畅显示与实时检测,传统的单线程循环模式(采集->检测->显示)会导致界面严重卡顿。本工程采用 多线程流水线 架构:
- **主线程 (UI Thread)**:负责界面绘制、视频渲染、用户交互。
- **采集线程 (Video Thread)**:使用
cv::VideoCapture读取摄像头数据。 - **检测线程 (Detection Thread)**:运行 YOLOv5-Face 模型,定位人脸坐标。
- **识别线程 (Recognition Thread)**:运行 MobileFaceNet 模型,提取 128维 特征向量并比对。
1.1 Qt 多线程实现模式
不同于继承 QThread 并重写 run() 的方式,本工程使用了更灵活的 moveToThread 模式,利用信号槽机制实现流水线解耦。
1 | // MainWindow.cpp 初始化片段 |
关键点:Qt::QueuedConnection 确保了 cv::Mat 数据在跨线程传递时不仅是安全的,而且会让槽函数在接收者线程的事件循环中执行,从而真正实现并行。
2. NPU 推理流程详解 (RKNN)
RKNN Runtime 的调用流程是通用的:init -> input_set -> run -> output_get。以下以 YOLOv5-Face 检测模块为例。
2.1 模型加载与上下文初始化
在类构造函数中,我们将 .rknn 模型文件加载到内存,并建立 NPU 上下文。
1 | // FaceDetection.cpp |
2.2 输入张量封装
RKNN API 需要特定的输入结构。OpenCV 的 cv::Mat 数据通常是 BGR 也可以是 RGB,需要根据模型训练时的预处理要求进行转换,并填入 rknn_input 结构体。
1 | void FaceDetection::startDetection(const cv::Mat &frame) { |
2.3 获取推理结果与后处理
模型输出通常是未经过处理的多维数组。为了简化代码,我们可以让 NPU 驱动自动将量化后的输出反量化为浮点数 (want_float = 1)。
1 | // 准备输出结构 |
技术细节:outputs[i].want_float = 1 极大地简化了应用层代码,虽然会增加一点 CPU 转换开销,但避免了手动处理 zero_point 和 scale 的繁琐过程。
2.4 YOLOv5 后处理算法解析
YOLO 模型输出的是三个不同尺度(Stride 8, 16, 32)的 Feature Map。在 yolov5_post_process 中,需要根据预定义的 Anchors 将 Feature Map 上的偏移量映射回原图坐标。
核心步骤包括:
- Sigmoid 激活:将 NPU 输出的原始数据映射到 (0, 1)。
- 坐标还原:
$$
b_x = 2 \sigma(t_x) - 0.5 + c_x
$$
$$
b_y = 2 \sigma(t_y) - 0.5 + c_y
$$
$$
b_w = p_w (2 \sigma(t_w))^2
$$
$$
b_h = p_h (2 \sigma(t_h))^2
$$
其中 $c_x, c_y$ 是网格坐标,$p_w, p_h$ 是 Anchor 宽高。 - **NMS (非极大值抑制)**:使用 OpenCV 的
dnn::NMSBoxes剔除重叠度高的冗余框。
1 | // FaceDetection.cpp 中对输出的处理 |
3. 人脸特征比对与加速 (FLANN)
在 Mobilefacenet 模块提取出 128维 人脸特征向量后,需要与数据库中已存的 N 个人脸特征进行比对。传统的暴力遍历(Brute-force)在人脸库较大时效率低下。
本工程采用了 OpenCV 的 FLANN (Fast Library for Approximate Nearest Neighbors) module 进行高维向量检索。
1 | // Mobilefacenet.cpp 初始化 |
使用 KD-Tree 结构可以将搜索复杂度从 $O(N)$ 降低到 $O(\log N)$ 级别,显著提升了在数百人脸库规模下的响应速度。
4. 业务逻辑与数据库管理
除了算法核心,工程还集成了 SQLite 数据库用于从管理人员信息。DatabaseUtils 命名空间封装了基础的 CRUD 操作:
- face 表:存储当前系统的最大人脸ID。
- IDtoPeople 表:存储 ID 与姓名、部门的映射关系。
- 特征存储:由于 SQLite 存取 vector 较慢,特征文件被序列化为
.xml或直接读取到内存中建立索引,数据库仅存储元数据。
这套轻量级的数据库方案非常适合嵌入式单机门禁系统,既保证了数据持久化,又没有引入过重的 MySQL 等依赖。
5. 遇到的坑与优化策略
Mat 数据类型注册:
由于信号槽参数必须是 Qt 元对象系统已知的类型,在main.cpp或构造函数中必须调用:
qRegisterMetaType<cv::Mat>("cv::Mat");
否则信号虽然发出,但槽函数可以通过编译却永远不会被调用。跳帧策略:
检测线程的处理速度可能低于采集速度(例如检测耗时 30ms,采集耗时 16ms)。为了防止信号队列堆积导致的图像延迟,我们在FaceDetection中加入了简单的跳帧逻辑:1
2
3if (frameCount % 5 != 0) { // 每5帧处理1帧
return;
}这保证了 UI 显示的是最新鲜的画面,而不是几百毫秒前的“旧账”。
内存对齐与格式:
NPU 对输入数据的步长(stride)和对齐敏感。使用cv::Mat连续内存(Continuous Memory)通常没问题,但如果是裁剪后的 ROI 区域(非连续内存),必须执行clone()或memcpy到一块新的连续缓冲区再传给 API。
6. Zero-Copy 优化
目前的实现中,图像数据在 CPU (OpenCV) 和 NPU 之间存在 memcpy 拷贝。RK3566 的 NPU 支持 DMA 分配器(DRM/DMA-BUF)。
未来的优化方向是使用 rknn_mem_create 分配物理连续内存,并将该内存直接映射给 OpenCV 的 cv::Mat 使用(通过自定义 Allocator)。这样,摄像头的采集数据可以直接写入 NPU 可访问的物理内存区域,实现真正的 Zero-Copy 零拷贝推理,进一步降低 CPU 占用率。
7. 总结
通过上述实现,我们在 RK3566 平台上构建了一套完整的人脸门禁系统逻辑。Qt 负责调度与展现,OpenCV 负责“搬运”与“预处理”,RKNN 负责核心的“思考”。