☰
Python人脸识别签到系统源码解析:特征向量、SQLite考勤与避坑指南
2026/10/11 21:32:10 网站建设 项目流程

简介:基于Python的人脸识别签到系统源码,面向计算机专业毕业生、课程设计学生以及需要快速落地人脸识别应用的开发者,既可作为毕业设计直接使用,也适合参考二次开发。资源共27个文件,以8个Python脚本、7个HTML页面、SQLite数据库为核心,配套配置文件、字体文件与说明文档,整体约101.47MB;其中HTML页面覆盖登录、用户编辑、后台管理等信息展示,Python脚本负责路由控制、人脸特征提取与识别判断,目录按Flask框架分层,便于定位与修改。已有219人浏览学习,项目经过严格调试,下载后简单部署即可运行。压缩包内代码附有注释,新手也能看懂关键逻辑,系统功能完整,包括用户管理、人脸录入、签到识别等流程,界面简洁易操作。这套源码具备较高的完成度与工程实用性,适合作为高分毕业设计、期末大作业的参考模板。

1. 基于 python 人脸识别签到系统源码.zip:这个包到底能帮你省多少事

把一个人脸识别签到系统的源码包解开、跑通、最后顺利通过毕业设计答辩,这个过程里最容易翻车的往往不是模型精度,而是你对“这套系统是给谁用的”这件事的理解。基于 python 人脸识别签到系统源码.zip 这类项目,本质上是一条完整链路:摄像头抓帧,人脸检测,提取成 128 维特征向量,与注册库比对,命中后把姓名和当前时间写入 SQLite 签到表。它能替掉的,是传统的按名单点名和手动登记;它要面对的,是光线、角度、遮挡、重复签到这些被 demo 忽略的脏现实。适合做毕设的学生,也适合想快速搭一套内部考勤 Demo 的工程师。这份笔记就顺着这条链路,把代码、参数和坑一次讲完。

2. 人脸识别签到的技术选型:从 128 维特征向量到 SQLite 签到表

市面上能下到的基于 python 的人脸识别签到系统源码包,差别往往不在界面,而在识别层用的是哪条技术路线。这个决定会影响后面所有代码的写法,所以第一步不是写代码,而是把方案定下来。我见过太多人把一整套 demo 跑通了才发现识别库选错,重新返工比从零写还痛苦。

2.1 识别方案怎么选:face_recognition、dlib 还是 OpenCV + LBPH

常见做法有三条路线,各有各的适用场景。

第一条是用face_recognition,这也是我最推荐给毕设场景的方案。这个库封装了 dlib 的人脸检测和特征提取模型,使用者不需要关心人脸识别图像进入神经网络到输出高维度向量的过程具体怎么实现,只需要调用接口拿结果。它输出的特征就是一个 128 维的浮点向量,注册时把这个人所有照片的特征存进本地文件,识别时拿摄像头实时算出来的向量跟库里所有向量做欧氏距离比较,距离小于阈值就算命中。

第二条是直接用 dlib 写。能拿到更底层的控制权,比如自己加载模型、自己调度 GPU,但代价是要自己处理很多细节,光是一个模型的下载和序列化格式就够折腾半天。除非你要做深度学习方向的课题,否则不建议在签到系统里直接碰 dlib。

第三条是 OpenCV 自带的 LBPH 人脸识别器。它的特点是训练快、纯 CPU 也能跑,但识别率跟深度模型差一个量级,换个发型、戴个眼镜就可能翻车。它不是不能用,而是更适合放在论文里作为“传统方法与深度方法的对比实验”,用来衬托主方案的优越性。

另外还有一种思路是接在线人脸识别 API,包括 easyai 人脸识别那一类平台。这类 API 的识别率确实高,但它要求把注册人脸照片传到服务端,校园场景里涉及个人信息出境和数据隐私问题,答辩时很容易被老师追问。本地方案虽然精度略低,但整个流程完全可控,我的选择是face_recognition + OpenCV 采集,几乎不写模型代码,把精力花在签到业务和界面这些真正能给分的地方。

