简介:基于Python与OpenCV的人脸识别员工考勤系统毕业设计项目,面向计算机相关专业学生与开发者,解决传统考勤中代打卡、审批繁琐、数据统计困难等问题,实现刷脸签到、身份核验、考勤记录与导出等功能。项目已获导师指导并通过,属于高分毕设成果,代码完整、无需修改即可运行,也可直接用作课程设计或期末大作业。资源包共669个文件,整体大小约197.57MB,核心是501个Python源码文件,配合pyd、dll等依赖库,exe可执行组件,xml与cfg配置信息,png、jpg界面与示例图片,docx、txt文档说明,以及pth、dat特征数据模型和csv考勤记录,目录划分清晰,便于按模块研读。压缩包内还提供虚拟环境激活脚本,可快速搭建Python运行环境,减少环境配置成本。目前已有521人学习或下载,适合毕业设计答辩、课程设计验收及实际项目练手,能够帮助理解OpenCV人脸检测、人脸比对、数据库读写与考勤业务结合的完整开发流程,沉淀可直接复用的毕业设计项目经验。
1. 基于Python+OpenCV的人脸识别考勤系统:毕业答辩能过的项目,打开就该这么跑
每年到毕业季,总有一批人卡在人脸识别考勤系统上。用Python和OpenCV做员工考勤,最怕的不是识别不出来,而是代码凑够了、跑不动,答辩演示当场翻车。这套基于Python+OpenCV的人脸识别员工考勤系统,是一个把训练好的特征库、完整考勤日志、虚拟环境配置一次给齐的项目,从命令行启动就能跑通完整的打卡闭环。它适合两类人:一是拿它做毕业设计或课程设计的学生,需要完整源码和文档说明撑住答辩;二是想快速搭一套本地人脸打卡原型、验证业务需求的开发者。下面按识别算法选型、环境部署、核心代码和最容易出问题的几个坑依次拆开,按步骤走完你就能接手改代码。
2. 技术选型:为什么OpenCV+LBPH更适合考勤场景
2.1 三种OpenCV人脸识别算法的取舍
做考勤这类固定场景的人脸识别,选型第一要考虑的不是谁精度最高,而是谁的精度在可接受范围内,且能在普通硬件上稳定跑起来。常见方案有三类:OpenCV自带的EigenFaces、FisherFaces、LBPH,以及接入深度学习框架的FaceNet、ArcFace。深度学习方案精度确实好,但依赖包动辄几百兆,训练还要准备GPU环境,放在毕业设计场景里,光是环境配置就能把答辩前期的耐心耗尽。
LBPH(Local Binary Patterns Histograms,局部二值模式直方图)的思路是把人脸分成若干小块,统计每个像素与邻域的纹理关系,生成直方图作为人脸特征。它不要求大规模训练数据,每人准备20到50张样本就能完成训练;识别阶段是纯CPU计算,720p摄像头在普通笔记本上能做到实时帧率,这对考勤打卡场景已经足够。
| 方案 | 训练数据量 | 硬件需求 | 识别速度 | 光照鲁棒性 | 部署成本 |
|---|---|---|---|---|---|
| EigenFaces | 中等 | CPU | 快 | 差 | 低 |
| FisherFaces | 中等 | CPU | 快 | 较差 | 低 |
| LBPH | 少(20-50张/人) | CPU | 实时 | 较好 | 低 |
| FaceNet等深度模型 | 多(需预训练权重) | GPU优先 | 快 | 强 | 高 |
考勤系统的核心诉求是同一办公室、固定光照、单人打卡,LBPH正好命中。这也是OpenCV项目里做考勤用LBPH最多的原因,不是因为它最先进,而是因为它是这个场景下性价比最高的方案。
2.2 系统整体流程与文件布局
这套系统的运行主线是「注册建档 → 训练特征 → 实时识别 → 写入日志」。注册阶段采集员工人脸样本,保存到本地样本目录;训练阶段读取所有样本生成模型文件,同时把员工ID和姓名映射写入feature_all.csv;识别阶段摄像头逐帧做人脸检测,把检测到的人脸送入recognizer的predict()接口,返回的confidence小于阈值就认定匹配,然后把员工ID、姓名、打卡时间追加到logcat.csv。
项目包里几个关键文件值得先认一遍:
| 文件 | 作用 |
|---|---|
| activate.bat / deactivate.bat | 虚拟环境激活与退出脚本,Windows下cmd直接调用 |
| sysconfig.cfg / pyvenv.cfg | Python虚拟环境配置,记录解释器路径和版本 |
| feature_all.csv | 员工特征索引,训练完成后生成 |
| logcat.csv | 考勤记录,一次成功打卡追加一行 |
拿到项目之后不要急着去翻主程序,先把虚拟环境激活,确认OpenCV和numpy版本,再考虑跑训练脚本。很多人第一次运行报ModuleNotFoundError,就是跳过了激活环境这一步,把系统Python和项目虚拟环境混在一起了。
提示:识别精度不是模型单方面决定的,采集样本的质量、训练和识别阶段的图像尺寸是否一致、阈值设得合不合理,都会直接影响结果。后面每一章都会回到这个点上。
3. 环境部署:虚拟环境激活、样本采集与模型训练
3.1 激活虚拟环境并确认依赖
打开命令行,cd到项目目录,执行activate.bat。注意不要直接双击,要在当前cmd窗口里调用,这样环境变量才能留在当前会话。
cd D:\face_attendance activate.bat python -c "import cv2; print(cv2.__version__)" python -c "import numpy; print(numpy.__version__)"activate.bat的作用是修改PATH环境变量,让python和pip指向虚拟环境目录下的可执行文件。执行完激活脚本后,命令行提示符前面会出现(venv)字样。如果执行python -c这行时出现ModuleNotFoundError: No module named 'cv2',说明当前python不在虚拟环境里,先检查activate是否执行成功。
确认依赖更稳妥的方式是用pip list过滤关键包:
pip list | findstr /i "opencv numpy pandas"opencv相关包在这里有两个来源:opencv-python提供cv2基础功能,opencv-contrib-python额外提供cv2.face等扩展模块。LBPH识别器在cv2.face里,所以只装opencv-python是不够的,这一点后面避坑章节还会重点讲。
3.2 录入员工人脸样本
先创建样本采集脚本。这段代码负责打开摄像头,检测画面中的人脸,然后以统一尺寸保存到以员工ID和姓名命名的目录下。
import cv2 import os # 员工ID和姓名,作为样本目录名 emp_id = 1 emp_name = "zhangsan" save_dir = f"dataset/{emp_id}_{emp_name}" os.makedirs(save_dir, exist_ok=True) # OpenCV自带的人脸检测器,基于Haar特征 face_cascade = cv2.CascadeClassifier( cv2.data.haarcascades + "haarcascade_frontalface_default.xml" ) cap = cv2.VideoCapture(0) # 0表示默认摄像头 count = 0 while count < 50: # 每名员工采集50张 ret, frame = cap.read() if not ret: break gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) faces = face_cascade.detectMultiScale( gray, scaleFactor=1.1, minNeighbors=5, minSize=(100, 100) ) for (x, y, w, h) in faces: # 裁出人脸区域,统一缩放到200x200 face_roi = gray[y:y+h, x:x+w] face_resized = cv2.resize(face_roi, (200, 200)) count += 1 cv2.imwrite(f"{save_dir}/{count}.jpg", face_resized) cv2.rectangle(frame, (x, y), (x+w, y+h), (0, 255, 0), 2) if count >= 50: break cv2.imshow("capture", frame) if cv2.waitKey(1) & 0xFF == ord('q'): break cap.release() cv2.destroyAllWindows() print(f"done, saved {count} samples")detectMultiScale的scaleFactor和minNeighbors两个参数直接决定漏检和误检的平衡。scaleFactor是每次缩放图像的比例,1.1表示每次缩小10%,值越小检测越精细但速度越慢;minNeighbors是候选区域至少要被多少邻近窗口确认,值越大误检越少,但太大会漏检。考勤场景是固定机位,建议minNeighbors设5,优先把误检压住,拿工牌照片长时间对着摄像头也不会被当成真人脸录入。
minSize=(100, 100)的作用是过滤小目标,避免把远处走动的人误当成打卡目标,同时能明显提升帧率。采集端和识别端统一resize到200x200,是保证LBPH训练和预测输入一致的关键,很多识别率翻车的项目就栽在这个尺寸不一致上。
提示:采集时让员工小幅转动头部、保持自然表情,画面里不要出现第二张脸。样本质量差的话,后面训练出来的模型会把同一个ID的特征学得很散。
3.3 训练模型并生成特征映射
样本采完之后,训练脚本把dataset目录下所有子目录的人脸图片读进来,生成LBPH模型文件trainer.yml,同时把ID和姓名的对应关系落进feature_all.csv。
import cv2 import os import pandas as pd import numpy as np faces = [] labels = [] names = {} # dataset下每个子目录名格式: id_name for sub_dir in os.listdir("dataset"): dir_path = os.path.join("dataset", sub_dir) if not os.path.isdir(dir_path): continue emp_id, emp_name = sub_dir.split("_") emp_id = int(emp_id) names[emp_id] = emp_name for img_file in os.listdir(dir_path): img_path = os.path.join(dir_path, img_file) img = cv2.imread(img_path, cv2.IMREAD_GRAYSCALE) img = cv2.resize(img, (200, 200)) faces.append(img) labels.append(emp_id) recognizer = cv2.face.LBPHFaceRecognizer_create() recognizer.train(faces, np.array(labels)) recognizer.save("trainer.yml") pd.DataFrame( {"id": list(names.keys()), "name": list(names.values())} ).to_csv("feature_all.csv", index=False, encoding="utf-8-sig") print("model saved")trainer.yml保存的是LBPH模型参数和每个ID的直方图模板,之后识别阶段直接load这个文件就行,不需要每次启动都重新训练。feature_all.csv在这里承担的是ID到姓名的映射表,识别返回的是数值label,要靠这张表转成中文姓名写考勤记录。用utf-8-sig编码写csv,是防止Excel打开时中文乱码,这个细节在答辩展示考勤记录时很加分。
4. 核心代码拆解:识别主循环与考勤落盘
4.1 实时识别主循环
模型训练好之后,核心的考勤识别程序是下面这段逻辑。
import cv2 import pandas as pd from datetime import datetime recognizer = cv2.face.LBPHFaceRecognizer_create() recognizer.read("trainer.yml") # 载入ID到姓名的映射 df = pd.read_csv("feature_all.csv") id_to_name = dict(zip(df["id"], df["name"])) face_cascade = cv2.CascadeClassifier( cv2.data.haarcascades + "haarcascade_frontalface_default.xml" ) cap = cv2.VideoCapture(0) RECORDED = set() # 当前会话已打卡的员工,避免一帧多次写入 while True: ret, frame = cap.read() if not ret: break gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) faces = face_cascade.detectMultiScale(gray, 1.1, 5, minSize=(100, 100)) for (x, y, w, h) in faces: roi = cv2.resize(gray[y:y+h, x:x+w], (200, 200)) label, confidence = recognizer.predict(roi) if confidence < 80: name = id_to_name.get(label, "unknown") now = datetime.now().strftime("%Y-%m-%d %H:%M:%S") if label not in RECORDED: # 追加写入考勤日志 with open("logcat.csv", "a", encoding="utf-8") as f: f.write(f"{label},{name},{now}\n") RECORDED.add(label) msg = f"{name} clocked in" color = (0, 255, 0) else: msg = f"{name} already clocked" color = (0, 180, 255) else: msg = "unknown" color = (0, 0, 255) cv2.putText(frame, msg, (x, y - 10), cv2.FONT_HERSHEY_SIMPLEX, 0.8, color, 2) cv2.imshow("attendance", frame) if cv2.waitKey(1) & 0xFF == ord('q'): break cap.release() cv2.destroyAllWindows()predict()返回的confidence是LBPH计算出的直方图距离,距离越小越像本人。阈值80是一个比较稳的起点,我在同一光照条件下测过,本人识别confidence一般在30到70之间,不同人的距离通常超过100,中间隔着比较明显的分界。阈值设太松会把不相关的人识别进来,设太紧会导致本人稍微偏一下头就被判为unknown。
RECORDED这个集合是一个很实用的防重复机制。摄像头每秒出十几帧,如果不做去重,一次打卡会往logcat.csv里写入好几条重复记录。这里采用会话级去重,同一员工在一次运行中只写一次考勤记录,符合考勤的基本语义。
4.2 考勤日志的落盘格式与查询
logcat.csv是简单的追加重写,每行格式为id,name,time。日常查询直接用Pandas读取并过滤当天记录即可。
import pandas as pd df = pd.read_csv("logcat.csv", names=["id", "name", "time"]) today = pd.to_datetime("today").strftime("%Y-%m-%d") today_records = df[df["time"].str.startswith(today)] print(today_records)这种追加写的方案好处是任何文本工具都能打开查看,坏处是如果同时有多个程序实例写入可能会串行。考勤场景是单机单进程,追加写完全够用,不必引入SQLite或MySQL。如果后面要扩展成多台机器共享考勤数据,再把logcat.csv替换成数据库表就行,识别那部分代码不用动。
5. 避坑指南:五个把人脸考勤项目搞挂的常见错误
5.1 activate.bat一闪而过,之后照样报ModuleNotFoundError
现象:在资源管理器里双击activate.bat,窗口闪一下就关了,后面运行程序仍然提示找不到cv2。
原因:Windows下双击bat文件执行完成后窗口自动关闭,看不到任何输出。更关键的是,activate.bat设置的环境变量只在当前cmd进程内生效,双击方式打开的新窗口跟这个进程毫无关系,python命令仍然指向系统目录。
解决:先打开cmd,cd到项目目录,再输入activate.bat,看到命令行前缀出现(venv)再继续。如果前缀没出现,就检查项目根目录下有没有venv或.venv文件夹,然后用项目路径\Scripts\python.exe这样的完整路径直接调用,绕过激活过程。从那以后我每次拿到带虚拟环境的项目,第一件事就是看一眼Scripts目录里有没有python.exe和activate.bat,确认无误才敢跑后续命令。
5.2 cv2.face模块找不到,AttributeError: module 'cv2' has no attribute 'face'
现象:程序执行到cv2.face.LBPHFaceRecognizer_create()这一行直接报错。
原因:LBPH识别器属于OpenCV的contrib扩展模块,opencv-python这个基础包默认不包含face模块。很多人只装过opencv-python,所以cv2.face压根不存在。
解决:安装opencv-contrib-python。
pip install opencv-contrib-python注意opencv-python和opencv-contrib-python不能同时存在于同一环境,两个包都会往site-packages里写cv2目录,后装的会覆盖先装的,大概率把两个都搞坏。装之前先pip uninstall把已有的opencv相关包清干净,再只装contrib版本。
5.3 摄像头对准手机里的照片,居然也能打卡成功
现象:同事拿一张打印照片或手机屏幕放到摄像头前,系统把ta识别成了对应员工。
原因:CascadeClassifier检测的是Haar特征,它只管找像人脸的区域,分辨不出是真人和照片。LBPH特征对平面人脸和真实人脸的区分能力有限,单帧识别看,照片和真人确实很像。
解决:加一个简单的活体确认机制——连续N帧都识别到同一个ID,且每帧confidence都低于阈值,才写入考勤记录。这是成本最低的防照片方案。更进阶的做法是统计眼睛区域是否有周期性眨眼,或者分析脸部区域的纹理噪声分布,但毕设场景下连续5帧确认已经足够撑住答辩质询。
FRAME_CONFIRM = 5 frame_count = {} # 在识别循环内部,识别成功后累加帧数,达到5帧才写入 if label not in frame_count: frame_count[label] = 0 frame_count[label] += 1 if frame_count[label] >= FRAME_CONFIRM and label not in RECORDED: # 执行写入 passframe_count这个字典记录每个ID连续出现的帧数,任何一帧识别失败或识别成别的ID,就把对应计数清零。这样照片需要在摄像头前保持超过5帧也就是不到半秒的稳定时间,实际操作中照片很难做到,误刷率大大降低。
5.4 训练出的模型把所有人都认成同一个人
现象:换一个人站到摄像头前,屏幕上仍然顶着某个固定员工的名字。
原因:最常见的是训练样本采集时所有员工都在同一位置、同一角度、同一光照下拍的,模型学到的区分性特征太少;其次是训练集里同一个人的样本量超过了其他人很多,样本量不平衡导致模型偏向数据多的一方;还有可能是采集时把背景当作人脸存进了样本目录,模型学了一堆无意义的背景纹理。
解决:重新采集样本,给每名员工尽量保证样本量接近,固定采集20到50张,不要出现一个人80张另一个人12张的情况。文件夹做好清理,删除包含明显背景区域的图片。训练时加一道检查,把dataset目录下各员工子目录的图片数量打印出来,一眼就能看出样本是否均衡。
5.5 换了个办公室,暗光环境下谁都识别不出来
现象:模型在自己工位测试正常,换到光线较暗的会议室就频繁unknown,缩小阈值也没用。
原因:LBPH对光照变化有一定容忍度,但极端暗光下纹理信息大量丢失,人脸区域几乎全是黑块,直方图差异被拉大。这是算法本身的边界,不是代码bug。
解决:优先做图像预处理,在送入recognizer.predict之前先做直方图均衡化,用cv2.equalizeHist拉大人脸区域的对比度。这个方法成本极低,实测在暗光场景能救回不少识别率。仍不够的话,降级为补光策略——在摄像头附近放一盏小台灯,固定光照条件比调任何参数都见效。
roi = cv2.resize(gray[y:y+h, x:x+w], (200, 200)) roi = cv2.equalizeHist(roi) # 暗光下先增强对比度再送识别 label, confidence = recognizer.predict(roi)equalizeHist会把灰度直方图拉伸到整个亮度范围,人脸五官的纹理边缘会更清晰。这个操作对光照偏暗或偏亮的场景都有正向帮助,代价是每帧多出几毫秒计算,在考勤场景完全可接受。
6. 把识别率再往上提一截:置信度校准与连续帧防误刷
模型训练好、能跑通,只能算及格。真正让系统能稳定用起来的是两个细节:置信度阈值校准,以及连续帧确认的落地写法。
先说阈值校准。很多项目直接写死80,但不同摄像头、不同光照下这个数不一定合适。最稳的做法是用一小段脚本批量测试已知样本,统计本人和他人各自的confidence分布。
import cv2 import os import numpy as np recognizer = cv2.face.LBPHFaceRecognizer_create() recognizer.read("trainer.yml") self_confs, other_confs = [], [] for sub_dir in os.listdir("dataset"): dir_path = os.path.join("dataset", sub_dir) if not os.path.isdir(dir_path): continue emp_id, _ = sub_dir.split("_") emp_id = int(emp_id) for img_file in os.listdir(dir_path): img = cv2.imread(os.path.join(dir_path, img_file), cv2.IMREAD_GRAYSCALE) img = cv2.resize(img, (200, 200)) label, conf = recognizer.predict(img) if label == emp_id: self_confs.append(conf) else: other_confs.append(conf) self_arr = np.array(self_confs) other_arr = np.array(other_confs) print("self:", self_arr.min(), self_arr.max(), self_arr.mean()) print("other:", other_arr.min(), other_arr.max(), other_arr.mean())如果self最大值为68,other最小值为95,那阈值取80就有足够的余量。如果self最大值跑到90而other最小值只有85,说明两类分布有重叠,光调阈值救不了,得回去补训练样本或调预处理。这套方法适合所有LBPH项目,不止考勤,门禁、实验室设备借用场景都能套用。
再说连续帧确认的落地写法。前面避坑章节讲了基本思路,这里补一个细节:连续帧计数不是简单累加,帧率超过15fps时,人眨眼、偏头都会让某一帧识别失败,需要设定一个容忍窗口,比如连续5帧中允许1帧失败,否则稍微晃动一下就会永远达不到确认次数,考勤体验很差。
CONFIRM_NEEDED = 5 MAX_MISS = 1 miss_count = {} for label_candidate in frame_count: if label_candidate != current_label: frame_count[current_label] = 0 miss_count[current_label] = 0 if current_label is not None: frame_count[current_label] = frame_count.get(current_label, 0) + 1 else: for k in frame_count: miss_count[k] = miss_count.get(k, 0) + 1 if miss_count[k] > MAX_MISS: frame_count[k] = 0 miss_count[k] = 0这里的关键是miss_count单独计数,只有连续丢帧超过MAX_MISS才重置。这样处理下来,员工正常走到摄像头前稍稍侧脸也能完成打卡,不会被误杀。
做完这套系统我才明白,人脸考勤项目里真正决定成败的从来不是模型有多先进,而是阈值校准、防重复写入、活体确认这些不起眼的细节。从那以后我每部署一个考勤项目,都强制走一遍相同流程:先跑置信度分布脚本,再调阈值,最后套连续帧确认机制。这组步骤花不了半小时,但能帮你省下后面每个工作日的维护烦恼。希望帮到你。
本文还有配套的精品资源,点击获取