简介:基于深度学习的人脸识别签到系统毕业设计源码包,主要面向计算机相关专业正在完成课程设计、毕业设计或需要项目实战练习的学生,可帮助解决人脸识别与自动签到相结合的系统实现问题。项目在导师指导下完成并获评审98分,源码经本地编译调试均能运行,整体难度适中,流程完整,覆盖人脸录入、特征提取、识别签到及后台管理等关键环节。资源压缩包共27个文件,约101.48MB,除8个Python后端逻辑文件与7个HTML前端页面外,还包含SQLite数据库、4个dat特征数据文件、数据库迁移脚本、CSS样式、中文字体、依赖清单及使用说明等,便于快速配置环境并启动项目,目前已有63人学习下载。整套资源既可作为人脸识别签到系统的完整设计范例,也便于二次开发与功能扩展,适合用于毕业设计文档撰写、系统演示及答辩展示,对入门深度学习者也有较强的参考价值。
1. 从“刷卡机”到“刷脸”:这篇毕业设计到底要写什么
答辩时导师最容易追问的一句话不是“你怎么识别出这个人”,而是“你怎么证明他当时在场”。基于深度学习的人脸识别签到系统,表面上是把摄像头里的人脸变成名字,实际上是三个问题的叠加:人脸检测做得好不好、特征比对准不准、签到业务闭环严不严。很多人把精力全放在深度学习模型上,最后被一句“两个人并排走你签给谁”问住。这篇指南从源代码组织、模型选型、参数调节到验证方法拆开讲,适合正在做软件综合实践选题或毕业设计、手里只有基础 Python 经验的读者,也适合想把人脸识别门禁机那套逻辑移植到 PC 上做演示的同学。读完能跑通一条完整链路,也能在答辩时讲清楚每个模块为什么这么设计。
2. 深度学习怎么把人脸变成一串数字:特征提取模型与选型依据
早期人脸识别系统采用的是传统手工特征路线:检测到人脸后,把图像转成 LBP 纹理直方图,再和库里的直方图算卡方距离。问题在于光照一变、角度偏一点、分辨率低一些,同一个人的直方图差异比不同人还大。深度学习把这件事换了个思路:不去设计“什么样的纹理更像人脸”,而是用大量人脸图片训练一个神经网络,让网络输出一个固定长度的向量,比如 128 维或 512 维。要求是同一个人的不同照片映射到向量空间后距离很近,不同人的照片距离很远,这个向量就叫 embedding 或特征向量。
2.1 认识路线之争:分类任务和度量学习为什么不是一回事
最容易写进开题报告里的一点:人脸识别不是做分类。分类任务训练时输出的是“这个人是谁”的概率,类别数量锁死,来一个新人就要重新训练。人脸识别的目标是得到一个特征提取器,把一张图“压”成一个向量,之后无论库里多少人,都只是向量之间的相似度比较。
常见的开源实现里有两套训练范式。一套是三元组损失,输入锚点样本、正样本、负样本三张图,让锚点与正样本的距离尽量小,与负样本的距离尽量大。FaceNet 系列用的就是这类思路,输出通常是 128 维向量。另一套是带间隔的 Softmax 变体,如 ArcFace 角度间隔损失,不直接训练距离,而是在分类层的余弦夹角上加入角度余量,训练出的 embedding 判别性更强,维度通常取 512。ArcFace 在很多人脸识别算法榜单和实际项目里表现更稳,原因在于角度间隔比欧氏距离约束更符合人脸特征的分布特性。
2.2 从输入到向量:一条最简特征提取链路
实际写代码时,不需要自己训练模型。常见做法是找一个在公开人脸数据集上预训练好的模型,用 ONNX Runtime 或 PyTorch 加载,直接做推理。下面是用 ONNX Runtime 加载人脸特征提取模型的典型调用链,输入一张对齐后的人脸图像,输出 512 维向量。
import cv2 import numpy as np import onnxruntime as ort class FaceEncoder: def __init__(self, model_path: str): self.session = ort.InferenceSession( model_path, providers=["CUDAExecutionProvider", "CPUExecutionProvider"] ) input_info = self.session.get_inputs()[0] self.input_name = input_info.name self.input_size = (input_info.shape[2], input_info.shape[1]) # (h, w) def encode(self, face_bgr: np.ndarray) -> np.ndarray: # 缩放到模型要求尺寸,并转为 RGB、归一化到 [-1, 1] resized = cv2.resize(face_bgr, self.input_size) rgb = cv2.cvtColor(resized, cv2.COLOR_BGR2RGB) rgb = (rgb.astype(np.float32) / 127.5) - 1.0 blob = np.transpose(rgb, (2, 0, 1))[None, ...] feat = self.session.run(None, {self.input_name: blob})[0] # 余弦相似度比较前必须做 L2 归一化 feat = feat / (np.linalg.norm(feat) + 1e-10) return feat.flatten()注意providers的写法:优先使用 CUDA,机器没有 GPU 时自动回落 CPU。self.input_size直接取自模型输入张量,避免手写写错。最后一步归一化非常关键,因为后续计算余弦相似度时,向量的模长会被约掉,不归一化相当于把“脸的清晰程度”也当作区分依据了。
2.3 选型对照:给 CPU 机器留条活路
毕业设计最常见的情况是实验室一台普通电脑,没有独立显卡。这种条件下,直接上 512 维的 ArcFace + ResNet50 不是不行,而是摄像头画面里每来一帧人脸都要推理一次,CPU 占用率会被拉满,签到演示时画面一顿一顿的。选型要在模型精度和推理延迟之间取舍。
| 模型方案 | 嵌入维度 | 损失函数 | 典型主干 | 部署倾向 |
|---|---|---|---|---|
| FaceNet 风格 | 128 | Triplet | Inception-ResNet | GPU 推理 |
| ArcFace 风格 | 512 | 加性角度间隔 | ResNet50 / IR-SE50 | GPU 最佳,CPU 可量化 |
| MobileFaceNet 风格 | 128 或 512 | ArcFace 变体 | Mobile 轻量骨干 | CPU 推荐,边缘设备可用 |
我建议 CPU 机器优先选择 MobileFaceNet 这类轻量模型,即便把输入分辨率压到 112×112,单次推理也能控制在几十毫秒。如果还想再快,可以把模型转成 FP16 或 INT8 量化。演示场景对“认出人”的要求高于“区分双胞胎”,轻量模型完全够用。
3. 源代码怎么组织:模块划分、关键代码与数据库设计
很多毕业设计的代码是单个main.py从头写到尾,人脸检测、特征提取、数据库操作全塞在一起,答辩时老师一问“改阈值要动哪一行”就翻车。比较好的做法是把系统按职责拆成三个包:engine放模型推理,app放签到业务,storage放持久化。这样源代码本身就能当使用指南读,也方便后续扩展门禁机模式或换模型。
3.1 一个可维护的目录结构
下面这份结构不是唯一标准,但适合作为起点,目录里的每个文件职责明确,答辩时可以对着讲。
face_checkin/ ├── app/ │ ├── __init__.py │ ├── checkin_service.py # 签到业务:去重、时间窗口、结果写入 │ └── camera_bridge.py # 摄像头采集与抽帧 ├── engine/ │ ├── __init__.py │ ├── detector.py # 人脸检测器封装 │ ├── encoder.py # 特征提取器封装 │ └── matcher.py # 向量检索与阈值判定 ├── storage/ │ ├── __init__.py │ ├── db.py # SQLite 连接与初始化 │ └── face_repo.py # 特征库读写与热更新 ├── config.py # 所有可调参数集中放这里 ├── requirements.txt └── main.py # 入口,组装各模块config.py单独存在的意义是:所有阈值、路径、模型参数集中管理,而不是散落在各个模块里。engine层不知道签到业务的任何逻辑,它只负责“给你一张图,还你一个向量”和“给你两个向量,告诉你相似度是多少”。业务层只调engine的接口,不碰模型文件。这样后期把签到系统改成门禁逻辑,engine和storage都不用动。
3.2 人脸检测与特征提取的完整调用链
人脸识别算法流程上分两段:先用检测器把人脸框出来,再把框内图像送入特征提取器。检测器可以直接用 OpenCV 的 DNN 人脸检测模块,也可以接 RetinaFace 类模型。为了少引入依赖,项目里我用 OpenCV DNN,模型文件放在models/目录下。
# engine/detector.py import cv2 import numpy as np class FaceDetector: def __init__(self, proto_path: str, model_path: str, conf_thresh: float = 0.7): self.net = cv2.dnn.readNetFromCaffe(proto_path, model_path) self.conf_thresh = conf_thresh def detect(self, frame_bgr: np.ndarray): h, w = frame_bgr.shape[:2] blob = cv2.dnn.blobFromImage( cv2.resize(frame_bgr, (300, 300)), 1.0, (300, 300), (104.0, 177.0, 123.0) ) self.net.setInput(blob) detections = self.net.forward() faces = [] for i in range(detections.shape[2]): conf = detections[0, 0, i, 2] if conf < self.conf_thresh: continue box = detections[0, 0, i, 3:7] * np.array([w, h, w, h]) x1, y1, x2, y2 = box.astype(int) # 裁掉边缘越界部分,并把边缘稍微外扩一点 x1, y1 = max(x1 - 10, 0), max(y1 - 10, 0) x2, y2 = min(x2 + 10, w), min(y2 + 10, h) faces.append(((x1, y1, x2, y2), conf)) return faces这里conf_thresh是检测置信度阈值,默认 0.7,实际场景里如果漏检,可以降到 0.5,但代价是误检变多。框体边缘外扩 10 像素是为了保证额头和下巴不截断,特征提取模型对这两块区域很敏感。OpenCV DNN 的 Caffe 模型是 300×300 输入,和特征提取模型的 112×112 不一样,两套分辨率分开记。
3.3 签到业务逻辑:去重、时间窗口与并发写入
签到系统最容易漏的环节是“重复签到”。一次识别成功后,同一人 10 秒内再次出现在画面里,不应该再写一条记录。如果一天只有一节签到课,就按“同一个人、同一天、同一课程”做唯一约束。下面这段服务层代码把“识别结果”变成“签到事件”。
# app/checkin_service.py from datetime import datetime class CheckinService: def __init__(self, face_repo, db, time_window_minutes: int = 15): self.face_repo = face_repo self.db = db self.time_window_minutes = time_window_minutes def try_checkin(self, user_id: int, course_id: int, similarity: float, min_score: float) -> tuple[bool, str]: if similarity < min_score: return False, "score_low" now = datetime.now() # 1. 判定当前时间是否在课程签到窗口内 if not self._in_window(course_id, now): return False, "out_of_window" # 2. 查数据库,同人同日同课程是否已签到 if self.db.has_checkin(user_id, course_id, now.date()): return False, "duplicated" self.db.insert_checkin(user_id, course_id, now, round(float(similarity), 4)) return True, "ok"try_checkin的分层判断顺序是有讲究的:先判断相似度够不够,再判断时间窗口,最后判断重复。这样阈值最低的检查放最前面,可以少两次数据库查询。has_checkin在 SQLite 里用SELECT 1 ... LIMIT 1,避免把整张表查出来。相似度保留 4 位小数写入,是为了答辩时展示“这个人识别得分是多少”时有据可查。
3.4 建表 SQL:特征库和签到记录分开
数据库设计上,我一般用 SQLite,零配置,单文件,交源代码时直接把.db文件一并打包即可。两张核心表:一张存签到记录,一张存人脸特征。
CREATE TABLE IF NOT EXISTS users ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, student_no TEXT UNIQUE NOT NULL ); CREATE TABLE IF NOT EXISTS face_features ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL, feature BLOB NOT NULL, created_at TEXT DEFAULT (datetime('now', 'localtime')), FOREIGN KEY (user_id) REFERENCES users(id) ); CREATE TABLE IF NOT EXISTS checkin_logs ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL, course_id INTEGER NOT NULL, checkin_time TEXT NOT NULL, similarity REAL NOT NULL, UNIQUE(user_id, course_id, date(checkin_time)) );face_features表里的feature字段存的是 512 维向量的二进制 pickle 数据,而不是逐个字段存 512 个浮点数,这样查询时一次性读出来,用 numpy 直接还原。UNIQUE(user_id, course_id, date(checkin_time))是数据库层的最后一道防线,就算业务代码里的has_checkin因为并发漏判,数据库也会拒绝重复写入,这个约束在答辩时值得重点讲。
4. 使用指南:从环境准备到跑通一次实时签到
代码写得再清晰,环境配不对也是白搭。这一章直接给最小可用的操作路径:装什么、跑什么、参数怎么调、最常见的问题出在哪。
4.1 环境与依赖:按 CPU 和 GPU 两个方案给
先给一份不区分显卡的requirements.txt,再单独说明 CUDA 相关处理。
numpy>=1.24 opencv-python>=4.8 onnxruntime>=1.17 scipy>=1.10 pandas>=2.0 requests>=2.31PyTorch 如果只是用来跑预训练模型,可以不装。特征提取和 face 检测都用 ONNX Runtime 处理,这也是为什么前面代码里用onnxruntime而不是torch。有 NVIDIA 显卡时,需要额外安装onnxruntime-gpu,版本要和 CUDA 版本匹配,比如 CUDA 11.8 配onnxruntime-gpu==1.17.*。装错版本最常见的报错是CUDAExecutionProvider初始化失败,此时程序会自动回落到 CPU,但观察日志能看到警告。
4.2 实时签到最小可运行脚本
下面这个脚本把检测、编码、检索、签到串成一条最简单的流水线。摄像头每 3 帧做一次检测,检测到人脸后直接调用服务层。
# main.py import cv2 from engine.detector import FaceDetector from engine.encoder import FaceEncoder from engine.matcher import FaceMatcher from app.checkin_service import CheckinService def main(): detector = FaceDetector("models/face_det.caffemodel", "models/face_det.prototxt") encoder = FaceEncoder("models/mobilefacenet.onnx") matcher = FaceMatcher("storage/face_db.npz") service = CheckinService(matcher.face_repo, db=None) cap = cv2.VideoCapture(0) frame_id = 0 while True: ret, frame = cap.read() if not ret: break frame_id += 1 if frame_id % 3 != 0: continue faces = detector.detect(frame) for (box, conf) in faces: x1, y1, x2, y2 = box face_img = frame[y1:y2, x1:x2] feat = encoder.encode(face_img) user_id, score = matcher.match(feat) if user_id is not None: ok, msg = service.try_checkin(user_id, course_id=1, similarity=score) # 把识别框和名字画到画面上 cv2.imshow("checkin", frame) if cv2.waitKey(1) & 0xFF == ord("q"): break cap.release() cv2.destroyAllWindows()抽帧策略frame_id % 3 != 0的意思是每 3 帧只处理 1 帧,摄像头通常 30fps,相当于每秒只做 10 次识别。这个参数是为了降低 CPU 占用。如果机器性能好,可以改成每 1 帧处理一次;如果画面里的人移动特别快,抽帧太稀会导致角度变化大却漏检。
4.3 必调参数表:先调哪个、调到多少、以什么为依据
参数调整不能靠感觉。下面是这套系统里最关键的 5 个参数,以及我给出的调整建议:
| 参数 | 初始推荐值 | 调整依据 |
|---|---|---|
人脸检测置信度conf_thresh | 0.7 | 漏检多就降,误检多就升 |
签到相似度阈值min_score | 0.5 | 用验证集算误识率,后用最低正确分数打折 |
| 检测框外扩像素 | 10 | 框太小扩展,太大则抬手误判 |
| 抽帧间隔 | 3 帧 | CPU 占用高就调大,人移动快就调小 |
| 签到时间窗口 | 15 分钟 | 看课程开始时间,窗口越小越能防代签 |
相似度阈值是整套系统最重要的参数。调法不是拍脑袋设 0.5,而是先收集 10 个人的照片各 5 张,算组内相似度和组间相似度,取两者分界线的中间值。比如同一人相似度最低是 0.62,不同人最高是 0.38,那阈值取 0.5 左右最安全。阈值设太高会把真实签到的人拦在外面,设太低则可能把长得像的同班同学认成同一个人。
4.4 新手最容易踩的三个坑
CPU 推理太慢是第一个坑。症状是画面卡顿、签到按钮点了没反应,解决方法是把检测输入从 300×300 换成更小的模型变体,或者把抽帧间隔从 3 改成 5。第二个坑是多人同时出现在画面里,这时检测器会返回多个框,识别结果可能签到到错误的人。常见做法是只取画面中心、面积最大的框签入,其余框只画框不处理。
第三个坑是数据库写入失败却不打印原因。UNIQUE约束触发异常时,程序会静默崩溃。建议在insert_checkin里捕获sqlite3.IntegrityError,把它当成“重复签到”的正常返回而不是异常。这样代码里永远不会有未处理的数据库错误。
提示:开发时把
config.py里的日志级别设为DEBUG,把每帧的相似度分数打出来,不要只看最终结果。分数日志能直接告诉你阈值是否设得太严,省去反复试错的成本。
5. 答辩前必做的验证:评估识别精度与特征库热更新
这一章不聊大框架,只讲两件能在演示现场救命的事:怎么证明模型在你的数据集上真的能用,以及新增一个人进特征库时怎么不重启程序。
5.1 写一个离线验证脚本,统计正确接受率和误拒率
收集一批真实签到照片和公开数据集混合样本,按 person_id 组织好目录结构,跑一遍全量比对,输出置信度矩阵。下面这个脚本思路简洁,适合直接嵌入源代码注释里。
# tools/evaluate.py import numpy as np from engine.encoder import FaceEncoder encoder = FaceEncoder("models/mobilefacenet.onnx") probe = encoder.encode("data/probe/zhang_san.jpg") # 待识别样本 gallery = np.load("storage/face_db.npz")["features"] # 库里所有人的特征 scores = gallery @ probe.T # 余弦相似度的矩阵乘法实现 top_idx = np.argsort(scores, axis=0)[::-1] for rank, idx in enumerate(top_idx[:3]): print(rank + 1, idx, float(scores[idx]))gallery @ probe.T利用了“如果所有特征向量都是单位向量,余弦相似度等于向量内积”这条性质,写起来比循环算cosine_similarity快得多。验证时重点看两个数:正确样本的最低分(决定阈值上限)和错误样本的最高分(决定阈值下限)。这两个值之间的间隔越大,系统越稳。
5.2 特征库热更新:不重启进程也能加人
演示现场最尴尬的场景是“老师我加一个人,然后重启一下”。为了流畅,把特征库的加载和写入做成两个独立操作。新特征写入时先写磁盘,再更新内存里的 numpy 数组,这样下一次匹配立刻生效。
def upsert_feature(self, user_id: int, feature: np.ndarray): # 1. 把新特征拼到内存数组尾部 self._features = np.vstack([self._features, feature]) self._user_ids = np.append(self._user_ids, user_id) # 2. 持久化到 npz 文件,下次启动也能加载 np.savez_compressed("storage/face_db.npz", features=self._features, user_ids=self._user_ids)注意np.savez_compressed每次全量写文件,特征库到几千人时写盘会有几十毫秒延迟,但签到场景完全够用。极端情况下可以把写盘放进后台线程,内存数组更新是即时的,不阻塞识别主流程。这套热更新机制的代码量只有 6 行,却能把“新增成员”从一项操作变成一个随时可用的功能,答辩演示时会是加分项。
本文还有配套的精品资源,点击获取