2.2 识别链路的五个环节与模块边界

不管源码包长什么样,一套能干活的人脸签到系统在逻辑上必然包含五个环节,这也是你拿到任何一份源码后应该最先找出来的五块。

第一是注册环节。摄像头采集用户若干帧人脸图像,经过检测和特征提取,把这个人对应的 128 维特征向量序列化到本地特征库文件里。这个文件后续被识别环节反复加载,相当于系统的“底库”。

第二是识别环节。摄像头实时读帧,先做人脸检测,再对检测框内的人脸做特征提取,得到当前帧的特征向量,然后用这个向量跟底库里所有向量算相似度。这个环节直接决定用户体验,也是性能瓶颈所在。

第三是签到环节。识别命中后,把姓名和当前时间写入数据库。这里的核心约束是一个人在同一天只能签到一次,不能刷脸一次写一条记录,否则考勤数据就废了。

第四是展示环节。用户需要看到摄像头画面、当前识别结果、当天已签到名单。毕业设计里最常见的实现是用 PyQt5 或 Tkinter 做桌面界面,把识别结果实时渲染到界面上。

第五是配置环节。识别阈值、摄像头编号、识别频率这些参数应该从配置文件读取,而不是写死在代码里。这看起来是个小点,但换机器调试时能省大量时间。

把五个环节拆清楚之后,你会发现源码包的核心价值不在某一个环节,而在它们之间的数据格式约定:注册时存的是什么格式,识别时必须读取同一种格式;识别结果怎么转换成签到记录,签到记录又怎么被界面展示。顺着这条数据流去读代码,比从头到尾逐行读要高效得多。

2.3 三个必调参数:tolerance、model、num_jitters

识别效果好不好,很多时候不是模型的锅,而是参数没调对。face_recognition 库里有三个参数是签到系统必须理解的,我把它们列成一张表。

参数含义常见取值调整方向
tolerance特征距离阈值,判定“是不是同一个人”的界限0.5 到 0.6总是识别失败就调大,总是误识别就调小
model人脸检测模型hog 或 cnnCPU 机器用 hog,有 NVIDIA GPU 再考虑 cnn
num_jitters提取特征时的重采样次数注册时 10 到 100,识别时保持 1数值越大特征越稳定,但耗时线性增加

tolerance是最容易被忽略的一个参数。face_recognition 的compare_faces默认值是 0.6,这个值在光线稳定的办公室环境通常够用,但教室、走廊这些环境变化大的场景,0.6 会出现陌生人误判为熟人的情况。我的习惯是先按 0.6 跑一版,统计误识别率,再往 0.4 到 0.5 方向去压,一步一个脚印调到刚好够用。

model参数很多人默认用 hog,这没错。cnn 模型在 CPU 上检测一帧要几百毫秒,直接让实时签到变成幻灯片。只有当你确实有一块可用的 NVIDIA GPU,并且把识别丢到独立线程里跑时,cnn 才值得考虑。

num_jitters的使用场景比较特殊。识别的时候必须保持 1,因为实时视频流里每秒要处理好几帧,多了扛不住。但在注册环节,我会把它调到 10 以上,让算法对同一张人脸图像做多次重采样再取平均特征,这样得到的底库特征更能抵抗光照和角度的变化,属于花一次时间让后面每天都省心的做法。

3. 把核心模块跑通:人脸注册、实时识别与 SQLite 签到的 Python 实现

方案定了,接下来就是动手写代码。这一章的三个模块是人脸签到系统的骨架,分别对应注册、识别、签到三个环节。我会给出可直接运行的代码,并说明每一段为什么这么写。

3.1 人脸注册与特征序列化:从摄像头帧到本地特征库

注册模块的输入是摄像头画面,输出是一个包含用户名和特征向量的本地文件。这里我选择用 pickle 序列化,因为它能把 Python 的 dict 结构原样存下来,读回来也不需要额外解析,对毕设规模的项目足够简单。

