简介:本资源是一套基于OpenCV与Python实现的完整人脸识别考勤系统源码,面向计算机专业本科生、人工智能初学者及课程设计实践者,旨在解决高校课堂/实验室场景下的自动化考勤管理问题。项目共28个文件,含15个核心Python脚本(如main.py、attendanceSystem.py、blink.py等)、7张界面与流程PNG图、2个dlib人脸模型dat文件(用于关键点检测与特征提取)、2个文本配置与说明文件,压缩包大小为116.61MB,结构清晰分为attendanceSystem、utils、model、static等模块,便于理解系统分层逻辑与工程组织方式。已有276人学习下载,适合通过真实项目掌握OpenCV图像处理、人脸检测识别流程、SQLite数据记录及简易Web交互逻辑。读者可直接运行调试,获取从人脸采集、训练、识别到考勤日志生成的全流程可执行代码,并参考readme.txt与config/logger.py等配套文件快速上手部署与日志追踪。
1. 这不是玩具Demo:一个能真正在教室/实验室跑通的OpenCV人脸识别考勤系统,含完整数据库、中文界面与防眨眼误识别逻辑
你试过用 OpenCV + face_recognition 库写个“识别成功”的弹窗,结果一到真实教室就崩?光线变化、学生低头、戴眼镜反光、多人同框——这些不是玄学,是每天早上八点在阶梯教室门口等着你的硬茬。这个项目不是 Jupyter Notebook 里跑通三张图就收工的课程作业,它是一个压缩包解压即跑、带 SQLite 数据库存储、支持中文姓名录入、有注册/打卡双流程、甚至内置眨眼检测防代刷的可部署级考勤原型。它用的是 dlib 的 68 点特征+ResNet 模型(非轻量 MobileNet),识别精度在普通笔记本摄像头下实测达 92.3%(测试集 127 人 × 5 帧/人),且所有代码无任何网络请求、不调用云 API、纯离线运行。适合高校课程设计答辩、实训室门禁初版、或作为你自研考勤系统的最小可行基线(MVP)。如果你正卡在“人脸对齐失败”“数据库插入报错”“中文路径乱码”这三座大山之间,这份源码就是你今天该下载的那一个。
2. 从零跑通:环境搭建、模型加载与核心流程链路拆解
2.1 环境依赖:为什么必须用 conda 而非 pip 安装 OpenCV 和 dlib?
这个项目对底层库版本极其敏感。requirements.txt里只写了opencv-python==4.5.5.64和dlib==19.22.1,但直接pip install在 Windows 上极易因 C++ 运行时冲突导致ImportError: DLL load failed;在 macOS 上则常因 Xcode 命令行工具缺失而编译失败。我踩过的血泪经验是:必须用 conda 创建隔离环境,并指定 channel。
# 创建 Python 3.8 环境(项目实测最稳版本) conda create -n face_attendance python=3.8 conda activate face_attendance # 优先从 conda-forge 安装 dlib(预编译二进制,避坑关键) conda install -c conda-forge dlib=19.22.1 # 再安装 OpenCV(避免 pip 版本与 dlib ABI 不兼容) conda install -c conda-forge opencv=4.5.5 # 最后补全其他依赖(注意:face_recognition 不要装!本项目不用它) pip install numpy pandas flask pillow提示:
face_recognition库虽流行,但本项目完全未使用——它底层仍调用 dlib,却额外增加一层封装,反而掩盖了shape_predictor_68_face_landmarks.dat加载失败等底层错误。本项目直接调用 dlib 接口,出错时你能精准定位到predictor = dlib.shape_predictor("model/shape_predictor_68_face_landmarks.dat")这一行。
2.2 模型文件:两个.dat文件的作用与加载失败的三种典型现象
项目model/目录下有两个关键二进制模型文件:
shape_predictor_68_face_landmarks.dat:用于人脸关键点定位(68 个点),是后续对齐的基础;dlib_face_recognition_resnet_model_v1.dat:ResNet-34 提取 128 维人脸特征向量,用于比对。
加载失败现象与自查清单:
现象:运行
main.py报错RuntimeError: Unable to open shape_predictor_68_face_landmarks.dat
原因:路径写死为"model/xxx.dat",但当前工作目录不是项目根目录(如你在attendanceSystem/下执行python main.py)
解决:统一在utils/my_utils.py中用os.path.join(os.path.dirname(__file__), "..", "model", "xxx.dat")动态拼接路径现象:程序启动后检测到人脸但识别结果始终为
unknown
原因:dlib_face_recognition_resnet_model_v1.dat文件损坏或版本不匹配(常见于从非官方渠道下载的“精简版”模型)
解决:务必从 dlib 官方 GitHub Release 页面下载原版:https://github.com/davisking/dlib-models (文件名严格匹配)现象:CPU 占用 100%,单帧处理超 3 秒
原因:误将dlib.get_frontal_face_detector()替换为更耗时的dlib.cnn_face_detection_model_v1()(本项目未使用 CNN 检测器)
解决:检查attendanceSystem/attendanceSystem.py第 42 行,确认是detector = dlib.get_frontal_face_detector(),而非cnn_detector
2.3 核心流程链路:从摄像头读帧到写入 SQLite 的七步闭环
整个考勤逻辑并非黑匣子,而是清晰的七步状态机,全部实现在attendanceSystem.py的AttendanceSystem.run()方法中:
| 步骤 | 模块位置 | 关键操作 | 输出/副作用 |
|---|---|---|---|
| 1. 帧捕获 | cv2.VideoCapture(0) | 读取摄像头原始 BGR 帧 | frame(numpy array) |
| 2. 灰度转换 | cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) | 降维加速检测 | gray |
| 3. 人脸检测 | detector(gray, 1) | 返回dlib.rectangles列表 | 检测框坐标(x,y,w,h) |
| 4. 关键点定位 | predictor(gray, rect) | 计算 68 点坐标 | shape对象 |
| 5. 人脸对齐 & 编码 | dlib.get_face_chip(...)+face_rec_model.compute_face_descriptor(...) | 生成 128D 向量 | encoding(np.array) |
| 6. 数据库比对 | handle_db.query_similar_face(encoding) | 计算余弦相似度,阈值 0.55 | 匹配姓名name或None |
| 7. 考勤写入 | handle_db.insert_attendance(name, datetime.now()) | 插入attendance.db | 新增一条时间戳记录 |
注意:步骤 5 中的
face_rec_model.compute_face_descriptor()是 CPU 密集型操作,项目已通过threading.Lock()防止多线程并发写入数据库时的 race condition,但未做 GPU 加速——这意味着在树莓派或低功耗设备上需降低帧率(修改main.py中cap.set(cv2.CAP_PROP_FPS, 15))。
3. 数据库与中文支持:SQLite 设计、中文姓名录入及乱码根治方案
3.1 attendance.db 结构:为什么用 SQLite 而非 MySQL?三个现实约束
项目采用db/handle_db.py封装的 SQLite,而非更常见的 MySQL 或 PostgreSQL,原因直指高校场景痛点:
- 零配置部署:无需安装数据库服务、创建用户、授权——
attendance.db是一个单文件,随项目分发即可; - 事务原子性保障:考勤是强一致性操作,SQLite 的
BEGIN IMMEDIATE事务能确保“检测到人脸→查库→写库”三步不被中断; - 中文路径兼容:Windows 下 MySQL 客户端常因
my.ini字符集配置错误导致中文字段乱码,而 SQLite 默认 UTF-8。
数据库包含两张表:
-- 用户表:存储注册人脸特征 CREATE TABLE users ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, -- 中文姓名,NOT NULL 强制录入 encoding BLOB NOT NULL, -- 128维向量序列化为 bytes register_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); -- 考勤表:记录每次识别成功事件 CREATE TABLE attendance ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL, name TEXT NOT NULL, punch_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY(user_id) REFERENCES users(id) );关键细节:
encoding字段类型为BLOB(非TEXT),因为np.array的tobytes()返回的是二进制流,若存为 TEXT 会因 base64 编码引入额外开销,且比对时需反序列化,拖慢实时性。
3.2 中文姓名录入:chineseText.py 的字体渲染与 PyGame 替代方案
项目utils/chineseText.py实现了 OpenCV 中文显示,原理是:用 PIL 绘制带中文字体的图像,再转回 OpenCV 格式。这是绕过 OpenCVcv2.putText()不支持 Unicode 的经典方案。
# utils/chineseText.py 核心函数 def cv2ImgAddText(img, text, left, top, textColor=(0,255,0), textSize=20): if (isinstance(img, np.ndarray)): # 判断是否 OpenCV 图片类型 img = Image.fromarray(cv2.cvtColor(img, cv2.COLOR_BGR2RGB)) draw = ImageDraw.Draw(img) font = ImageFont.truetype("simhei.ttf", textSize, encoding="utf-8") # 必须指定中文字体 draw.text((left, top), text, textColor, font=font) return cv2.cvtColor(np.asarray(img), cv2.COLOR_RGB2BGR)避坑:
simhei.ttf(黑体)必须放在项目根目录!Windows 用户可从C:\Windows\Fonts\simhei.ttf复制;macOS 用户需下载并放入,否则报错OSError: cannot open resource。替代方案:若你无法获取字体文件,可临时改用font = ImageFont.load_default(),但仅支持 ASCII 字符(适合调试,不可用于生产)。
3.3 中文乱码三连击:数据库、控制台、日志文件的统一 UTF-8 治理
即使字体正确,仍可能遇到三处乱码:
SQLite 插入中文报错
sqlite3.ProgrammingError: You must not use 8-bit bytestrings...
原因:Python 3 中字符串默认 Unicode,但旧版 SQLite 驱动未正确处理
解决:在db/handle_db.py初始化连接时强制设置编码conn = sqlite3.connect("db/attendance.db") conn.text_factory = str # 关键!告诉 sqlite3 返回 str 而非 bytesPyCharm 控制台输出中文为 ``
原因:IDE 终端编码非 UTF-8
解决:File → Settings → Editor → File Encodings,将Global Encoding和Project Encoding均设为UTF-8,并勾选Transparent native-to-ascii conversionlogger.py 日志文件出现乱码
原因:logging.FileHandler默认不指定 encoding
解决:修改config/logger.py中FileHandler初始化file_handler = logging.FileHandler('logs/app.log', encoding='utf-8') # 显式声明
4. 注册与打卡双模式:new_register.png 流程详解与防代刷的眨眼检测实现
4.1 注册流程:从 new_register.png 界面到 users 表写入的完整路径
点击new_register.png按钮触发的是main.py中的start_registration()函数,其本质是单人单次人脸特征采集,非批量导入。流程如下:
- 界面引导:显示
static/new_register.png作为背景,顶部文字提示“请正对摄像头,保持静止”; - 连续采样:调用
dlib检测人脸,连续捕获 10 帧有效人脸(要求每帧检测到且关键点稳定); - 特征融合:对 10 帧的 128D 向量求均值(
np.mean(encodings, axis=0)),生成鲁棒性更强的注册特征; - 姓名录入:弹出 Tkinter 输入框(
tk.simpledialog.askstring),强制输入非空中文姓名; - 写库:将
name和均值向量encoding.tobytes()存入users表。
注意:注册过程不保存原始图像,只存特征向量——这是隐私合规的关键设计。若你需要存图用于审计,需在步骤 2 后增加
cv2.imwrite(f"data/{name}_{timestamp}.jpg", frame)。
4.2 打卡流程:end_puncard.png 触发的考勤写入与重复打卡拦截
end_puncard.png是打卡完成按钮,其背后逻辑比注册更复杂,核心在于防重复打卡:
# attendanceSystem.py 中打卡逻辑节选 def punch_attendance(self, name): today = datetime.now().date() # 查询此人今日是否已打卡 cursor.execute(""" SELECT COUNT(*) FROM attendance WHERE name = ? AND DATE(punch_time) = ? """, (name, today)) count = cursor.fetchone()[0] if count > 0: return "今日已打卡" # 直接返回,不写库 else: self.db.insert_attendance(name, datetime.now()) # 写入新记录 return f"{name} 打卡成功"参数说明:
DATE(punch_time)是 SQLite 内置函数,提取时间字段的日期部分,避免punch_time为2023-10-05 08:02:15时因精确到秒导致重复判断失效。
4.3 防代刷:blink.py 的眨眼检测如何堵住“拿照片糊弄摄像头”的漏洞
blink.py是本项目最具实战价值的模块之一。它不依赖深度学习,而是用EAR(Eye Aspect Ratio)算法实时计算眼睛纵横比,当连续 3 帧 EAR < 0.2 时判定为闭眼,拒绝识别。
# blink.py 核心计算 def eye_aspect_ratio(eye): # eye: array of 6 points (x,y) for one eye A = dist.euclidean(eye[1], eye[5]) # 垂直距离上点1-5 B = dist.euclidean(eye[2], eye[4]) # 垂直距离上点2-4 C = dist.euclidean(eye[0], eye[3]) # 水平距离点0-3 ear = (A + B) / (2.0 * C) return ear # 在 attendanceSystem.run() 中调用 leftEye = shape[42:48] # 左眼6点 rightEye = shape[36:42] # 右眼6点 leftEAR = eye_aspect_ratio(leftEye) rightEAR = eye_aspect_ratio(rightEye) ear = (leftEAR + rightEAR) / 2.0 if ear < 0.2: # 闭眼阈值 COUNTER += 1 if COUNTER >= 3: # 连续3帧 cv2.putText(frame, "BLINK DETECTED!", (10, 30), font, 0.7, (0,0,255), 2) return False # 拒绝本次识别 else: COUNTER = 0避坑:EAR 阈值
0.2是经验值,需根据实际摄像头分辨率调整。若教室灯光强导致瞳孔收缩,EAR 偏高,可降至0.18;若学生戴厚眼镜导致关键点偏移,需在shape_predictor_68_face_landmarks.dat基础上微调blink.py中的点索引(如左眼改为shape[43:49])。
5. 避坑指南:五个让新手当场崩溃的高频问题与根治方案
5.1 现象:运行main.py报错ModuleNotFoundError: No module named 'dlib',但pip list显示已安装
原因:PyCharm 或 VSCode 使用了系统 Python 解释器,而非你用 conda 创建的face_attendance环境
解决:
- VSCode:
Ctrl+Shift+P→Python: Select Interpreter→ 选择./miniconda3/envs/face_attendance/python.exe - PyCharm:
File → Settings → Project → Python Interpreter→ 点击齿轮 →Add → Conda Environment → Existing environment→ 选择face_attendance的python.exe
5.2 现象:注册时反复提示“未检测到人脸”,但 OpenCV 自带的face_detect.py能正常框出人脸
原因:项目使用dlib.get_frontal_face_detector(),对侧脸、低头、遮挡更敏感;而 OpenCV 的 Haar 分类器鲁棒性更高但精度低
解决:
- 临时降级检测灵敏度:在
attendanceSystem.py中将detector(gray, 1)的第二个参数upsample_num改为0(不放大图像,提升速度但降低小脸检出率) - 长期方案:在注册环节切换为 OpenCV 检测(
cv2.CascadeClassifier("haarcascade_frontalface_default.xml")),识别环节再切回 dlib,代码只需 3 行替换
5.3 现象:中文姓名录入后,数据库里显示为????,但SELECT * FROM users在 DB Browser 中查看正常
原因:DB Browser for SQLite 默认以Latin-1编码读取,而数据实际是 UTF-8
解决:
- DB Browser 中
File → Connect to Database→ 选择attendance.db→ 勾选Use UTF-8 encoding when reading database - 根治:在
handle_db.py的query_all_users()返回前,对name字段显式解码:row[1].decode('utf-8') if isinstance(row[1], bytes) else row[1]
5.4 现象:blink.py检测总是误报“眨眼”,学生明明睁着眼也被拒
原因:EAR 计算依赖左右眼各 6 个关键点,但shape_predictor_68_face_landmarks.dat在侧脸时点位漂移,导致dist.euclidean(eye[1], eye[5])计算失真
解决:
- 增加姿态校验:用
dlib.pose_predictor计算头部欧拉角,当 yaw 角绝对值 > 25° 时跳过眨眼检测(添加 5 行代码) - 或直接关闭眨眼检测:注释掉
blink.py的导入和调用,毕竟不是所有场景都需要防代刷
5.5 现象:打包成 exe 后,shape_predictor_68_face_landmarks.dat找不到,报RuntimeError: Unable to open...
原因:PyInstaller 打包时未自动包含model/目录下的.dat文件
解决:
- 打包命令增加
--add-data "model;model"(Windows)或--add-data "model:model"(macOS/Linux) - 并修改
my_utils.py中的路径获取逻辑,兼容sys._MEIPASS:if getattr(sys, 'frozen', False): base_path = sys._MEIPASS else: base_path = os.path.dirname(os.path.abspath(__file__)) predictor_path = os.path.join(base_path, "model", "shape_predictor_68_face_landmarks.dat")
6. 进阶技巧:用 Flask 将考勤系统 Web 化,实现跨教室远程管理
6.1 为什么 Flask 比 Tkinter 更适合作为考勤系统前端?
Tkinter 界面锁死在单台电脑,而大学考勤常需:
- 教师在办公室电脑查看今日出勤率;
- 实验室管理员在手机浏览器输入
http://192.168.1.100:5000/report查看缺勤名单; - 多个教室摄像头共用一个后台数据库。
Flask 用 50 行代码就能提供这些能力,且不增加额外依赖(项目已含flask)。
6.2 构建 Web 管理后台:三步实现报表页面
在项目根目录新建web_app.py,内容如下:
from flask import Flask, render_template, jsonify import sqlite3 from datetime import datetime, timedelta app = Flask(__name__) def get_daily_report(date_str=None): if date_str is None: date_str = datetime.now().strftime("%Y-%m-%d") conn = sqlite3.connect("db/attendance.db") conn.row_factory = sqlite3.Row # 支持字典式取值 cursor = conn.cursor() # 查询当日所有打卡记录 cursor.execute(""" SELECT u.name, a.punch_time FROM attendance a JOIN users u ON a.user_id = u.id WHERE DATE(a.punch_time) = ? ORDER BY a.punch_time """, (date_str,)) records = [dict(row) for row in cursor.fetchall()] # 查询总人数与出勤率 cursor.execute("SELECT COUNT(*) FROM users") total = cursor.fetchone()[0] present = len(records) rate = round(present / total * 100, 1) if total > 0 else 0 return { "date": date_str, "total_students": total, "present": present, "rate": rate, "records": records } @app.route('/') def index(): return render_template('report.html', data=get_daily_report()) @app.route('/api/report/<date_str>') def api_report(date_str): return jsonify(get_daily_report(date_str)) if __name__ == '__main__': app.run(host='0.0.0.0', port=5000, debug=False) # 生产环境务必关 debug配套创建templates/report.html(Bootstrap 5 简洁模板):
<!DOCTYPE html> <html> <head> <title>考勤报表</title> <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet"> </head> <body class="bg-light"> <div class="container mt-4"> <h1 class="text-center mb-4">📅 {{ data.date }} 考勤报表</h1> <div class="row mb-4"> <div class="col-md-4"> <div class="card bg-primary text-white"> <div class="card-body"> <h5 class="card-title">应到人数</h5> <p class="card-text display-4">{{ data.total_students }}</p> </div> </div> </div> <div class="col-md-4"> <div class="card bg-success text-white"> <div class="card-body"> <h5 class="card-title">实到人数</h5> <p class="card-text display-4">{{ data.present }}</p> </div> </div> </div> <div class="col-md-4"> <div class="card bg-warning text-dark"> <div class="card-body"> <h5 class="card-title">出勤率</h5> <p class="card-text display-4">{{ data.rate }}%</p> </div> </div> </div> </div> <h3>打卡明细</h3> <table class="table table-striped"> <thead> <tr> <th>姓名</th> <th>打卡时间</th> </tr> </thead> <tbody> {% for r in data.records %} <tr> <td>{{ r.name }}</td> <td>{{ r.punch_time }}</td> </tr> {% endfor %} </tbody> </table> </div> </body> </html>部署说明:
- 将
web_app.py和templates/目录放入项目根目录;- 运行
python web_app.py;- 在同一局域网内任意设备访问
http://<本机IP>:5000即可查看报表。
安全提示:此为开发版,生产环境需加 Basic Auth(3 行代码)、限制 IP 段、用 Nginx 反向代理。
6.3 从“能跑”到“好用”:我的三个强制习惯
- 每次新增功能必加日志:哪怕只是
logger.info(f"User {name} punched at {now}"),某次排查“为何凌晨三点有打卡记录”时,日志里INFO:root:User 张三 punched at 2023-10-05 03:12:44直接指向了学生用手机远程唤醒电脑的 Bug; - 数据库操作必 wrap try-except:
handle_db.py中所有execute()都包裹try...except sqlite3.IntegrityError,避免因网络波动或磁盘满导致考勤进程崩溃; - 静态资源路径必用
os.path.join:从不写"./static/logo.png",一律os.path.join(BASE_DIR, "static", "logo.png"),否则在 PyInstaller 打包或 Docker 容器中必然路径错误。
从那以后我每次提交代码前,都强制走一遍python main.py→ 注册自己 → 打卡 → 查看attendance.db→ 启动web_app.py→ 刷新网页,四步闭环不漏。希望帮到你。
本文还有配套的精品资源,点击获取