简介:基于OpenCV和Python的人脸识别门禁系统实现,采用LBPH算法,面向计算机视觉学习者与Python开发者。资源覆盖人脸数据准备、Haar级联检测、LBPH特征提取、模型训练与相似度判别的完整流程,并提供了门禁场景下的实际应用示例,适合作为课程设计或入门项目的参考。压缩包内含49个文件,以30个pgm人脸样本、5个png图标、4个xml级联分类器、4个Python脚本及两个pyc编译文件为主,另有说明文档与样例图片,整体约987KB,结构清晰便于按模块学习。已有1727人学习下载,内容包括面部检测分类器、LBPH识别模型、UI界面代码及README说明,可直接运行或二次开发,帮助读者快速掌握基于OpenCV的人脸识别系统搭建思路与核心API用法。
1. OpenCV + Python 人脸识别:为什么这个组合能快速出活
如果你想在本地摄像头前判断“这是谁”,而不是只画个框,OpenCV + Python 是目前门槛最低的落地路径。它不需要 GPU、不需要装 PyTorch、不需要自己训练卷积网络,靠 OpenCV 自带的 Haar 级联检测器 + LBPH 人脸识别器,几十行代码就能跑通“检测人脸 → 提取特征 → 识别身份”的完整链路。这套方案非常适合做小型考勤、实验室门禁模拟、毕设原型,也适合想搞懂人脸识别底层逻辑的开发者。它的边界也很明确:不是为百万级人脸库设计的,但对 5~50 人的小规模场景,够用且可控。这篇文章就沿着环境、原理、代码、排错、进阶五个方向,把这条链路彻底讲透。
2. 环境准备:OpenCV 版本选择是第一坑,别急着写代码
很多人在这一步就翻车,而且翻得莫名其妙:代码明明照着教程敲的,import cv2也不报错,但一调用cv2.face.LBPHFaceRecognizer_create()就提示找不到属性。这不是你写错了,是装错了包。
2.1 opencv-python 和 opencv-contrib-python 有什么区别
OpenCV 官方把核心模块和扩展模块分开打包。opencv-python只包含核心模块,通常能用 Haar 级联做检测、图像处理这些常规功能;但人脸识别器LBPHFaceRecognizer、人脸特征提取这些属于opencv_contrib模块,只有opencv-contrib-python才带。所以做基于 OpenCV 的人脸识别,需要装的是 contrib 版本,而不是普通版本。
如果你之前已经装过opencv-python,再装opencv-contrib-python极有可能导致包冲突,两个包的cv2会互相覆盖。我踩过的做法是:先全部卸载,再单独装 contrib。命令如下:
pip uninstall opencv-python opencv-contrib-python -y pip install opencv-contrib-python这里-y是为了跳过卸载确认,避免卡在中间步骤。装完之后验证一下版本和 face 模块是否真的可用。
import cv2 print("OpenCV version:", cv2.__version__) recognizer = cv2.face.LBPHFaceRecognizer_create() print("face module OK:", type(recognizer))如果第一行输出了类似4.10.0的版本号,第二行没有抛异常,说明 face 模块已经就位。很多教程只说pip install opencv-python,这恰恰是后续报AttributeError: module 'cv2' has no attribute 'face'的根源。
2.2 推荐环境与安装命令
我一般建议用 Python 3.9~3.12 之间的版本,配一个虚拟环境再做项目。不建议直接往系统 Python 里塞依赖,尤其是 Linux 自带 Python 的情况下,权限和包冲突会消耗大量时间。创建虚拟环境的步骤:
python -m venv face_env source face_env/bin/activate # Windows 下用 face_env\Scripts\activate pip install --upgrade pip pip install opencv-contrib-python numpy为什么用虚拟环境?因为不同 OpenCV 版本对 numpy、Python 版本的兼容性不一样,虚拟环境可以让你在多个项目之间隔离依赖,不会因为有一次pip install 某某包把 OpenCV 的依赖搞坏。
安装时如果网络慢,可以加镜像源,但我不推荐在教程里固定某个镜像地址,因为各网络环境差异很大。一个更稳的做法是先用默认源装,如果超时再将源换成国内镜像重试。
在 Windows 上需要特别注意:如果电脑上同时装了 Anaconda 和原生 Python,pip可能指向的不是你当前激活环境里的那个 Python。建议先执行:
import sys print(sys.executable)确认你的 Python 解释器路径,再决定用哪个 pip。否则经常出现“明明装了包,另一个环境里却 import 不到”的鬼问题。
2.3 准备摄像头与人脸检测器模型
OpenCV 安装包自带 Haar 级联模型,路径通过cv2.data.haarcascades获取,不需要额外下载。常见的有几个:haarcascade_frontalface_default.xml用于正面人脸检测,haarcascade_frontalface_alt2.xml精度略高但更慢,haarcascade_eye.xml用于眼睛检测。做人脸识别项目,默认用frontalface_default即可,它兼顾速度和召回率。
摄像头的打开方式也有讲究:
import cv2 cap = cv2.VideoCapture(0) # 0 是默认摄像头索引 if not cap.isOpened(): print("无法打开摄像头,请检查索引号") exit(1) ok, frame = cap.read() if not ok: print("无法读取画面,检查摄像头是否被占用") exit(1) print("摄像头分辨率:", frame.shape) cap.release()VideoCapture(0)的 0 代表系统默认摄像头,外接摄像头通常是 1 或 2,Linux 下可以用ls /dev/video*查看设备节点来确认。如果摄像头被其他程序占用(比如直播软件、另一段 Python 进程),cap.read()会一直返回 False,这时把占用程序关掉再试即可。另外,Windows 上如果打开摄像头卡死,可以改成cv2.VideoCapture(0, cv2.CAP_DSHOW),这是 DirectShow 后端,兼容性更好,代价是初始化稍慢一点。
环境这一关过去之后,后面才谈得上写算法逻辑。
3. 先弄懂再动手:Haar 级联与 LBPH 的原理和分工
人脸识别不是一个“单步操作”,而是两段式流水线:先用检测器从画面里把人脸的位置找出来,再用识别器判断这个人是谁。很多新手把这两步混为一谈,导致识别结果惨不忍睹。这里拆开讲清楚。
3.1 Haar 级联负责检测:把“脸在哪”找出来
Haar 级联的原理可以简单理解为:用一组像小矩形模板的特征(眼睛区域比脸颊暗、鼻梁比两侧亮等),通过滑动窗口在图像上逐级筛选,每一级都是一组弱分类器,全部通过才认为是人脸。OpenCV 中对应CascadeClassifier和detectMultiScale方法。
import cv2 cascade_path = cv2.data.haarcascades + 'haarcascade_frontalface_default.xml' face_cascade = cv2.CascadeClassifier(cascade_path) frame = cv2.imread('test.jpg') gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) faces = face_cascade.detectMultiScale( gray, scaleFactor=1.1, minNeighbors=5, minSize=(64, 64) ) print("检测到的人脸:", faces)核心参数有三个。
scaleFactor:每轮滑动窗口缩小的比例。越大检测越快,但容易漏掉偏小的人脸;越小检测越慢,召回率更高。常规取值在1.05到1.3之间,1.1是多数项目的平衡点。
minNeighbors:一个候选区域周围至少要保留多少个相邻检测框才算数。数值越大,误检越少,但真正的人脸也可能因相邻框不够而被丢弃。5到7比较常用,室外强阴影场景我会调到8。
minSize:最小人脸尺寸。低于这个尺寸的候选框直接忽略。如果摄像头离人脸远,这个值要调小;如果只做近距离考勤,调大到80x80能显著减少误检。
detectMultiScale输入建议用灰度图,因为 Haar 特征是基于亮度对比的,彩色通道没有额外价值。直接把彩色图丢进去也能跑,但内部依然会转灰度,还浪费一次转换时间。
3.2 LBPH 负责识别:把“脸是谁”认出来
LBPH 全称 Local Binary Patterns Histograms,局部二值模式直方图。它把灰度图分成一个个小区域,每个像素和它周围像素比较,生成一个二值模式串,然后统计成直方图作为这张脸的特征向量。训练阶段就是给每个人保存一份特征直方图,识别阶段把当前人脸的特征和库里所有特征做距离比较,距离最小的那个标签就是结果。
predict()返回两个值:label 和 confidence。label 是训练时设置的整数标签,confidence 是特征距离,越小表示越相似。注意这里和很多人的直观相反:confidence 不是置信度百分比,而是差距值,超过一定阈值就应该判定为“陌生人”。
import cv2 recognizer = cv2.face.LBPHFaceRecognizer_create() # 参数含义见下 recognizer.setRadius(2) recognizer.setNeighbors(8) recognizer.setGridX(8) recognizer.setGridY(8)setRadius(2):LBP 采样半径。半径越大,感受野越大,能捕捉更粗粒度的纹理,但也会丢失细节,典型取值是 1 或 2。
setNeighbors(8):邻域采样点数。8 是默认值,对应 256 种二值模式。调大会提高区分度,但计算量也线性增长。
setGridX / setGridY:将人脸图划分成 8x8 的小格子,每个格子独立统计直方图。格子越多,空间信息保留得越多,但样本不足时容易过拟合,识别率反而下降。对 200x200 的训练图,8x8 是一个相对稳妥的选择。
LBPH 最大的优点是抗光照变化能力比 EigenFaces、FisherFaces 好。它不直接依赖像素绝对值,而是依赖局部像素的相对关系,因此对室内灯光明暗变化有一定容忍度。这也是我选它做主识别的核心理由。
3.3 检测与识别的分工:检测负责框,识别负责命名
检测器输出的是一个矩形(x, y, w, h),识别器需要的是裁剪好的、固定尺寸的灰度图。所以流程必须严格是:检测 → 裁剪 → 灰度化 → resize → 送入识别器。任何一步缺失,识别结果都会崩坏。
常见错误是直接把整帧彩色图送给predict(),OpenCV 不会明确报错,但结果全是乱标签。还有一个更隐蔽的问题:训练阶段用了 200x200,识别阶段忘了 resize,或者用了不同尺寸,会导致直方图维度对不上。在 OpenCV 的实现里,predict()会按训练尺寸内部 resize,但手动统一尺寸仍然更可靠、更快。
这个“检测 + 识别”的分工决定了后续代码的结构:一个循环里先detectMultiScale,再对每个检测框做裁剪和识别。下一章就会把这条流水线完整实现出来。
4. 全流程代码:采集样本、训练模型、实时识别一次跑通
这一章是可以直接抄作业的部分。我按采集、训练、识别三个步骤拆成三份脚本,每份脚本单独运行,便于单步排查。文件组织方式为:
dataset/ person_tony/ 01.jpg 02.jpg person_jack/ 01.jpg 02.jpg每个人在dataset下建一个子目录,目录名就是人名,子目录里的图片是这个人的人脸灰度图。这种组织结构比文件名塞标签更直观,代码里也更容易扩展新人。
4.1 第一步:采集人脸样本
采集脚本负责打开摄像头,用 Haar 检测器把每帧画面里的人脸裁下来,缩放成固定尺寸写入对应的子目录。这里不直接保存原始画面,而是保存裁剪后的灰度人脸,目的是减少后续训练时的数据清洗工作。
import cv2 import os save_dir = 'dataset/person_tony' # 改成要采集的人名 os.makedirs(save_dir, exist_ok=True) cascade_path = cv2.data.haarcascades + 'haarcascade_frontalface_default.xml' face_cascade = cv2.CascadeClassifier(cascade_path) cap = cv2.VideoCapture(0) count = 0 while True: ok, frame = cap.read() if not ok: print("读取摄像头失败") break gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) faces = face_cascade.detectMultiScale( gray, scaleFactor=1.1, minNeighbors=6, minSize=(80, 80) ) for (x, y, w, h) in faces: count += 1 face_img = gray[y:y+h, x:x+w] face_img = cv2.resize(face_img, (200, 200)) img_path = os.path.join(save_dir, f'{count:03d}.jpg') cv2.imwrite(img_path, face_img) print('saved:', img_path) cv2.rectangle(frame, (x, y), (x+w, y+h), (0, 255, 0), 2) cv2.imshow('collect', frame) if cv2.waitKey(30) & 0xFF == ord('q') or count >= 80: break cap.release() cv2.destroyAllWindows() print('采集完成,共', count, '张')逻辑说明:每次检测到人脸就保存一张,直到按q或攒满 80 张自动停止。minSize=(80, 80)可以让近距离的头发、下巴等区域不被误判为人脸。face_img先转灰度再 resize 到 200x200,这与后面训练识别时保持一致。
采集时的姿势要稍微动一动:左右转头 15 度左右、抬头低头、有表情和无表情、戴不戴眼镜都要拍几张。不要死盯着摄像头拍 80 张一模一样的,否则训练出的模型只认一个角度,换个角度就识别失败。光照场景建议在正常室内灯光下采集,避免一半脸亮一半脸暗。
4.2 第二步:训练 LBPH 模型
训练脚本会遍历dataset下所有子目录,为每个人分配整数标签,读取全部灰度图并统一尺寸后,交给 LBPH 训练。
import os import cv2 import numpy as np base_dir = 'dataset' X = [] y = [] label_dict = {} current_label = 0 for person_name in sorted(os.listdir(base_dir)): person_dir = os.path.join(base_dir, person_name) if not os.path.isdir(person_dir): continue if person_name not in label_dict: label_dict[person_name] = current_label current_label += 1 for img_name in os.listdir(person_dir): if not img_name.lower().endswith(('.jpg', '.jpeg', '.png')): continue img_path = os.path.join(person_dir, img_name) img = cv2.imread(img_path, cv2.IMREAD_GRAYSCALE) img = cv2.resize(img, (200, 200)) X.append(img) y.append(label_dict[person_name]) print(f'{person_name}: {img_path} -> label {label_dict[person_name]}') # 这里 X 是列表,y 是 numpy 数组,LBPH 训练要求 label 是整数 recognizer = cv2.face.LBPHFaceRecognizer_create() recognizer.train(X, np.array(y)) recognizer.write('face_model.yml') # 保存标签映射表,识别时要用 with open('label_map.txt', 'w', encoding='utf-8') as f: for name, label in label_dict.items(): f.write(f'{label}:{name}\n') print('模型已保存,共', len(label_dict), '个人')逻辑说明:label_dict将目录名映射成从 0 开始的整数,train()要求 label 必须是整数数组,不能直接传人名。训练完成后label_map.txt保存了整数和人名的对应关系,识别时需要反查名字。
有个容易忽略的点:recognizer.train()接受的 X 可以是列表,但 y 必须是整数数组,通常用np.array(y)转换。如果忘记了这一步,OpenCV 在某些版本里会报类型错误,或者训练出的模型标签全是错的。
训练结束时终端会打印每个人名和文件路径,可以核对是否有重复图片、是否有人漏了采集。如果某个人的样本少于 10 张,模型很容易过拟合,建议至少采集 20 张以上。
4.3 第三步:实时识别
识别脚本加载训练好的模型和标签映射,然后进入摄像头循环,不断检测人脸、裁剪、送入predict(),最后在画面中绘制名字和置信度。
import cv2 import numpy as np # 加载 LBPH 模型 recognizer = cv2.face.LBPHFaceRecognizer_create() recognizer.read('face_model.yml') # 加载标签映射 label_map = {} with open('label_map.txt', 'r', encoding='utf-8') as f: for line in f: line = line.strip() if not line: continue label, name = line.split(':', 1) label_map[int(label)] = name # 人脸检测器 face_cascade = cv2.CascadeClassifier( cv2.data.haarcascades + 'haarcascade_frontalface_default.xml' ) cap = cv2.VideoCapture(0) while True: ok, frame = cap.read() if not ok: break gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) faces = face_cascade.detectMultiScale( gray, scaleFactor=1.1, minNeighbors=6, minSize=(80, 80) ) for (x, y, w, h) in faces: face_img = gray[y:y+h, x:x+w] face_img = cv2.resize(face_img, (200, 200)) label, confidence = recognizer.predict(face_img) name = label_map.get(label, 'unknown') # LBPH 的置信度是距离,越小越像,超过阈值判为陌生人 if confidence < 60: display_name = name else: display_name = 'unknown' cv2.rectangle(frame, (x, y), (x+w, y+h), (0, 255, 0), 2) cv2.putText( frame, f'{display_name} ({confidence:.1f})', (x, y - 10), cv2.FONT_HERSHEY_SIMPLEX, 0.8, (0, 255, 0), 2 ) cv2.imshow('face recognition', frame) if cv2.waitKey(30) & 0xFF == ord('q'): break cap.release() cv2.destroyAllWindows()逻辑说明:predict()内部会对输入图做和训练时相同的直方图提取,因此我们只要保证在送入前 resize 到 200x200,结果就是稳定的。confidence越小说明当前脸和训练样本越接近,60这个阈值是我在常见室内灯光下用 30 人数据集调出来的一个经验值,它因人脸库大小、图片清晰度不同而有差异。
参数说明:如果你的人脸库只有几个人,且样本采集很规范,confidence通常会在 30~50 之间波动。如果发现熟人频繁被识别成 unknown,说明你的阈值太严格,把60往上调到70或80。如果发现陌生人也被识别成库内的人,说明阈值太宽松,往下调到50甚至40。
注意代码里的f'{display_name} ({confidence:.1f})'把置信度也显示出来,这在调参阶段非常重要。建议先跑一遍录制好的视频,把所有帧的 confidence 记录下来,再决定最终阈值。
4.4 四个常用参数的调节经验
下表是这套链路中最值得调的四个参数,也是网上教程很少说清的部分。
| 参数 | 位置 | 建议范围 | 大了的后果 | 小了的后果 |
|---|---|---|---|---|
| scaleFactor | detectMultiScale | 1.05~1.3 | 漏检多、跑得快 | 误检多、跑得慢 |
| minNeighbors | detectMultiScale | 5~9 | 一些真实人脸被丢弃 | 背景区域被当成脸 |
| LBPH neighbors | recognizer.setNeighbors | 6~12 | 过于敏感、训练慢 | 区分度不足 |
| confidence 阈值 | predict 结果比较 | 40~80 | 陌生人混入 | 熟人被拒识 |
调参的通用思路:先固定人脸检测参数,保证每帧都能稳定框住人脸,再调识别阈值。不要同时动所有参数,否则翻车了不知道是哪一步出的问题。
我习惯的做法是:写一个离线测试脚本,读入测试图片集,把每张图的置信度打印出来,统计同一个人的置信度分布区间和不同人之间的重叠区间,然后取一个能拉开差距的阈值。这个流程每次换摄像头或换光照环境时都要重新走一遍,因为它和应用现场的硬件条件强相关。
这个参数表不是万能公式,但它能让你面对“识别不准”的问题时,有一个明确的排查方向,而不是靠玄学调参。
5. 避坑指南:人脸识别里常见的 5 个翻车现场
这一章是我自己的血泪总结,每条都按照“现象 → 原因 → 解决”的结构写,方便你对号入座。
5.1 运行时报 ModuleNotFoundError 或 AttributeError: module 'cv2' has no attribute 'face'
现象一:import cv2直接报ModuleNotFoundError: No module named 'cv2'。现象二:import cv2正常,但调用cv2.face时提示模块里没有这个属性。
原因一:根本没安装,或者安装在别的 Python 环境。原因二:装的是opencv-python而不是opencv-contrib-python,face 扩展模块不在这个包里。
解决:先确认当前解释器路径,再卸载重装。我建议执行:
python -m pip uninstall opencv-python opencv-contrib-python -y python -m pip install opencv-contrib-python注意一定要用python -m pip,而不是直接敲pip,这样能确保命令指向当前激活环境。装完再跑一次前文的验证脚本,确认cv2.face.LBPHFaceRecognizer_create()能正常执行。
另一个隐性原因:OpenCV 的.pyd或.so文件被安全软件隔离,导致加载不完整。这种情况在 Windows 上偶有发生,重新安装一遍并加入信任列表即可。
5.2 陌生人被识别成自己人
现象:随便一张没见过的脸放到摄像头前,画面里照样显示某个库内人名的名字,而且 confidence 还挺高(比如 30+)。
原因:predict()不管这张脸像不像库里的,它只返回“所有已训练标签中距离最小的那个”。如果训练样本本身不够多样,或者阈值没设,程序就会把任何输入脸都归类到某个已知人物。
解决:在识别代码中强行加阈值判断,confidence 超过阈值一律显示 unknown。阈值不能拍脑袋,建议先不设阈值跑一批熟人照片,记录区间,再跑一批陌生人照片,记录区间,然后取两个区间的中间值。在我自己的项目里,室内灯光下 30 人数据集的分离点通常在 55 到 75 之间。
如果你的陌生人脸 confidence 和熟人脸几乎重叠,说明训练样本太少、太单一,应该回到第 4 章的采集步骤,为每个人补拍不同角度和表情的照片。靠阈值掩盖训练问题是治标不治本。
5.3 训练时报尺寸报错,或者识别率极低
现象一:train()抛异常,提示list of images must have the same size之类的信息。现象二:训练不报错,但识别时同一个人的不同顿照片给出不同标签。
原因:采集的数据集中,图片尺寸不统一。虽然我在采集脚本里做了resize(200, 200),但如果你手动把图片放入 dataset 目录,或者用了不同的采集脚本,尺寸就会混乱。识别端对 frame 裁剪后如果不 resize,同样会造成特征向量错乱。
解决:训练脚本里对每张图片强制cv2.resize(img, (200, 200)),识别端也必须用完全相同的尺寸。这里的关键是训练和识别要一致,不只是统一一个地方。
我踩过的一次翻车是:训练时用 200x200,识别时误写成cv2.resize(face_img, (100, 100)),OpenCV 没有报错,但识别准确率直接掉到 50% 以下。后来我把训练和识别脚本里的 resize 常量抽到同一个配置文件里,才杜绝了这类问题。
5.4 摄像头画面里的人脸框乱跳、身份标签反复横跳
现象:手机稍微动一下,人脸框在脸上到处飘;识别结果一会儿是自己的名字,一会儿是 unknown,甚至闪出别人的名字。
原因:Haar 检测器对角度和光照非常敏感,每一帧检测到的框位置、大小都不稳定。识别器基于裁剪区域内容做判断,框一歪,特征就变。置信度在阈值边缘时,就会表现为标签跳动。
解决:一是调检测参数,把minNeighbors从 5 调大到 7,让误检框更少;把scaleFactor从 1.1 调大到 1.15,让框更稳定。二是对识别结果做平滑,不单帧判定身份。我常用的做法是维护一个最近 5 帧的标签投票队列,只有同一 label 出现 3 次以上才更新显示。
from collections import Counter history = [] # 每帧识别到 label 后 history.append(label) if len(history) > 5: history.pop(0) counter = Counter(history) best_label, best_count = counter.most_common(1)[0] if best_count >= 3: display_label = best_label else: display_label = -1 # 暂不更新这段代码的代价是识别结果会滞后几帧,但对门禁、考勤这类静态场景无所谓,换来的稳定性反而更重要。如果你做的是实时人机交互,可以把窗口缩小到 3 帧。
5.5 装了很多环境后时好时坏:OpenCV 版本混成一锅粥
现象:今天代码能跑,明天同样的代码开始报错;import cv2的路径指向莫名其妙的外部 Python。
原因:同一个机器上有系统 Python、Anaconda、虚拟环境、Docker 等,每个环境里的 OpenCV 版本不同。你激活某个环境安装包,但 IDE 解释器却指向另一个环境的 Python。
解决:在项目根目录运行带路径的 Python,强制使用当前环境的解释器:./face_env/bin/python app.py。同时建议在代码开头打印版本:
import cv2 import sys print(cv2.__version__) print(cv2.__file__)cv2.__file__会告诉你当前加载的 OpenCV 模块实际路径,一旦路径指向了非虚拟环境,就能立刻发现问题。我经历过一次 Conda 环境里 OpenCV 被系统包覆盖,打印路径后发现cv2指向了/usr/lib/python3/dist-packages,解决方案是在虚拟环境里手动pip install --force-reinstall opencv-contrib-python,强制覆盖。
如果以上都试过还不行,最后一招是删掉__pycache__再跑,有些混淆的 .pyc 缓存会 mask 掉新安装的模块。
6. 进阶一点:用 YuNet 替换 Haar,检测精度和稳定性明显提升
当你已经用 Haar + LBPH 跑通流程后,下一个提升点往往不在识别器,而在检测器。Haar 的短板是角度受限、光照敏感,且同一张脸容易产生多个框。OpenCV 4.5 之后提供了基于深度学习的 YuNet 人脸检测器,加载一个 ONNX 模型就能用,不需要额外配置深度学习框架。
import cv2 model_path = 'face_detection_yunet_2023mar.onnx' detector = cv2.FaceDetectorYN.create( model_path, '', (320, 320), # 输入尺寸 score_threshold=0.8, # 人脸得分阈值 nms_threshold=0.3, # 非极大值抑制阈值 top_k=5000 ) cap = cv2.VideoCapture(0) while True: ok, frame = cap.read() if not ok: break _, faces = detector.detect(frame) if faces is None: continue for det in faces: x, y, w, h = det[:4].astype(int) score = det[-1] cv2.rectangle(frame, (x, y), (x+w, y+h), (0, 255, 0), 2) cv2.putText(frame, f'{score:.2f}', (x, y-10), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 255, 0), 2) cv2.imshow('yunet', frame) if cv2.waitKey(30) & 0xFF == ord('q'): break cap.release() cv2.destroyAllWindows()参数说明:score_threshold是判定为人脸的最低分数,0.8 在大多数场景是合理的,调到 0.6 会召回更多脸但误检也增多。nms_threshold控制重叠框合并力度,调小会让相邻框更少合并,调大则合并更激进。(320, 320)是模型输入尺寸,检测结果是相对原图坐标,不需要手动缩放回原本尺寸。
YuNet 的检测框相比 Haar 稳定很多,正脸侧脸都能覆盖,而且一次返回 5 个关键点坐标,可以进一步做姿态判断,比如只在人脸朝前时才识别,防止员工侧身刷脸。
识别部分仍然可以用 LBPH,把检测器从 Haar 换成 YuNet 后,裁剪出来的人脸区域更精确,confidence 的抖动也会减小,这是投入产出比最高的一次升级。
验证一个系统做得好不好,我自己的习惯是:先录一段 3 分钟的视频,里面包含库内人员、库外人员、遮挡眼镜、转头等变化,离线跑完整的识别管道,统计每个小组的准确率和虚警率。阈值调好后,再切回实时摄像头。这比对着摄像头现场猜参数高效得多。
还有一个很少人提的实践:采集样本时记录拍摄时的光照情况,比如在文件名里加_day、_night后缀,训练后按光照分组交叉验证。你会发现 LBPH 并没有传说中那么抗光照,只是比 Eigen 好一点而已。所以最好的“算法优化”,其实是让人脸样本的光照分布覆盖目标场景。
做到这一步,基本已经超过多数照着教程敲代码的入门者了。这个项目我陆陆续续做了一年,最大的教训是:人脸识别的瓶颈通常不在模型,而在数据规范和阈值标定。清洗好数据集、把阈值调准,比换一个更花哨的网络结构对实际效果的提升更立竿见影。希望这些踩坑记录能帮你少走几条弯路。
本文还有配套的精品资源,点击获取