import os import pickle import cv2 import face_recognition import numpy as np DB_PATH = "encodings.pkl" def register_user(name: str): cap = cv2.VideoCapture(0) if not cap.isOpened(): raise RuntimeError("摄像头打不开,检查编号或占用情况") frames = [] print("按空格键抓取人脸,抓满 3 帧后自动注册,按 q 退出") while len(frames) < 3: ok, frame = cap.read() if not ok: continue cv2.imshow("register", frame) key = cv2.waitKey(200) if key == ord(" "): frames.append(frame) elif key == ord("q"): break cap.release() cv2.destroyAllWindows() if len(frames) < 3: raise RuntimeError("采集帧数不足,请重新运行") encodings = [] for frame in frames: face_encs = face_recognition.face_encodings(frame, num_jitters=10) if face_encs: encodings.append(face_encs[0]) if not encodings: raise RuntimeError("这 3 帧里没有检测到人脸,换个光线条件再试") # 多帧特征取平均,降低单帧模糊或侧脸带来的偏差 avg_enc = np.mean(encodings, axis=0) data = {} if os.path.exists(DB_PATH): with open(DB_PATH, "rb") as f: data = pickle.load(f) data[name] = avg_enc with open(DB_PATH, "wb") as f: pickle.dump(data, f) print(f"注册成功:{name},特征维度 {avg_enc.shape}")

这段代码里值得注意的有三点。第一,face_encodings返回的是一个列表,因为一张画面里可能有多张人脸,这里只取列表第一个元素,也就是画面里最靠前的那个人脸,注册场景下通常就是摄像头前的用户本人。第二,把三帧的特征做np.mean取平均,是因为单帧可能碰巧模糊或者角度偏转,平均之后特征更接近这个人的典型外观。第三,特征库文件如果已存在就加载后追加,而不是直接覆盖,这样多次注册不同用户时不会把前面的人弄丢。

注册时我一般会让用户稍微转动头部,让三帧分别覆盖正面、微左、微右三个角度。这个操作不改变代码,但能让底库特征更鲁棒,属于零成本换识别率的细节。

3.2 实时识别签到:识别线程里最容易卡住的一帧

识别模块要同时解决两个问题:能不能认出来,以及认出来的是谁。下面的函数输入一帧摄像头画面,输出人脸框位置和对应的姓名。

def recognize(known_names, known_encodings, frame): # 缩小帧加速:识别不需要全分辨率,640 宽足够 small = cv2.resize(frame, (0, 0), fx=0.5, fy=0.5) rgb = cv2.cvtColor(small, cv2.COLOR_BGR2RGB) # hog 模型对 CPU 友好,upsample=1 提高小脸检测率 boxes = face_recognition.face_locations( rgb, model="hog", number_of_times_to_upsample=1 ) encs = face_recognition.face_encodings(rgb, boxes, num_jitters=1) results = [] for enc in encs: # tolerance 越小越严格,签到场景从 0.5 起步调 matches = face_recognition.compare_faces(known_encodings, enc, tolerance=0.5) dists = face_recognition.face_distance(known_encodings, enc) best = int(np.argmin(dists)) if matches[best]: results.append(known_names[best]) else: results.append("unknown") return boxes, results

这段代码有一个特别容易踩的坑:颜色空间。face_recognition内部处理的图像是 RGB 顺序,而cv2.imread读出来的是 BGR,如果你直接把 OpenCV 的帧丢给 face_recognition,检测结果会时好时坏。我在这里先cv2.cvtColor转成 RGB,再往下走。

另一个细节是缩小帧。cv2.resize把帧缩到一半,识别耗时能砍掉一大截,而识别精度几乎不受影响,因为人脸检测只需要几十像素宽就能定位。实际使用中如果机器性能差,我还会把 frame 宽度直接限制到 480,代价是稍远距离的人脸识别率下降。

