人脸检测与识别
目标平台: RK3576 | NPU 算力: 6 TOPS | 文档版本: V1.0
适用板型: C-3576BQ 核心板 + M4-R2 底板
最后更新: 2026-10-23
1. 概述
1.1 功能简介
人脸检测与识别是智能安防、门禁考勤、身份核验等场景的核心 AI 能力。RK3576 搭载 6 TOPS 算力的三核 NPU,能够在单芯片上实现人脸检测、人脸特征提取、人脸比对和人脸活体检测的完整流程,满足多路并发的实时人脸识别需求。
本文档介绍如何在 RK3576 上部署人脸检测与识别系统,涵盖以下核心模块:
- 人脸检测:基于 SCRFD 或 RetinaFace 模型,从图像中定位人脸区域
- 人脸对齐:通过关键点检测实现人脸仿射变换,标准化人脸姿态
- 人脸识别:基于 ArcFace 模型提取人脸特征向量,进行 1:N 比对搜索
- 活体检测:基于 RGB/IR 双模态的活体判断,防止照片/视频攻击
核心特性:
| 特性 | 说明 |
|---|---|
| 人脸检测模型 | SCRFD / RetinaFace-MobileNet |
| 人脸识别模型 | ArcFace-MobileFaceNet / ArcFace-IResNet |
| 活体检测 | RGB 单目 / RGB+IR 双目 |
| NPU 加速 | 三核 NPU 并行(NPU0+NPU1+NPU2) |
| 特征维度 | 128 / 256 / 512 维向量 |
| 人脸库规模 | 支持万级人脸库 1:N 搜索 |
| 推理框架 | RKNN Toolkit2 + RKNNLite2 |
1.2 应用场景
| 场景 | 说明 | 核心模块 |
|---|---|---|
| 门禁考勤 | 人脸刷卡、无感通行 | 检测 + 识别 |
| 安防监控 | 陌生人告警、重点人员布控 | 检测 + 识别 + 人脸库搜索 |
| 智慧零售 | VIP 识别、会员服务 | 检测 + 识别 |
| 考试/会议签到 | 快速人脸签到、防代考 | 检测 + 活体 + 识别 |
| 公共安全 | 机场/车站身份核验 | 检测 + 活体 + 识别 |
| 金融支付 | 人脸支付身份验证 | 检测 + 活体 + 识别 |
1.3 工作原理
人脸识别系统的工作流程分为检测、对齐、特征提取和比对四个阶段:
视频流输入
|
v
[人脸检测] ---- SCRFD/RetinaFace 模型 -----> 人脸边界框 + 关键点
|
v
[人脸对齐] ---- 仿射变换(5点/6点关键点)-----> 标准化人脸图像(112x112)
|
v
[特征提取] ---- ArcFace 模型 ----------------> 128/256/512 维特征向量
|
v
[比对/搜索] --- 余弦相似度 / L2 距离 --------> 身份匹配结果
|
v
[活体检测] --- RGB/IR 模态特征 -------------> 活体/攻击判断各阶段说明:
| 阶段 | 输入 | 输出 | 模型 | 说明 |
|---|---|---|---|---|
| 人脸检测 | 原始图像(640x640) | 人脸边界框列表 | SCRFD | 定位图像中所有人脸位置 |
| 人脸对齐 | 原始图像 + 关键点 | 112x112 标准化人脸 | 仿射变换 | 通过关键点做仿射变换,统一人脸姿态 |
| 特征提取 | 112x112 人脸图像 | 特征向量 | ArcFace | 提取具有区分度的人脸嵌入向量 |
| 活体检测 | RGB + IR 人脸图像 | 活体得分 | LivenessNet | 判断是否为真实活体(防止照片/视频攻击) |
| 特征比对 | 待识别特征 + 人脸库 | 身份 ID | 余弦相似度 | 在人脸库中搜索最近邻匹配 |
多核 NPU 并行处理架构:
RK3576 的三个 NPU 核心可以分别承载不同的模型推理任务,实现人脸处理流水线的并行加速:
┌─────────────────────────────────────────────────┐
│ RK3576 三核 NPU │
│ │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ NPU0 │ │ NPU1 │ │ NPU2 │ │
│ │ 人脸检测 │ │ 特征提取 │ │ 活体检测 │ │
│ │ SCRFD │ │ ArcFace │ │ Liveness │ │
│ └──────────┘ └──────────┘ └──────────┘ │
│ │
│ 流水线: 检测 --> 对齐 --> 特征提取 --> 比对 │
│ | | | │
│ └─ NPU0 └─ NPU1 └─ NPU2 │
└─────────────────────────────────────────────────┘2. 硬件准备
2.1 硬件清单
| 序号 | 物料名称 | 型号/规格 | 数量 | 用途 |
|---|---|---|---|---|
| 1 | 核心板 + 底板 | C-3576BQ 核心板 + M4-R2 底板 | 1 | 主开发板,6 TOPS NPU |
| 2 | USB 摄像头 | UVC 兼容(720p/1080p) | 1 | RGB 图像采集,人脸检测与识别 |
| 3 | IR 摄像头(可选) | UVC 兼容红外摄像头 | 1 | 红外活体检测,需与 RGB 摄像头同步 |
| 4 | 调试串口 | USB to TTL 3.3V 转换器 | 1 | 串口调试,波特率 1500000 |
| 5 | 电源适配器 | 5V/2A 电源(Type-C 或 6pin 供电) | 1 | 主板供电(空载最小 1000mA) |
| 6 | M.2 SSD(可选) | M.2 NVMe 固态硬盘 | 1 | 人脸库数据大容量存储 |
| 7 | 显示器 + HDMI 线 | 支持 4K 的显示器 | 1 | 人脸识别结果可视化(可选) |
2.2 硬件确认
接入调试串口后,启动开发板,通过串口终端确认硬件信息:
# 查看 SoC 型号
cat /proc/device-tree/compatible
# 预期输出包含 rk3576 关键字
# 查看 NPU 设备节点
cat /proc/device-tree/npu/compatible
# 预期输出: rockchip,rk3576-npu
# 查看内存信息
cat /proc/meminfo | grep MemTotal
# 建议 4GB 及以上内存用于人脸识别应用
# 查看 USB 摄像头设备
ls /dev/video*
# 预期输出: /dev/video0 /dev/video1 ...
# 确认摄像头工作正常
v4l2-ctl --list-devices摄像头同步说明(RGB+IR 双目方案):
若使用 RGB+IR 双目活体检测方案,需确保两个摄像头能够同步采集:
# 查看已连接的视频设备
v4l2-ctl --list-devices
# 测试摄像头
ffmpeg -f v4l2 -i /dev/video0 -frames 1 test_rgb.jpg
ffmpeg -f v4l2 -i /dev/video2 -frames 1 test_ir.jpg3. 软件准备
3.1 内核配置
人脸检测与识别应用需要 NPU 驱动和 V4L2 摄像头驱动支持:
# 路径: Device Drivers → Accelerators → Rockchip NPU
CONFIG_ROCKCHIP_RKNPU=y
CONFIG_RKNPU=y
# 路径: Device Drivers → Multimedia support → V4L2
CONFIG_V4L2=y
CONFIG_USB_VIDEO_CLASS=y确认内核版本及驱动加载情况:
# 查看内核版本(应为 Linux 6.1)
uname -a
# 查看 NPU 驱动
grep -i rknpu /boot/config-$(uname -r)
# 预期输出: CONFIG_ROCKCHIP_RKNPU=y
# 查看 V4L2 驱动
ls /sys/class/video4linux/3.2 设备树
NPU 设备树节点定义了 NPU 硬件的寄存器地址、中断号、时钟和复位等信息:
npu: npu@<base_addr> {
compatible = "rockchip,rk3576-npu";
reg = <0x0 <base_addr> 0x0 0x20000>;
interrupts = <GIC_SPI <irq_num> IRQ_TYPE_LEVEL_HIGH>;
clocks = <&cru CLK_NPU>, <&cru PCLK_NPU>;
clock-names = "clk_npu", "pclk_npu";
resets = <&cru SRST_NPU>;
reset-names = "npu";
status = "disabled";
#cooling-cells = <2>;
};以上寄存器基地址、中断号与时钟标识为 RK 平台通用结构示例,具体数值以实际发布的 RK3576 SDK 设备树为准(待板上确认)。
在板级设备树中启用 NPU:
&npu {
status = "okay";
};3.3 开发环境依赖
PC 端(模型转换):
# 创建 Python 虚拟环境
python3 -m venv rknn_env
source rknn_env/bin/activate
# 安装 rknn-toolkit2
pip install rknn-toolkit2
# 安装模型导出依赖
pip install torch torchvision onnx onnxsim
# 验证安装
python3 -c "from rknn.api import RKNN; print('RKNN Toolkit2 OK')"板端(推理运行):
# 安装 rknn-lite2 运行时
pip3 install rknn-lite2
# 安装 Python 依赖
pip3 install numpy opencv-python-headless
# 验证 NPU 驱动
lsmod | grep rknpu
cat /sys/kernel/debug/rknpu/version3.4 模型文件准备
# 创建模型目录
mkdir -p /data/models
cd /data/models
# 人脸检测模型(SCRFD)-- 已转换的 RKNN 模型
# 需要从 SCRFD 官方仓库导出 ONNX 后转 RKNN,详见 4.1 节
# 人脸识别模型(ArcFace)
# 需要从 insightface 官方仓库导出 ONNX 后转 RKNN,详见 4.2 节
# 校准数据集(用于 INT8 量化)
# 从实际场景中采集 100-200 张包含人脸的图像
mkdir -p calibration_data
# 将校准图像路径写入 dataset 文件
ls calibration_data/*.jpg > face_calibration.txt4. 操作步骤
4.1 SCRFD 人脸检测模型转换
SCRFD 是百度提出的轻量级人脸检测模型,适合嵌入式部署。以下为转换为 RKNN 格式的流程:
from rknn.api import RKNN
# SCRFD 模型参数
MODEL_INPUT_SIZE = 640
DATASET_FILE = 'face_calibration.txt'
rknn = RKNN()
# 配置输入预处理(SCRFD 标准预处理:mean=[127.5,127.5,127.5], std=[128,128,128])
rknn.config(
mean_values=[[127.5, 127.5, 127.5]],
std_values=[[128, 128, 128]],
target_platform='rk3576',
quantized_dtype='asymmetric_quantized-8'
)
# 加载 ONNX 模型
ret = rknn.load_onnx(model='scrfd_2.5g.onnx')
if ret != 0:
print('加载 SCRFD 模型失败')
exit(ret)
# 量化校准
ret = rknn.build(do_quantization=True, dataset=DATASET_FILE)
if ret != 0:
print('构建 SCRFD 模型失败')
exit(ret)
# 导出 RKNN 模型
ret = rknn.export_rknn('scrfd_2.5g.rknn')
if ret != 0:
print('导出 SCRFD 模型失败')
exit(ret)
rknn.release()
print('SCRFD 模型转换完成: scrfd_2.5g.rknn')SCRFD 模型选择指南:
| 模型 | 输入尺寸 | 参数量 | 检测精度 (WIDER FACE Easy) | 推荐场景 |
|---|---|---|---|---|
| scrfd_0.5g | 320x320 | 0.5M | 93.5% | 低算力、小人脸场景 |
| scrfd_2.5g | 640x640 | 2.5M | 95.8% | 通用场景,精度与速度均衡 |
| scrfd_5g | 640x640 | 5M | 96.5% | 高精度场景 |
说明: 检测精度为各模型公开基准的典型值,实际值随数据集与量化配置变化。
4.2 ArcFace 人脸识别模型转换
ArcFace 是业界广泛使用的人脸识别模型,通过加性角度间隔损失函数提升特征区分度:
from rknn.api import RKNN
rknn = RKNN()
# ArcFace 标准预处理:输入 112x112 RGB,归一化到 [-1, 1]
rknn.config(
mean_values=[[127.5, 127.5, 127.5]],
std_values=[[128, 128, 128]],
target_platform='rk3576',
quantized_dtype='asymmetric_quantized-8'
)
# 加载 ONNX 模型
ret = rknn.load_onnx(model='arcface_mobileface.onnx')
if ret != 0:
print('加载 ArcFace 模型失败')
exit(ret)
# 量化校准
ret = rknn.build(do_quantization=True, dataset='face_calibration.txt')
if ret != 0:
print('构建 ArcFace 模型失败')
exit(ret)
# 导出 RKNN 模型
ret = rknn.export_rknn('arcface_mobileface.rknn')
if ret != 0:
print('导出 ArcFace 模型失败')
exit(ret)
rknn.release()
print('ArcFace 模型转换完成: arcface_mobileface.rknn')ArcFace 模型选择指南:
| 模型 | 输入尺寸 | 特征维度 | LFW 准确率 | 推荐场景 |
|---|---|---|---|---|
| MobileFaceNet | 112x112 | 128 | 99.28% | 嵌入式端侧,低功耗 |
| IResNet-50 | 112x112 | 512 | 99.78% | 高精度场景 |
| IResNet-100 | 112x112 | 512 | 99.83% | 最高精度,算力充足 |
说明: LFW 准确率为各模型公开基准的典型值,实际值随数据集与量化配置变化。
4.3 人脸检测与识别完整流程
以下为板端完整的 Python 推理流程,包含人脸检测、对齐、特征提取和人脸库搜索:
from rknnlite.api import RKNNLite
import cv2
import numpy as np
import time
import os
class FaceRecognizer:
"""RK3576 人脸识别完整流程"""
def __init__(self, det_model_path, rec_model_path,
face_db_path=None, det_threshold=0.5,
rec_threshold=0.4):
"""
初始化人脸识别器
Args:
det_model_path: 人脸检测 RKNN 模型路径
rec_model_path: 人脸识别 RKNN 模型路径
face_db_path: 人脸特征库路径(.npy 格式)
det_threshold: 检测置信度阈值
rec_threshold: 识别相似度阈值
"""
self.det_threshold = det_threshold
self.rec_threshold = rec_threshold
# 初始化检测模型
self.det_rknn = RKNNLite()
ret = self.det_rknn.load_rknn(det_model_path)
if ret != 0:
raise RuntimeError(f'加载检测模型失败: {det_model_path}')
ret = self.det_rknn.init_runtime(core_mask=RKNNLite.NPU_CORE_0)
if ret != 0:
raise RuntimeError('初始化检测运行时失败')
# 初始化识别模型
self.rec_rknn = RKNNLite()
ret = self.rec_rknn.load_rknn(rec_model_path)
if ret != 0:
raise RuntimeError(f'加载识别模型失败: {rec_model_path}')
ret = self.rec_rknn.init_runtime(core_mask=RKNNLite.NPU_CORE_1)
if ret != 0:
raise RuntimeError('初始化识别运行时失败')
# 加载人脸特征库
self.face_db = {}
self.face_embeddings = None
if face_db_path and os.path.exists(face_db_path):
self.load_face_db(face_db_path)
def load_face_db(self, db_path):
"""加载人脸特征库"""
data = np.load(db_path, allow_pickle=True).item()
self.face_db = data.get('names', [])
self.face_embeddings = data.get('embeddings', None)
print(f'已加载人脸库: {len(self.face_db)} 人')
def detect_faces(self, image):
"""
人脸检测
Args:
image: BGR 格式原始图像
Returns:
boxes: 人脸边界框列表 [[x1,y1,x2,y2], ...]
scores: 置信度列表
landmarks: 关键点列表
"""
img_h, img_w = image.shape[:2]
# 预处理:resize 到模型输入尺寸
input_size = 640
img = cv2.resize(image, (input_size, input_size))
img_rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
# NPU 推理
outputs = self.det_rknn.inference(inputs=[img_rgb])
# 后处理:解析检测结果
boxes, scores, landmarks = self._parse_det_output(
outputs, img_w, img_h, input_size
)
# NMS
keep = self._nms(boxes, scores, iou_threshold=0.4)
boxes = [boxes[i] for i in keep]
scores = [scores[i] for i in keep]
if landmarks:
landmarks = [landmarks[i] for i in keep]
return boxes, scores, landmarks
def _parse_det_output(self, outputs, img_w, img_h, input_size):
"""解析 SCRFD 检测输出"""
boxes, scores, landmarks = [], [], []
# SCRFD 输出格式:每个 stride 的得分和偏移量
for output in outputs:
if output is None:
continue
# 简化解析:实际需根据模型具体输出格式调整
if len(output.shape) == 3:
for det in output[0]:
score = det[0]
if score > self.det_threshold:
x1 = int(det[1] * img_w)
y1 = int(det[2] * img_h)
x2 = int(det[3] * img_w)
y2 = int(det[4] * img_h)
boxes.append([x1, y1, x2, y2])
scores.append(float(score))
if len(det) > 9:
lm = det[5:15].reshape(5, 2)
lm[:, 0] *= img_w
lm[:, 1] *= img_h
landmarks.append(lm.tolist())
return boxes, scores, landmarks
def _nms(self, boxes, scores, iou_threshold=0.4):
"""非极大值抑制"""
if not boxes:
return []
boxes_arr = np.array(boxes, dtype=float)
scores_arr = np.array(scores, dtype=float)
x1 = boxes_arr[:, 0]
y1 = boxes_arr[:, 1]
x2 = boxes_arr[:, 2]
y2 = boxes_arr[:, 3]
areas = (x2 - x1) * (y2 - y1)
order = scores_arr.argsort()[::-1]
keep = []
while len(order) > 0:
i = order[0]
keep.append(i)
if len(order) == 1:
break
xx1 = np.maximum(x1[i], x1[order[1:]])
yy1 = np.maximum(y1[i], y1[order[1:]])
xx2 = np.minimum(x2[i], x2[order[1:]])
yy2 = np.minimum(y2[i], y2[order[1:]])
inter = np.maximum(0, xx2 - xx1) * np.maximum(0, yy2 - yy1)
iou = inter / (areas[i] + areas[order[1:]] - inter)
order = order[np.where(iou <= iou_threshold)[0] + 1]
return keep
def align_face(self, image, landmarks, target_size=112):
"""
人脸对齐:通过 5 点关键点做仿射变换
Args:
image: 原始图像
landmarks: 5 点关键点 [[x,y], ...]
target_size: 输出尺寸
Returns:
aligned: 对齐后的人脸图像
"""
if landmarks is None or len(landmarks) < 5:
# 无关键点时使用边界框中心裁剪
return None
lm = np.array(landmarks[:5], dtype=float)
# 标准模板坐标(ArcFace 训练时使用的 5 点坐标)
template = np.array([
[30.2946, 51.6963],
[65.5318, 51.5014],
[48.0252, 71.7366],
[33.5493, 92.3655],
[62.7299, 92.2041]
], dtype=float)
# 缩放模板到目标尺寸
if target_size == 112:
template = template * (112.0 / 96.0)
template += 8.0
else:
template *= (target_size / 96.0)
template += (target_size - 96.0) / 2.0
# 计算仿射变换矩阵
M = cv2.estimateAffinePartial2D(lm, template, method=cv2.LMEDS)[0]
# 执行仿射变换
aligned = cv2.warpAffine(
image, M, (target_size, target_size),
flags=cv2.INTER_LINEAR
)
return aligned
def extract_embedding(self, face_image):
"""
提取人脸特征向量
Args:
face_image: 112x112 RGB 人脸图像
Returns:
embedding: 归一化后的特征向量
"""
# 预处理
img = cv2.resize(face_image, (112, 112))
img_rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) if len(img.shape) == 3 else img
# NPU 推理
outputs = self.rec_rknn.inference(inputs=[img_rgb])
# L2 归一化
embedding = outputs[0].flatten().astype(np.float32)
norm = np.linalg.norm(embedding)
if norm > 0:
embedding = embedding / norm
return embedding
def search_face(self, embedding, top_k=5):
"""
在人脸库中搜索最相似的人脸
Args:
embedding: 待识别特征向量
top_k: 返回前 K 个匹配结果
Returns:
results: [(name, similarity), ...]
"""
if self.face_embeddings is None or len(self.face_db) == 0:
return []
# 余弦相似度(特征已归一化,直接点积)
similarities = np.dot(self.face_embeddings, embedding)
top_indices = similarities.argsort()[::-1][:top_k]
results = []
for idx in top_indices:
name = self.face_db[idx]
sim = float(similarities[idx])
results.append((name, sim))
return results
def recognize_frame(self, image):
"""
完整识别流程:检测 -> 对齐 -> 特征提取 -> 搜索
Args:
image: BGR 格式原始图像
Returns:
results: 识别结果列表
"""
results = []
boxes, scores, landmarks = self.detect_faces(image)
for i, (box, score) in enumerate(zip(boxes, scores)):
face_result = {
'box': box,
'det_score': score,
'name': 'Unknown',
'rec_score': 0.0
}
# 人脸对齐
lm = landmarks[i] if landmarks and i < len(landmarks) else None
x1, y1, x2, y2 = box
face_crop = image[max(0,y1):y2, max(0,x1):x2]
if lm is not None:
# 将关键点坐标从原图映射到裁剪区域
lm_crop = [[p[0]-x1, p[1]-y1] for p in lm]
aligned = self.align_face(face_crop, lm_crop)
else:
aligned = cv2.resize(face_crop, (112, 112))
if aligned is not None:
# 特征提取
embedding = self.extract_embedding(aligned)
# 人脸库搜索
matches = self.search_face(embedding, top_k=1)
if matches and matches[0][1] > self.rec_threshold:
face_result['name'] = matches[0][0]
face_result['rec_score'] = matches[0][1]
results.append(face_result)
return results
def release(self):
"""释放资源"""
self.det_rknn.release()
self.rec_rknn.release()
def main():
"""主函数:实时人脸识别"""
recognizer = FaceRecognizer(
det_model_path='scrfd_2.5g.rknn',
rec_model_path='arcface_mobileface.rknn',
face_db_path='face_database.npy',
det_threshold=0.5,
rec_threshold=0.4
)
cap = cv2.VideoCapture(0)
cap.set(cv2.CAP_PROP_FRAME_WIDTH, 1280)
cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 720)
print('人脸识别已启动,按 q 退出...')
fps_count = 0
fps_start = time.time()
while True:
ret, frame = cap.read()
if not ret:
break
results = recognizer.recognize_frame(frame)
for r in results:
x1, y1, x2, y2 = r['box']
color = (0, 255, 0) if r['name'] != 'Unknown' else (0, 0, 255)
cv2.rectangle(frame, (x1, y1), (x2, y2), color, 2)
label = f"{r['name']} ({r['rec_score']:.2f})"
cv2.putText(frame, label, (x1, y1 - 10),
cv2.FONT_HERSHEY_SIMPLEX, 0.6, color, 2)
# FPS 计算
fps_count += 1
elapsed = time.time() - fps_start
if elapsed >= 1.0:
fps = fps_count / elapsed
fps_count = 0
fps_start = time.time()
cv2.putText(frame, f'FPS: {fps:.1f}', (10, 30),
cv2.FONT_HERSHEY_SIMPLEX, 1.0, (0, 0, 255), 2)
cv2.imshow('Face Recognition', frame)
if cv2.waitKey(1) & 0xFF == ord('q'):
break
cap.release()
cv2.destroyAllWindows()
recognizer.release()
if __name__ == '__main__':
main()4.4 人脸库管理
人脸特征库的构建和管理是人脸识别系统的关键环节:
import numpy as np
import cv2
import os
from rknnlite.api import RKNNLite
class FaceDatabase:
"""人脸数据库管理"""
def __init__(self, rec_model_path):
self.rknn = RKNNLite()
ret = self.rknn.load_rknn(rec_model_path)
if ret != 0:
raise RuntimeError('加载识别模型失败')
ret = self.rknn.init_runtime(core_mask=RKNNLite.NPU_CORE_0)
if ret != 0:
raise RuntimeError('初始化运行时失败')
self.names = []
self.embeddings = None
def register_face(self, name, face_images):
"""
注册人脸:从多张图像中提取特征向量并取平均
Args:
name: 人名标识
face_images: 人脸图像列表(112x112 RGB)
"""
embeddings = []
for img in face_images:
img_rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) \
if len(img.shape) == 3 else img
outputs = self.rknn.inference(inputs=[img_rgb])
emb = outputs[0].flatten().astype(np.float32)
norm = np.linalg.norm(emb)
if norm > 0:
emb = emb / norm
embeddings.append(emb)
# 多张图像取均值作为注册特征
mean_emb = np.mean(embeddings, axis=0)
norm = np.linalg.norm(mean_emb)
if norm > 0:
mean_emb = mean_emb / norm
self.names.append(name)
if self.embeddings is None:
self.embeddings = mean_emb.reshape(1, -1)
else:
self.embeddings = np.vstack([self.embeddings, mean_emb.reshape(1, -1)])
print(f'注册成功: {name},当前人脸库: {len(self.names)} 人')
def search(self, embedding, top_k=5):
"""搜索最相似的人脸"""
if self.embeddings is None or len(self.names) == 0:
return []
similarities = np.dot(self.embeddings, embedding)
top_idx = similarities.argsort()[::-1][:top_k]
return [(self.names[i], float(similarities[i])) for i in top_idx]
def save(self, db_path):
"""保存人脸库到文件"""
data = {'names': self.names, 'embeddings': self.embeddings}
np.save(db_path, data)
print(f'人脸库已保存: {db_path} ({len(self.names)} 人)')
def load(self, db_path):
"""从文件加载人脸库"""
data = np.load(db_path, allow_pickle=True).item()
self.names = data['names']
self.embeddings = data['embeddings']
print(f'人脸库已加载: {len(self.names)} 人')
def delete(self, name):
"""删除指定人员"""
if name in self.names:
idx = self.names.index(name)
self.names.pop(idx)
self.embeddings = np.delete(self.embeddings, idx, axis=0)
print(f'已删除: {name}')
else:
print(f'未找到: {name}')
def release(self):
self.rknn.release()人脸库数据格式:
# 人脸库文件格式 (.npy)
{
'names': ['张三', '李四', '王五', ...], # 人员名称列表
'embeddings': np.array([ # 特征向量矩阵
[0.012, -0.034, 0.056, ...], # 张三的特征
[0.023, -0.045, 0.067, ...], # 李四的特征
...
]) # shape: (N, feature_dim)
}4.5 活体检测(RGB/IR 双模态)
活体检测用于防止照片、视频回放等攻击手段。RGB+IR 双模态方案通过红外图像辅助判断真实活体:
from rknnlite.api import RKNNLite
import cv2
import numpy as np
class LivenessDetector:
"""RGB/IR 双模态活体检测"""
def __init__(self, liveness_model_path):
self.rknn = RKNNLite()
ret = self.rknn.load_rknn(liveness_model_path)
if ret != 0:
raise RuntimeError('加载活体检测模型失败')
ret = self.rknn.init_runtime(core_mask=RKNNLite.NPU_CORE_2)
if ret != 0:
raise RuntimeError('初始化活体运行时失败')
def check_liveness(self, rgb_face, ir_face):
"""
RGB+IR 双模态活体检测
Args:
rgb_face: RGB 模态人脸图像(112x112)
ir_face: IR 模态人脸图像(112x112,与 RGB 对齐)
Returns:
is_live: 是否为活体
confidence: 活体置信度
"""
# 预处理
rgb_input = cv2.resize(rgb_face, (112, 112))
rgb_input = cv2.cvtColor(rgb_input, cv2.COLOR_BGR2RGB)
ir_input = cv2.resize(ir_face, (112, 112))
if len(ir_input.shape) == 2:
ir_input = cv2.cvtColor(ir_input, cv2.COLOR_GRAY2RGB)
# 拼接 RGB+IR 作为输入(根据模型定义调整)
combined = np.concatenate([rgb_input, ir_input], axis=-1)
# NPU 推理
outputs = self.rknn.inference(inputs=[combined])
# 解析输出
score = float(outputs[0].flatten()[0])
is_live = score > 0.5
return is_live, score
def check_liveness_rgb_only(self, rgb_face):
"""
RGB 单目活体检测(无 IR 摄像头时使用)
Args:
rgb_face: RGB 人脸图像
Returns:
is_live: 是否为活体
confidence: 活体置信度
"""
img = cv2.resize(rgb_face, (112, 112))
img_rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
outputs = self.rknn.inference(inputs=[img_rgb])
score = float(outputs[0].flatten()[0])
is_live = score > 0.5
return is_live, score
def release(self):
self.rknn.release()活体检测方案对比:
| 方案 | 模态 | 安全等级 | 硬件要求 | 适用场景 |
|---|---|---|---|---|
| RGB 单目 | 仅 RGB | 中 | 单 RGB 摄像头 | 普通门禁、考勤 |
| RGB+IR 双目 | RGB + IR | 高 | RGB + IR 摄像头 | 金融支付、高安全门禁 |
| RGB+深度 | RGB + Depth | 极高 | RGB + 结构光/ToF | 银行柜台、高端安防 |
4.6 多线程流水线处理
利用 RK3576 三核 NPU 实现人脸处理流水线,最大化吞吐量:
from rknnlite.api import RKNNLite
import cv2
import numpy as np
import threading
import queue
import time
class FacePipeline:
"""三核 NPU 人脸处理流水线"""
def __init__(self, det_model, rec_model, face_db_path):
self.det_queue = queue.Queue(maxsize=4)
self.rec_queue = queue.Queue(maxsize=4)
self.result_queue = queue.Queue(maxsize=8)
# 检测模型(NPU0)
self.det_rknn = RKNNLite()
self.det_rknn.load_rknn(det_model)
self.det_rknn.init_runtime(core_mask=RKNNLite.NPU_CORE_0)
# 识别模型(NPU1)
self.rec_rknn = RKNNLite()
self.rec_rknn.load_rknn(rec_model)
self.rec_rknn.init_runtime(core_mask=RKNNLite.NPU_CORE_1)
# 加载人脸库
self.face_db = np.load(face_db_path, allow_pickle=True).item()
self.running = False
def start(self):
"""启动流水线"""
self.running = True
self.det_thread = threading.Thread(target=self._detect_loop, daemon=True)
self.rec_thread = threading.Thread(target=self._recognize_loop, daemon=True)
self.det_thread.start()
self.rec_thread.start()
def _detect_loop(self):
"""检测线程:运行在 NPU0"""
while self.running:
try:
frame = self.det_queue.get(timeout=1.0)
except queue.Empty:
continue
# 预处理
img = cv2.resize(frame, (640, 640))
img_rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
# NPU0 推理
outputs = self.det_rknn.inference(inputs=[img_rgb])
# 简化解析:提取检测到的人脸区域
# 实际项目中需根据具体模型输出格式解析
h, w = frame.shape[:2]
face_regions = self._parse_faces(outputs, w, h)
if face_regions:
self.rec_queue.put((frame, face_regions))
def _recognize_loop(self):
"""识别线程:运行在 NPU1"""
while self.running:
try:
frame, face_regions = self.rec_queue.get(timeout=1.0)
except queue.Empty:
continue
results = []
for (x1, y1, x2, y2) in face_regions:
face_crop = frame[y1:y2, x1:x2]
if face_crop.size == 0:
continue
# 对齐
face_aligned = cv2.resize(face_crop, (112, 112))
face_rgb = cv2.cvtColor(face_aligned, cv2.COLOR_BGR2RGB)
# NPU1 推理
outputs = self.rec_rknn.inference(inputs=[face_rgb])
emb = outputs[0].flatten().astype(np.float32)
norm = np.linalg.norm(emb)
if norm > 0:
emb = emb / norm
# 搜索人脸库
name = 'Unknown'
score = 0.0
if self.face_db['embeddings'] is not None:
sims = np.dot(self.face_db['embeddings'], emb)
best_idx = sims.argmax()
if sims[best_idx] > 0.4:
name = self.face_db['names'][best_idx]
score = float(sims[best_idx])
results.append({
'box': (x1, y1, x2, y2),
'name': name,
'score': score
})
self.result_queue.put(results)
def _parse_faces(self, outputs, img_w, img_h):
"""解析检测输出"""
faces = []
scale_x = img_w / 640.0
scale_y = img_h / 640.0
for output in outputs:
if output is None:
continue
if len(output.shape) == 3:
for det in output[0]:
if det[0] > 0.5:
x1 = int(det[1] * scale_x)
y1 = int(det[2] * scale_y)
x2 = int(det[3] * scale_x)
y2 = int(det[4] * scale_y)
faces.append((x1, y1, x2, y2))
return faces
def stop(self):
self.running = False
self.det_rknn.release()
self.rec_rknn.release()5. 代码详解
5.1 SCRFD 人脸检测原理
SCRFD(Sample and Computation Redistribution for Face Detection)是基于 anchor-free 的人脸检测器:
SCRFD 核心流程:
1. Backbone: MobileNetV2 提取多尺度特征
2. FPN: 特征金字塔网络融合不同尺度特征
3. 检测头: 对每个特征图位置预测:
- 分类分支: 该位置是否为人脸
- 回归分支: 人脸边界框偏移量
- 关键点分支: 5 个人脸关键点位置
4. 解码: 将网络输出解码为实际坐标
5. NMS: 去除重叠的检测框SCRFD vs RetinaFace 对比:
| 对比项 | SCRFD | RetinaFace |
|---|---|---|
| 架构 | anchor-free | anchor-based |
| 检测速度 | 更快 | 较慢 |
| 小人脸检测 | 优秀 | 良好 |
| NPU 兼容性 | 更优 | 良好 |
| 推荐模型 | scrfd_2.5g | retinanet-mnet |
5.2 ArcFace 特征提取
ArcFace(Additive Angular Margin Loss)通过在角度空间添加间隔来增强特征区分度:
ArcFace 特征提取流程:
1. 输入: 112x112 RGB 人脸图像
2. Backbone: MobileNetV2 / IResNet 提取特征
3. 特征归一化: L2 归一化到单位超球面
4. 输出: 128/256/512 维特征向量
损失函数:
L = -log(e^(s*cos(θ_yi+m)) / (e^(s*cos(θ_yi+m)) + Σ_j≠yi e^(s*cos(θ_j))))
其中:
s = scale (通常 64)
m = margin (通常 0.5 弧度)
θ = 特征向量与权重向量的角度特征向量维度选择:
| 维度 | 存储开销 (1万人) | 搜索速度 | 适用场景 |
|---|---|---|---|
| 128 | 5 MB | 极快 | 嵌入式端侧,小规模人脸库 |
| 256 | 10 MB | 快 | 通用场景,万级人脸库 |
| 512 | 20 MB | 中等 | 高精度场景,百万级人脸库 |
5.3 人脸对齐原理
人脸对齐通过仿射变换将不同姿态的人脸标准化:
仿射变换矩阵计算:
1. 提取 5 个关键点: 左眼、右眼、鼻尖、左嘴角、右嘴角
2. 定义标准模板坐标(ArcFace 训练时使用的固定坐标)
3. 通过 least-squares 求解 2x3 仿射变换矩阵 M
4. 使用 cv2.warpAffine() 对原图进行变换
变换公式:
[x'] [m00 m01 m02] [x]
[y'] = [m10 m11 m12] [y]
[1 ] [0 0 1 ] [1]5.4 相似度度量方法
人脸比对常用的相似度度量方法:
| 方法 | 公式 | 特点 | | ---------- | --------------------------- | ------------------------------ | --- | --- | --- | ------------------------ | | 余弦相似度 | cos(A,B) = A.B / ( | A | * | B | ) | 对向量长度不敏感,最常用 | | L2 距离 | d(A,B) = sqrt(sum((A-B)^2)) | 直观的距离度量 | | 内积 | dot(A,B) = sum(A*B) | 特征已归一化时等价于余弦相似度 |
相似度阈值参考:
| 阈值范围 | 误识率 (FAR) | 拒识率 (FRR) | 推荐场景 |
|---|---|---|---|
| 0.3 ~ 0.4 | 极低 | 较高 | 高安全门禁 |
| 0.4 ~ 0.5 | 低 | 中等 | 通用人脸识别 |
| 0.5 ~ 0.6 | 中等 | 较低 | 宽松匹配场景 |
5.5 NPU 三核分工策略
RK3576 的三核 NPU 在人脸识别系统中的最优分工:
| NPU 核心 | 任务 | 模型 | 说明 |
|---|---|---|---|
| NPU0 | 人脸检测 | SCRFD | 检测为计算密集型,独立核心避免竞争 |
| NPU1 | 特征提取 | ArcFace | 识别需要等待检测结果,独立核心并行 |
| NPU2 | 活体检测 | LivenessNet | 活体检测可与识别并行执行 |
说明: 各阶段端到端延迟与流水线帧率需在 C-3576BQ + M4-R2 V10 板卡上实际测量(待板上验证)。上表核心分工为 RK3576 三核 NPU 通用调度结构描述。
6. 常见问题
6.1 检测不到人脸
现象: 图像中有人脸但检测框为空。
解决方案:
# 1. 降低检测阈值
recognizer = FaceRecognizer(
det_model_path='scrfd_2.5g.rknn',
rec_model_path='arcface_mobileface.rknn',
det_threshold=0.3 # 从 0.5 降低到 0.3
)
# 2. 检查预处理参数是否与训练一致
rknn.config(
mean_values=[[127.5, 127.5, 127.5]], # SCRFD 标准预处理
std_values=[[128, 128, 128]],
target_platform='rk3576'
)
# 3. 检查输入图像质量
# 确保图像亮度充足,人脸区域不小于 20x20 像素
# 4. 使用更敏感的模型
# 从 scrfd_2.5g 切换到 scrfd_0.5g(更敏感但精度略低)6.2 识别准确率低
现象: 人脸识别错误率高,匹配结果不准确。
解决方案:
# 1. 提高识别相似度阈值
recognizer = FaceRecognizer(
...,
rec_threshold=0.5 # 从 0.4 提高到 0.5
)
# 2. 增加注册图像数量(每人 5-10 张,覆盖不同角度和光照)
for img in multiple_images:
db.register_face(name, [img]) # 多次注册取均值
# 3. 检查人脸对齐质量
# 确保关键点检测准确,对齐后的人脸眼睛位置大致在 (38,38) 和 (74,38)
# 4. 使用更大模型
# 从 MobileFaceNet 切换到 IResNet-50(精度更高)6.3 活体检测误判
现象: 真人被误判为攻击,或照片攻击通过检测。
解决方案:
# 1. RGB 单目方案:调整纹理分析参数
# - 增加多帧检测(连续 3 帧一致才判定)
# - 结合眨眼检测、头部转动等行为分析
# 2. RGB+IR 方案:确保 IR 摄像头工作正常
ffmpeg -f v4l2 -i /dev/video2 -frames 1 test_ir.jpg
# 检查 IR 图像是否包含有效信息
# 3. 调整活体阈值
is_live, score = detector.check_liveness(rgb_face, ir_face)
# score > 0.5 判定为活体,可调整阈值6.4 推理帧率低
现象: 人脸检测与识别帧率低于预期。
解决方案:
# 1. 确认使用三核 NPU 流水线
# 检测(NPU0) + 识别(NPU1) + 活体(NPU2)
# 2. 确认 NPU 使能与频率状态
cat /sys/kernel/debug/clk/clk_npu/clk_rate
# 3. 降低检测输入分辨率
# 640x640 -> 320x320 可明显提速
# 4. 优化人脸库搜索
# 使用 FAISS 等库加速大规模向量搜索
pip3 install faiss-cpu
# 5. 检查 CPU 负载
top -d 1
# 将 Python 进程绑定到大核
taskset -c 4-7 python3 face_recognition.py6.5 模型转换失败
现象: rknn.load_onnx() 或 rknn.build() 返回错误。
解决方案:
# 1. 升级 rknn-toolkit2
pip3 install --upgrade rknn-toolkit2
# 2. 简化 ONNX 模型
pip install onnx-simplifier
python3 -m onnxsim scrfd.onnx scrfd_sim.onnx
# 3. 检查不支持的算子
# 常见问题算子及替代方案:
# - GatherND -> Reshape + Gather
# - InstanceNorm -> LayerNorm 替代
# - SiLU -> 在导出时替换为 ReLU6 组合
# 4. 固定输入维度(避免动态 shape)
# 在模型导出时指定固定 batch size 和空间维度6.6 人脸库加载失败
现象: 加载 .npy 格式人脸库时出错。
解决方案:
# 1. 检查文件格式
import numpy as np
data = np.load('face_database.npy', allow_pickle=True).item()
print(type(data)) # 应为 dict
print(data.keys()) # 应包含 'names' 和 'embeddings'
# 2. 检查数据一致性
assert len(data['names']) == data['embeddings'].shape[0], \
f"名称数量({len(data['names'])})与特征数量({data['embeddings'].shape[0]})不匹配"
# 3. 检查特征维度
# MobileFaceNet: 128 维
# IResNet-50/100: 512 维
assert data['embeddings'].shape[1] in [128, 256, 512], \
f"特征维度异常: {data['embeddings'].shape[1]}"
# 4. 检查文件权限
ls -la face_database.npy
chmod 644 face_database.npy相关文档
| 文档名称 | 路径 | 说明 |
|---|---|---|
| NPU 架构与性能概览 | NPU架构与性能概览 | RK3576 NPU 架构介绍 |
| RKNN Toolkit2 安装与使用 | RKNN Toolkit2安装与使用 | 工具链安装与基本使用 |
| 模型转换与量化 | 模型转换与量化 | 模型转换流程与量化 |
| 分类模型部署(LeNet/ResNet) | 分类模型部署(LeNet-ResNet) | 分类模型部署 |
| 目标检测部署(YOLOv5/v7/v8) | 目标检测部署(YOLOv5-v7-v8) | YOLO 系列检测部署 |
| 语义分割部署 | 语义分割部署 | 分割模型部署 |
| OCR 文字识别 | OCR文字识别 | OCR 模型部署 |
| NPU 性能调优与调试 | NPU性能调优与调试 | NPU 推理性能调优 |
