简介:面向计算机专业学生的毕业设计与人脸识别实战,这套基于深度学习的人脸识别签到系统提供了完整源码与使用指南,项目经导师指导并获评审高分,难度适中,适合作为课程设计、毕业设计或项目练习的参考。资源压缩包共27个文件,以8个Python源代码和7个HTML页面为主,同时包含数据库文件、配置文件、前端样式、说明文档等,整体大小约101.48MB,目录结构清晰,便于按模块查阅与学习。目前已有63人学习下载,具备一定的学习热度。所有源码均经过本地编译与严格调试,解压后即可运行;实现上覆盖了人脸识别模型、后台接口逻辑、前端交互页面、数据库迁移与配置等关键环节,配合使用指南可帮助读者快速掌握系统设计思路,也可作为二次开发或论文撰写的支撑材料。
1. 人脸识别签到系统为什么值得自己动手做一遍
人脸识别签到系统的核心不是“刷脸”这个动作,而是把身份确认和出勤记录这两件事串成一条可靠的数据链路。相比指纹打卡和门禁卡,它的优势在于无接触、难代打、不需要额外硬件,摄像头加上一台普通电脑就能跑起来,这也是很多毕业设计和公司内部考勤 Demo 选择这个方向的原因。
从技术拆解上看,这个系统包含三块:人脸检测、人脸特征提取与比对、签到记录管理。前两块依赖深度学习模型,第三块则是常规的应用层开发。常见做法是检测用 RetinaFace / YOLOv5-Face,特征提取用 FaceNet / ArcFace,签到逻辑用 SQLite 或 MySQL 存储记录,再配一个简单的 Web 管理界面。这套组合兼顾了识别精度和开发效率,即使是单机运行也能达到实时效果。
适合做这个项目的人有两类:一类是计算机视觉方向的在校生,用毕设题目把模型训练、部署、接口开发全流程走一遍;另一类是公司内部需要快速搭建轻量考勤系统的开发人员,不想采购昂贵的门禁一体机,手里刚好有普通摄像头和 GPU 服务器。无论哪类,都需要先理清一个关键问题:深度学习模型只负责“这个人是谁”,签到记录怎么判定、重复签到怎么拦截、误识别怎么处理,这些逻辑才是系统能否真正落地使用的分水岭。本文就按这条主线把方案讲清楚。
2. 人脸识别签到系统的模型选型与识别原理
2.1 为什么选 ArcFace 作为特征提取模型
人脸识别系统的核心不是检测框画得准不准,而是两张人脸图片能不能在特征空间里被正确地区分。业界经过多年验证,度量学习加归一化嵌入是主流方案,其中 ArcFace 又是最常用的实锤选手。它在 Softmax 的基础上增加角度边际,使得同类样本在超球面上更紧凑,异类样本更分散。
ArcFace 的核心公式可以理解为:
L = -log( e^(s * cos(θ_y + m)) / ( e^(s * cos(θ_y + m)) + Σ_j≠y e^(s * cos(θ_j)) ) )参数含义:
θ_y:样本特征与正确类别中心的角度s:特征缩放因子,一般取 64m:角度边际,常见取 0.5
这个设计让模型在训练时不仅要求分类正确,还要求特征在角度上足够靠近类中心。对签到系统来说,这意味着同一张脸在不同光线、不同角度下提取出的特征向量距离更近,误识率更低。实际使用时我们并不需要自己从零训练,开源的 ArcFace 预训练模型在 LFW 上已经能到 99% 以上的准确率,直接拿来做人脸编码足够用。
选择 ArcFace 还有一个现实理由:它输出的 512 维特征向量可以提前计算并存入数据库,签到时只需要计算当前帧的特征向量,再与库里存好的向量做余弦相似度比对,不需要每次都过一遍分类头。这种“预抽取 + 实时比对”的结构,正是人脸识别签到系统能够低延迟运行的关键。
2.2 人脸检测环节的轻量化方案
特征提取之前必须先找到人脸在哪。检测方案常见的有三条路:
| 方案 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| MTCNN | 轻量、部署简单 | 极端角度和遮挡表现一般 | CPU 跑 Demo |
| RetinaFace | 精度高、带关键点 | 模型体积较大 | GPU 服务器 |
| YOLOv5-Face | 速度快、支持批量 | 依赖训练数据质量 | 实时视频流签到 |
我一般会选 RetinaFace 的 MobileNet 版本,它在精度和速度之间比较均衡,在 CPU 上处理一帧约 80ms,GPU 上可以跑到 10ms 以内。检测的同时还能输出 5 个关键点(双眼、鼻尖、嘴角),关键点可以用来做人脸对齐,把歪着的脸校正成正脸。对齐之后再送入 ArcFace 提取特征,识别率会明显提升。
对齐操作可以用 OpenCV 的相似变换实现。以下是一个最小可用的对齐函数:
import cv2 import numpy as np def align_face(image, landmarks): # landmarks: 5 个关键点坐标,顺序为左眼、右眼、鼻尖、左嘴角、右嘴角 left_eye = landmarks[0] right_eye = landmarks[1] # 计算旋转角度 d_x = right_eye[0] - left_eye[0] d_y = right_eye[1] - left_eye[1] angle = np.degrees(np.arctan2(d_y, d_x)) # 以左眼为基准点旋转 center = tuple(left_eye.astype(np.int32)) rot_mat = cv2.getRotationMatrix2D(center, angle, scale=1.0) # 扩展旋转矩阵以支持仿射变换 rot_mat = np.vstack([rot_mat, [0, 0, 1]]) align_mat = np.array([ [1, 0, 112 - left_eye[0]], [0, 1, 112 - left_eye[1]], [0, 0, 1] ]) # 最终变换矩阵 warp_mat = align_mat.dot(rot_mat)[:2, :] aligned = cv2.warpAffine(image, warp_mat, (112, 112), flags=cv2.INTER_LINEAR) return aligned代码说明:这个函数只做了旋转和平移,没有做缩放归一化,因为 ArcFace 的输入规格是 112x112,我们直接输出这个尺寸。center取左眼是为了让旋转中心稳定,避免画面跳动。实际项目中建议把两眼距离也归一到固定像素,这样对远近不同的脸更鲁棒。
2.3 比对阈值怎么设才不容易误判
特征提取完成后,识别就变成纯数学问题:计算当前人脸向量与库中向量的余弦相似度,超过阈值就认为是同一个人。阈值设置是整个系统里最需要调参的地方。
余弦相似度的计算公式:
similarity = (A · B) / (||A|| * ||B||)A 和 B 分别是数据库里存的注册特征和当前采集的特征。ArcFace 的训练目标使得同一人的余弦相似度通常在 0.6 以上,不同人一般在 0.3 以下。因此常见的初始阈值可以设在 0.5 左右。
但阈值不能一刀切,要根据实际场景调整:
- 门禁场景:要求误识率低,阈值调高到 0.6,宁可漏识也不放错人
- 课堂签到:要求通过率高,阈值降到 0.45,减少学生刷脸失败的烦躁感
- 公司考勤:取中间值 0.5,再配合多次采样投票
一个最坑的点是:同一个人的相似度分布在不同摄像头下差异很大。使用广角摄像头时,人脸畸变会导致相似度下降 0.1-0.2,原来设 0.5 的系统可能突然大面积识别失败。解决办法是在部署现场采集一批实际照片,画出相似度分布图再定阈值。这步不要省。
# 简单阈值判断逻辑 def recognize(feature, db_features, threshold=0.5): max_sim = -1 best_id = None for person_id, db_feature in db_features.items(): sim = float(np.dot(feature, db_feature) / (np.linalg.norm(feature) * np.linalg.norm(db_feature))) if sim > max_sim: max_sim = sim best_id = person_id if max_sim >= threshold: return best_id, max_sim else: return None, max_sim这里用np.dot直接算内积,再加上 L2 范数归一化。需要注意的是,ArcFace 输出的 512 维向量在推理时就已经做了 L2 归一化,所以np.linalg.norm(feature)理论上等于 1。但保险起见还是显式归一化一次,防止某些模型导出工具搞丢归一化层。
3. 签到系统的数据链路与应用层设计
3.1 从摄像头帧到签到记录的完整流程
模型选好后,整个系统的数据流就清晰了。每一次签到请求会依次经过:视频帧采集 -> 人脸检测 -> 对齐 -> 特征提取 -> 比对 -> 写数据库。这几步每一环都可能成为瓶颈,所以设计时要考虑每帧做一次还是每几帧做一次。
对于摄像头实时流,我建议用队列解耦:采集线程不停往队列里丢帧,识别线程从队列取帧处理。当队列积压超过一定数量时,丢弃最旧的帧而不是最新的帧,保证识别响应及时。
import cv2 import queue import threading frame_queue = queue.Queue(maxsize=10) def capture_loop(camera_id): cap = cv2.VideoCapture(camera_id) while True: ret, frame = cap.read() if not ret: break if frame_queue.full(): try: frame_queue.get_nowait() # 丢最旧的帧 except queue.Empty: pass frame_queue.put(frame) def process_loop(): while True: frame = frame_queue.get() # 检测 + 对齐 + 特征提取 + 比对 handle_frame(frame)把采集和计算分到两个线程,即使模型推理偶尔变慢,也不会阻塞摄像头的读取,避免画面卡死。maxsize=10限制队列长度,防止内存被未处理的帧占满。签到系统不是视频分析系统,不需要每帧都处理,所以队列里积压的帧直接丢弃是正确策略。
3.2 签到记录的数据库设计与判重逻辑
数据库表结构是签到系统真正体现工程能力的地方。一张表记录人员信息,一张表记录签到流水,两张表的关联方式决定了查询效率。以下是一个简洁可用的设计:
CREATE TABLE persons ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, employee_no TEXT UNIQUE NOT NULL, feature BLOB NOT NULL, -- 512 维 float32 的二进制 created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE attendance_logs ( id INTEGER PRIMARY KEY AUTOINCREMENT, person_id INTEGER NOT NULL, check_in_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP, check_date DATE NOT NULL, photo_path TEXT, similarity REAL, FOREIGN KEY (person_id) REFERENCES persons(id) ); CREATE UNIQUE INDEX idx_attendance_once ON attendance_logs(person_id, check_date);feature用BLOB存储比用文本存数组要高效得多。读取时直接np.frombuffer(blob, dtype=np.float32)就能还原成向量,省去 JSON 序列化和解析的开销。idx_attendance_once这个唯一索引是防重复签到的核心:同一个人员同一天最多一条记录。
但这里的唯一索引只能拦截并发不高的写入。如果签到请求先查后写,在高并发下可能出现两条记录都通过了查询,然后写入时报错。更稳妥的做法是:插入时捕获唯一约束冲突异常,冲突了就当作已签到处理。Python 中 SQLite 会抛sqlite3.IntegrityError,捕获它然后返回“今日已签到”给前端。
签到判重逻辑:
def sign_in(person_id, similarity, photo_path): today = datetime.date.today() try: conn.execute( "INSERT INTO attendance_logs (person_id, check_date, similarity, photo_path) " "VALUES (?, ?, ?, ?)", (person_id, today, similarity, photo_path) ) conn.commit() return {"status": "success", "message": "签到成功"} except sqlite3.IntegrityError: return {"status": "duplicate", "message": "今日已签到"}这里把判重完全交给数据库约束,不依赖先 SELECT 再 INSERT 的时序,逻辑最简单且不出错。photo_path字段保存识别成功那一刻的截图,后续有人对签到记录有疑议时,可以回溯查看当时的抓拍照片。
3.3 批量录入人脸库的两种方式
人脸库的录入方式直接决定系统好不好用。逐个拍照注册太慢,适合少量人员;批量导入适合初始化。我一般会提供两个入口:实时注册接口和目录批量导入脚本。
实时注册接口的思路是:用户走到摄像头前,系统连续采集 5 帧,检测到人脸就提取特征,5 帧特征求平均后写入数据库。多帧求平均能减少光线和姿态带来的随机误差,比单帧注册更稳。
批量导入脚本则简单粗暴:指定一个文件夹,里面每个子目录以“工号_姓名”命名,目录下的所有图片都是该人的样本。脚本遍历所有图片,提取特征后存入数据库。注意,批量导入时不是每张图片都提取特征后平均,而是逐张提取后全部存储为一个特征列表,比对时取最大相似度。这样对同一人多张照片(不同角度、不同表情)有更好的鲁棒性。
import os import numpy as np import sqlite3 def batch_register(db_path, root_dir, model): conn = sqlite3.connect(db_path) for person_dir in os.listdir(root_dir): if '_' not in person_dir: continue emp_no, name = person_dir.split('_', 1) person_path = os.path.join(root_dir, person_dir) features = [] for img_file in os.listdir(person_path): img = cv2.imread(os.path.join(person_path, img_file)) face = model.detect_and_align(img) if face is not None: feature = model.extract(face) features.append(feature) if features: avg_feature = np.mean(features, axis=0) # 归一化 avg_feature = avg_feature / np.linalg.norm(avg_feature) conn.execute( "INSERT INTO persons (name, employee_no, feature) VALUES (?, ?, ?)", (name, emp_no, avg_feature.astype(np.float32).tobytes()) ) conn.commit() conn.close()注意avg_feature除以 L2 范数,是因为计算出均值后向量的模长不再是 1,直接存会影响后续余弦相似度的绝对值。很多人在这一步出错,导致比对分数整体偏低。
4. 源码结构与关键模块的最小实现
4.1 推荐的项目目录和模块职责
一个适合作为毕业设计或者内部工具使用的人脸识别签到系统,源码结构应该足够清晰,让评审或接手的人一眼能看懂。下面是推荐的目录组织方式:
face-sign-system/ ├── main.py # 程序入口,支持 train / web / debug 三种模式 ├── requirements.txt # Python 依赖清单 ├── config.yaml # 模型路径、阈值、摄像头 ID 等配置 ├── models/ │ ├── detection/ # 检测模型文件 │ └── recognition/ # ArcFace 模型文件 ├── core/ │ ├── detector.py # 人脸检测和对齐封装 │ ├── recognizer.py # 特征提取和比对封装 │ ├── database.py # SQLite 操作封装 │ └── pipeline.py # 完整识别流水线 ├── web/ │ ├── app.py # Flask 或 FastAPI 服务 │ └── templates/ # 前端页面 └── utils/ ├── register.py # 注册脚本 └── align.py # 对齐工具config.yaml集中管理参数是个好习惯,因为模型路径、阈值、摄像头编号这些值是经常变的。用 YAML 比硬编码在代码里方便得多,不同现场部署时只需要改配置文件,不用改代码重新打包。
4.2 识别流水线的核心封装
pipeline.py是系统的中枢,把所有模型调用串起来,对外只暴露一个简单的接口:输入一帧图片,输出识别结果。这种封装让上层 Web 服务完全不用关心模型细节。以下是识别流水线的骨架:
import cv2 import numpy as np class RecognitionPipeline: def __init__(self, detector, recognizer, db_manager, threshold=0.5): self.detector = detector self.recognizer = recognizer self.db = db_manager self.threshold = threshold def process_frame(self, frame): # 1. 检测人脸 faces = self.detector.detect(frame) if not faces: return [] results = [] for face in faces: # 2. 对齐 aligned = self.detector.align(frame, face.landmarks) # 3. 提取特征向量 feature = self.recognizer.embedding(aligned) # 4. 与数据库比对 person, sim = self.db.match(feature, self.threshold) if person is not None: results.append({ "box": face.box, "person_id": person["id"], "name": person["name"], "similarity": float(sim) }) return results这个流水线的设计要点是:检测器返回的每个face对象同时携带边界框和 5 个关键点坐标,这样对齐操作不需要额外搜索人脸关键点。如果检测模型只输出框不输出关键点,对齐步骤就要换成在框内再跑一次关键点检测模型,性能会差不少。
4.3 Web 服务接口与前端交互
Web 服务层可以用 Flask,也可以用 FastAPI,具体选择看个人习惯。Flask 代码更少更适合快速验证,FastAPI 自带文档和异步支持,更适合正式项目。这里用 Flask 展示核心接口。
前端需要两个页面:一个是签到页,调用摄像头并定时向后端发送截图;一个是管理页,展示今日考勤记录和注册新用户。签到页的核心逻辑是一个 JavaScript 定时器,每秒向后端 POST 一帧图片数据。
async function sendFrame() { const canvas = document.getElementById('canvas'); const ctx = canvas.getContext('2d'); ctx.drawImage(video, 0, 0, 640, 480); // 只发送 JPEG 压缩后的数据,减少网络开销 const base64 = canvas.toDataURL('image/jpeg', 0.7); const response = await fetch('/api/check_in', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ image: base64 }) }); const result = await response.json(); if (result.status === 'success') { // 显示签到成功,并停止刷新,避免重复提交 clearInterval(timer); document.getElementById('result').innerText = '签到成功:' + result.name; } }前端每 1 秒发一次请求,后端每次请求做一次完整识别。这个频率下视频流看起来是实时的,又不会让后端压力过大。注意canvas.toDataURL的第二个参数0.7是 JPEG 质量,质量越低传输越快,但对人脸识别精度影响很小,在这个场景下可以放心用。
后端的接口实现:
@app.route('/api/check_in', methods=['POST']) def api_check_in(): data = request.get_json() img_data = data['image'].split(',')[1] # 去掉 base64 头 img_bytes = base64.b64decode(img_data) frame = cv2.imdecode(np.frombuffer(img_bytes, np.uint8), cv2.IMREAD_COLOR) results = pipeline.process_frame(frame) if results: # 取最大相似度的识别结果 top = max(results, key=lambda r: r['similarity']) return jsonify({"status": "success", "name": top["name"], "similarity": round(top["similarity"], 4)}) else: return jsonify({"status": "fail", "message": "未识别到有效人脸"})后端在base64解码后要先np.frombuffer再cv2.imdecode,这是把字节流转成 OpenCV 图像的固定套路。直接在 OpenCV 里读 base64 会导致中文路径或网络传输中的编码问题,所以统一走字节流最稳妥。
5. 模型压缩和验证技巧
5.1 用 ONNX 导出加速推理并减小体积
PyTorch 模型直接用 Python 推理,部署时不仅依赖重,而且首次加载慢。常见做法是把模型导出为 ONNX 格式,再配合onnxruntime推理,速度比 PyTorch 的 eager 模式快 20%-30%,而且可以离线部署到没有 GPU 的机器上。导出逻辑很简单:
import torch import onnxruntime as ort # 假设 model 是训练好的 ArcFace 模型输入为 112x112 的 RGB 图 dummy_input = torch.randn(1, 3, 112, 112) torch.onnx.export( model, dummy_input, "arcface.onnx", input_names=["input"], output_names=["embedding"], dynamic_axes={"input": {0: "batch_size"}}, opset_version=11 ) # 加载 ONNX 模型做推理 sess = ort.InferenceSession("arcface.onnx", providers=["CUDAExecutionProvider", "CPUExecutionProvider"])导出时设置dynamic_axes允许动态 batch,这样接口调用时可以一次传入多张脸,减少整体推理次数。推理时providers列表的排列顺序有讲究:优先填 CUDA,后面的 CPU 作为兜底。这样在装了 GPU 的机器上自动用 GPU,没装也能退到 CPU 跑。
ONNX 优化还有一个重要技巧是输入不做归一化。ArcFace 模型训练时输入是经过特定 mean/std 归一化的,这个归一化可以放在前端的 numpy 操作里,也可以并入 ONNX 图。建议把Normalize操作在导出前预先固化到模型里,这样调用方只需要传原图,省掉一层易错操作。
5.2 单测与端到端验证方法
人脸识别系统的最怕问题是“在测试集很好,到现场就崩”。所以验证不能只测模型准确率,要测整个链路。我一般会做三层验证:
第一层是模型层验证,选 50 对同一人不同照片,计算相似度分布;再选 50 对不同人照片,计算相似度分布,两者之间的间隔宽度决定阈值是否有余量。
第二层是接口层验证,直接调用/api/check_in接口,上传不同照片,检查返回值是否正确。这一步主要验证数据的编解码和数据库读写是否正常。
第三层是并发验证,用测试脚本模拟 10 个不同人同时打卡,观察是否出现重复签到、数据库锁死等问题。SQLite 并发性能有限,如果并发超过 20,建议换 PostgreSQL。一个简单的最小并发测试:
from concurrent.futures import ThreadPoolExecutor def test_concurrent(): pool = ThreadPoolExecutor(max_workers=10) tasks = [pool.submit(api_client.check_in, user_images[i % len(user_images)]) for i in range(100)] results = [t.result() for t in tasks] success_count = len([r for r in results if r["status"] == "success"]) assert success_count <= 100, "总量不能超过一百人次" # 验证每个人最多一条记录 print("并发测试通过,成功签到", success_count, "人次,无重复记录")5.3 模型效果调优的三个页面级参数
部署后的调优实际上集中在三个参数:签到时间窗口、重复识别冷却时长和抓拍照片的存储策略。
签到时间窗口是指一个人识别成功后,多久内不再重复签到。数据库的唯一索引拦截每天的重复签到,但在同一天内,如果识别成功后又失败,系统可能提示“已签到”,这体验不好。我一般会在前端做冷却:识别成功后在页面上显示名字和“已签到”,然后冻结 3 秒,这期间不再发送请求。
抓拍照片存储建议保留最近 90 天,每天一个目录,按日期命名。方便事后回溯的同时,还方便统计不同时间段的人流量。这不只是签到记录,更是整个办公空间的使用数据,很多人没意识到这个附加价值。
最后检查一遍:模型文件是否经过 ONNX 压缩,阈值是否在现场重新校验过,数据库索引是否齐全,摄像头画面是否有人流遮挡。这四个点都确认后,这个系统才能从“能演示”变成“能天天用”。
验证完成后,把系统的签到统计页面打开,对比现场人数和记录条数,差距在合理范围内就可以正式投入使用。后续如果发现误识率升高,优先怀疑摄像头角度变化而不是模型退化,重新拍一组现场照片测试阈值即可。
本文还有配套的精品资源,点击获取