识别结果里我用face_distance算出距离最小的那条记录,再用compare_faces判断这个人是否在底库里。之所以不直接用compare_faces的返回值,是因为它返回一个布尔列表,当底库人数多了以后,你很难分辨“完全陌生”和“距离刚好卡在阈值边缘”这两种情况。先取最小距离,再跟阈值比较,逻辑更直观。

3.3 把签到写进 SQLite:表结构与唯一约束

识别命中之后,很多人会直接往数据库里插一条记录,这种做法在演示时没问题,但一旦画面中的人在镜头前停留超过一秒钟,签到表就会被刷出好几行。解决这个问题不是在代码里加一堆 if 判断,而是从表结构上就堵死重复。

CREATE TABLE IF NOT EXISTS attendances ( id INTEGER PRIMARY KEY AUTOINCREMENT, user TEXT NOT NULL, date TEXT NOT NULL, time TEXT NOT NULL, UNIQUE(user, date) );

UNIQUE(user, date)是这个表的灵魂。它让“同一用户同一天只能有一条记录”变成数据库层面的约束,不管代码里有多少线程、多少逻辑分支在写库,数据库都会拒绝第二条同用户同日期的记录。

对应 Python 侧写入时,用INSERT OR IGNORE而不是普通INSERT:

import sqlite3 from datetime import datetime def sign_in(user: str): conn = sqlite3.connect("attendances.db") today = datetime.now().strftime("%Y-%m-%d") now = datetime.now().strftime("%H:%M:%S") sql = "INSERT OR IGNORE INTO attendances (user, date, time) VALUES (?, ?, ?)" conn.execute(sql, (user, today, now)) conn.commit() conn.close()

INSERT OR IGNORE的行为是:如果这条记录因为唯一约束被拒绝,SQLite 不会报错,只是默默跳过。配合唯一约束之后,签到逻辑就不需要在代码里先查一遍“今天有没有签过”,既省了一次查询,又避免了并发场景下的竞态条件。如果做考勤系统还想要“迟到”状态,只需要再加一个status字段,在写入时按当前时间判断是否晚于上课时间。

4. 用 PyQt5 把签到系统包成毕业设计能交的界面:线程分离、配置项与导出结果

命令行版的签到系统已经能跑,但毕业设计需要界面。PyQt5 是这类项目最常见的界面方案,原因很实际:控件齐全、文档多、跟 OpenCV 的兼容性好。这一章只讲界面工程里真正影响成败的三个点。

4.1 为什么摄像头识别必须放在独立线程

如果你把while True读摄像头这件事直接塞进 PyQt 主窗口里,程序打开后会卡死:窗口无法拖动,按钮点了没反应,整个界面像死机一样。原因很简单,PyQt 的事件循环被摄像头读帧阻塞了。

正确做法是把识别逻辑放进QThread,通过信号把结果传回主线程更新界面。PyQt 的界面控件只能在主线程操作,子线程里直接修改控件文本轻则警告,重则闪退。

from PyQt5.QtCore import QThread, pyqtSignal import cv2 import numpy as np from datetime import datetime class RecognizeThread(QThread): signed = pyqtSignal(str, str) # 姓名, 时间 frame_ready = pyqtSignal(object) # 处理后待显示的帧 def __init__(self, cfg): super().__init__() self.cfg = cfg self._running = True def stop(self): self._running = False self.wait() def run(self): cap = cv2.VideoCapture(self.cfg["camera_id"]) frame_idx = 0 while self._running and cap.isOpened(): ok, frame = cap.read() if not ok: continue frame_idx += 1 # 每隔 N 帧做一次识别,其余帧只做显示,大幅降 CPU 占用 if frame_idx % self.cfg["recognize_every"] != 0: self.frame_ready.emit(frame) continue # 这里调用 3.2 节的 recognize 函数 boxes, names = recognize( self.cfg["known_names"], self.cfg["known_encodings"], frame ) for box, name in zip(boxes, names): if name != "unknown": self.signed.emit(name, datetime.now().strftime("%H:%M:%S")) self.frame_ready.emit(frame) cap.release()

