用OpenCV+DNN提取人脸特征向量并计算余弦相似度的关键在于:先用ONNX版ArcFace等模型提取归一化特征,再手动计算dot(a,b)/(norm(a)*norm(b)),注意对齐、预处理和阈值校准。

用 OpenCV + DNN 提取人脸特征向量
余弦相似度本身只是向量运算,关键在怎么拿到两张图对应的人脸特征向量。OpenCV 自带的 dnn::readNetFromTorch 或 dnn::readNetFromTensorflow 可以加载预训练的人脸嵌入模型(如 FaceNet、ArcFace 的 ONNX 或 TorchScript 版本),但注意:OpenCV 4.5+ 才较稳定支持 ONNX;低于该版本容易报 Unsupported layer type "BatchNormalization" 或输入尺寸不匹配。
实操建议:
- 优先用 ONNX 格式的 ArcFace 模型(如
arcface_r100_v1.onnx),比旧版 FaceNet Torch 模型兼容性更好 - 输入图像必须先做对齐:用
cv::dnn::blobFromImage前,确保人脸已通过关键点(如 5 点)仿射变换归一化到 112×112 或 128×128(看模型要求) - 模型输入 blob 的
scalefactor通常为 1.0/127.5,mean为(127.5, 127.5, 127.5),不是 ImageNet 的 (104,117,123)
手动实现余弦相似度计算(别用 OpenCV 的 norm)
OpenCV 的 cv::norm(a, b, NORM_L2) 算的是欧氏距离,不是余弦值。余弦相似度公式是 dot(a,b)/(norm(a)*norm(b)),必须自己算,否则结果会全在 0.3~0.7 区间漂移,无法用于阈值判断。
实操建议:
- 用
cv::Mat::dot()计算点积,它默认按通道展开为一维向量再点乘 - 用
cv::norm(feature, NORM_L2)分别算两个向量模长,别用cv::magnitude() - 如果特征向量是 float32 类型但 shape 为
(1, 512),先feature = feature.reshape(1, 512)再计算,否则dot()可能返回 0 - 结果范围是 [-1, 1],实际人脸比对中正常值集中在 [0.3, 0.8],低于 0.4 基本可判为不同人
阈值设定和跨模型差异
0.5 不是通用阈值。ArcFace 模型在 LFW 上推荐阈值约 0.68,而 FaceNet 常用 0.4,同一模型在不同预处理(灰度/RGB、归一化方式)下阈值偏移可达 ±0.05。
实操建议:
- 不要直接抄网上“0.5 就行”——先用你自己的数据集抽 100 对同人图、100 对不同人图,统计余弦得分分布,选 ROC 曲线下面积最大处的阈值
- 如果用 ONNX Runtime 替代 OpenCV DNN(更推荐),注意
Ort::SessionOptions::SetIntraOpNumThreads(1)避免多线程导致输出不稳定 - 两张图光照、姿态差异大时,余弦值可能低于 0.35,这时单纯提高阈值没用,得加活体检测或重采样对齐
常见错误:特征未归一化就计算余弦
有些模型(如部分 PyTorch 导出的 ArcFace)输出特征**已经做过 L2 归一化**,此时分母 norm(a)*norm(b) 恒为 1,余弦值就等于点积。但如果你用的模型没归一化(比如自己训的没加 F.normalize),直接套用会导致结果偏高且不可靠。
验证方法很简单:
- 对任一特征向量
feat,打印cv::norm(feat, NORM_L2)—— 若非常接近 1.0(如 0.9998),说明已归一化;若为 12.3 或 45.7,则必须手动归一化后再算余弦 - 未归一化时,错用点积当余弦,会导致“看起来相似度很高”,但换一张图就崩,尤其在小样本比对中极易误判
实际部署时最容易被忽略的是对齐质量。哪怕模型再强,眼睛位置偏移 5 像素,余弦得分就可能掉 0.1 以上。与其调阈值,不如先确保 cv::getAffineTransform 的三点定位足够稳。
C++免费学习笔记(深入):立即使用
在学习笔记中,你将探索 C++ 的入门与实战技巧!











