首页
/ OpenCV DNN 人脸检测与识别:FaceDetectorYN 与 FaceRecognizerSF 实战指南

OpenCV DNN 人脸检测与识别:FaceDetectorYN 与 FaceRecognizerSF 实战指南

2026-09-06 17:38:27作者:苗圣禹Peter

本文基于 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_yunetface_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.cppface_recognize.cpp。两者都内置于 objdetect 模块,但依赖 dnn 模块加载 ONNX 网络——从源码结构看,若编译时未启用 dnn(无 HAVE_OPENCV_DNN),FaceDetectorYN::createFaceRecognizerSF::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 参数较简单:modelconfigbackend_idtarget_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]

要点:

  1. 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")。
  2. 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,可以还原出几个关键实现细节:

  1. 输入强制按 32 对齐。构造函数中 divisor(32)padW = ((inputW - 1) / 32 + 1) * 32setInputSize 时重新计算 pad 尺寸并调用 net.setInputShape("input", MatShape({1, 3, padH, padW}))。这意味着即使你传入 320×320 之外的任意尺寸,网络实际看到的是向上取整到 32 倍数的尺寸,detect 内部会先 copyMakeBorder 补零再 dnn::blobFromImage

  2. 三路 stride 的级联解码。网络前向一次性取出 12 个输出层:cls_8/cls_16/cls_32(分类分数)、obj_8/obj_16/obj_32(目标分数)、bbox_*(框回归)、kps_*(关键点回归),对应 strides({8, 16, 32}) 三个特征层级——小脸由大 stride 层负责,大脸由小 stride 层负责。

  3. 分数合成与框解码。每个网格位置的最终分数是 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]);
  1. 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

image1image2 同时给出时,示例会走"检测 → 对齐裁剪 → 特征提取 → 身份比对"流程(摘自 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_typeFR_COSINE(余弦相似度,越大越相似,上限 1.0)或 FR_NORM_L2(L2 距离,越小越相似,下限 0.0)。

判定规则一句话总结:cosine 距离 ≥ 0.363,或 normL2 距离 ≤ 1.128,即可判定为同一身份(阈值出处见上文 SFace 精度表,对应 LFW 数据集)。

源码深入:对齐变换与距离计算

face_recognize.cpp 中的 FaceRecognizerSFImpl 揭示了细节:

  1. 固定的 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。这就是为什么检测阶段的关键点质量直接影响识别精度。

  2. 特征归一化match 内部先对两个特征做 normalize(L2 归一化),再按类型计算:FR_COSINE 返回逐元素相乘后的总和(单位向量下的内积即余弦相似度);FR_NORM_L2 返回 cv::norm(f1, f2)。传其他 dis_type 会抛出 invalid_argument

  3. 特征提取的前处理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 维向量)→ matchFR_COSINE ≥ 0.363 或 FR_NORM_L2 ≤ 1.128 判为同人,阈值可依据目标数据集从精度表中调整)。
  • 工程入口:C++ 与 Python 双语言示例见 samples/dnn/face_detect.cppsamples/dnn/face_detect.py,API 声明见 face.hpp,实现细节见 face_detect.cppface_recognize.cpp
  • 前提:编译时需启用 dnn 模块,且 OpenCV 版本 ≥ 4.5.4;模型从官方 opencv_zoo 仓库的 face_detection_yunet / face_recognition_sface 目录获取。
登录后查看全文
热门项目推荐
相关项目推荐