注意signed信号只在识别命中时发射,frame_ready信号每帧都发射用于刷新画面。把识别频率和控制台版本的实时识别隔离开,是让界面保持流畅的关键。recognize_every设成 3,意味着每秒大约只做 5 次识别,肉眼几乎感知不到差别,CPU 占用却能降到原来的三分之一。

4.2 配置模块:把阈值、摄像头编号和模型选型交给配置文件

写死的参数是毕业设计的大忌。答辩老师只要问一句“换一台摄像头要改哪行代码”,如果你说要去源码里找,分就没了。把所有运行参数收敛到一个 JSON 文件里,既方便自己调试,也是工程化水平的体现。

{ "camera_id": 0, "frame_width": 640, "recognize_every": 3, "tolerance": 0.5, "num_jitters": 10, "database": "attendances.db", "encodings": "encodings.pkl", "model": "hog" }

启动时读这个文件,全局只保留一份配置对象:

import json def load_config(path="config.json"): with open(path, "r", encoding="utf-8") as f: cfg = json.load(f) return cfg

这张表给出配置项的实际作用,方便你按自己的机器调整。

配置键作用我常用的值
camera_id选择电脑的哪个摄像头笔记本内置通常是 0,外接 USB 摄像头可能是 1
frame_width处理前把帧缩到多少宽640 平衡精度与速度
recognize_every每几帧做一次识别3,视频流畅度无明显下降
tolerance识别距离阈值0.5 起步,误识别多就往下调

配置项外置还有一个好处:换机器调试时,不用动一行代码就能把摄像头编号和识别频率调好。这是我在多台笔记本上演示这个项目后的血泪经验。

4.3 出勤统计与导出 CSV:把签到表变成答辩素材

界面这部分,签到列表用QTableWidget实时刷新即可。每当signed信号到来,主线程往表格里插入一行,或者更新当天的记录。为了让数据能带出教室、导出给评委看,我把导出 CSV 的功能也做上了。

import csv def export_csv(records, path): with open(path, "w", newline="", encoding="utf-8-sig") as f: writer = csv.writer(f) writer.writerow(["姓名", "日期", "签到时间"]) writer.writerows(records)

utf-8-sig编码是给 Excel 留的后路。用纯utf-8导出的 CSV 在中文版 Excel 里打开会是乱码,加一个 BOM 头就好。这个细节很多源码包都没处理好,做到位了反而能成为答辩时的加分小亮点。

5. 运行期避坑排查:dlib 编译、摄像头黑屏、识别率玄学的五类高频问题

把一套人脸识别签到系统从“代码写完”推到“能稳定演示”,中间隔着五类高频问题。这一章直接按现象、原因、解决来写,每一条都是真实环境里反复出现过的问题。

5.1 Windows 上安装 dlib 卡在编译阶段

现象:pip install face_recognition时终端刷一堆 C++ 报错,最后失败;或者在pip install dlib时长时间卡在构建进度条上。

原因:dlib 在 Windows 上默认需要本地编译,系统缺 C++ 编译器和 CMake,或者当前 Python 版本太新,没有对应的预编译包。

解决:在命令行依次执行pip install cmake,然后安装 Visual Studio Build Tools,勾选“使用 C++ 的桌面开发”工作负载。装完之后再执行pip install dlib和pip install face_recognition。另一个更省事的办法是换 Python 3.8 或 3.9,这两个版本在 Windows 上最常能找到预编译好的 wheel 包,直接从 pip 拉下来就能用。安装完成后一定要先执行python -c "import dlib"验证能导入,再往下跑。

5.2 摄像头打不开或画面全黑

现象:VideoCapture(0)不报错,但read()一直返回 False;或者画面是黑色,程序当作没检测到人脸。

原因:笔记本内置摄像头不一定是设备 0,外接摄像头和虚拟摄像头软件会占用其他编号;也可能是摄像头被浏览器或其他软件抢占,权限被系统拦截。

