简介:这是一份基于深度学习的人脸识别签到系统的毕业设计资源包,面向计算机、人工智能相关专业学生完成课程设计或毕业设计,也适合开发者快速搭建人脸识别考勤应用。项目采用Python后端与Flask框架,内置基于人脸特征提取的识别模型,并配套完整的页面模板、数据库迁移脚本、字体转换工具及运行说明文档。压缩包共27个文件,约101.47MB,其中Python脚本负责应用启动、接口逻辑和模型加载,HTML模板覆盖登录、注册、用户管理等签到相关界面,SQLite数据库及dat文件提供底层数据支持。当前已有405人学习下载。通过该项目可以深入学习深度模型在真实场景中的调用方式、Flask项目结构设计和数据库迁移流程,运行后可快速看到前后端联动效果,亦可在此基础上扩展签到记录、课表关联等功能,是毕业设计答辩演示与代码复现的实用参考。
1. 基于深度学习的人脸识别签到系统:毕业设计怎么做得比点名可靠
人脸识别签到系统是这两年毕业设计里出现频率很高的题目,但大多数同学做到最后只是“能跑通”,而不是“能用”。我拆过好几套类似项目,也帮人调过不少模型,最真实的感受是:如果只靠摄像头截一张脸、拿去和库里比对,过不了三五天就会被各种光照、姿态和相似脸问题教做人。面向签到场景,真正能落地的系统得是“检测、对齐、特征提取、比对、去重、记录”一整条链路,而且模型和阈值参数必须根据实际场景重新调。这篇文章会把整条链路的选型、脚本和踩坑项拆开来讲,适合正在做毕业设计、课程设计,或者想在公司里快速搭一套无感签到工具的人直接照着复现。
2. 技术选型与整体架构:为什么是 RetinaFace + MobileFaceNet
2.1 三套人脸识别方案对比:传统方法、预训练模型、自训练模型
人脸识别签到这件事,网上能搜到的方案大致分三类。第一类是传统视觉方法,OpenCV 的 Haar 级联检测加 LBPH/EigenFaces 特征匹配,优点是代码量小、依赖少,缺点是光照稍微一乱、脸稍微一转就识别出错,用来交作业勉强可以,用来考勤基本会翻车。第二类是直接调用预训练模型,比如 DeepFace 或者某个开源的人脸识别库,优点是省事,缺点是你不知道它的训练数据长什么样,放到教室、办公室这种具体场景后准确率不稳定。第三类是自己用深度学习训一个识别模型,检测用 RetinaFace,对齐后把 112x112 的人脸送入 MobileFaceNet 提取特征,配合 ArcFace 做训练。这套方案在 LFW 等公开数据集上能到 99% 以上的人脸识别准确率,而且模型参数量小,CPU 也能跑到实时。
我给模拟项目X做选型时,最终选的是第三套。签到场景的真正难点不是“谁是谁”,而是“同一张脸在不同角度、不同光线下的稳定程度”。传统方法把这个问题简化成像素匹配,本质上就是拿模板硬套;自训练方案通过大量样本学到人脸的本质特征,抗干扰能力完全不在一个量级。所以我的建议是,毕业设计别怕工作量,直接上深度学习方案,答辩时能讲的技术点也多得多。
2.2 模块拆分与数据流:从一帧视频到一条考勤记录
这套系统的完整数据流可以拆成六个环节:摄像头采集、人脸检测、人脸对齐、特征提取、向量比对、结果入库。摄像头采集这一步要考虑帧率,普通 USB 摄像头设 15 帧左右就够,关键是别让整条链路被检测拖慢。人脸检测用 RetinaFace,它会返回人脸框和五个关键点(左眼、右眼、鼻尖、左嘴角、右嘴角),这一步会把人脸从视频帧里抠出来。对齐环节根据五个关键点做仿射变换,把脸摆正并缩放到 112x112,这一步非常关键,它直接决定了后面特征提取的输入质量。
特征提取用的是 MobileFaceNet,输入对齐后的人脸图片,输出一个 512 维的特征向量。有人会问为什么不用 ResNet,因为签到系统一般部署在教室或公司门口,GPU 不一定是标配,MobileFaceNet 在相同精度下参数量只有 ResNet 的十分之一,CPU 推理一帧大约 30 到 50 毫秒,完全够用。向量比对阶段,计算当前人脸特征和注册用户特征之间的余弦相似度,高于阈值就判定为同一人。结果入库前还要做去重,防止同一张脸在一分钟内被连续签到三四次。这六个环节想要长期稳定运行,就不能把它们写在一个大脚本里,最好拆成独立模块。
我在项目里习惯把目录结构分成下面这样,逻辑清楚,后面也好移植:
face-sign-system/ ├── config/ │ └── config.yaml # 模型路径、阈值、摄像头编号、数据库配置 ├── data/ │ ├── raw/ # 原始人脸数据集 │ ├── aligned/ # 对齐后 112x112 的图片 │ └── splits/ # train.txt / val.txt / test.txt ├── models/ │ ├── detector.pt # RetinaFace 模型文件 │ ├── recognizer.onnx # MobileFaceNet 转 ONNX 后的模型 │ └── arcface.pt # 训练好的特征提取权重 ├── scripts/ │ ├── align_dataset.py # 数据集检测、对齐、生成训练文件 │ ├── train_arcface.py # 模型训练脚本 │ ├── export_onnx.py # 把模型导成 ONNX,方便部署 │ └── sign_server.py # 签到推理服务,包含摄像头采集和业务逻辑 └── web/ ├── app.py # Flask 页面,查看签到记录 └── templates/2.3 硬件与部署形态:先想清楚跑在哪里再动手
很多同学一开始就在本地电脑上训练和推理,最后发现换台机器就跑不动。我建议开发阶段分两层:训练用带 NVIDIA 显卡的电脑,显存 6G 以上就够;推理部署则要考虑目标机器,如果是教室门口的一台旧电脑,没有 GPU 就导出 ONNX 模型用 CPU 跑。MobileFaceNet 在这种部署形态下表现很稳定,单帧特征提取在 CPU 上不会超过 60 毫秒。至于有人问能不能直接上树莓派,能,但检测、对齐、识别三个步骤串行起来会比较吃紧,建议把检测改为 MTCNN 或者干脆用 USB 摄像头配合服务端计算。选型这件事没有银弹,先定好“机器在哪、谁用、每天跑多少小时”,再决定模型大小,顺序不能反。
3. 数据准备与人脸对齐:从公开数据集到 112x112 训练样本
3.1 数据集从哪里来:公开数据集为主,自采数据为辅
训练一个可用的识别模型,最关键的是数据量。一般用公开人脸数据集,比如 CASIA-WebFace 这类千万级的数据集直接下载,用来做大规模预训练;然后拿 LFW 作为验证集,用来评估模型精度。对于签到系统,我们还需要把教室里参与签到的每个人多拍几张照片,补充进训练集,光靠公开数据集而不加真实场景数据,会出现“训练时很准、实测时不行”的情况。每个人员类别建议至少 20 张照片,覆盖正脸、左右侧脸、抬头低头、不同光线条件。如果某些人脸数量不够,可以先用 RetinaFace 对齐后做简单的水平翻转和随机亮度变换再补进训练集,不要直接用原图硬凑。
3.2 人脸检测与对齐脚本:RetinaFace 的实操写法
对齐是整个系统中最重要的步骤,直接决定特征提取的质量。最靠谱的做法是用 RetinaFace 检测人脸并提取五个关键点,再做仿射变换。下面的脚本读取原始图片,检测到人脸后裁剪、缩放并保�存为对齐图。
import cv2 import numpy as np from insightface.app import FaceAnalysis import os # 初始化 RetinaFace 检测器 detector = FaceAnalysis(name="buffalo_l", providers=["CPUExecutionProvider"]) detector.prepare(ctx_id=0, det_size=(640, 640)) def align_face(img_path, output_dir, size=112): img = cv2.imread(img_path) img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) faces = detector.get(img) if len(faces) == 0: return False # 取置信度最高的人脸 face = max(faces, key=lambda x: x.det_score) # 仿射变换矩阵,将五个关键点映射到标准位置 # src 是五个关键点,dst 是对应的目标坐标 src = face.landmark.astype(np.float32) dst = np.array([ [30.2946, 51.6963], # 左眼 [65.5318, 51.5014], # 右眼 [48.0252, 71.7366], # 鼻尖 [33.5493, 92.3655], # 左嘴角 [62.7299, 92.2041] # 右嘴角 ], dtype=np.float32) tform = cv2.estimateAffinePartial2D(src, dst)[0] aligned = cv2.warpAffine(img, tform, (size, size)) os.makedirs(output_dir, exist_ok=True) filename = os.path.basename(img_path) cv2.imwrite(os.path.join(output_dir, filename), aligned) return True # 批量处理数据集中所有图片 for root, dirs, files in os.walk("data/raw"): for f in files: if f.lower().endswith((".jpg", ".jpeg", ".png")): align_face(os.path.join(root, f), "data/aligned")这段脚本里有几个东西要特别说明。det_size=(640, 640)决定了检测器看到的输入尺寸,数值越小检测越快,但小尺寸人脸容易漏检,如果摄像头距离人比较远,建议提到 960。仿射变换的目标坐标不是随便写的,它是 InsightFace 的标准五点对齐坐标,来源于训练集的统计结果,所有人都对齐到这个位置后,模型能看到的人脸姿态分布会变得一致。size=112是 MobileFaceNet 的标准输入尺寸,不要轻易改成 128,除非你同时修改模型第一层结构,否则会引入输入分布偏移。
3.3 训练集划分与标签文件:用 txt 还是直接用目录结构
对齐后的图片需要组织成训练脚本能读的格式。最常见的是直接用目录结构作为标签,或者生成train.txt,每行一个图片路径加一个类别编号。我一般会生成 txt 文件,因为这样便于后续做数据清洗和类别筛选。
# train.txt 格式示例 data/aligned/0000001.jpg 0 data/aligned/0000002.jpg 0 data/aligned/0000101.jpg 1 data/aligned/0000102.jpg 1注意这里有个容易出现的问题:类别编号必须从 0 开始连续编号,不能跳号。ArcFace 分类头的输出维度等于类别总数,如果编号里有空洞,训练时会出现“标签越界”的报错。如果每个人只拍了 20 张照片,建议先做数据增强再训练,增强方式包括随机水平翻转、随机亮度调整、随机旋转正负 15 度,这是防止过拟合性价比最高的手段。
4. 模型训练与特征嵌入:ArcFace 实战和阈值校准
4.1 MobileFaceNet + ArcFace:为什么这个组合适合签到场景
MobileFaceNet 是轻量面部识别网络,基础结构类似 MobileNetV3,但它针对人脸任务做了替换,比如用线性瓶颈层替换部分 ReLU,减少信息丢失。ArcFace 是损失函数上的改进,它把普通的 Softmax 交叉熵变成了带角度间隔的 Softmax。简单解释:常规 Softmax 只要求特征分对类,ArcFace 额外要求特征和正确类别中心之间有一个至少 m=0.5 的间隔,这样学出来的特征聚类更紧凑,不同人之间区分更明显。对签到系统来说,这个“间隔”就是安全区,阈值就落在这个间隔里。这个组合的检索准确率在 LFW 上可以做到 99% 以上,模型却只有几 MB,做部署很合适。
4.2 训练脚本:从数据加载到 ArcFace 损失
训练脚本的核心有三个部分:数据加载器、ArcFace 多头、训练主循环。下面给出一个能跑通的最小骨架,重点是看清数据流向和损失计算方式。
import torch import torch.nn as nn import torch.nn.functional as F from torch.utils.data import Dataset, DataLoader from torchvision import transforms from PIL import Image # 1. 自定义数据集,读取 train.txt class FaceDataset(Dataset): def __init__(self, txt_path): self.samples = [] with open(txt_path, "r") as f: for line in f: path, label = line.strip().split() self.samples.append((path, int(label))) self.transform = transforms.Compose([ transforms.RandomHorizontalFlip(), transforms.ColorJitter(brightness=0.2, contrast=0.2), transforms.ToTensor(), transforms.Normalize([0.5, 0.5, 0.5], [0.5, 0.5, 0.5]) ]) def __len__(self): return len(self.samples) def __getitem__(self, idx): path, label = self.samples[idx] img = Image.open(path).convert("RGB") return self.transform(img), label # 2. MobileFaceNet 的特征层,输出 512 维特征 class MobileFaceNet(nn.Module): def __init__(self): super().__init__() # 这里省略具体卷积层定义,加载预训练权重即可 self.features = nn.Sequential() self.embedding = nn.Linear(512, 512, bias=False) def forward(self, x): x = self.features(x) x = x.view(x.size(0), -1) return F.normalize(self.embedding(x)) # 3. ArcFace 分类头 class ArcFaceHead(nn.Module): def __init__(self, feat_dim=512, num_classes=857, s=32.0, m=0.50): super().__init__() self.weight = nn.Parameter(torch.FloatTensor(num_classes, feat_dim)) nn.init.xavier_normal_(self.weight) self.s = s self.m = m def forward(self, features, labels): cosine = F.linear(F.normalize(features), F.normalize(self.weight)) theta = torch.acos(torch.clamp(cosine, -1.0 + 1e-7, 1.0 - 1e-7)) target_logits = torch.cos(theta + self.m) one_hot = torch.zeros_like(cosine) one_hot.scatter_(1, labels.view(-1, 1), 1.0) output = one_hot * target_logits + (1.0 - one_hot) * cosine output *= self.s return output # 4. 训练主循环 dataset = FaceDataset("data/splits/train.txt") loader = DataLoader(dataset, batch_size=128, shuffle=True, num_workers=4) model = MobileFaceNet() arcface = ArcFaceHead(num_classes=dataset.max_label + 1) optimizer = torch.optim.SGD(model.parameters(), lr=0.01, momentum=0.9, weight_decay=5e-4) criterion = nn.CrossEntropyLoss() for epoch in range(30): for images, labels in loader: features = model(images) logits = arcface(features, labels) loss = criterion(logits, labels) optimizer.zero_grad() loss.backward() optimizer.step() print(f"epoch {epoch} loss {loss.item():.4f}")这段代码里,ArcFace 的forward做得比较直观:先算特征和权重归一化后的余弦相似度,再用acos把余弦值转成角度,加上m=0.5的角度间隔,最后乘上缩放系数s=32。这里有一个容易踩的坑:theta + self.m如果超过 π,会导致cos计算不稳定,所以通常会对 target_logits 做一个额外防梯度爆炸的处理,初版训练可以不管,但 loss 出现 NaN 时第一件事查这里。batch_size=128是个安全值,6G 显存刚好够,如果你的显卡只有 4G,降到 64。学习率 0.01 配合 SGD 是训练人脸模型最常见配置,Adam 在这个任务上反而容易收敛到次优解。
4.3 模型评估与阈值校准:用验证集算相似度分布
训练完成后,不要直接部署,先用验证集把相似度分布算出来。做法是把验证集图片两两组对,计算同一个人两张脸的相似度和不同人两张脸的相似度,然后统计分布并选阈值。
import numpy as np import torch def compute_similarity(model, img_path1, img_path2): img1 = load_and_preprocess(img_path1) img2 = load_and_preprocess(img_path2) with torch.no_grad(): feat1 = model(img1) feat2 = model(img2) return F.cosine_similarity(feat1, feat2).item() # 模拟验证集结果:正样本对(同一个人)和负样本对(不同人) pos_scores = [] # 用真实同人照片对计算填充 neg_scores = [] # 用真实不同人照片对计算填充 all_scores = np.concatenate([pos_scores, neg_scores]) threshold = np.percentile(pos_scores, 5) # 保证 95% 的正样本对能通过 print(f"suggested threshold: {threshold:.3f}")这个地方就是在给后面的签到系统定安全线。np.percentile(pos_scores, 5)表示取正样本分布的第 5 百分位,即 95% 的真实签到都能通过。如果这个阈值导致负样本误判率太高,就要回头调整 ArcFace 的 margin 或者增加训练数据,而不是硬调阈值。
超参数参考表:
| 参数 | 推荐值 | 说明 |
|---|---|---|
| 检测输入尺寸 | 640 或 960 | 人离摄像头远就取大值 |
| 对齐输出尺寸 | 112x112 | MobileFaceNet 标准输入 |
| 特征维度 | 512 | 不影响识别率,但影响存储 |
| ArcFace margin | 0.5 | 过大不收敛,过小易混淆 |
| ArcFace scale | 32 | 不用改 |
| 相似度阈值 | 0.45~0.55 | 用验证集统计后决定 |
| 初始学习率 | 0.01 | SGD,权重衰减 5e-4 |
5. 签到系统集成与常见问题排查:推理逻辑、数据库与五个坑
5.1 推理服务设计:Flask + 摄像头 + 双线程队列
部署阶段我习惯把摄像头采集和识别放在两个线程里。采集线程一直抓帧并放入队列,识别线程从队列取帧,执行检测、对齐、特征提取和比对。这样即使识别耗时波动,也不至于导致画面卡死。比对通过后,把签到记录写入数据库,并设置一个时间窗口,比如 60 秒内同一人不再重复签到。
import cv2, threading, queue, time import numpy as np from flask import Flask, request, jsonify from model_wrapper import FaceRecognizer recognizer = FaceRecognizer(model_path="models/recognizer.onnx") frame_queue = queue.Queue(maxsize=2) app = Flask(__name__) recent_sign = {} # user_id -> timestamp def capture_loop(): cap = cv2.VideoCapture(0) while True: ret, frame = cap.read() if not ret: continue if frame_queue.qsize() < 2: frame_queue.put(frame) time.sleep(0.03) def recognize_loop(): while True: frame = frame_queue.get() result = recognizer.recognize(frame) if result is None: continue user_id, name, similarity = result last = recent_sign.get(user_id, 0) if time.time() - last > 60: recent_sign[user_id] = time.time() save_attendance(user_id, similarity) else: print(f"{name} 已在窗口内签到,跳过") threading.Thread(target=capture_loop, daemon=True).start() threading.Thread(target=recognize_loop, daemon=True).start() @app.route("/attendance", methods=["GET"]) def get_attendance(): return jsonify(query_attendance())这段代码里队列深度设成 2 是为了避免延迟累积,如果设成无限大,处理不过来时内存会一直涨。recent_sign是简单的内存去重,重启后清空没关系,因为数据库里已经有记录。识别部分的recognize方法做的工作是:检测人脸、对齐、提取特征、遍历注册用户特征库、算余弦相似度、和阈值比较。整个过程封装在模型包装器里,这样替换模型时不用改业务代码。
5.2 签到数据表设计:既能查明细也能查统计
数据表要设计成两张,一张存注册用户,一张存签到记录。注册用户表里最关键的是face_embedding字段,存的是 512 维特征向量,可以转成二进制存入BLOB字段,也可以用 JSON 文本存。用 BLOB 查询时不需要反序列化所有向量,配合 SQLite 直接按行读取,几百人的规模完全无压力。
CREATE TABLE users ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, face_embedding BLOB NOT NULL, created_at DATETIME DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE attendance ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL, sign_time DATETIME DEFAULT CURRENT_TIMESTAMP, similarity REAL, device_id TEXT, FOREIGN KEY (user_id) REFERENCES users(id) ); CREATE INDEX idx_attendance_time ON attendance(sign_time);这里有个细节,similarity字段要存下来,方便后面排查问题,比如某个人老是签不上,把相似度拿出来一看就明白是阈值问题还是特征问题。给sign_time加索引是为了后面按天查询签到报表时不至于扫全表。
5.3 五大常见问题排查:从摄像头到模型输出的翻车记录
第一,摄像头画面里人离得远就检测不到人脸。现象:站在 3 米外,RetinaFace 偶尔能框住,一到 5 米就完全漏检。原因:检测输入尺寸固定 640,远距离下人脸在帧里只占几十个像素。解决:把det_size提到 960,或者调整摄像头焦距,让目标人脸在画面中占比不小于 10%。
第二,识别结果张冠李戴,A 的照片签成了 B。现象:把阈值调到 0.5 之后,一天内出现四五次误判。原因:训练集里 A 和 B 照片数量差距悬殊,或者相似度阈值设得太低。解决:先看验证集上正负样本分布,如果两个分布重叠明显,说明模型特征不够好,优先增加这两个人的训练照片,其次再考虑调阈值。
第三,训练时 loss 一直降但验证集精度上不去。现象:训练到第 10 个 epoch 时 train accuracy 接近 95%,验证集只有 70%。原因:训练时数据没充分 shuffle,或者数据增强太弱,模型背住了训练集的排列。解决:在 DataLoader 里把shuffle设为 True,数据增强里加入随机裁剪和旋转,必要时增加 dropout。
第四,刚训练完的模型在教室实测时误检率高。现象:在教室里隔 4 米测试,把后墙上的海报人脸也识别出来了。原因:检测器没有做“活体”过滤,任何人脸框都会送到识别模型。解决:加一个最小人脸框尺寸限制,比如宽高都小于 100 像素的直接丢弃;同时注册库里不要出现海报这种非真实人脸数据。
第五,CUDA out of memory 一跑就崩。现象:训练脚本一启动就报显存不足。原因:除了模型本身,输入图片和中间特征都占显存;而且 PyTorch 默认会缓存整个 CUDA 上下文。解决:把 batch_size 降到 64,图片加载换成pin_memory=False,同时检查后台是否有其他进程占用显存,用nvidia-smi看实时占用。
6. 进阶:把模型和服务换个场景复用之前,先跑一遍自检脚本
训练好的模型换到新的教室、新的摄像头,或者换了一套注册照片,都算场景变更。场景一变,原来的阈值基本都会失灵,这不是模型玄学,而是特征分布变了。我吃过一次亏:在实验室调好阈值 0.48,换到另一间采光完全不同的教室后,误签率直接翻倍。从那以后,我每次部署前都会强制跑一遍自检脚本,大概三步:把新的注册照片全部录进系统,拍一段 10 分钟模拟签到视频,跑脚本统计“应到签到人数、实际签到人数、误签率”,三个数字都达标才放行。
这个自检脚本本身不复杂,核心就是读取视频流里出现的每一张人脸,和注册库比对,最后输出统计结果。
def self_check(video_path, expected_ids, threshold): cap = cv2.VideoCapture(video_path) detected = set() wrong = [] while True: ret, frame = cap.read() if not ret: break result = recognizer.recognize(frame, threshold) if result: user_id, name, score = result if user_id in expected_ids: detected.add(user_id) else: wrong.append((name, score)) miss = expected_ids - detected print("签到成功:", len(detected), "漏签:", len(miss), "误签:", len(wrong)) return len(miss) == 0 and len(wrong) == 0如果漏签多,先查光线和对齐;如果误签多,先查阈值和相似脸。数据不够时,还可以把这个脚本接进 CI,每次更新注册库后自动跑一遍,省得人工盯着。这套流程虽然朴素,但能挡住绝大多数上线事故。希望帮到你。
本文还有配套的精品资源,点击获取