简介:一份面向人脸识别学习与课堂考勤场景的Python实战项目,以OpenCV实现Haar级联人脸检测,借助dlib完成人脸的实时跟踪与关键点定位,并应用LBPH局部二值模式直方图算法进行身份特征提取与匹配,最终通过PyQt5构建可视化操作界面。系统内置登录验证、学生信息管理、考勤记录、数据导出等模块,配合MySQL持久化存储,形成从人脸录入、识别到点名统计的完整闭环。压缩包内共49个文件,涵盖Python源码、多个PyQt5界面文件、Haar级联XML模型、dlib人脸关键点模型、图标与提示音效、依赖库安装包、配置说明及测试数据等,整体体积约108.74MB,目录按功能模块整理,便于快速定位和二次开发。同时提供考勤CSV样例与依赖清单,帮助使用者理解人脸检测、特征提取、数据库存储等关键流程。目前已有157人学习下载,适合人脸识别初学者、毕业设计选题者及需要构建简易考勤系统的开发者参考与复现。
1. 为什么人脸识别系统选了opencv+dlib+LBPH这条技术栈
人脸识别系统不一定需要GPU和深度学习框架。在很多考勤和门禁项目里,opencv+dlib+LBPH这套组合在纯CPU环境下就能把识别速度做到实时,训练几十人规模的人脸库用不了五秒。它的核心思路是先用dlib检测人脸并给出68个关键点,把歪着的脸对齐成统一尺寸,再让OpenCV的LBPH识别器在局部二值模式直方图上做匹配。PyQt5则把这套流程封装成带注册、训练、识别界面的成品,适合毕业设计、企业内部工具,也适合刚入门CV的开发者拿来做工程化练习。下面把从环境配置到打包发布的技术路径完整走一遍,每个环节的坑都会提到。
2. 环境准备:opencv、dlib与pyqt5的安装联动
很多项目不是卡在算法上,而是卡在环境配置。opencv-python装完之后导入cv2正常,一调cv2.face就报AttributeError;dlib安装卡在编译阶段;PyQt5装好又提示找不到Qt platform plugin。这些问题几乎每个项目都会遇到,而且都和依赖顺序、扩展包选择有关。
2.1 为什么标准opencv-python包里找不到LBPH识别器
OpenCV在pip上主要以两个包分发:opencv-python是核心包,只包含基础模块;opencv-contrib-python是扩展包,包含了人脸识别、文本检测等额外模块。LBPH识别器所在的face模块被放在contrib里,所以只安装opencv-python时,import cv2不报错,但cv2.face不存在,直到运行cv2.face.LBPHFaceRecognizer_create()才报AttributeError。这种滞后报错很容易让人误判问题源头。
解决方法是直接安装opencv-contrib-python,并且不要和opencv-python混装。推荐在独立虚拟环境里操作,避免系统Python旧包干扰:
python -m venv face_env # Windows 激活 face_env\Scripts\activate # Linux / macOS 激活 source face_env/bin/activate pip install --upgrade pip setuptools wheel pip install opencv-contrib-python pip install cmake dlib pip install pyqt5 numpy命令顺序是有原因的:opencv-contrib-python会顺带安装好numpy;dlib需要cmake参与源码编译,所以cmake必须先装;最后装pyqt5,避免它链接不到刚装好的numpy。numpy建议锁在1.26系列,numpy 2.x和较旧版本的dlib在运行时可能报dlib.capi模块找不到。
提示:opencv-python和opencv-contrib-python不要同时安装,二者都提供同名cv2目录,混装后会形成动态库覆盖,模块时有时无。
表:三个核心依赖的安装错误对照
| 依赖包 | Windows常见报错 | Linux常见报错 | 处理方式 |
|---|---|---|---|
| opencv-contrib-python | pip下载速度慢 | ImportError: libGL.so.1 | 换国内镜像;安装libgl1 |
| dlib | error C2653 / LNK1104 | 找不到cmake | 先装VS Build Tools和cmake |
| pyqt5 | 找不到Qt platform plugin | 依赖libxcb相关库 | 配置platforms目录;安装libxcb-xinerama0 |
这些错误里只有小部分来自网速或镜像问题,大部分来自依赖顺序和编译组件缺失。对照表格处理,比反复重装有效得多。
2.2 dlib在Windows/Linux下的编译安装步骤
dlib在pip上默认走源码构建,所以编译工具链决定成败。Windows下需要Visual Studio的C++生成工具和CMake;Linux下需要build-essential、libx11-dev和libopenblas-dev。
# Windows pip install cmake pip install dlib # Linux sudo apt-get install build-essential libx11-dev libopenblas-dev pip install cmake dlib编译日志是排错的关键。出现“Unable to find cmake”,说明cmake没有成功进入PATH,先执行cmake --version验证;出现“error C2653”,说明C++工具链版本和Python架构不匹配;出现“fatal error C1083”,多半是Windows SDK组件没装全。Linux下如果缺libopenblas,编译可能通过,但在后期训练做矩阵运算时会崩溃,这一点比编译失败更难排查。
2.3 pyqt5安装与OpenCV的GUI窗口冲突
pyqt5的安装本身只有一条命令pip install pyqt5,但运行时会遇到两类典型问题。第一类是PyQt5的Qt基础库版本冲突,某些环境下pip会将PyQt5-qt5解析成registry+https这样的本地注册路径,随后报DistributionNotFound。此时统一pip源,清空缓存重装即可。第二类是当界面同时使用cv2.imshow和QMainWindow时,HighGUI窗口会抢焦点,甚至导致退出卡死。
Windows下修复Qt平台插件路径:
set QT_QPA_PLATFORM_PLUGIN_PATH=face_env\Lib\site-packages\PyQt5\Qt5\plugins\platforms如果项目本身以PyQt5为主界面,建议直接放弃cv2.imshow调试窗口,所有画面统一用QLabel刷新。在cmd里安装pyqt5时,先激活face_env再执行pip install pyqt5;用PyCharm时,把项目解释器指向face_env的python.exe即可,没有额外配置。这样能避免许多边界问题。
3. 用dlib做人脸检测与关键点对齐,补齐LBPH识别的成败前提
在纯CPU、轻量部署的需求下,dlib比OpenCV的Haar级联和DNN检测器更适合。Haar级联对光照和侧脸太敏感,DNN检测器即使使用精简模型也要多下载几十MB权重。dlib的HOG加线性SVM检测器只有几百KB,检测一张脸在普通笔记本上大约30到60ms,已经足够支撑实时识别流程。
3.1 HOG检测器跑通第一张人脸的流程
首先从静态图片开始,把检测结果和68个关键点一起绘制出来。这里需要dlib预训练的关键点模型文件shape_predictor_68_face_landmarks.dat。
import cv2 import dlib detector = dlib.get_frontal_face_detector() predictor = dlib.shape_predictor("shape_predictor_68_face_landmarks.dat") img = cv2.imread("sample.jpg") gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) rects = detector(gray, 1) # 1表示做一次金字塔上采样 for rect in rects: x, y, w, h = rect.left(), rect.top(), rect.width(), rect.height() cv2.rectangle(img, (x, y), (x + w, y + h), (0, 255, 0), 2) shape = predictor(gray, rect) for i in range(68): pt = (shape.part(i).x, shape.part(i).y) cv2.circle(img, pt, 1, (0, 0, 255), -1) cv2.imshow("result", img) cv2.waitKey(0) cv2.destroyAllWindows()detector的第二个参数是upsample值,指把图像放大几倍再检测,值越大越容易找到小脸,但耗时成倍增加。720P画面取1已经够用,人脸只有几十像素宽时调到2,超过2后CPU算力会成为瓶颈,不如直接提高摄像头采集分辨率。关键点索引36和45分别是左眼外眼角和右眼外眼角,接下来对齐要用这两个点。
3.2 用68关键点把脸摆正:仿射变换与尺寸归一化
LBPH统计的是局部像素关系,对几何变化没有自适应能力。左半边脸训练、右半边脸识别,直方图分布会明显偏移。所以在送进识别器之前,必须把脸旋转到水平方向,裁出统一大小的人脸区域。
import math import cv2 def align_face(gray, rect, shape, output_size=112): left_eye = shape.part(36) right_eye = shape.part(45) dy = right_eye.y - left_eye.y dx = right_eye.x - left_eye.x angle = math.degrees(math.atan2(dy, dx)) eyes_center = ((left_eye.x + right_eye.x) / 2, (left_eye.y + right_eye.y) / 2) rot_mat = cv2.getRotationMatrix2D(eyes_center, angle, scale=1.0) rotated = cv2.warpAffine(gray, rot_mat, (gray.shape[1], gray.shape[0])) x1 = max(0, int(eyes_center[0] - rect.width() * 0.6)) y1 = max(0, int(eyes_center[1] - rect.height() * 0.5)) x2 = min(rotated.shape[1], int(eyes_center[0] + rect.width() * 0.6)) y2 = min(rotated.shape[0], int(eyes_center[1] + rect.height() * 0.7)) face = rotated[y1:y2, x1:x2] face = cv2.resize(face, (output_size, output_size)) face = cv2.equalizeHist(face) # 直方图均衡化,降低光照影响 return face两点说明:旋转角度由两眼坐标差算出,正值代表顺时针,warpAffine会做双线性插值重采样;裁剪范围不是直接使用检测矩形,而是以眼睛中心外扩,宽度取矩形宽的1.2倍、高度取矩形高的1.2倍并向下多留一点,这样额头和下巴都能保住,LBPH的纹理特征不会因为裁得过紧而损失。统一输出到112x112是LBPH比较合适的尺寸,分辨率太低丢失纹理,太高则直方图噪声变大。训练和推理必须使用同一条对齐链路,任何一端变动都会影响最终准确率。
3.3 对齐质量对LBPH识别准确率影响的对比
在相同训练数据和阈值条件下,对齐前后差异十分明显。几类常见情况的经验数据如下,置信度越低表示匹配越可靠:
表:对齐前后置信度对比
| 测试情形 | 未对齐置信度 | 对齐后置信度 | 说明 |
|---|---|---|---|
| 轻微侧脸 | 96 | 73 | 角度偏差被旋转修正 |
| 戴眼镜 | 118 | 81 | 关键点稳定,眼镜干扰减少 |
| 亮度偏低 | 134 | 92 | 均衡化后纹理关系更稳定 |
从数据可以看到,对齐加直方图均衡化能让置信度稳定下降三成左右。很多LBPH项目被说准确率低,真正原因不是算法弱,而是没有做几何归一化。这一步看似简单,却直接决定模型能不能用。
4. 训练LBPH人脸识别模型:从样本采集到置信度调优
LBPH在OpenCV里有现成实现,不需要手动构造二进制编码。但参数不调,默认值直接上生产环境,效果不会太好。而从样本采集到阈值设定,每个环节都影响最终识别率。
4.1 LBPH核心参数解释:radius、neighbors与grid划分
LBPH把灰度图划分成若干小网格,在每个网格内用局部二值模式比较中心像素和邻域像素的灰度关系,小于中心像素记0,大于记1,再把这个二进制串转成十进制数,统计成直方图。所有网格的直方图拼起来就是一张人脸的特征向量。识别时计算两张特征向量的欧氏距离,输出距离作为置信度。
表:LBPH参数与经验取值
| 参数 | 含义 | 经验取值范围 |
|---|---|---|
| radius | 邻域采样半径,单位为像素 | 1到2,数值太大会丢失细节 |
| neighbors | 邻域采样点数 | 固定8,这是圆形邻域标准取法 |
| grid_x / grid_y | 水平和垂直方向网格数 | 8x8常用,低分辨率降到4x4 |
| threshold | 预测时的置信度阈值 | 独立调节,不参与训练 |
半径取1能捕捉眼镜、胡须这类细节纹理;取2对噪声更鲁棒,但识别精度会下降。网格数要跟着输入尺寸走,如果图像缩到64x64,grid还取8会导致单个网格太小,直方图不稳定。
4.2 用Python代码完成样本采集与模型训练
样本采集是数据基础。每个人建议采集25到40张图,覆盖正脸、左右偏转15度、上下俯仰和室内常见光照变化。采集时只在单人脸画面下保存,避免把背景裁进样本。
import cv2 import dlib import os save_root = "faces/zhangsan" os.makedirs(save_root, exist_ok=True) cap = cv2.VideoCapture(0) detector = dlib.get_frontal_face_detector() predictor = dlib.shape_predictor("shape_predictor_68_face_landmarks.dat") count = 0 while count < 30: ret, frame = cap.read() if not ret: break gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) rects = detector(gray, 1) if len(rects) == 1: # 单人脸才保存 rect = rects[0] shape = predictor(gray, rect) face = align_face(gray, rect, shape) # 复用3.2节对齐函数 cv2.imwrite(os.path.join(save_root, f"{count:03d}.jpg"), face) count += 1 cv2.imshow("collect", frame) key = cv2.waitKey(1) & 0xFF if key == ord("q"): break cap.release() cv2.destroyAllWindows()这个函数依赖第3.2节的align_face函数,预处理链路和之后推理时保持一致。count每保存一张才加1,避免连续帧产生大量相似样本。30张左右的数据对LBPH来说方差是可接受的,少于15张时直方图统计意义会明显下降。
训练并保存模型:
import cv2 import numpy as np from pathlib import Path face_root = Path("faces") images, labels = [], [] label_to_name = {} for label_id, person_dir in enumerate(sorted(face_root.iterdir())): if not person_dir.is_dir(): continue label_to_name[label_id] = person_dir.name for img_file in person_dir.glob("*.jpg"): img = cv2.imread(str(img_file), cv2.IMREAD_GRAYSCALE) images.append(img) labels.append(label_id) recognizer = cv2.face.LBPHFaceRecognizer_create( radius=1, neighbors=8, grid_x=8, grid_y=8) recognizer.train(images, np.array(labels)) recognizer.save("lbph_model.yml") print("训练完成,标签映射:", label_to_name)代码要点:标签从0开始连续分配,中间不能跳号,否则predict返回的label无法对应人名;LBPH训练必须传灰度图,彩色图会报size mismatch;模型文件本身只保存特征,不保存人名,label_to_name需要单独持久化,可以用numpy的save或json。
4.3 置信度阈值怎么定:识别、拒绝和误判的平衡
predict返回两个值,label是身份编号,confidence是直方图欧氏距离,越小越相似。真实项目不能只看label,必须用阈值把陌生人挡在系统外:
label_id, confidence = recognizer.predict(face) if confidence < 80: name = label_to_name.get(label_id, "unknown") else: name = "unknown"阈值和业务场景强相关。不同取值会改变误识和拒识的比例:
表:阈值在不同场景下的取舍
| 阈值 | 误识表现 | 拒识表现 | 适用场景 |
|---|---|---|---|
| 50 | 陌生人几乎不会通过 | 本人偶尔被拒 | 严格门禁 |
| 80 | 有一定误识风险 | 绝大多数用户通过 | 考勤打卡 |
| 120 | 陌生人也可能放行 | 极少误拒 | 黑名单辅助比对 |
建议先离线跑一遍所有样本,把同一人置信度分布和不同人置信度分布画出来,取两类分布中位点作为初始阈值。上线后记录拒识日志,再微调10到20个数值。不要把阈值写死在代码参数里,做成配置文件或界面输入框,现场调整会方便很多。
5. PyQt5界面整合:摄像头实时识别与打包发布
检测、对齐、训练都跑通之后,剩下的工作是把流程封装进PyQt5界面。需要解决两个问题:摄像头画面如何稳定刷新,以及打包成独立可执行文件后依赖还能不能完整。
5.1 用QTimer驱动摄像头刷新,避免QThread多线程复杂度
在摄像头识别场景里,用QThread反而会增加复杂度。摄像头读取本身是阻塞的,每次读取30ms左右,QTimer每30ms触发一次process_frame即可。如果某次处理超过30ms,QTimer会自动跳过下一次触发,不会产生队列堆积。
from PyQt5.QtCore import QTimer, Qt from PyQt5.QtGui import QImage, QPixmap from PyQt5.QtWidgets import QMainWindow class MainWindow(QMainWindow): def __init__(self): super().__init__() self.cap = cv2.VideoCapture(0) self.timer = QTimer() self.timer.timeout.connect(self.process_frame) self.timer.start(30) def process_frame(self): ret, frame = self.cap.read() if not ret: return gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) rects = detector(gray, 1) for rect in rects: shape = predictor(gray, rect) face = align_face(gray, rect, shape) label_id, conf = recognizer.predict(face) name = label_to_name.get(label_id, "?") if conf < 80 else "unknown" cv2.rectangle(frame, (rect.left(), rect.top()), (rect.right(), rect.bottom()), (0, 255, 0), 2) cv2.putText(frame, name, (rect.left(), rect.top() - 10), cv2.FONT_HERSHEY_SIMPLEX, 0.8, (0, 255, 0), 2) rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) h, w, ch = rgb.shape qimg = QImage(rgb.data, w, h, ch * w, QImage.Format_RGB888) self.label.setPixmap(QPixmap.fromImage(qimg) .scaled(self.label.size(), Qt.KeepAspectRatio))cv2默认输出BGR,显示到Qt界面必须先转RGB;QImage构造函数里bytes_per_line必须传ch * w,否则画面会错位。scaled加上Qt.KeepAspectRatio保证人脸不变形。窗口关闭时记得执行self.cap.release(),否则摄像头资源会被占用到进程结束。
5.2 从源码到exe:pyinstaller打包opencv+dlib+pyqt5的依赖收集
打包是交付的最后一步,也是最容易遗漏依赖的地方。dlib动态库、cv2子模块和PyQt5插件目录都不会被自动收集。推荐用PyInstaller并显式指定:
pyinstaller -w -F main.py \ --collect-all dlib \ --collect-all cv2 \ --add-data "shape_predictor_68_face_landmarks.dat;." \ --add-data "lbph_model.yml;."-w表示不显示控制台窗口,-F打成单个exe,--collect-all把dlib和cv2的完整子模块收进来。模型文件用--add-data加进包内,运行时需要用PyInstaller的路径机制找到它们:
import sys, os base_path = getattr(sys, "_MEIPASS", os.path.abspath(".")) model_path = os.path.join(base_path, "lbph_model.yml")打包后如果在某些机器上报Qt platform plugin错误,最省事的做法是在exe同级目录放一个platforms文件夹,也就是从PyQt5的plugins目录直接拷贝。上线机器没有摄像头驱动时,VideoCapture(0)会返回False,界面应弹一个警告框再退出,而不是在process_frame里静默return导致界面卡死。
本文还有配套的精品资源,点击获取