解决:先把启动脚本里的摄像头编号改成 1、2 逐个试,多数情况下内置和外接混用时会遇到编号漂移。代码里加一段启动自检,cap.isOpened()为 False 时立即打印提示,而不是静默黑屏继续跑。清理后台占用摄像头的软件后重试。

5.3 识别率“今天行明天不行”的玄学问题

现象:同一个用户,白天识别正常,傍晚逆光就变成 unknown;或者同一台机器换个房间,识别成功率断崖式下跌。

原因:特征向量对光照敏感,注册时如果只在某一个固定光线下采集了三帧,底库特征就只代表这一种光照条件。逆光、顶光、侧脸都会让实时特征偏离底库。

解决:注册时让人脸在画面里缓慢左右转头,采集的 3 帧覆盖不同角度;最好在不同时段各注册一次,用多次注册的平均特征合并到底库里。tolerance 从 0.6 往下压到 0.5 的过程中,每次只调 0.05,不要一次跨太大。这个问题的核心是底库样本多样性,属于数据问题,不是模型问题。

5.4 一天签了无数次,签到表重复刷屏

现象:人站在摄像头前两秒钟,数据库里多了五六条同一个人同一天的时间记录。

原因:识别线程每帧都在识别,每帧都会调用签到函数;如果没有唯一约束,日志表里自然全是重复记录。

解决:按 3.3 节给attendances表加上UNIQUE(user, date),写入时用INSERT OR IGNORE。同时在程序内存里维护一个当日已签到集合,命中后直接跳过,减少无意义的数据库写入。两层防护下,即使识别线程有并发问题也不会污染数据。

5.5 CPU 占用过高,画面掉到个位数帧率

现象:程序能跑,但视频画面像幻灯片,电脑风扇狂转,CPU 占用接近满格。

原因:每一帧都做了完整的人脸检测和特征提取,而这两个操作都是计算密集型的。连续帧之间的画面高度相似,逐帧识别的收益很低,浪费却很大。

解决:使用配置里的recognize_every跳帧,每 3 帧处理一次;把帧宽度缩到 640 以下;如果有条件,把识别线程的优先级调低,保证界面渲染不被抢资源。做完这三步之后,帧率通常能从个位数回到 20 帧以上,识别成功率几乎不受影响。

6. 交付前过一遍这四个验证点:换机重跑、冷启动、阈值回退与导出结果

代码写完、界面跑通,不代表项目已经能答辩。我吃过一次亏:把识别阈值调到 0.35,在自己的工位上完美识别,答辩前一天换到教室,全场 unknown。后来定位到问题是注册样本太单一,但那种临场翻车的滋味不想再体验第二次。所以现在每次交付前,我都会花一个小时过四个验证点,当成一张最小的验收清单。

第一是冷启动验证。删掉encodings.pkl和数据库文件,从零开始注册一个新用户,再启动签到识别。这个动作确认系统不依赖历史遗留数据,评委拿到手上从第一天就能独立跑起来。第二是换机重跑验证。在另一台机器上按依赖清单重新安装环境,确认 dlib 能编译通过、摄像头编号可配置。这一步是最耗时的,但也最能暴露环境依赖隐患。第三是阈值回退验证。识别错误时把当前帧保存到 debug 目录,同时打印出最小距离对应的姓名和距离值,用数据判断是光照问题还是阈值问题,而不是凭感觉调参。第四是导出验证。签到几条记录后点导出按钮,打开 CSV 确认中文不乱码、时间字段正确。

python -m pip install -r requirements.txt python main.py

我一般把系统拆成main.py作为唯一入口,一行命令拉起注册和签到两种模式,配合配置文件切换。这样不管谁拿到手,都能在五分钟内从安装走到第一次签到成功。这一行命令背后,是你对整套系统可靠性的判断:它能不能在别人手里不出岔子地跑起来。提前用这份清单过一遍,比答辩现场手忙脚乱强十倍,希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询