深度学习人脸识别签到系统:从模型训练到部署的全流程实战

发布时间:2026/10/21 2:45:03
深度学习人脸识别签到系统:从模型训练到部署的全流程实战
简介这是一份基于深度学习的人脸识别签到系统的毕业设计资源包面向计算机、人工智能相关专业学生完成课程设计或毕业设计也适合开发者快速搭建人脸识别考勤应用。项目采用Python后端与Flask框架内置基于人脸特征提取的识别模型并配套完整的页面模板、数据库迁移脚本、字体转换工具及运行说明文档。压缩包共27个文件约101.47MB其中Python脚本负责应用启动、接口逻辑和模型加载HTML模板覆盖登录、注册、用户管理等签到相关界面SQLite数据库及dat文件提供底层数据支持。当前已有405人学习下载。通过该项目可以深入学习深度模型在真实场景中的调用方式、Flask项目结构设计和数据库迁移流程运行后可快速看到前后端联动效果亦可在此基础上扩展签到记录、课表关联等功能是毕业设计答辩演示与代码复现的实用参考。1. 基于深度学习的人脸识别签到系统毕业设计怎么做得比点名可靠人脸识别签到系统是这两年毕业设计里出现频率很高的题目但大多数同学做到最后只是“能跑通”而不是“能用”。我拆过好几套类似项目也帮人调过不少模型最真实的感受是如果只靠摄像头截一张脸、拿去和库里比对过不了三五天就会被各种光照、姿态和相似脸问题教做人。面向签到场景真正能落地的系统得是“检测、对齐、特征提取、比对、去重、记录”一整条链路而且模型和阈值参数必须根据实际场景重新调。这篇文章会把整条链路的选型、脚本和踩坑项拆开来讲适合正在做毕业设计、课程设计或者想在公司里快速搭一套无感签到工具的人直接照着复现。2. 技术选型与整体架构为什么是 RetinaFace MobileFaceNet2.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(namebuffalo_l, providers[CPUExecutionProvider]) detector.prepare(ctx_id0, det_size(640, 640)) def align_face(img_path, output_dir, size112): 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, keylambda 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] # 右嘴角 ], dtypenp.float32) tform cv2.estimateAffinePartial2D(src, dst)[0] aligned cv2.warpAffine(img, tform, (size, size)) os.makedirs(output_dir, exist_okTrue) 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 的标准五点对齐坐标来源于训练集的统计结果所有人都对齐到这个位置后模型能看到的人脸姿态分布会变得一致。size112是 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 额外要求特征和正确类别中心之间有一个至少 m0.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(brightness0.2, contrast0.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, biasFalse) 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_dim512, num_classes857, s32.0, m0.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_size128, shuffleTrue, num_workers4) model MobileFaceNet() arcface ArcFaceHead(num_classesdataset.max_label 1) optimizer torch.optim.SGD(model.parameters(), lr0.01, momentum0.9, weight_decay5e-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(fepoch {epoch} loss {loss.item():.4f})这段代码里ArcFace 的forward做得比较直观先算特征和权重归一化后的余弦相似度再用acos把余弦值转成角度加上m0.5的角度间隔最后乘上缩放系数s32。这里有一个容易踩的坑theta self.m如果超过 π会导致cos计算不稳定所以通常会对 target_logits 做一个额外防梯度爆炸的处理初版训练可以不管但 loss 出现 NaN 时第一件事查这里。batch_size128是个安全值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(fsuggested threshold: {threshold:.3f})这个地方就是在给后面的签到系统定安全线。np.percentile(pos_scores, 5)表示取正样本分布的第 5 百分位即 95% 的真实签到都能通过。如果这个阈值导致负样本误判率太高就要回头调整 ArcFace 的 margin 或者增加训练数据而不是硬调阈值。超参数参考表参数推荐值说明检测输入尺寸640 或 960人离摄像头远就取大值对齐输出尺寸112x112MobileFaceNet 标准输入特征维度512不影响识别率但影响存储ArcFace margin0.5过大不收敛过小易混淆ArcFace scale32不用改相似度阈值0.45~0.55用验证集统计后决定初始学习率0.01SGD权重衰减 5e-45. 签到系统集成与常见问题排查推理逻辑、数据库与五个坑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_pathmodels/recognizer.onnx) frame_queue queue.Queue(maxsize2) 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(targetcapture_loop, daemonTrue).start() threading.Thread(targetrecognize_loop, daemonTrue).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_memoryFalse同时检查后台是否有其他进程占用显存用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每次更新注册库后自动跑一遍省得人工盯着。这套流程虽然朴素但能挡住绝大多数上线事故。希望帮到你。本文还有配套的精品资源点击获取