资讯详情

资讯详情

OpenCV DNN人脸识别实战:从YuNet检测到特征提取全流程解析

简介基于OpenCV深度学习神经网络人脸模块OpenCV DNN Face的实时人脸识别程序是一份面向OpenCV初学者、毕业设计及课程设计学生的完整示例代码包。压缩包共15个文件总大小34.89MB核心内容涵盖两个ONNX格式的预训练模型、两段Python源码分别对应人脸识别与人脸样本采集、多张用于测试的JPG图片、XML配置文件以及README和注意事项文档。代码目录按模型、图片与脚本分模块组织便于对照学习模型与脚本均已就绪可直接运行帮助读者快速理解Yunet人脸检测与Face Recognizer特征提取的串联流程掌握从图像输入、人脸定位、特征比对到身份输出的完整实现思路。注意事项文档还对运行环境和常见报错给出提示能有效减少调试障碍。已有127人学习下载适合作为实时人脸识别入门实战、课设演示或毕设二次开发的基础模板。1. 从Haar到YuNet这个OpenCV DNN人脸识别工程为什么值得拆很多人第一次搭人脸识别用的还是 Haar 级联检测加 LBP 直方图相似度那套老流程。换到教室门口这种光照不固定的场景漏检和误报同时出现排查起来很被动。这个工程把 OpenCV DNN 模块串成完整链路yunet.onnx 负责检测人脸face_recognizer_fast.onnx 负责把对齐后的人脸提取成特征向量再用向量距离判断身份。工程自带样本采集和实时识别两个脚本适合当毕业设计、课程设计基座也适合想从传统方案切到深度学习方案的人。我拆过不少同类代码很多同学拿到源码只会改图片路径换摄像头、换人、换光照之后就失灵。真正值得理解的不是跑通而是模型调用顺序、输入尺寸和阈值背后的逻辑。2. 模型选型与文件布局yunet.onnx 和 face_recognizer_fast.onnx 各管哪一段这个工程最大的特点是没走 OpenCV 老式的CascadeClassifier而是把检测和识别都压在 DNN 模块上。OpenCV 从 4.x 中期版本开始提供了FaceDetectorYN和FaceRecognizerSF两套高层 API分别对应检测和特征提取加载 ONNX 模型后不再需要自己写前后处理。先从环境说起pip install opencv-python是基础国内网络建议直接走清华镜像源避免装到一半超时。装完确认cv2.__version__在较新的 4.x 版本FaceDetectorYN_create和FaceRecognizerSF_create这两个接口才能用。2.1 用 FaceDetectorYN 加载 yunet.onnximport cv2 detector cv2.FaceDetectorYN_create( modelmodels/yunet.onnx, input_size(320, 320), score_threshold0.7, nms_threshold0.3, top_k100 )input_size不是模型固定输入而是一个建议检测尺度320x320 在速度与召回之间比较平衡如果你用的摄像头是 1080p可以改成 640x640小脸更容易被检出代价是每帧推理时间翻倍。score_threshold低于这个置信度的人脸会被丢弃默认 0.7 比较保守门禁场景我会提 to 0.8 来压误检。nms_threshold控制重叠框合并的严格程度0.3 意味着两个框 IoU 超过 30% 就合并多脸密集场景可以调到 0.5。top_k限制最多返回的人脸数正常场景 100 完全够用但如果你做密集人群统计建议调回 500。注意FaceDetectorYN的detect方法返回两个值第一个是返回码第二个才是人脸数组数组形状是 Nx15前 4 列是人脸框的 x、y、width、height第 5 到 14 列是左右眼、鼻尖、左右嘴角五个关键点的坐标最后 1 列是置信度。这个结构后面alignCrop要直接用所以别只取框坐标就把整个数组丢掉。2.2 FaceRecognizerSF 加载 face_recognizer_fast.onnxrecognizer cv2.FaceRecognizerSF_create( modelmodels/face_recognizer_fast.onnx, config )config参数在 ONNX 模型通常传空字符串即可它主要是给 Caffe 模型用的 prototxt 路径。FaceRecognizerSF的核心方法有三个alignCrop接收原始图像和 YuNet 输出的人脸数组利用关键点做仿射变换把人脸摆正feature接收对齐后的图输出特征向量match接收两个特征向量返回它们之间的距离。这个fast版本从命名上能看出是为实时推理优化的输出向量维度比完整版低但在这个工程的人脸库规模下辨识度足够。实际部署时我习惯把alignCrop的结果缓存下来尤其是做样本库构建时同一张图反复调feature会白白浪费一次对齐计算。后面第 4 章的代码就是这么处理的。2.3 两个模型的分工边界文件职责关键输出models/yunet.onnx人脸检测人脸框 5 个关键点 置信度models/face_recognizer_fast.onnx人脸特征提取高维特征向量code/sample_collection.py采集与预处理样本按命名规范保存的人脸图code/face_recognition.py实时识别主程序身份标签 距离分数images/已知人脸图片库用于构建特征数据库检测和识别分开是两个模型独立演进的结果YuNet 换掉不影响特征向量维度识别模型升级也不需要重新标框。工程里images/目录放着 zhaoyi.jpg、lihao.jpg 这类单张单人照片实际使用建议每个身份放 5 张以上姿态和光照拉开差距库的泛化能力才够。OpenCV DNN 模块本身不挑 ONNX 来源只要你把模型文件放到 models 目录下路径和上面代码对得上就能跑。3. 样本采集sample_collection.py 里的抓帧节奏与命名规范很多人的毕设翻车不是栽在识别模型上而是栽在样本库上。采集时如果按住摄像头连拍几十张相邻帧几乎一样特征空间里这些点高度重合等于只提供了一个有效样本。反过来每 5 帧才处理一次让人脸在画面里自然移动采集到的样本才覆盖不同角度和表情变化。这个工程里的sample_collection.py解决的就是这件事。3.1 为什么采集节奏影响识别率face_recognizer_fast.onnx提取特征时对姿态和光照非常敏感。同一个人的正脸和低头侧脸特征向量的欧氏距离可能比两个不同人的正脸还要大。采集阶段刻意引入差异等于在训练特征库时就做了数据增强。我一般会让人站在固定位置头部缓慢左右转动摄像头按固定间隔截取画面这样 30 秒就能拿到覆盖正面、左右各 15 度、轻微仰头和低头的样本集。3.2 采集程序的核心逻辑import os import cv2 OUTPUT_DIR dataset TARGET_COUNT 80 SKIP_FRAMES 5 detector cv2.FaceDetectorYN_create( modelmodels/yunet.onnx, input_size(320, 320), score_threshold0.6, nms_threshold0.3, top_k100 ) os.makedirs(OUTPUT_DIR, exist_okTrue) cap cv2.VideoCapture(0) frame_id 0 saved 0 while saved TARGET_COUNT: ret, frame cap.read() if not ret: break frame_id 1 if frame_id % SKIP_FRAMES ! 0: continue _, faces detector.detect(frame) if faces is None: continue for face in faces: x, y, w, h face[:4].astype(int) # 外扩 20%把额头和下巴完整包进来 x max(0, int(x - 0.2 * w)) y max(0, int(y - 0.2 * h)) w int(w * 1.4) h int(h * 1.4) crop frame[y:y h, x:x w] # 统一缩放到 112x112和识别模型的输入对齐 crop cv2.resize(crop, (112, 112)) cv2.imwrite(os.path.join(OUTPUT_DIR, fimg_{saved:03d}.jpg), crop) saved 1 cap.release()SKIP_FRAMES 5意思是每 5 帧才做一次检测和保存靠帧间隔自然引入画面差异。外扩比例 0.2 不是随便定的YuNet 的框通常贴着脸部边缘直接裁剪会切掉部分额头导致alignCrop时关键点比例失衡。缩放到 112x112 是为了和face_recognizer_fast.onnx训练时的输入尺寸对齐ONNX 模型内部有 Resize 层但外部先缩放可以让预处理更可控。3.3 样本多样性设计和命名规范采集变量建议做法原因头部姿态正面、左右 15 度、抬头低头覆盖特征空间的不同方向光照条件顺光、侧光、暗光各采一部分避免模型只记住单一亮度表情变化自然表情 轻微张嘴防止闭眼、抿嘴时匹配失败背景环境尽量贴近实际使用场景减少背景干扰带来的特征偏移保存命名我建议直接做成姓名_序号.jpg的格式比如zhangsan_001.jpg。这样第 4 章构建特征库时直接用文件名前缀作为身份标签省掉一份额外的映射表。工程里 images/ 目录用单张图做演示没问题但正式用的时候样本量至少 50 张起步并且要在和部署摄像头相同的位置采集。如果是在教室门口部署就别拿宿舍里拍的样本凑数光线角度完全不同。4. face_recognition.py 全链路检测、对齐、特征提取与阈值权衡主程序的完整流程是读帧 → YuNet 检测人脸 →alignCrop对齐 →feature提取特征向量 → 和特征库里的向量做距离比对 → 输出身份。每一步的数据形状都要对得上最常见的报错是alignCrop收到的人脸数组维度不对YuNet 输出的 Nx15 不能只截前 4 列传进去。4.1 初始化与特征库构建import os import cv2 BASE_DIR os.path.dirname(os.path.dirname(os.path.abspath(__file__))) DETECT_MODEL os.path.join(BASE_DIR, models, yunet.onnx) RECOG_MODEL os.path.join(BASE_DIR, models, face_recognizer_fast.onnx) IMAGE_DIR os.path.join(BASE_DIR, images) detector cv2.FaceDetectorYN_create( modelDETECT_MODEL, input_size(640, 640), score_threshold0.7, nms_threshold0.3, top_k100 ) recognizer cv2.FaceRecognizerSF_create(modelRECOG_MODEL, config) def build_database(image_dirIMAGE_DIR): database {} for filename in os.listdir(image_dir): if not filename.lower().endswith((.jpg, .jpeg, .png)): continue name os.path.splitext(filename)[0] img cv2.imread(os.path.join(image_dir, filename)) if img is None: continue _, faces detector.detect(img) if faces is None: continue # 取画面中最大的人脸避免背景路人干扰 face sorted(faces, keylambda f: f[2] * f[3])[-1] aligned recognizer.alignCrop(img, face) database[name] recognizer.feature(aligned) return databaseinput_size(640, 640)是为了在 1080p 摄像头画面里尽量召回远处的小脸代价是检测耗时增加。如果你的电脑没有 GPU纯 CPU 推理建议改回 320x320换帧率更划算。sorted(...)[-1]取最大人脸这个逻辑很重要样本照片里如果背后有人群最大框通常才是被采集者。特征库构建完成之后database里的每个 value 都是一个形状为(1, N)的特征矩阵N 取决于 onnx 模型输出维度。4.2 实时识别循环def identify(feature, database, threshold0.5): best_name unknown best_score float(inf) for name, db_feature in database.items(): # cosine 距离越小越相似范围约 0~2 score recognizer.match( feature, db_feature, cv2.FaceRecognizerSF_FR_COSINE ) if score best_score: best_score score best_name name return best_name if best_score threshold else unknown cap cv2.VideoCapture(0) database build_database() while True: ret, frame cap.read() if not ret: break _, faces detector.detect(frame) if faces is not None: for face in faces: aligned recognizer.alignCrop(frame, face) feature recognizer.feature(aligned) name identify(feature, database, threshold0.5) x, y, w, h face[:4].astype(int) cv2.rectangle(frame, (x, y), (x w, y h), (0, 255, 0), 2) cv2.putText(frame, name, (x, y - 10), cv2.FONT_HERSHEY_SIMPLEX, 0.8, (0, 255, 0), 2) cv2.imshow(face recognition, frame) if cv2.waitKey(1) 0xFF ord(q): break cap.release() cv2.destroyAllWindows()FaceRecognizerSF_FR_COSINE使用的 cosine 距离等于1 - cos(夹角)所以距离越小特征越接近0 表示完全同向2 表示完全反向。有人习惯用FaceRecognizerSF_FR_NORM_L2也就是欧氏距离两个方式都行但阈值不能通用L2 的数值范围更大一般落在 0.8 到 1.8 之间。代码里threshold0.5是把判断逻辑留在了identify函数内部实际部署建议把阈值提出来做成配置文件或者命令行参数方便不同场景切换。4.3 匹配阈值怎么选匹配方式距离含义参考阈值区间FaceRecognizerSF_FR_COSINE1 - 余弦相似度越小越相似0.35 ~ 0.6FaceRecognizerSF_FR_NORM_L2欧氏距离越小越相似1.0 ~ 1.6阈值是一个纯经验值和模型输出分布强相关。我在实际项目里的做法是先把所有已知样本两两配对算一遍距离得到库内距离最大值阈值放在这个最大值附近然后专门找几个不在库里的路人做冒名测试看误识率是否在可接受范围。毕业设计答辩时最容易翻车的点就是阈值设得太松屏幕上随便路过一个人都显示成库里的名字这种视觉事故比识别不出本人要严重得多。5. 上线前必做的调参把误识率压下去的四个手尾5.1 用库内距离自动推算阈值底线def estimate_threshold(database): names list(database.keys()) max_dist 0.0 count 0 for i in range(len(names)): for j in range(i 1, len(names)): d recognizer.match( database[names[i]], database[names[j]], cv2.FaceRecognizerSF_FR_NORM_L2 ) max_dist max(max_dist, d) count 1 if count 0: return 1.2 # 取库内最远距离的 0.9 倍作为底线 return max_dist * 0.9注意库内最远距离描述的只是库里的人之间的最大差异它不能保证库外的人一定比这个更远所以这个值只能当底线。我一般会额外采集几个绝对不应识别成功的人脸图跑一遍看距离分布如果冒名者的最小距离比库内最大距离还小说明样本采集出了问题得回去补样本而不是继续调阈值。5.2 多帧投票抗抖动单帧识别只要有一帧模糊就掉线门禁类场景需要连续多帧同一身份才输出结果。常见做法是维护一个长度为 5 的滑动窗口统计最近 5 帧的识别结果同一个名字出现 3 次才作为最终判断。这个机制在 face_recognition.py 基础上加个collections.deque就行。5.3 检查是不是 CPU 推理拖了后腿如果实时画面明显卡顿先用cv2.getTickCount()分别打点检测和特征提取的耗时通常检测占大头。OpenCV 会优先尝试 ONNX Runtime 后端没有就退回 CPU 通用实现。低配机器上把input_size从 640 降到 320帧率能提升一倍左右代价是远处人脸召回率下降需要根据实际摄像头距离重新标定一次阈值。最后说一个容易被忽略的点很多人把score_threshold从 0.7 调到 0.85 来压误检但过高的检测阈值会让低头、侧脸的人直接检测不到表现为画面里人脸框闪烁。正确顺序是先固定检测阈值 0.7把识别阈值单独调好最后再回来根据漏检率决定要不要动检测侧参数。本文还有配套的精品资源点击获取
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →