☰
Python+深度学习人脸识别签到系统:从技术选型到工程闭环的毕设实战
2026/9/28 2:05:44 网站建设 项目流程

简介:这是一套面向计算机相关专业毕业设计场景的深度学习人脸识别签到系统完整源码包,适合正在准备毕设、课程设计或期末大作业的学生,以及需要项目实战练习的开发者。项目经导师指导并获评审99分,代码完整可运行,对新手较为友好。压缩包共27个文件,约101.48MB,以Python脚本、HTML页面、数据文件为主,另含数据库、配置文件、样式表与说明文档,覆盖模型加载、人脸识别、用户管理、签到记录等模块,目录结构清晰。目前已有159人学习下载。读者可获得一套可直接运行的高分毕设方案,包括完整源码、数据库文件、前端模板与README说明,便于快速理解系统架构、复现识别流程,并在此基础上进行二次开发或撰写论文,节省从零搭建的时间成本。

1. 从一张签到表到一套人脸识别系统:毕业设计选题的落地边界

每年到了毕设开题季,计算机、电子信息、自动化几个专业的学生都会撞上同一个尴尬:想做一个“有深度学习味道”的题目,又怕模型跑不起来、系统拼不完整、答辩时被老师追问“你这人脸识别到底怎么识别的”。人脸识别签到系统恰好卡在一个甜点位上——它足够具体,能在一台普通笔记本上跑通;又足够完整,从数据采集、模型推理到业务落库,每一层都有东西可讲。我带过几届本科毕设,也帮同事看过不少“高分毕设”源码包,发现真正拉开差距的从来不是模型多深,而是工程闭环有没有做扎实:摄像头能不能稳定取帧、识别结果能不能可靠地写进签到记录、断网或光线变化时系统会不会直接崩掉。这篇笔记就围绕“Python + 深度学习 + 人脸识别 + 签到系统”这条主线,把选题里最常被含糊带过的环节拆开讲清楚。适合正在做毕设、需要一套能演示、能写论文、能经得起追问方案的同学,也适合想快速搭一个内部签到原型的工程师。

2. 人脸识别签到系统的技术选型:为什么不是越深越好

2.1 检测、对齐、识别三段式,哪一段最值得花时间

人脸识别签到系统在工程上通常拆成三个独立阶段:人脸检测(找到画面里有没有脸、在哪)、人脸对齐(把歪头、侧脸摆正到标准姿态)、人脸识别(把对齐后的人脸映射成特征向量并比对)。很多同学一上来就想自己训练一个端到端模型,结果卡在数据量和算力上,最后连演示都跑不起来。更稳妥的做法是:检测和对齐直接用成熟的开源库,把精力放在识别阈值的调优和签到业务逻辑上。

常见做法是检测用 MTCNN 或 RetinaFace,对齐用五点关键点做仿射变换,识别用 FaceNet 或 ArcFace 提取 512 维特征。这个组合在普通 CPU 上单帧推理大约 200 到 500 毫秒,完全够签到场景用。如果你非要用 YOLO 系列做人脸检测,也不是不行,但 YOLO 的通用权重对人脸小目标召回率一般,需要自己微调,对毕设来说性价比不高。

提示:毕设答辩时老师最常问的是“你为什么选这个模型”。答案不要只说“因为准”,要落到你的场景约束:签到是 1:N 比对,底库通常几十到几百人,要求误识率低而不是追求极致速度。

2.2 底库规模决定你该用分类还是度量学习

这是选型里最容易翻车的地方。如果你的签到系统只服务一个班级 30 个人,完全可以把识别当成 30 分类问题,用 Softmax 训练一个小 CNN,简单直接。但一旦人数上百,或者要求“新员工入职当天就能签到”,分类模型就得重新训练,这在毕设演示里是灾难。度量学习(Metric Learning)路线,也就是训练模型输出特征向量、用余弦距离比对,优势就在这里:新增人员只需要录入几张照片生成特征入库,不用动模型。

我一般会建议毕设采用“预训练 FaceNet + 余弦相似度阈值”的方案。阈值设多少?在 LFW 这类标准集上,余弦相似度阈值 0.6 左右能取得较好的平衡,但你的实际场景受摄像头质量、光照影响很大,必须自己采一组正负样本对来画 ROC 曲线定阈值。别直接抄网上的 0.6,那是别人的场景。

2.3 签到业务层:别让“识别成功”等于“签到成功”

很多源码包把识别和签到写成一件事,识别到人脸就直接插一条记录。这在演示时看着流畅,实际全是坑:同一个人站在摄像头前 3 秒,会插入几十条重复记录;识别错了人,签到记录就错了,还没有补救机制。正确的做法是把识别和签到解耦:识别模块只负责输出“当前画面里是谁、置信度多少”,签到模块负责去重、时间窗口判断、异常标记。

一个可用的签到状态机大致是:检测到人脸 → 识别出身份 → 判断该身份在最近 N 秒内是否已签到 → 未签到则写入记录并标记时间 → 已签到则忽略。N 一般取 5 到 10 秒,太短会重复,太长会漏掉快速连续签到的人。这个逻辑不复杂,但它是区分“能演示”和“能写进论文”的关键。

3. 用 Python 把识别流程跑通:从取帧到特征比对的完整代码

3.1 环境准备与依赖安装的版本坑

先明确一点:深度学习环境最怕版本冲突。我见过太多人卡在numpy和tensorflow版本不匹配上,报一堆看不懂的错。毕设场景建议用 Python 3.8 到 3.10,太新的版本有些老库还没适配。下面这套依赖是我在多个机器上验证过能跑通的组合,用requirements.txt管理。

# 创建虚拟环境,避免污染系统 Python python -m venv venv # Windows 激活 venv\Scripts\activate # Linux/Mac 激活 source venv/bin/activate # 安装核心依赖,指定版本避免冲突 pip install opencv-python==4.8.1.78 pip install tensorflow==2.13.0 pip install keras-facenet==0.3.1 pip install numpy==1.24.3 pip install pillow==10.0.1

这里keras-facenet封装了 FaceNet 的预训练权重加载和特征提取,省去自己写网络结构的时间。opencv-python负责摄像头取帧和图像预处理。tensorflow作为后端。注意numpy不要装 2.x,很多老库还不兼容。

注意:如果你用的是 Apple Silicon 的 Mac,tensorflow安装方式不同,需要装tensorflow-macos,但keras-facenet在 ARM 上可能有兼容问题,建议毕设演示用 x86 机器或 Windows 笔记本。

3.2 摄像头取帧与人脸检测的最小可用循环

下面这段代码是整个系统的入口:打开摄像头,逐帧检测人脸,把检测到的人脸区域裁剪出来备用。关键参数是scaleFactor和minNeighbors,前者控制每次图像缩小的比例,后者控制检测框的严格程度。

import cv2 # 加载 OpenCV 自带的人脸检测器,适合快速验证 face_cascade = cv2.CascadeClassifier( cv2.data.haarcascades + 'haarcascade_frontalface_default.xml' ) cap = cv2.VideoCapture(0) # 0 表示默认摄像头 if not cap.isOpened(): raise RuntimeError("摄像头打开失败,检查是否被其他程序占用") while True: ret, frame = cap.read() if not ret: break # 转灰度可以加速检测,Haar 特征在灰度图上工作 gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) # scaleFactor=1.1 表示每次缩小 10%,minNeighbors=5 过滤误检 faces = face_cascade.detectMultiScale( gray, scaleFactor=1.1, minNeighbors=5, minSize=(80, 80) ) for (x, y, w, h) in faces: # 画框并裁剪人脸区域,后续送入识别模型 cv2.rectangle(frame, (x, y), (x + w, y + h), (0, 255, 0), 2) face_roi = frame[y:y + h, x:x + w] cv2.imshow('Face Detection', frame) if cv2.waitKey(1) & 0xFF == ord('q'): break cap.release() cv2.destroyAllWindows()

scaleFactor设太小(比如 1.01)检测慢但漏检少,设太大(比如 1.5)快但容易漏。minNeighbors越大误检越少,但太小的人脸可能被过滤掉。minSize根据你的摄像头分辨率调,1080P 下 80x80 是合理起点。Haar 检测器在正脸、光照均匀时够用,但侧脸和暗光下召回率明显下降,这也是为什么正式方案建议换 MTCNN。

3.3 用 FaceNet 提取特征并做 1:N 比对

检测到人脸后,下一步是提取特征向量。keras-facenet的FaceNet类封装了模型加载和embeddings方法,输入是 RGB 图像数组,输出是 128 维或 512 维向量(取决于权重)。下面代码演示如何把一张人脸转成特征,并与底库比对。

from keras_facenet import FaceNet import numpy as np import cv2 # 加载预训练模型,第一次运行会下载权重 embedder = FaceNet() def get_embedding(face_img): """输入 BGR 人脸图,返回归一化后的特征向量""" # FaceNet 要求输入 RGB,且尺寸通常为 160x160 face_rgb = cv2.cvtColor(face_img, cv2.COLOR_BGR2RGB) face_resized = cv2.resize(face_rgb, (160, 160)) # 增加 batch 维度,embeddings 返回 shape (1, 512) embedding = embedder.embeddings(np.expand_dims(face_resized, axis=0)) # L2 归一化,方便用余弦相似度比对 embedding = embedding / np.linalg.norm(embedding) return embedding[0] def cosine_similarity(a, b): """两个已归一化向量的余弦相似度就是点积""" return np.dot(a, b) # 假设底库是一个字典:姓名 -> 特征向量 database = { "张三": get_embedding(cv2.imread("zhangsan.jpg")), "李四": get_embedding(cv2.imread("lisi.jpg")), } def recognize(face_img, threshold=0.6): """返回最匹配的姓名和相似度,低于阈值返回未知""" emb = get_embedding(face_img) best_name, best_score = "未知", 0.0 for name, db_emb in database.items(): score = cosine_similarity(emb, db_emb) if score > best_score: best_score = score best_name = name if best_score < threshold: return "未知", best_score return best_name, best_score

get_embedding里做了三件事:颜色空间转换、尺寸缩放、归一化。归一化很重要,不归一化的话余弦相似度计算就不准。threshold是识别阈值,前面说过要自己根据场景调。底库用字典存是最简单的实现,人数多了应该换成向量数据库或至少用矩阵批量计算,但毕设几十人规模字典足够。

提示:录入底库照片时,每个人至少采 3 张不同角度和光照的图,取特征平均,比单张图鲁棒得多。这是血泪经验,单张图录入在演示时经常因为角度差一点就识别失败。

4. 签到业务逻辑与数据落库:让系统真正“能用”

4.1 签到去重与时间窗口的实现

识别出身份后,不能直接写库。下面这段代码实现了一个简单的签到管理器,核心是记录每个人最后一次签到时间,在时间窗口内忽略重复签到。

import time import sqlite3 class AttendanceManager: def __init__(self, db_path="attendance.db", cooldown=8): self.cooldown = cooldown # 同一人签到冷却秒数 self.last_seen = {} # 姓名 -> 上次签到时间戳 self.conn = sqlite3.connect(db_path) self._init_table() def _init_table(self): self.conn.execute(""" CREATE TABLE IF NOT EXISTS records ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, timestamp REAL NOT NULL, confidence REAL NOT NULL ) """) self.conn.commit() def try_check_in(self, name, confidence): """尝试签到,成功返回 True,冷却中返回 False""" now = time.time() last = self.last_seen.get(name, 0) if now - last < self.cooldown: return False self.last_seen[name] = now self.conn.execute( "INSERT INTO records (name, timestamp, confidence) VALUES (?, ?, ?)", (name, now, confidence) ) self.conn.commit() return True

cooldown设 8 秒是个经验值,够一个人走到摄像头前完成签到,又不会因为停留太久重复记录。last_seen用内存字典存,重启后丢失,但签到记录本身在数据库里,不影响业务。如果要求更严格,可以把last_seen也持久化,或者直接在数据库里查最近一条记录的时间。

4.2 用 SQLite 存签到记录:表结构与查询

SQLite 对毕设来说是最省事的选择,不用装数据库服务,一个文件搞定。表结构上面已经建了,三个关键字段:姓名、时间戳、置信度。置信度存下来很有用,答辩时老师问“识别错了怎么办”,你可以说“低于阈值的记录会被标记,支持人工复核”。

def export_today_records(conn): """导出今天的签到记录,按时间排序""" import datetime today_start = datetime.datetime.now().replace( hour=0, minute=0, second=0, microsecond=0 ).timestamp() cursor = conn.execute( "SELECT name, timestamp, confidence FROM records " "WHERE timestamp >= ? ORDER BY timestamp ASC", (today_start,) ) for name, ts, conf in cursor.fetchall(): time_str = datetime.datetime.fromtimestamp(ts).strftime("%H:%M:%S") print(f"{time_str} {name} 置信度 {conf:.3f}")

这个查询按天过滤,输出格式直接可以贴到论文的“系统运行结果”章节。置信度保留三位小数,看起来专业,也方便你分析阈值设得合不合理。

4.3 把识别和签到串成主循环

最后把前面的模块拼起来,形成一个完整的主循环。注意这里加了异常处理,摄像头断连或识别报错时不会直接崩掉。

def main_loop(): manager = AttendanceManager() cap = cv2.VideoCapture(0) while True: ret, frame = cap.read() if not ret: print("取帧失败,尝试重连") cap.release() time.sleep(1) cap = cv2.VideoCapture(0) continue gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) faces = face_cascade.detectMultiScale(gray, 1.1, 5, minSize=(80, 80)) for (x, y, w, h) in faces: face_roi = frame[y:y + h, x:x + w] try: name, score = recognize(face_roi) except Exception as e: print(f"识别异常: {e}") continue if name != "未知": if manager.try_check_in(name, score): print(f"签到成功: {name} 置信度 {score:.3f}") cv2.putText(frame, f"{name} {score:.2f}", (x, y - 10), cv2.FONT_HERSHEY_SIMPLEX, 0.8, (0, 255, 0), 2) cv2.imshow('Attendance', frame) if cv2.waitKey(1) & 0xFF == ord('q'): break cap.release() cv2.destroyAllWindows()

这个主循环里,识别异常被捕获后继续处理下一帧,不会因为一张图出错就整个程序退出。摄像头断连重连的逻辑也是实际部署时必备的,演示中途摄像头被拔掉的情况并不罕见。

5. 避坑与排查:那些让演示翻车的细节

5.1 识别忽好忽坏,同一个人有时能识别有时不能

现象:演示时同一个人站在摄像头前,有时识别成功,有时显示“未知”,置信度在阈值附近跳动。

原因:人脸检测框的裁剪位置每帧都在变,导致送入识别模型的人脸区域有偏移,特征向量不稳定。另外光照变化也会让特征漂移。

解决:在检测和对齐之间加一步人脸对齐,用五点关键点把眼睛和嘴巴位置摆正。如果不想引入额外库,至少把检测框向外扩 10% 再裁剪,给识别模型更多上下文。阈值不要卡太死,0.6 不行就降到 0.55,同时观察误识率有没有上升。

5.2 底库录入照片和现场摄像头差异大,识别率断崖下跌

现象:用手机自拍照录入底库,现场用笔记本摄像头识别,几乎认不出来。

原因:不同摄像头的色彩响应、焦距、白平衡都不一样,模型提取的特征受这些因素影响很大。

解决:底库照片必须用现场同一台摄像头采集。如果做不到,至少用同一品牌、类似焦距的设备。录入时让每个人在签到位置实际站一下,采 3 到 5 张,覆盖不同光照。这个坑我踩过,换摄像头后重新录入底库,识别率从 60% 回到 95% 以上。

5.3 多人同时出现在画面里,签到记录乱序

现象:两个人一起走到摄像头前,系统只识别到一个人,或者两个人的签到顺序和实际顺序对不上。

原因:主循环是逐帧处理,一帧里有多张人脸时,代码按检测框顺序依次识别,没有优先级。而且如果两个人脸靠得近,检测框可能重叠导致漏检。

解决:一帧内检测到多张人脸时,按人脸面积从大到小排序,优先处理离摄像头近的。签到记录里加一个frame_id字段,同一帧的签到标记相同,方便事后分析。如果要求严格,可以限制同一帧只允许一个人签到,其他人等下一帧。

5.4 长时间运行后内存持续增长,最终卡死

现象:系统跑半小时后越来越卡,最后无响应。

原因:OpenCV 的VideoCapture缓冲区没有及时释放,或者特征向量在内存里不断累积没有清理。last_seen字典如果只增不减,人数多了也会占内存。

解决:每处理 1000 帧主动gc.collect()一次。last_seen定期清理超过冷却时间 10 倍的条目。另外cv2.imshow和waitKey在有些系统上会泄漏资源,确保退出时destroyAllWindows被调用。

5.5 答辩现场没有网络,模型权重下载失败

现象:换到答辩教室的电脑上运行,程序卡在下载 FaceNet 权重,或者直接报连接错误。

原因:keras-facenet第一次运行会从网络下载权重文件,答辩现场可能没有外网。

解决:提前在能联网的机器上跑一次,把权重文件缓存找到(通常在~/.keras/或项目目录下),拷贝到答辩电脑的对应位置。或者用embedder = FaceNet()时指定本地权重路径。这个坑每年都有人踩,提前一天去答辩教室试跑一遍是最稳妥的。

6. 把系统做“厚”的一个技巧:用特征底库做增量注册与阈值自校准

毕设如果只做到“能识别、能签到”,分数不会低,但也不会高。想让老师眼前一亮,可以在底库管理上做一个增量注册功能:新用户不用改代码、不用重训模型,通过一个简单的命令行交互就能录入。更进一步,做一个阈值自校准的小工具,用你实际采集的正负样本对自动推荐阈值。

具体做法是:准备两组数据,一组是同一个人不同角度的照片对(正样本),一组是不同人的照片对(负样本)。对每组算余弦相似度,正样本的相似度分布和负样本的相似度分布会有一个交叉区域,取交叉点附近的相似度作为阈值,通常能让误识率和拒识率达到较好的平衡。

def calibrate_threshold(positive_pairs, negative_pairs): """ positive_pairs: [(emb1, emb2), ...] 同一人 negative_pairs: [(emb1, emb2), ...] 不同人 返回推荐的余弦相似度阈值 """ pos_scores = [cosine_similarity(a, b) for a, b in positive_pairs] neg_scores = [cosine_similarity(a, b) for a, b in negative_pairs] # 简单策略:取正样本 5% 分位数和负样本 95% 分位数的中点 pos_low = np.percentile(pos_scores, 5) neg_high = np.percentile(neg_scores, 95) threshold = (pos_low + neg_high) / 2 print(f"正样本相似度 5% 分位: {pos_low:.3f}") print(f"负样本相似度 95% 分位: {neg_high:.3f}") print(f"推荐阈值: {threshold:.3f}") return threshold

这个函数不复杂,但能让你在论文里写“基于实际数据自校准阈值”,比直接写“阈值设为 0.6”有说服力得多。正样本至少采 20 对,负样本 50 对以上,数据越多推荐越可靠。采数据的过程本身也是系统测试的一部分,一举两得。

增量注册的实现思路是:命令行输入姓名,摄像头连续采集 5 帧,取特征平均后存入底库文件(可以用numpy.save存成.npy,或者存进 SQLite 的 BLOB 字段)。下次启动时加载底库文件,不需要重新提取已知人员的特征。这样底库从几十人扩展到几百人,启动时间也不会明显增加。

我自己的习惯是,任何识别类项目,先把底库管理和阈值校准做出来,再去做花哨的界面。因为底库和阈值是系统的地基,地基不稳,界面再好看,演示时识别不出来就是翻车。希望帮到你。

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

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

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

立即咨询