
简介这份资源是面向高校学生与Python初学者的人脸识别考勤系统完整项目源码适合作为课程设计、期末大作业或OpenCV实战练手参考。项目以OpenCV为核心结合dlib人脸关键点与特征模型实现用户注册、人脸检测识别、打卡考勤、日志查询等模块并配有数据库操作、日志记录与中文文本处理等工具代码。压缩包共28个文件以15个Python源码为主另含7张PNG界面截图、2个dat模型数据文件及txt说明文档整体约116.61MB目录按attendanceSystem、utils、db、config等模块划分结构清晰便于二次开发。目前已有276人学习下载。读者可从中获得一套可直接运行的人脸识别考勤方案理解人脸检测、特征比对与考勤数据落库的完整链路并参考界面截图与说明快速上手调试。1. 从一次期末答辩说起这套 OpenCV 人脸考勤源码到底能跑出什么去年帮学弟看一个课程设计答辩题目就是「基于 OpenCV 的 Python 人脸识别考勤系统」。他演示到一半翻车了——摄像头识别出人脸但名字全标成同一个人考勤记录还写进了错误的日期。评委老师问了一句「你这识别阈值设多少」他答不上来。这个场景我印象很深因为绝大多数拿这套源码的人卡的不是「跑不起来」而是「跑起来了但结果不对又不知道去哪调」。这份资源就是一套完整的 Python 人脸识别考勤系统源码23 个文件11 个 Python 源文件核心逻辑集中在attendanceSystem.py、blink.py、handle_db.py和my_utils.py里模型目录下放了 dlib 的shape_predictor_68_face_landmarks.dat和dlib_face_recognition_resnet_model_v1.dat两个数据文件。它解决的是「从摄像头采集到考勤落库」这条完整链路人脸检测、特征提取、活体判断眨眼、数据库读写、界面交互。适合正在做课程设计的学生、想快速搭一个可演示原型的开发者以及需要一份能改能扩的 Python 视觉项目底稿的人。下面我按实际拆包的顺序把这份源码怎么用、参数怎么调、哪里容易踩坑讲清楚。2. 环境与依赖把 dlib 和 OpenCV 装对比写代码更费时间2.1 为什么这套源码的依赖顺序不能乱很多人拿到requirements.txt直接pip install -r然后卡在 dlib 编译报错上。这套源码的人脸识别链路是OpenCV 负责读摄像头帧和图像预处理dlib 负责 68 点关键点检测和 ResNet 人脸特征向量提取两者缺一不可。dlib 的安装之所以容易翻车是因为它依赖 CMake 和 C 编译工具链在 Windows 上如果没有 Visual Studio Build Toolspip 会直接编译失败。常见做法是先用 conda 装 dlib因为 conda 有预编译好的二进制包能绕开本地编译。我一般会按这个顺序来# 第一步创建独立环境避免污染系统 Python conda create -n attendance python3.8 -y conda activate attendance # 第二步先装 dlibconda 有预编译包比 pip 稳 conda install -c conda-forge dlib -y # 第三步再装 OpenCV 和其他依赖 pip install opencv-python opencv-contrib-python numpy pillow # 第四步验证两个核心库都能正常导入 python -c import cv2; import dlib; print(cv2.__version__, dlib.__version__)这里有几个参数值得说清楚。Python 版本建议锁 3.8 或 3.9因为 dlib 和 OpenCV 在这两个版本上的预编译包最全3.11 以上经常出现某个包没有对应 wheel 的情况。opencv-contrib-python比opencv-python多了 contrib 模块虽然这套源码不一定用到但后面想加人脸对齐或更复杂的检测器时不用重装。验证那一步如果import dlib报ModuleNotFoundError说明 conda 装到了别的环境用conda list | grep dlib确认一下当前环境里到底有没有。2.2 模型文件放哪、路径怎么改源码的model/目录下有两个.dat文件这是 dlib 的预训练模型不是可执行文件不需要安装但路径必须对。shape_predictor_68_face_landmarks.dat负责把人脸映射成 68 个关键点dlib_face_recognition_resnet_model_v1.dat负责把这 68 点区域转成一个 128 维的特征向量。识别比对就是算两个 128 维向量之间的欧氏距离。我见过最常见的翻车是代码里写的是相对路径model/xxx.dat但运行时工作目录不在项目根目录导致RuntimeError: Unable to open shape_predictor。解决办法有两个要么在代码里用绝对路径拼接import os import dlib # 以当前脚本所在目录为基准拼出模型绝对路径 BASE_DIR os.path.dirname(os.path.abspath(__file__)) predictor_path os.path.join(BASE_DIR, model, shape_predictor_68_face_landmarks.dat) recognizer_path os.path.join(BASE_DIR, model, dlib_face_recognition_resnet_model_v1.dat) # 加载模型路径不对会在这里直接抛异常方便定位 predictor dlib.shape_predictor(predictor_path) face_rec dlib.face_recognition_model_v1(recognizer_path) print(模型加载完成)要么在运行前cd到项目根目录再执行python main.py。我倾向于第一种因为换机器或换 IDE 运行时不会因为工作目录变化而失效。os.path.abspath(__file__)拿到的是当前脚本的绝对路径再往上拼model目录这样无论从哪个目录启动都能找到模型文件。提示如果model/目录下只有.gitkeep没有.dat文件说明模型文件没随源码一起下载需要单独确认资源包里是否包含这两个数据文件。3. 核心链路拆解从摄像头帧到考勤记录落库3.1 人脸检测与 68 点关键点blink.py 里的活体判断逻辑blink.py这个文件名字很直白就是做眨眼检测的。为什么考勤系统要检测眨眼因为照片攻击是最低成本的作弊方式——拿一张打印的人脸照片对着摄像头纯识别算法照样能匹配成功。眨眼检测的逻辑是通过 68 点关键点拿到眼睛区域的坐标计算眼睛纵横比EAREye Aspect Ratio当 EAR 在连续几帧内先降后升就判定为一次眨眼。import dlib import numpy as np from scipy.spatial import distance as dist def eye_aspect_ratio(eye_points): # eye_points 是 6 个关键点的坐标顺序为 [左角, 上左, 上右, 右角, 下右, 下左] # 垂直距离上左到上右、下左到下右 vertical_1 dist.euclidean(eye_points[1], eye_points[5]) vertical_2 dist.euclidean(eye_points[2], eye_points[4]) # 水平距离左角到右角 horizontal dist.euclidean(eye_points[0], eye_points[3]) # EAR 公式垂直距离均值除以水平距离 ear (vertical_1 vertical_2) / (2.0 * horizontal) return ear # 眨眼判定阈值经验值 0.2~0.25低于此值认为眼睛闭合 EAR_THRESHOLD 0.22 # 连续帧数阈值避免单帧噪声误判 CONSEC_FRAMES 3这段代码的关键参数是EAR_THRESHOLD和CONSEC_FRAMES。EAR 阈值设太高正常睁眼会被判成闭眼设太低眨眼检测不出来。0.22 是一个比较通用的起点但不同摄像头焦距和分辨率下需要微调。CONSEC_FRAMES的作用是防抖只有连续 3 帧 EAR 都低于阈值才计数避免因为图像噪声导致误触发。实际调试时我一般会先把阈值打印出来观察几秒看正常睁眼时 EAR 在什么范围再定阈值。3.2 特征提取与比对128 维向量怎么算距离识别环节的核心是dlib_face_recognition_model_v1输出的 128 维特征向量。每张注册人脸存一个向量考勤时把当前帧的人脸向量和库里所有向量逐一算欧氏距离取最小距离对应的身份。距离小于阈值判定为同一人大于阈值判定为陌生人。import numpy as np def face_distance(known_encodings, face_encoding): # known_encodings: 已注册人脸的 128 维向量列表 # face_encoding: 当前帧提取的 128 维向量 if len(known_encodings) 0: return np.empty((0)) # 逐行计算欧氏距离 return np.linalg.norm(known_encodings - face_encoding, axis1) # 识别阈值经验值 0.4~0.6越小越严格 RECOGNITION_THRESHOLD 0.45 def identify_face(known_encodings, known_names, face_encoding): distances face_distance(known_encodings, face_encoding) if len(distances) 0: return unknown, 1.0 min_index np.argmin(distances) min_distance distances[min_index] if min_distance RECOGNITION_THRESHOLD: return known_names[min_index], min_distance return unknown, min_distanceRECOGNITION_THRESHOLD是整套系统里最需要调的参数。设 0.4 太严格同一个人换个角度或光线就识别不出来设 0.6 太宽松不同的人可能被认成同一个。0.45 是一个折中起点。调试方法先注册 3 到 5 个人每人拍 5 张不同角度的照片分别算类内距离同一个人不同照片之间和类间距离不同人之间取一个能分开这两组数据的值。如果类内距离普遍在 0.35 以下、类间距离普遍在 0.55 以上那 0.45 就是安全的。3.3 数据库读写handle_db.py 里的考勤记录怎么落db/handle_db.py负责考勤数据的持久化。从文件结构看data/目录下有一个.gitkeep说明数据库文件是运行时生成的不随源码提交。常见做法是用 SQLite因为不需要额外装数据库服务一个文件就能跑。import sqlite3 import os from datetime import datetime DB_PATH os.path.join(os.path.dirname(__file__), .., data, attendance.db) def init_db(): conn sqlite3.connect(DB_PATH) cursor conn.cursor() # 考勤记录表姓名、日期、时间、状态 cursor.execute( CREATE TABLE IF NOT EXISTS attendance ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, date TEXT NOT NULL, time TEXT NOT NULL, status TEXT DEFAULT 正常 ) ) conn.commit() conn.close() def mark_attendance(name): now datetime.now() date_str now.strftime(%Y-%m-%d) time_str now.strftime(%H:%M:%S) conn sqlite3.connect(DB_PATH) cursor conn.cursor() # 同一天同一人只记一次避免重复打卡 cursor.execute( SELECT id FROM attendance WHERE name? AND date?, (name, date_str) ) if cursor.fetchone() is None: cursor.execute( INSERT INTO attendance (name, date, time) VALUES (?, ?, ?), (name, date_str, time_str) ) conn.commit() print(f{name} 打卡成功 {date_str} {time_str}) else: print(f{name} 今日已打卡) conn.close()这里有一个容易忽略的点DB_PATH用了os.path.dirname(__file__)往上拼data目录而不是直接写data/attendance.db。原因是如果从项目根目录以外的位置运行脚本相对路径会指向错误的位置数据库文件会建在奇怪的地方下次运行又找不到。用绝对路径拼接能保证无论从哪运行数据库都在data/下。另外去重逻辑用的是「同一天同一人只记一次」如果需要区分上午下午签到可以把date字段改成date session的组合。4. 避坑与排查这套源码最容易翻车的五个地方4.1 摄像头打不开或画面全黑现象是cv2.VideoCapture(0)返回False或者窗口打开了但画面是黑的。原因通常是摄像头索引不对或者被其他程序占用。Windows 上有些笔记本的内置摄像头索引是 0外接 USB 摄像头是 1但顺序不固定。解决办法是先枚举可用索引import cv2 # 枚举 0~3 四个索引找到能打开的那个 for i in range(4): cap cv2.VideoCapture(i) if cap.isOpened(): ret, frame cap.read() if ret: print(f摄像头索引 {i} 可用分辨率 {frame.shape}) cap.release()如果所有索引都打不开检查是否有其他程序如腾讯会议、钉钉占用了摄像头关掉再试。4.2 识别结果全是同一个人现象是不同的人站在摄像头前系统都识别成注册库里的第一个人。原因通常是特征向量没有正确对应到姓名或者距离计算时用了错误的索引。检查known_encodings和known_names两个列表是否一一对应注册时有没有把新向量追加到列表末尾但姓名插到了前面。另一个可能是RECOGNITION_THRESHOLD设得太大导致所有人都落进第一个人的距离范围内。先把阈值降到 0.35 测试如果还是全认成同一人那就是数据对应关系错了。4.3 dlib 安装报 CMake 错误现象是pip install dlib过程中出现CMake Error或Unable to find Visual Studio。原因是 dlib 需要本地编译而 Windows 上缺少 C 编译工具链。最省事的解决办法是改用 conda 安装conda install -c conda-forge dlib。如果必须用 pip先装 Visual Studio Build Tools勾选「使用 C 的桌面开发」工作负载再装 CMake然后重试。Linux 上则是sudo apt install cmake build-essential之后再 pip 安装。4.4 考勤记录日期不对或重复写入现象是数据库里的日期是前一天或者同一个人同一天出现多条记录。日期不对通常是时区问题datetime.now()拿的是系统本地时间如果系统时区设错了就会偏。重复写入则是去重查询没生效检查SELECT语句里的date字段格式和INSERT时的date_str格式是否一致一个是%Y-%m-%d另一个如果是%Y/%m/%d就匹配不上。4.5 眨眼检测不触发或频繁误触发现象是盯着摄像头半天不记录或者正常睁眼时疯狂触发。前者是EAR_THRESHOLD设太低后者是设太高。解决办法是加一段调试代码把每帧的 EAR 值打印出来观察正常睁眼时的数值范围然后把阈值设在略低于正常睁眼值的位置。CONSEC_FRAMES如果设成 1单帧噪声就会触发建议至少 2 到 3。5. 进阶调优把识别准确率从「能演示」拉到「能交差」5.1 注册照片的质量比算法更重要很多人把精力花在调阈值上但忽略了注册照片本身的质量。dlib 的 128 维特征向量对光照和角度很敏感如果注册时只拍了一张正面强光照片考勤时侧面弱光就很容易识别失败。我一般会建议每人注册 3 到 5 张正面一张、左转 30 度一张、右转 30 度一张、稍暗环境一张。然后把这几张的特征向量取平均或者全部存入列表识别时取最小距离。取平均的做法更省事import numpy as np def register_face(face_encodings_list): # face_encodings_list: 同一个人多张照片提取的向量列表 # 取平均得到一个更鲁棒的代表向量 avg_encoding np.mean(face_encodings_list, axis0) return avg_encoding取平均能平滑掉单张照片的噪声但前提是这几张照片都是同一个人且质量过关。如果其中一张拍糊了平均后的向量反而会偏离。5.2 用距离分布来定阈值而不是拍脑袋前面说的 0.45 只是起点。真正靠谱的做法是收集一批数据算类内和类间距离分布。具体操作注册 5 个人每人 5 张照片两两算距离。同一个人不同照片之间的距离是类内距离不同人之间的距离是类间距离。把这两组数打印出来找一个能最大程度分开它们的值。距离类型样本量最小值最大值均值类内距离50 组0.280.410.34类间距离200 组0.520.780.63如果类内最大 0.41、类间最小 0.52那阈值设在 0.45 到 0.50 之间都是安全的。如果两组数据有重叠说明注册照片质量不够需要重新采集而不是硬调阈值。5.3 把识别日志写进文件方便回溯调试阶段最有用的一招是把每次识别的结果写进日志文件包括时间、识别到的姓名、距离值、是否通过阈值。这样出问题的时候不用猜直接翻日志看是哪一步偏了。config/logger.py这个文件应该就是干这个的如果没有可以自己加一个简单的import logging import os LOG_PATH os.path.join(os.path.dirname(__file__), .., data, recognition.log) logging.basicConfig( filenameLOG_PATH, levellogging.INFO, format%(asctime)s - %(message)s ) def log_recognition(name, distance, passed): status 通过 if passed else 拒绝 logging.info(f识别: {name}, 距离: {distance:.4f}, 结果: {status})距离值保留 4 位小数方便对比。如果某个人频繁被拒绝看日志里的距离值是不是刚好卡在阈值附近是的话要么调阈值要么重新注册这个人。5.4 从单机到多人的扩展边界这套源码的架构是单机单摄像头注册和识别都在同一台机器上。如果想扩展成多人同时考勤需要改两个地方一是识别循环里要支持一帧多脸dlib 的检测器本身支持返回多个人脸框但后面的特征提取和比对要改成循环处理二是数据库写入要加并发控制SQLite 在多线程写入时容易锁表可以改成每次写入单独开连接、写完就关或者换 PostgreSQL。不过对于课程设计来说单机版本已经够用扩展之前先确认需求是否真的需要。从那以后我每次拿到类似的人脸识别项目都会先把注册照片的质量和距离分布跑一遍再动阈值。这套源码的骨架是完整的能不能跑出稳定结果差别就在这些参数和细节上。希望帮到你。本文还有配套的精品资源点击获取