简介:这是一套面向高校计算机相关专业学生的人脸识别签到系统完整源码,适用于毕业设计、期末大作业与课程设计场景,对Python初学者同样友好。项目采用Python开发,结合人脸识别模型与Web界面,实现用户管理、签到记录、登录验证等核心功能,代码均附有注释,便于理解与二次修改。压缩包共27个文件,约101.47MB,包含8个py源码文件、7个html页面模板、4个dat数据文件,以及sqlite数据库、ini配置、css样式、mako模板、ttc字体与说明文档等,覆盖后端逻辑、前端展示与数据存储各环节。目前已有219人学习下载。项目经过严格调试,部署简单,下载后即可运行,界面美观、操作便捷,功能完善且管理方便,可直接作为毕设或大作业提交,具有较高的实际应用与参考价值。
1. 从一份人脸识别签到系统源码说起:毕业设计怎么做出能跑、能讲、能改的交付物
每年到了毕设季,问得最多的一类问题就是:有没有一份基于 Python 的人脸识别签到系统源码,能直接跑起来,还能在答辩时讲清楚。这个标题背后其实藏着三层需求:一是要有一套完整可运行的签到系统,二是人脸识别这条链路要真的能识别、能记录、能查询,三是代码结构要经得起老师追问。很多人拿到一份源码,装完依赖就报错,改两行就崩,最后连答辩演示都撑不过去。这篇笔记就按一线做项目的思路,把「人脸识别签到系统」从环境搭建、识别链路、签到逻辑到避坑排查完整拆一遍,适合正在做计算机毕业设计、想拿一份能改能讲的 Python 项目的同学,也适合想快速搭一个人脸签到原型的开发者。读完你至少能自己跑通一套最小可用系统,并且知道每个参数为什么这么设。
2. 人脸识别签到系统的技术选型:为什么是 Python + OpenCV + 轻量模型
2.1 签到场景对识别链路的三条硬约束
签到系统和门禁系统看起来像,实际约束差别很大。门禁追求的是「不让陌生人进」,签到追求的是「让该来的人快速留下记录」。这个差别直接决定了技术选型。
第一条约束是速度。签到往往发生在几分钟内几十上百人集中到达的场景,单次识别如果超过一秒,队伍就会堵。所以模型不能太重,检测和识别要能在普通笔记本 CPU 上跑到可接受帧率。
第二条约束是准确率和误识的平衡。签到场景里,把张三认成李四(误识)比把张三认不出来(拒识)更麻烦,因为会留下错误记录。但也不能太严,否则戴眼镜、换发型就签不上,体验很差。常见做法是把识别阈值设在 0.5 到 0.6 之间,具体要看用的特征模型。
第三条约束是可追溯。签到不是识别完就完了,要落库、要能按人按时间查、要能导出。所以系统里数据库这一环不能省,哪怕用 SQLite 也得有。
这三条约束合起来,指向的方案就是:OpenCV 做图像采集和预处理,轻量人脸检测模型做定位,人脸特征提取模型做比对,SQLite 或 MySQL 做记录。整套用 Python 串起来,依赖清晰,改起来也方便。
2.2 检测模型和识别模型要分开选
新手最容易犯的错,是把「人脸检测」和「人脸识别」当成一件事。检测是回答「画面里有没有脸、在哪」,识别是回答「这张脸是谁」。两者用的模型完全不同,选型逻辑也不同。
检测环节常见的有 Haar 级联、DNN 人脸检测器、以及一些轻量级方案。Haar 级联胜在极轻、OpenCV 自带、不需要额外下载权重,但侧脸和遮挡下容易漏检。DNN 检测器准确率高不少,代价是要加载模型文件,速度略慢。做毕设演示,如果摄像头正对、光线正常,Haar 够用;如果想稳一点,用 DNN 检测器。
识别环节,常见做法是用一个人脸特征提取模型,把每张脸转成一个固定长度的向量,然后算两个向量的距离。距离小于阈值就认为是同一个人。这里的关键是:注册时每个人要存一张或多张特征向量,签到时的向量去和库里的比。
提示:不要把识别模型和检测模型混在一个文件里下载,两者的输入尺寸和输出格式完全不同,混用是新手翻车的重灾区。
2.3 最小依赖清单与版本选择思路
依赖不是越多越好。毕设项目最怕的是环境装不上,所以依赖要精简。核心就是 OpenCV、NumPy、以及一个数据库驱动。如果用人脸特征模型,再加对应的推理库。
版本选择上,OpenCV 建议用 4.x 的较新小版本,因为 4.x 对 DNN 模块支持稳定。NumPy 注意不要装太新的版本,有些老代码用了已废弃的 API,装最新版会直接报 AttributeError。数据库如果用 SQLite,Python 标准库自带,不用额外装。
我一般会先建一个虚拟环境,把依赖写进 requirements.txt,这样换机器也能复现。虚拟环境这一步很多人跳过,结果系统 Python 里一堆包冲突,排查半天。
# 创建虚拟环境,避免污染系统 Python python -m venv venv # 激活虚拟环境(Windows) venv\Scripts\activate # 激活虚拟环境(macOS / Linux) source venv/bin/activate # 安装核心依赖,版本按实际可用调整 pip install opencv-python==4.8.1.78 pip install numpy==1.24.3 pip install opencv-contrib-python==4.8.1.78这段命令的逻辑是:先隔离环境,再装依赖。opencv-contrib-python比基础版多了一些扩展模块,人脸相关的部分功能在里面。版本号不是死的,如果装不上就换邻近小版本,但不要跨大版本乱跳。装完后用python -c "import cv2; print(cv2.__version__)"验证,能打印出版本号才算成功。
3. 从零跑通签到主流程:采集、注册、识别、落库四步走
3.1 摄像头采集与画面预处理
签到系统的入口是摄像头。采集这一步看着简单,坑却不少。首先是摄像头索引,笔记本自带的一般是 0,外接的可能是 1 或 2,选错了就是黑屏。其次是分辨率,太高会拖慢后续处理,太低人脸太小检测不到,常见做法是设成 640x480。
预处理主要是灰度化和直方图均衡。灰度化是因为很多检测器只吃灰度图,直方图均衡是为了在光线不均时提升对比度,让脸更清楚。这两步不复杂,但直接影响检测率。
import cv2 # 打开摄像头,索引 0 通常是默认设备 cap = cv2.VideoCapture(0) # 设置采集分辨率,640x480 是速度与清晰度的平衡点 cap.set(cv2.CAP_PROP_FRAME_WIDTH, 640) cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 480) while True: ret, frame = cap.read() if not ret: print("摄像头读取失败,检查索引或设备占用") break # 转灰度,减少计算量 gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) # 直方图均衡,改善光照不均 gray = cv2.equalizeHist(gray) cv2.imshow("sign-in", gray) if cv2.waitKey(1) & 0xFF == ord('q'): break cap.release() cv2.destroyAllWindows()逻辑说明:循环读帧,每帧做灰度化和均衡,然后显示。按 q 退出。参数上,CAP_PROP_FRAME_WIDTH和HEIGHT是请求值,实际能不能设成要看摄像头支持,读回来验证一下更稳。如果ret一直是 False,先查摄像头是不是被其他程序占用,比如视频会议软件没关。
3.2 人脸注册:把特征存进库
注册就是把人脸和身份绑定。流程是:采集若干张人脸图,提取特征向量,连同姓名、学号一起存进数据库。这里有个关键点:注册时最好多采几张,不同角度、不同表情,这样签到时的容错率更高。
特征提取用的是一个模型,输入是对齐后的人脸图,输出是一个向量。存的时候把向量序列化,比如转成二进制或 JSON 存进数据库的 BLOB 字段。
import cv2 import numpy as np import sqlite3 import pickle # 初始化数据库,建表 conn = sqlite3.connect("signin.db") cursor = conn.cursor() cursor.execute(""" CREATE TABLE IF NOT EXISTS users ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, student_id TEXT UNIQUE NOT NULL, feature BLOB NOT NULL ) """) conn.commit() # 假设 detector 和 recognizer 已经初始化好 def register_face(name, student_id, frame): gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) faces = detector.detectMultiScale(gray, 1.1, 5, minSize=(80, 80)) if len(faces) == 0: return False, "未检测到人脸" # 取最大的一张脸,避免背景干扰 x, y, w, h = max(faces, key=lambda r: r[2] * r[3]) face_roi = gray[y:y+h, x:x+w] face_roi = cv2.resize(face_roi, (200, 200)) # 提取特征向量 feature = recognizer.predict(face_roi) # 实际用特征提取接口 blob = pickle.dumps(feature) try: cursor.execute( "INSERT INTO users (name, student_id, feature) VALUES (?, ?, ?)", (name, student_id, blob) ) conn.commit() return True, "注册成功" except sqlite3.IntegrityError: return False, "学号已存在"逻辑说明:先建表,再检测人脸,取最大区域,缩放后提特征,序列化入库。参数上,detectMultiScale的1.1是缩放步长,越小越慢但越细;5是邻域阈值,越大越严。minSize设 80x80 是为了过滤太小的误检。学号设 UNIQUE,防止重复注册。特征用 pickle 序列化,简单直接,但注意 pickle 有安全风险,自己用没问题,别加载来路不明的文件。
3.3 签到识别:比对与阈值判定
签到时的流程是:检测人脸、提特征、和库里所有特征比距离、取最小距离、判断是否小于阈值。小于就签到成功,否则提示未注册。
距离度量常见用欧氏距离或余弦距离。欧氏距离直观,余弦距离对向量长度不敏感。用哪个取决于特征模型,一般特征模型文档会说明。阈值是核心参数,设高了误识多,设低了拒识多。
def sign_in(frame): gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) faces = detector.detectMultiScale(gray, 1.1, 5, minSize=(80, 80)) if len(faces) == 0: return None, "未检测到人脸" x, y, w, h = max(faces, key=lambda r: r[2] * r[3]) face_roi = cv2.resize(gray[y:y+h, x:x+w], (200, 200)) current_feature = recognizer.predict(face_roi) cursor.execute("SELECT id, name, student_id, feature FROM users") best_match = None best_distance = float("inf") for uid, name, sid, blob in cursor.fetchall(): stored = pickle.loads(blob) # 欧氏距离,越小越像 dist = np.linalg.norm(current_feature - stored) if dist < best_distance: best_distance = dist best_match = (uid, name, sid) # 阈值 0.6 是经验值,按实际模型调整 if best_match and best_distance < 0.6: record_signin(best_match[0]) return best_match, f"签到成功:{best_match[1]}" return None, "未注册或识别失败"逻辑说明:遍历库里所有特征,算距离,取最小。阈值 0.6 不是万能值,要拿真实数据试。如果发现同一个人经常签不上,把阈值调大;如果发现别人能签上你的名,调小。record_signin是写签到记录的函数,下面讲。
3.4 签到记录落库与查询
签到记录表要存用户 id、签到时间。时间用数据库的 CURRENT_TIMESTAMP 或 Python 的 datetime 都行。查询就是按人、按日期筛。
from datetime import datetime # 建签到记录表 cursor.execute(""" CREATE TABLE IF NOT EXISTS records ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL, sign_time TEXT NOT NULL, FOREIGN KEY (user_id) REFERENCES users(id) ) """) conn.commit() def record_signin(user_id): now = datetime.now().strftime("%Y-%m-%d %H:%M:%S") cursor.execute( "INSERT INTO records (user_id, sign_time) VALUES (?, ?)", (user_id, now) ) conn.commit() def query_records(date): cursor.execute(""" SELECT u.name, u.student_id, r.sign_time FROM records r JOIN users u ON r.user_id = u.id WHERE r.sign_time LIKE ? ORDER BY r.sign_time """, (date + "%",)) return cursor.fetchall()逻辑说明:记录表用外键关联用户表,查询时 JOIN 出姓名学号。LIKE date + "%"是按日期前缀匹配,比如传 "2024-06-01" 就能查出当天所有记录。外键约束在 SQLite 里默认不开启,要显式PRAGMA foreign_keys = ON,否则约束不生效,这点很多人不知道。
4. 避坑与排查:人脸识别签到系统最常见的五个翻车点
4.1 摄像头打开成功但画面全黑
现象:cap.read()返回 True,但显示出来是黑屏。
原因:摄像头索引选错,或者摄像头被其他程序占用,或者驱动问题。有些笔记本有多个摄像头,0 不一定是你要的那个。
解决:先遍历索引 0 到 3,看哪个能读到非黑帧。再检查有没有其他程序在用摄像头。Linux 下可以用ls /dev/video*看设备。
4.2 检测不到人脸,但画面里明明有脸
现象:detectMultiScale返回空列表。
原因:光线太暗或太亮、人脸太小、检测器参数太严、或者图像没转灰度。
解决:先确认传进去的是灰度图。再把minSize调小,scaleFactor调小到 1.05,minNeighbors调到 3。如果还不行,换 DNN 检测器。光线问题就加个补光灯,或者做直方图均衡。
4.3 同一个人签到经常失败
现象:注册了,但签到十次有五次认不出。
原因:阈值设太严、注册时采的样本太少、或者签到时光线和注册时差别太大。
解决:注册时多采几张,至少三张不同角度。阈值从 0.6 往上试到 0.7。如果还是不稳,考虑换特征模型,或者加人脸对齐步骤。
4.4 数据库写入报 UNIQUE 约束错误
现象:注册时提示学号已存在,但明明是新用户。
原因:之前测试时插过同一个学号,或者学号字段有空格导致看起来不同实际相同。
解决:注册前先查一下学号是否存在,存在就提示更新而不是插入。学号入库前 strip 一下去空格。
4.5 程序跑一会儿就卡死
现象:运行几分钟后画面卡住,或者内存一直涨。
原因:摄像头没释放、数据库连接没关、或者每帧都在做重计算没做节流。
解决:确保退出时cap.release()和conn.close()。识别不用每帧都做,可以隔几帧做一次,中间帧只显示。数据库操作用完就提交,别攒着。
5. 让签到系统更稳的几个进阶技巧
5.1 用多帧投票降低误识
单帧识别容易受表情、角度影响。一个简单有效的改进是:连续采 5 帧,每帧都识别,取出现次数最多的结果作为最终判定。这样偶尔一帧认错不会影响整体。代价是响应慢一点,但签到场景完全能接受。
from collections import Counter def sign_in_stable(cap, frames=5): results = [] for _ in range(frames): ret, frame = cap.read() if not ret: continue match, msg = sign_in(frame) if match: results.append(match[0]) if not results: return None, "多次识别均失败" # 取出现次数最多的用户 id most_common = Counter(results).most_common(1)[0][0] return most_common, "签到成功"逻辑说明:采多帧,每帧调一次识别,把成功的用户 id 收集起来,取众数。参数frames设 5 比较平衡,设 10 更稳但更慢。这个方法对「偶尔认错」特别有效,因为错误结果通常不会连续出现。
5.2 阈值不是固定的,按场景调
0.6 只是起点。实际项目里我会准备一组测试样本,正样本(同一个人不同照片)和负样本(不同人),然后画一条距离分布曲线,找一个让正样本尽量小于阈值、负样本尽量大于阈值的点。这个过程叫阈值标定,毕设答辩时能讲出来是加分项。
| 场景 | 建议阈值 | 说明 |
|---|---|---|
| 光线稳定、正脸 | 0.5 | 严一点,减少误识 |
| 光线多变、有角度 | 0.65 | 松一点,保证通过率 |
| 多人快速签到 | 0.6 | 平衡值,配合多帧投票 |
5.3 把识别和界面分开,方便改
很多毕设代码把识别逻辑和界面写在一个文件里,改一处牵动全身。我一般会把识别封装成一个类,界面只调类的方法。这样换界面、换模型都不影响主逻辑。答辩时老师问「如果换一个识别模型要改哪里」,你能答「只改类里的特征提取方法」,印象分就上去了。
class FaceSignIn: def __init__(self, db_path="signin.db"): self.conn = sqlite3.connect(db_path) self.detector = cv2.CascadeClassifier( cv2.data.haarcascades + "haarcascade_frontalface_default.xml" ) def extract_feature(self, face_roi): # 这里可以替换成任意特征提取实现 return face_roi.flatten().astype(np.float32) / 255.0 def register(self, name, sid, frame): ... def sign_in(self, frame): ...逻辑说明:把数据库连接、检测器、特征提取都收进类里。extract_feature是唯一需要改的地方,换模型只动这里。这种结构在答辩时很好讲,也方便后续加功能,比如加个导出 Excel 的方法。
5.4 答辩前一定要做的三件事
第一,准备一份离线可跑的版本。答辩现场网络不一定好,依赖提前装好,数据库提前塞几条测试数据。第二,准备一段失败演示。老师常问「如果识别不出来怎么办」,你现场遮一下脸,展示提示「未注册或识别失败」,比空口说更有说服力。第三,把阈值和参数记在纸上。被问到「这个 0.6 怎么来的」,能说出是测出来的,不是抄的。
我自己做第一个版本时,就是没做多帧投票,答辩演示时正好赶上光线变化,连续两次认错,场面很尴尬。后来加了投票和阈值标定,稳了很多。希望帮到你。
本文还有配套的精品资源,点击获取