简介:这份资源是一套基于Python与OpenCV实现的人脸识别员工考勤系统完整项目包,面向计算机、人工智能、通信工程、自动化等专业的在校学生、教师及企业开发者,可用于毕业设计、课程设计、作业提交或项目初期立项演示,也适合具备一定基础的小白进阶学习。压缩包共收录671个文件,以501个py源码文件为核心,辅以pyd、exe、dll等运行依赖,以及png、jpg图像素材、xml配置、pth模型权重、bat与cfg脚本等,整体约197.41MB,目录结构完整,便于按模块查阅与二次开发。目前已有60人学习关注。项目源码经过实际测试运行,功能可用,并附有详细文档,涵盖人脸检测、特征提取、考勤记录等关键环节,读者可据此理解系统架构、调试思路与部署流程,也能在现有代码基础上修改扩展,实现签到统计、数据导出等个性化功能。
1. 从一张打卡照片说起:Python + OpenCV 人脸考勤到底能跑多稳
公司行政把一台旧笔记本架在前台,屏幕上跑着一个 Python 脚本,摄像头对着进门方向。员工走近,框出人脸、比对工号、写一条打卡记录到 CSV,整个过程不到一秒。这套东西的核心就是 Python 加 OpenCV 的人脸识别考勤系统,也是很多中小团队想自己搭一套时最先想到的方案。它解决的不是"识别得多准"这种学术问题,而是"今天谁来了、几点来的、有没有代打卡"这种每天都要出结果的现实需求。适合谁?适合会一点 Python、能装库、愿意调参数的后端或运维同学,也适合想拿它当毕业设计或内部工具练手的开发者。资料齐全加详细文档这类打包资源,价值不在代码多花哨,而在于它把环境、采集、训练、识别、记录这条链路完整串起来了,你照着能跑通,再按自己场景改。
2. 环境搭不起来,后面全是空谈:OpenCV 与 Python 的安装与验证
2.1 为什么选 OpenCV 而不是别的库
人脸考勤这条链路要做的事很明确:读摄像头帧、检测人脸、提取特征、比对、写记录。OpenCV 一个人把读帧、图像预处理、检测器、绘图全包了,opencv-contrib-python里还直接带了 LBPH 人脸识别器,不需要你再单独编译 dlib 或者配深度学习环境。对于考勤这种几十到几百人的小规模场景,LBPH 加 Haar 级联的精度够用,速度在普通 CPU 上就能实时。选它的核心理由是依赖少、装得快、出问题好查。如果你一上来就上深度学习模型,显卡、CUDA、模型文件、推理框架一层层叠上去,光环境就能耗掉两天,而考勤系统真正难的地方在工程细节,不在模型本身。
2.2 安装命令与常见报错处理
先确认 Python 版本,建议 3.8 到 3.10,太新的版本某些轮子还没跟上。
python --version pip install opencv-python opencv-contrib-python numpy pillow装完立刻验证,别等到写代码时才发现没装上。
import cv2 import numpy as np print(cv2.__version__) # 确认 contrib 模块在,LBPH 依赖它 print(hasattr(cv2, 'face'))如果报ModuleNotFoundError: No module named 'opencv',九成是装到了另一个 Python 环境里。用pip -V看 pip 对应的解释器路径,和python -c "import sys; print(sys.executable)"对比,不一致就改用python -m pip install。如果cv2.face不存在,说明你装的是opencv-python而不是opencv-contrib-python,两个都装会互相覆盖,先pip uninstall再只装 contrib 版本。VSCode 里配环境时,右下角选解释器要选到你装库的那个,否则编辑器报红但命令行能跑,这种玄学问题多半就是解释器选错了。
提示:装库时加国内镜像源能省不少时间,但别把镜像源写死进项目文档,换台机器就失效。
2.3 摄像头读取的最小验证
在写考勤逻辑前,先用十行代码确认摄像头能出画面。
import cv2 cap = cv2.VideoCapture(0) # 0 是默认摄像头,外接的可能是 1 if not cap.isOpened(): print("摄像头打不开,检查占用或驱动") exit() while True: ret, frame = cap.read() if not ret: break cv2.imshow("test", frame) if cv2.waitKey(1) & 0xFF == ord('q'): break cap.release() cv2.destroyAllWindows()VideoCapture(0)的参数是设备索引,笔记本自带一般是 0,USB 外接的试 1 或 2。waitKey(1)里的 1 是毫秒等待,太小画面卡,太大延迟高,考勤场景 1 到 30 都行。如果read()一直返回 False,先看摄像头是不是被会议软件占用了,这是最常见的翻车点。
3. 从采集到训练:把人脸数据变成能比对的特征
3.1 采集阶段的关键参数
采集就是给每个员工拍一批人脸图存到以工号命名的文件夹里。这里有几个参数直接决定后面识别率。检测器用 Haar 级联,scaleFactor设 1.1 到 1.3,太小检测慢,太大漏脸;minNeighbors设 5 左右,太低误检多,太高漏检。采集数量每人 30 到 50 张,太少特征不稳,太多训练慢且收益递减。采集时要覆盖不同角度和光照,别只拍正脸一张,否则真人稍微侧头就认不出。
import cv2 import os face_cascade = cv2.CascadeClassifier( cv2.data.haarcascades + 'haarcascade_frontalface_default.xml') def collect(emp_id, save_dir='dataset', max_count=40): os.makedirs(os.path.join(save_dir, str(emp_id)), exist_ok=True) cap = cv2.VideoCapture(0) count = 0 while count < max_count: ret, frame = cap.read() if not ret: break gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) faces = face_cascade.detectMultiScale(gray, 1.2, 5) for (x, y, w, h) in faces: count += 1 face = gray[y:y+h, x:x+w] face = cv2.resize(face, (200, 200)) # 统一尺寸,训练必需 cv2.imwrite(f'{save_dir}/{emp_id}/{count}.jpg', face) cv2.rectangle(frame, (x, y), (x+w, y+h), (0, 255, 0), 2) cv2.imshow('collect', frame) if cv2.waitKey(1) & 0xFF == ord('q'): break cap.release() cv2.destroyAllWindows()resize到 200x200 是为了让所有样本尺寸一致,LBPH 虽然对尺寸不敏感,但统一后训练更稳。detectMultiScale返回的是列表,一帧可能检测到多张脸,采集时最好限制只取最大的一张,避免把背景里的人脸也存进去。
3.2 训练 LBPH 识别器
LBPH 的原理是把人脸分成小格,每格算局部二值模式直方图,最后拼成特征向量。它对人脸光照变化有一定容忍度,训练快,适合小数据集。
import cv2 import numpy as np import os def train(dataset_dir='dataset', model_path='trainer.yml'): recognizer = cv2.face.LBPHFaceRecognizer_create() faces, ids = [], [] for emp_id in os.listdir(dataset_dir): emp_dir = os.path.join(dataset_dir, emp_id) if not os.path.isdir(emp_dir): continue for img_name in os.listdir(emp_dir): img_path = os.path.join(emp_dir, img_name) gray = cv2.imread(img_path, cv2.IMREAD_GRAYSCALE) if gray is None: continue faces.append(gray) ids.append(int(emp_id)) recognizer.train(faces, np.array(ids)) recognizer.save(model_path) print(f'训练完成,共 {len(faces)} 张,{len(set(ids))} 人')LBPHFaceRecognizer_create()可以传参数,radius默认 1,neighbors默认 8,grid_x和grid_y默认 8,一般不用改。如果识别率低,先把grid_x、grid_y调到 10 试试,格子越细对细节越敏感,但也更容易受噪声影响。ids必须是整数数组,工号用字符串会报错,这是新手常踩的坑。
3.3 识别与置信度阈值
识别时predict返回工号和置信度,置信度是距离,越小越像。阈值设多少直接决定误识和漏识的平衡。
import cv2 recognizer = cv2.face.LBPHFaceRecognizer_create() recognizer.read('trainer.yml') face_cascade = cv2.CascadeClassifier( cv2.data.haarcascades + 'haarcascade_frontalface_default.xml') THRESHOLD = 70 # 经验值,需按实际数据调 cap = cv2.VideoCapture(0) while True: ret, frame = cap.read() gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) faces = face_cascade.detectMultiScale(gray, 1.2, 5) for (x, y, w, h) in faces: face = cv2.resize(gray[y:y+h, x:x+w], (200, 200)) emp_id, conf = recognizer.predict(face) if conf < THRESHOLD: label = f'ID:{emp_id} conf:{conf:.1f}' else: label = 'unknown' cv2.putText(frame, label, (x, y-10), cv2.FONT_HERSHEY_SIMPLEX, 0.8, (0, 255, 0), 2) cv2.rectangle(frame, (x, y), (x+w, y+h), (0, 255, 0), 2) cv2.imshow('attendance', frame) if cv2.waitKey(1) & 0xFF == ord('q'): break cap.release() cv2.destroyAllWindows()阈值 70 是常见起点,但必须用你自己的数据测。方法很简单:让同一个人在不同光照下测十次,记录 conf 值,取偏大的那个作为上限;再让陌生人测,记录最小 conf,取偏小的作为下限,阈值取两者中间。如果两个区间重叠,说明数据质量不够,回去补采集。
4. 考勤逻辑与数据落盘:别让识别结果只停在屏幕上
4.1 打卡去重与时间窗口
识别出人只是第一步,考勤要的是"今天第一次出现算上班,最后一次算下班"。最简单的做法是按天按人记录,同一天同一人只写第一条和最后一条。
import csv import os from datetime import datetime, date def mark_attendance(emp_id, record_file='attendance.csv'): today = date.today().isoformat() now = datetime.now().strftime('%H:%M:%S') rows = [] if os.path.exists(record_file): with open(record_file, 'r', newline='', encoding='utf-8') as f: rows = list(csv.reader(f)) # 找今天这个人的记录 found = None for r in rows: if len(r) >= 4 and r[0] == today and r[1] == str(emp_id): found = r break if found is None: rows.append([today, str(emp_id), now, now]) # 首次,上班时间 else: found[3] = now # 更新下班时间 with open(record_file, 'w', newline='', encoding='utf-8') as f: writer = csv.writer(f) writer.writerows(rows)这个逻辑每识别到一次就写一次文件,频繁 IO 在人多时会卡。实际部署时改成内存里维护一个字典,每隔几秒或识别状态变化时才落盘。date.today()和datetime.now()依赖系统时间,服务器时间不对会导致考勤全乱,部署前先对时。
4.2 防代打卡的几个土办法
纯人脸识别挡不住拿照片对着摄像头的人。低成本的做法是加活体检测的简化版:要求眨眼或转头。OpenCV 自带的眼睛检测器可以判断眼睛开合,连续几帧检测到闭眼再睁眼就算一次眨眼。
eye_cascade = cv2.CascadeClassifier( cv2.data.haarcascades + 'haarcascade_eye.xml') def blink_detected(gray_face): eyes = eye_cascade.detectMultiScale(gray_face, 1.1, 4) return len(eyes) == 0 # 简化:检测不到眼睛视为闭眼这个判断很粗糙,戴眼镜或光线暗时容易误判。更稳的做法是记录连续帧的眼睛数量变化,从有到无再到有才算一次眨眼。如果不想做活体,至少把摄像头架高一点、加个补光灯,让照片不好举。另一个办法是打卡后随机弹一个动作提示,比如"请向左转头",配合简单的头部姿态估计,成本比深度学习活体低得多。
4.3 数据表结构建议
CSV 适合几十人,上百人建议换 SQLite,查询和并发都更好。
| 字段 | 类型 | 说明 |
|---|---|---|
| date | TEXT | 日期,YYYY-MM-DD |
| emp_id | INTEGER | 工号 |
| check_in | TEXT | 上班打卡时间 |
| check_out | TEXT | 下班打卡时间 |
| confidence | REAL | 识别置信度,用于排查 |
置信度存下来很有用,后面发现某人总是识别失败,翻记录看 conf 是不是一直偏高,就能判断是数据问题还是阈值问题。
5. 避坑与排查:那些让考勤系统翻车的细节
5.1 识别忽好忽坏,同一个人有时认不出
现象是上午能识别,下午就报 unknown。原因多半是光照变化,LBPH 对强逆光和侧光敏感。解决方法是采集时就把不同时段的光照样本拍进去,或者在识别前做直方图均衡化cv2.equalizeHist(gray),让灰度分布拉平。如果还不行,在摄像头旁加一个恒定光源,减少环境光波动。
5.2 摄像头读几帧就卡死或花屏
现象是程序跑几分钟后画面冻结。原因是cap.read()的缓冲区堆积,处理速度跟不上采集速度。解决办法是每次循环只取最新帧,或者把VideoCapture的缓冲区设小:cap.set(cv2.CAP_PROP_BUFFERSIZE, 1)。另外waitKey不能省,没有它 GUI 事件不刷新,画面也会假死。
5.3 训练时报 labels 类型错误
现象是recognizer.train抛异常,提示标签类型不对。原因是ids列表里混了字符串。工号从文件夹名来,os.listdir返回的是字符串,必须int()转换。如果工号本身带字母,LBPH 不支持字符串标签,得自己维护一个映射表,把字符串工号转成整数再训练,识别后再转回来。
5.4 多人同时出现在画面里只识别一个
现象是两个人一起进门,只框出一个。原因是detectMultiScale虽然返回多张脸,但你的打卡逻辑只取了第一张。解决是把 for 循环里每个人都走一遍识别和记录,但要注意同一帧里同一个人不要重复写。另外 Haar 检测器对侧脸和遮挡本来就弱,两个人挨太近时可能只检出一张,这种场景建议改成一人一刷,或者换更稳的检测模型。
5.5 打包成 exe 后找不到模型文件
现象是脚本能跑,打包后报trainer.yml不存在。原因是打包工具没把模型和级联文件带进去。解决是用--add-data把trainer.yml和haarcascade_frontalface_default.xml一起打进去,代码里用sys._MEIPASS拼路径。级联文件也可以直接从cv2.data.haarcascades读,但打包后这个路径可能变,最稳的是把 xml 复制到项目目录里一起打包。
6. 把识别率再往上推一档:几个我实际用过的调优手法
第一招是样本增强。采集完每个人的人脸后,用 OpenCV 做一批小幅度的旋转、平移、加噪,把每人样本从 40 张扩到 200 张左右。LBPH 对这类增强不敏感,但能覆盖更多姿态,实测在侧脸场景下识别率能提十几个点。代码很简单,cv2.warpAffine做旋转,np.random.normal加高斯噪声,注意别增强过头,旋转超过 15 度就失真了。
第二招是双阈值策略。识别时设两个阈值,低于低阈值直接通过,高于高阈值直接拒绝,中间区间要求再测一帧,连续两帧都在中间区间且工号一致才通过。这样能压掉大部分偶发误识,代价是多花一两百毫秒。对于考勤这种不要求极致速度的场景,完全值得。
第三招是定期重训。人员变动、换发型、戴眼镜都会让旧模型变差。我一般设一个每月提醒,把当月识别置信度偏高的样本挑出来,人工确认后补进数据集重新训练。重训不用停服务,训练几秒钟的事,新模型文件替换后下次识别自动加载。
第四招是日志留痕。每次识别无论成功失败都写一条日志,包含时间、置信度、是否通过。出问题时翻日志比猜快得多。我吃过亏,有次行政说某人天天迟到,查日志发现是摄像头角度偏了,那人每次都要歪头才能被识别,记录时间自然偏晚。调了摄像头角度就正常了。
最后一招,别追求 100% 识别率。考勤系统里识别失败是可以接受的,只要失败时能走备用流程,比如输入工号加密码。真正不能接受的是把 A 认成 B,那会直接导致考勤数据错乱。所以阈值宁可严一点,让认不出的走人工,也别放宽到乱认。这套系统我前后搭过三次,每次都是识别本身不难,难的是让它在真实环境里稳定跑下去。希望帮到你。
本文还有配套的精品资源,点击获取