OpenCV DNN 人脸检测与识别:FaceDetectorYN 与 FaceRecognizerSF 实战指南
本文基于 OpenCV 官方教程 DNN-based Face Detection And Recognition(兼容版本要求 OpenCV >= 4.5.4)展开,系统讲解如何用 cv::FaceDetectorYN 完成人脸定位与关键点检测、用 cv::FaceRecognizerSF 完成人脸对齐、特征提取与身份比对。读完本文,你可以直接运行仓库中的 C++/Python 示例代码,理解检测输出 15 列结果的每一列含义,并从源码层面掌握 YuNet 多尺度解码、NMS 后处理以及 SFace 相似度对齐的底层实现细节。
前置条件:两个预训练 ONNX 模型
该功能依赖两个预训练的 ONNX 模型,官方模型仓库(opencv_zoo)中的 face_detection_yunet 与 face_recognition_sface 目录提供下载,示例代码的默认模型文件名分别对应:
- 检测模型:
face_detection_yunet_*.onnx(YuNet) - 识别模型:
face_recognition_sface_*.onnx(SFace)
YuNet 人脸检测模型
| 指标 | 数值 |
|---|---|
| 模型大小 | 338KB |
| WIDER Face Val(easy) | 0.830 |
| WIDER Face Val(medium) | 0.824 |
| WIDER Face Val(hard) | 0.708 |
小体积是其突出特点:338KB 的模型在保持较高速度的同时覆盖 easy/medium/hard 三个难度等级,适合集成到端侧或实时流水线中。
SFace 人脸识别模型
| 指标 | 数值 |
|---|---|
| 模型大小 | 36.9MB |
| 输出特征 | 128 维人脸特征向量(经 L2 归一化) |
在各主流验证集上的身份验证精度与推荐阈值如下(该表来自原文档,也是示例代码中默认阈值的出处):
| 数据库 | 精度(Accuracy) | 阈值(normL2) | 阈值(cosine) |
|---|---|---|---|
| LFW | 99.60% | 1.128 | 0.363 |
| CALFW | 93.95% | 1.149 | 0.340 |
| CPLFW | 91.05% | 1.204 | 0.275 |
| AgeDB-30 | 94.90% | 1.202 | 0.277 |
| CFP-FP | 94.80% | 1.253 | 0.212 |
注意:normL2 阈值越低、cosine 阈值越高,判定"同一人"就越严格。示例代码默认采用 LFW 的 0.363 / 1.128,这是精度与召回的一个平衡点。
API 总览
两个类的声明位于 objdetect 模块头文件,实现分别位于 face_detect.cpp 与 face_recognize.cpp。两者都内置于 objdetect 模块,但依赖 dnn 模块加载 ONNX 网络——从源码结构看,若编译时未启用 dnn(无 HAVE_OPENCV_DNN),FaceDetectorYN::create 与 FaceRecognizerSF::create 会直接抛出 StsNotImplemented 错误。
FaceDetectorYN::create 的完整参数(默认值取自头文件):
| 参数 | 说明 | 默认值 |
|---|---|---|
model |
检测模型路径(ONNX) | 必填 |
config |
配置文件路径,兼容旧格式;ONNX 模型传空字符串 | "" |
input_size |
网络输入尺寸 | 必填 |
score_threshold |
过滤低于该分数的检测框 | 0.9 |
nms_threshold |
IoU 大于该值的框被 NMS 抑制 | 0.3 |
top_k |
NMS 前保留的 top-K 候选框数 | 5000 |
backend_id / target_id |
DNN 后端与目标设备 | 0 / 0 |
FaceRecognizerSF::create 参数较简单:model、config、backend_id、target_id。此外两个类都提供基于内存缓冲区(framework + bufferModel + bufferConfig)的 create 重载,便于嵌入式场景从 RAM 直接加载模型。
人脸检测:FaceDetectorYN
完整可运行示例位于 samples/dnn/face_detect.cpp(C++)与 samples/dnn/face_detect.py(Python),支持单图检测、双图比对(检测 + 识别)与视频/摄像头实时检测三种模式。
命令行参数
示例程序支持的完整命令行参数如下(C++ 通过 CommandLineParser 实现):
CommandLineParser parser(argc, argv,
"{help h | | Print this message}"
"{image1 i1 | | Path to the input image1. Omit for detecting through VideoCapture}"
"{image2 i2 | | Path to the input image2. When image1 and image2 parameters given then the program try to find a face on both images and runs face recognition algorithm}"
"{video v | 0 | Path to the input video}"
"{scale sc | 1.0 | Scale factor used to resize input video frames}"
"{fd_model fd | face_detection_yunet_2026may.onnx| Path to the model}"
"{fr_model fr | face_recognition_sface_2021dec.onnx | Path to the face recognition model}"
"{score_threshold | 0.85 | Filter out faces of score < score_threshold}"
"{nms_threshold | 0.3 | Suppress bounding boxes of iou >= nms_threshold}"
"{top_k | 5000 | Keep top_k bounding boxes before NMS}"
"{save s | false | Set true to save results. This flag is invalid when using camera}"
);
| 参数 | 默认值 | 说明 |
|---|---|---|
-i1 / --image1 |
无 | 输入图片 1;省略则进入视频/摄像头模式 |
-i2 / --image2 |
无 | 输入图片 2;与 image1 同时给出时执行跨图人脸比对 |
-v / --video |
0 | 视频文件路径或摄像头设备号 |
-sc / --scale |
1.0 | 输入帧缩放因子 |
-fd / --fd_model |
face_detection_yunet_2026may.onnx |
检测模型路径 |
-fr / --fr_model |
face_recognition_sface_2021dec.onnx |
识别模型路径 |
--score_threshold |
0.85 | 过滤置信度低于该值的检测框(注意:比 API 默认值 0.9 更宽松) |
--nms_threshold |
0.3 | NMS 抑制阈值 |
--top_k |
5000 | NMS 前保留的候选框数 |
-s / --save |
false | 是否保存结果图 |
典型调用方式:
# 单图检测
face_detect --image1 lena.jpg
# 双图比对(检测 + 识别)
face_detect --image1 person_a.jpg --image2 person_b.jpg
# 摄像头实时检测,0.5 倍缩放,保存结果
face_detect --video 0 --scale 0.5 --save
C++ 核心流程
检测器初始化、推理两步如下(摘自示例):
//! [initialize_FaceDetectorYN]
// Initialize FaceDetectorYN
Ptr<FaceDetectorYN> detector = FaceDetectorYN::create(
fd_modelPath, "", Size(320, 320), scoreThreshold, nmsThreshold, topK);
//! [initialize_FaceDetectorYN]
//! [inference]
// Set input size before inference
detector->setInputSize(image1.size());
Mat faces1;
detector->detect(image1, faces1);
if (faces1.rows < 1)
{
std::cerr << "Cannot find a face in " << input1 << std::endl;
return 1;
}
//! [inference]
要点:
create时传入的Size(320, 320)只是初始输入尺寸。示例中每次读取新图片后都调用setInputSize(image1.size())将其同步为实际图像尺寸,否则detect内部会因尺寸校验失败(源码中对应CV_CheckEQ断言,报错信息为 "Size does not match. Call setInputSize(size) if input size does not match the preset size")。detect返回int(1 表示正常,0 表示输入为空),检测结果通过输出参数faces传出。
Python 侧等价代码:
## [initialize_FaceDetectorYN]
detector = cv.FaceDetectorYN.create(
args.face_detection_model, # YuNet ONNX 模型路径
"", # config,ONNX 模型传空
(320, 320), # 初始输入尺寸
args.score_threshold, # 0.85
args.nms_threshold, # 0.3
args.top_k # 5000
)
## [initialize_FaceDetectorYN]
## [inference]
# Set input size before inference
detector.setInputSize((img1Width, img1Height))
faces1 = detector.detect(img1) # faces1 为 (return_code, faces_mat) 元组
## [inference]
检测结果输出格式
faces 是一个 CV_32F 的二维矩阵,形状为 [num_faces, 15],每行一张脸,列的含义依次为:
x1, y1, w, h, x_re, y_re, x_le, y_le, x_nt, y_nt, x_rcm, y_rcm, x_lcm, y_lcm, score
x1, y1, w, h:人脸边界框左上角坐标与宽高;{x, y}_{re, le, nt, rcm, lcm}:右眼、左眼、鼻尖、右嘴角、左嘴角 5 个关键点的坐标;- 第 15 列(索引 14):人脸置信度分数。
示例中的可视化函数据此绘制框与 5 个关键点:
for (int i = 0; i < faces.rows; i++)
{
// 边界框
rectangle(input, Rect2i(int(faces.at<float>(i, 0)), int(faces.at<float>(i, 1)),
int(faces.at<float>(i, 2)), int(faces.at<float>(i, 3))),
Scalar(0, 255, 0), thickness);
// 5 个关键点:右眼、左眼、鼻尖、右嘴角、左嘴角
circle(input, Point2i(int(faces.at<float>(i, 4)), int(faces.at<float>(i, 5))), 2, Scalar(255, 0, 0), thickness);
circle(input, Point2i(int(faces.at<float>(i, 6)), int(faces.at<float>(i, 7))), 2, Scalar(0, 0, 255), thickness);
circle(input, Point2i(int(faces.at<float>(i, 8)), int(faces.at<float>(i, 9))), 2, Scalar(0, 255, 0), thickness);
circle(input, Point2i(int(faces.at<float>(i, 10)), int(faces.at<float>(i, 11))), 2, Scalar(255, 0, 255), thickness);
circle(input, Point2i(int(faces.at<float>(i, 12)), int(faces.at<float>(i, 13))), 2, Scalar(0, 255, 255), thickness);
}
这 5 个关键点不只是用于可视化——它们是后续 SFace 人脸对齐的必需输入(见下文 alignCrop)。
源码深入:YuNet 的多尺度解码与 NMS
阅读 face_detect.cpp 中的 FaceDetectorYNImpl,可以还原出几个关键实现细节:
-
输入强制按 32 对齐。构造函数中
divisor(32),padW = ((inputW - 1) / 32 + 1) * 32。setInputSize时重新计算 pad 尺寸并调用net.setInputShape("input", MatShape({1, 3, padH, padW}))。这意味着即使你传入 320×320 之外的任意尺寸,网络实际看到的是向上取整到 32 倍数的尺寸,detect内部会先copyMakeBorder补零再dnn::blobFromImage。 -
三路 stride 的级联解码。网络前向一次性取出 12 个输出层:
cls_8/cls_16/cls_32(分类分数)、obj_8/obj_16/obj_32(目标分数)、bbox_*(框回归)、kps_*(关键点回归),对应strides({8, 16, 32})三个特征层级——小脸由大 stride 层负责,大脸由小 stride 层负责。 -
分数合成与框解码。每个网格位置的最终分数是
sqrt(cls_score * obj_score)(两个分数先各自 clamp 到 [0,1]);低于scoreThreshold的候选直接丢弃。边界框由中心点偏移与exp()还原宽高后乘以 stride 得到:
float score = std::sqrt(cls_score * obj_score);
...
float cx = ((c + bbox_v[idx * 4 + 0]) * strides[i]);
float cy = ((r + bbox_v[idx * 4 + 1]) * strides[i]);
float w = exp(bbox_v[idx * 4 + 2]) * strides[i];
float h = exp(bbox_v[idx * 4 + 3]) * strides[i]);
- NMS 收尾。所有层级汇总后调用
dnn::NMSBoxes(faceBoxes, faceScores, scoreThreshold, nmsThreshold, keepIdx, 1.f, topK),即nms_threshold(默认 0.3)控制 IoU 抑制、top_k(默认 5000)限制 NMS 前的候选规模。这解释了为何示例把nms_threshold的默认值设为 0.3——与 API 默认一致。
人脸比对:FaceRecognizerSF
当 image1 与 image2 同时给出时,示例会走"检测 → 对齐裁剪 → 特征提取 → 身份比对"流程(摘自 face_detect.cpp):
//! [initialize_FaceRecognizerSF]
// Initialize FaceRecognizerSF
Ptr<FaceRecognizerSF> faceRecognizer = FaceRecognizerSF::create(fr_modelPath, "");
//! [initialize_FaceRecognizerSF]
//! [facerecognizer]
// Aligning and cropping facial image through the first face of faces detected.
Mat aligned_face1, aligned_face2;
faceRecognizer->alignCrop(image1, faces1.row(0), aligned_face1);
faceRecognizer->alignCrop(image2, faces2.row(0), aligned_face2);
// Run feature extraction with given aligned_face
Mat feature1, feature2;
faceRecognizer->feature(aligned_face1, feature1);
feature1 = feature1.clone();
faceRecognizer->feature(aligned_face2, feature2);
feature2 = feature2.clone();
//! [facerecognizer]
//! [match]
double cos_score = faceRecognizer->match(feature1, feature2, FaceRecognizerSF::DisType::FR_COSINE);
double L2_score = faceRecognizer->match(feature1, feature2, FaceRecognizerSF::DisType::FR_NORM_L2);
//! [match]
if (cos_score >= cosine_similar_thresh) // 0.363
std::cout << "They have the same identity;";
...
if (L2_score <= l2norm_similar_thresh) // 1.128
std::cout << "They have the same identity;";
Python 等价实现:
## [initialize_FaceRecognizerSF]
recognizer = cv.FaceRecognizerSF.create(args.face_recognition_model, "")
## [initialize_FaceRecognizerSF]
## [facerecognizer]
# Align faces
face1_align = recognizer.alignCrop(img1, faces1[1][0])
face2_align = recognizer.alignCrop(img2, faces2[1][0])
# Extract features
face1_feature = recognizer.feature(face1_align)
face2_feature = recognizer.feature(face2_align)
## [facerecognizer]
cosine_similarity_threshold = 0.363
l2_similarity_threshold = 1.128
## [match]
cosine_score = recognizer.match(face1_feature, face2_feature, cv.FaceRecognizerSF_FR_COSINE)
l2_score = recognizer.match(face1_feature, face2_feature, cv.FaceRecognizerSF_FR_NORM_L2)
## [match]
if cosine_score >= cosine_similarity_threshold:
msg = 'the same identity'
if l2_score <= l2_similarity_threshold:
msg = 'the same identity'
三个方法的分工:
alignCrop(src_img, face_box, aligned_img):输入原图与检测结果的某一行(15 列),内部取第 4~13 列的 5 个关键点,通过相似变换把人脸对齐到固定的 112×112 参考模板。face_box是单行向量,示例中取faces1.row(0),即第一张脸。feature(aligned_img, face_feature):把 112×112 对齐图送入 SFace 网络,输出 128 维特征向量。match(feature1, feature2, dis_type):计算两个特征的距离,dis_type取FR_COSINE(余弦相似度,越大越相似,上限 1.0)或FR_NORM_L2(L2 距离,越小越相似,下限 0.0)。
判定规则一句话总结:cosine 距离 ≥ 0.363,或 normL2 距离 ≤ 1.128,即可判定为同一身份(阈值出处见上文 SFace 精度表,对应 LFW 数据集)。
源码深入:对齐变换与距离计算
face_recognize.cpp 中的 FaceRecognizerSFImpl 揭示了细节:
-
固定的 112×112 对齐模板。
alignCrop使用 5 个标准参考点(右眼 38.2946, 51.6963;左眼 73.5318, 51.5014;鼻尖 56.0252, 71.7366;右嘴角 41.5493, 92.3655;左嘴角 70.7299, 92.2041),对"检测关键点 → 参考点"做闭式相似变换求解(SVD 分解求旋转+缩放,处理秩亏与反射),最后warpAffine到 112×112。这就是为什么检测阶段的关键点质量直接影响识别精度。 -
特征归一化。
match内部先对两个特征做normalize(L2 归一化),再按类型计算:FR_COSINE返回逐元素相乘后的总和(单位向量下的内积即余弦相似度);FR_NORM_L2返回cv::norm(f1, f2)。传其他dis_type会抛出invalid_argument。 -
特征提取的前处理。
feature内部调用dnn::blobFromImage(aligned_img, 1, Size(112, 112), Scalar(0,0,0), true, false),即不额外缩放、不做均值减除——对齐后的 112×112 图就是网络的原生输入。
回归测试与结果验证
仓库中的 modules/objdetect/test/test_face.cpp 提供了一条可验证的基准流程:加载 yunet-202303.onnx 模型、setScoreThreshold(0.7f),逐张读取带标注图片,用 IoU ≥ 0.7 匹配检测框、用人均位移 ≤ 15 像素匹配关键点,与文本标注比对。如果你的检测结果格式不确定,可以直接对照该测试中标签文件的解析逻辑(每行 4 + 2×5 个坐标)来核对 15 列输出的取值范围。
小结
- 检测:
FaceDetectorYN用 338KB 的 YuNet 模型输出[N, 15]结果(框 + 5 关键点 + 分数);换图前必须setInputSize,尺寸内部按 32 对齐 pad;score_threshold/nms_threshold/top_k分别控制置信度过滤、IoU 抑制与候选规模。 - 识别:
FaceRecognizerSF的三步流水线是alignCrop(5 关键点 → 112×112 相似变换)→feature(128 维向量)→match(FR_COSINE≥ 0.363 或FR_NORM_L2≤ 1.128 判为同人,阈值可依据目标数据集从精度表中调整)。 - 工程入口:C++ 与 Python 双语言示例见 samples/dnn/face_detect.cpp 与 samples/dnn/face_detect.py,API 声明见 face.hpp,实现细节见 face_detect.cpp 与 face_recognize.cpp。
- 前提:编译时需启用
dnn模块,且 OpenCV 版本 ≥ 4.5.4;模型从官方opencv_zoo仓库的face_detection_yunet/face_recognition_sface目录获取。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0623
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00