深度学习人脸识别签到系统实战:从ArcFace模型到FastAPI部署全流程
2026/9/11 14:06:01 网站建设 项目流程

简介:这是一份Python毕业设计项目,基于深度学习技术实现人脸识别签到系统,面向计算机相关专业正在做毕设的学生、需要项目实战练习的开发者,也可直接用于课程设计或期末大作业。项目经导师指导并认可,评审分高达99分,代码完整且确保可运行,从人脸检测识别、用户信息管理到签到记录查询均覆盖,能帮助读者完整掌握深度学习模型与Web应用结合的开发思路。资源包共27个文件,压缩包约101.48MB,包含8个Python脚本(模型处理、API接口、业务逻辑)、7个HTML模板(登录、用户编辑、签到展示等页面),以及SQLite数据库、样式表、配置文件与说明文档,目录结构清晰,便于按模块对照学习。目前已有158人学习下载,适合作为毕业设计参考或项目实战进阶素材,尤其适合希望独立上手完整系统的初中级学习者。

1. 深度学习人脸识别签到系统:别把创新点当高分核心

拿到“基于深度学习的人脸识别签到系统”这个毕业设计题目的同学,第一反应多半是去追 SOTA 模型,把公开数据集上的准确率刷到小数点后三位。但我复核毕业设计时发现,评阅老师的打分逻辑正好相反:模型再新,如果注册、签到、查询这条链路没有完整跑通,文档里也说不清训练数据怎么来、阈值怎么定,分数一样上不去。这个题目本质上是一道应用题——深度学习提供特征提取能力,人脸识别算法负责把照片变成可比对的向量,签到系统才是最终要交付的产品。

这套实现适合两类人。一类是时间紧、想拿高分的应届生,按文章里的顺序能少走大半年弯路;另一类是公司里要快速搭一个内部签到 MVP 的工程师,去掉答辩文档部分,模型和接口部分可以直接复用。下面从选型讲起,一路落到部署和答辩验证。

2. 人脸识别模型的底座选择:从 ArcFace 到 pytorch 实现路径

2.1 签到场景与人脸识别门禁机的选型差异

人脸识别签到和人脸识别门禁机用的是同一套算法体系,但约束条件不同。门禁机经常要处理逆光、遮挡、陌生人闯入,而签到场景通常在教室门口或会议室签到处,光线相对可控,用户会主动正对镜头。这意味着你可以把算力留给大量数据的频繁比对,而不是耗在极端难样本上。

很多同学一开始直接上 100 层 ResNet,理由是“越深越准”。模型在公开 benchmark 上确实表现更好,但放到只有几十个学生、每人一两张注册照的课程场景,深层网络的收益很小,显存占用和推理延迟的代价却很明确。我一般建议先做一轮小对比,在同一个验证集上用相同预处理管线评估三个候选,而不是凭直觉选主干。

Backbone特征维度公开集精度表现显存/部署成本签到场景适用性
MobileFaceNet128 或 512够用,轻量极低,可跑 CPU适合树莓派等边缘设备
IResNet18512良好低,GPU 上轻松跑推荐首选
IResNet50512更好中,推理约 10ms 级精度敏感时选用

这三个网络在结构上都属于深度学习 CNN 的范畴,区别在于深度和残差模块的堆叠方式。对毕设而言,IResNet18 配合 ArcFace 损失已经能拿到足够好的特征质量,把省下来的精力放到对齐和数据清洗上,性价比更高。

2.2 ArcFace 为什么是人脸识别算法的主流基线

人脸识别最终不是靠分类,分类只是中间产物。训练时模型最后一层是 softmax,每个类别对应一个人;推理时取 embedding 层输出的 512 维浮点向量,当作这个人的“人脸特征”,去和库里已有向量算余弦相似度。这个设计带来一个关键收益:新增人员不需要重训整个模型,部署阶段注册接口往向量库加一条记录即可。

ArcFace 的贡献在于把 margin 放到角度空间。通俗理解,普通 softmax 只要求不同人的人脸特征不重叠,ArcFace 强制要求他们必须相距足够远,这个“强制距离”就是 angular margin。它比 Triplet Loss 稳定得多,训练时不需要精心构造三元组,因此在 2019 年之后成为人脸识别算法的主流基线。如果你只看一本理论书,《动手学深度学习》里关于 softmax 和 embedding 的章节足够应付答辩提问。

2.3 用 pytorch 搭一个 ArcFace 训练骨架

下面给出训练部分的最小实现。Backbone 用 IResNet 或 MobileFaceNet 都可以,关键是接口约定:输入一张对齐后的 112x112 RGB 图像,输出 512 维特征。

import torch import torch.nn as nn import torch.nn.functional as F # 特征维度统一用 512,这是人脸识别算法里的常见设置 EMBEDDING_DIM = 512 class ArcFaceLoss(nn.Module): def __init__(self, embedding_dim=512, num_classes=1000, margin=0.5, scale=64.0): super().__init__() self.embedding = nn.Linear(embedding_dim, num_classes, bias=False) self.margin = margin self.scale = scale def forward(self, features, labels): # 特征和权重都做 L2 归一化,内积就是余弦相似度 features = F.normalize(features, dim=1) weight = F.normalize(self.embedding.weight, dim=1) cos_theta = F.linear(features, weight).clamp(-1.0 + 1e-7, 1.0 - 1e-7) theta = torch.acos(cos_theta) # 对目标类别加上角度 margin,再写回原余弦值 target_logit = torch.cos(theta + self.margin) one_hot = F.one_hot(labels, num_classes=self.embedding.out_features).bool() logits = torch.where(one_hot, target_logit, cos_theta) return F.cross_entropy(logits * self.scale, labels)

代码里margin=0.5控制类间距离,scale=64.0是特征缩放系数,这两个值在大多数公开实现里是固定推荐项。num_classes在训练阶段等于训练集里的人员身份数,也就是分类头的类别数;推理阶段完全不用这个分类头,只取 backbone 输出的 embedding 向量。需要注意,torch.acos对接近边界的输入会放大梯度,所以前面做了 clamp;工程上想省事可以替换为 insightface 官方仓库里的 ArcFace 实现,训练曲线更稳。

注意:train.py 里最常见的错误是让 num_classes 等于课堂人数后,每新增同学就重训一次。正确做法是训练时用尽可能大的公开身份数据集把模型训到收敛,部署阶段把注册照片重新过一遍模型得到向量入库,完全不碰训练代码。

3. 人脸识别签到数据管道与训练命令:从数据增强到特征收敛

3.1 数据目录怎么组织,公开数据与自采数据如何搭配

训练的第一步不是写模型,而是把数据目录定好。最常见的组织方式是一个身份一个子目录,目录名可以带学号或工号,方便后面做映射。

data/ ├── train/ │ ├── 2021001_zhangsan/ │ │ ├── 001.jpg │ │ ├── 002.jpg │ │ └── 003.jpg │ ├── 2021002_lisi/ │ └── ... ├── val/ │ └── ... └── test/ └── ...

目录名里包含 ID 和姓名,代码里按名字切分即可。训练前要做一次粗筛:模糊的、闭眼的、脸部占比过小的直接删除。每个人保持 5 到 20 张,不要刻意追求数量,但要做到角度和光照有差异。数据增强方面,随机水平翻转、小角度旋转、颜色抖动这三项足够,过强的增强反而会让小数据集难以收敛。

数据来源要在文档里交代清楚。毕设评阅最反感的是把别人训练好的权重说成自己从零训练的。更稳妥的路线是:主系统直接使用开源免费商用的人脸识别模型提取特征,把训练 ArcFace 的部分作为一个扩展章节写进文档,诚实说明“模型结构参考了公开方案,权重在公开数据集上预训练”。这条路线既能体现对深度学习 CNN 的理解,又回避了自采数据量不够的问题。

3.2 人脸检测与对齐:比换 Backbone 更影响精度的一步

人脸识别算法最容易被忽略的是对齐。同一个人的照片,眼睛位置差几个像素,余弦相似度可能从 0.75 掉到 0.55。检测可以先用 OpenCV 的 DNN 人脸检测器,或者 InsightFace 自带的检测模型,拿到人脸框和五个关键点后,再做一次相似变换。

import math import cv2 def align_face(img, landmarks): """landmarks: [左眼, 右眼, 鼻尖, 左嘴角, 右嘴角] 共 5 个点""" left_eye = landmarks[0] right_eye = landmarks[1] # 计算双眼连线与水平方向的夹角 dx = right_eye[0] - left_eye[0] dy = right_eye[1] - left_eye[1] angle = math.degrees(math.atan2(dy, dx)) # 以左眼为旋转中心,把双眼转到同一水平线 M = cv2.getRotationMatrix2D(left_eye, angle, scale=1.0) aligned = cv2.warpAffine(img, M, (img.shape[1], img.shape[0]), flags=cv2.INTER_CUBIC) # 以双眼中心为基准裁剪 112x112 cx = int(left_eye[0]) cy = int(left_eye[1]) crop = aligned[cy-40:cy+72, cx-56:cx+56] return cv2.resize(crop, (112, 112))

这段代码把双眼连线旋转到水平,再按左眼位置固定裁剪区域。112x112 是人脸识别模型最常用的输入尺寸,和 InsightFace 系模型直接对齐。旋转中心选左眼,裁剪窗口以左眼为中心向上下各取 40 和 72 像素,基本能覆盖完整面部。这里如果检测框抖动,建议保存对齐中间结果,训练时直接用对齐图,不要在每次训练轮次里重复做几何变换。

3.3 训练命令与超参数表:从 pytorch 启动到验证相似度

数据准备好之后,训练命令如下。建议先创建独立的 conda 环境,避免和系统 Python 混在一起。

conda create -n face python=3.9 -y conda activate face python train.py \ --train-root ./data/train \ --val-root ./data/val \ --backbone iresnet18 \ --loss arcface \ --embedding-dim 512 \ --batch-size 64 \ --lr 0.1 \ --warmup-epochs 5 \ --epochs 30 \ --checkpoint ./weights/best.pt

--lr 0.1配合 batch size 64 是 ImageNet 风格训练常见起点,人脸数据集通常还要配合余弦退火。--warmup-epochs 5让学习率从 0 线性升到 0.1,避免前期梯度爆炸。--epochs 30对几十个类别的身份分类足够,再往后训练的收益很小,更多是过拟合到训练集的光照条件。

参数推荐值说明
margin0.5角度间隔,太大会不收敛,太小区分度不够
scale64.0特征缩放,与 margin 配合使用
embedding_dim512特征向量维度,后续比对和存储都依赖它
batch_size64 至 128取决于显存,太小会导致 BN 统计不稳定
warmup_epochs5学习率预热的轮数
lr_schedulerCosineAnnealing收敛更平滑,答辩时也更好解释

训练结束后不要只看 loss。做人脸识别测试,最直接的方法是抽一组照片对,输出它们的余弦相似度分布,正样本对通常集中在 0.6 到 0.9,负样本对在 0.2 以下。用验证集扫一遍阈值,再去看签到场景需要多少误识容忍度。

# verify.py 的核心逻辑:提取两张图的 embedding,算余弦相似度 def cosine_similarity(emb1, emb2): emb1 = emb1 / (torch.norm(emb1, dim=-1, keepdim=True) + 1e-8) emb2 = emb2 / (torch.norm(emb2, dim=-1, keepdim=True) + 1e-8) return float((emb1 * emb2).sum(dim=-1).item())

verify.py在项目里作为独立脚本保留,答辩时可以现场对两张不同姿态的照片做演示。如果相似度普遍偏低,先查对齐是否准确,再查输入图像的通道顺序是否从 BGR 转成了 RGB,这两个问题占了人脸识别训练调试的大部分时间。

4. 可运行的签到系统服务:用 FastAPI 把模型包成高可用接口

4.1 签到系统的接口设计与数据表

模型训练好之后,系统的核心是把 embedding 提取和比对封装成三个接口:注册、签到、记录查询。FastAPI 在 Python 的 Web 框架里调试体验最好,自带 OpenAPI 文档,答辩演示时浏览器直接打开/docs就能现场调接口,对毕设很加分。

方法路径请求体返回
POST/api/v1/registeruser_id, name, image_base64embedding 已保存
POST/api/v1/checkinuser_id, image_base64success, score, time
GET/api/v1/recordsdate 参数当日签到记录列表

数据表不需要搞复杂。user 表存用户基本信息和 embedding 向量,checkin_records 表存每次签到的用户、时间、相似度得分。两张表足够支撑一个学期的使用。

# models.py 中两张核心表的结构 CREATE TABLE IF NOT EXISTS users ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id TEXT UNIQUE NOT NULL, name TEXT NOT NULL, embedding BLOB NOT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE IF NOT EXISTS checkin_records ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id TEXT NOT NULL, score REAL NOT NULL, check_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP );

embedding 以 BLOB 形式存储 512 个 float32,单人占用 2KB,1000 人也就 2MB,完全不需要引入专门的向量数据库。这里用 SQLite 最合适,答辩时可以解释清楚为什么不用 MySQL:数据量小、单机部署、避免额外服务依赖。

4.2 embedding 的注册与比对:阈值设定是这里的核心

模型权重文件在服务启动时只加载一次。签到照片经过与训练时完全相同的对齐流程,提取 512 维向量后与全库向量做余弦相似度匹配。

import numpy as np MODEL = None # 在 FastAPI 启动事件中加载一次权重 def register_embedding(user_id, name, image): face_img = align_and_preprocess(image) # 调 3.2 节的对齐函数 emb = MODEL.embed(face_img) # 返回 L2 归一化的 512 维向量 emb_bytes = emb.astype(np.float32).tobytes() save_to_db(user_id, name, emb_bytes) return {"ok": True} def match_embedding(image, threshold=0.40): face_img = align_and_preprocess(image) emb = MODEL.embed(face_img) # 从数据库读出所有向量组成矩阵,emb 与矩阵做点积即余弦相似度 user_ids, emb_matrix = load_all_embeddings() scores = emb_matrix @ emb best_idx = int(np.argmax(scores)) if scores[best_idx] >= threshold: return user_ids[best_idx], float(scores[best_idx]) return None, 0.0

阈值 0.40 是经验起点,但必须用验证集数据重新标定。签到场景里 FAR 和 FRR 的优先级不同:宁可让本人多刷一次脸,也不能容忍他人代签,所以阈值可以适当往高调,比如 0.45。这里如果不做归一化,点积结果会受向量长度影响,训练时输出的 embedding 一定要 L2 归一化后再入库,否则阈值没有任何意义。

4.3 启动命令与可复现环境:vscode python 环境配置与依赖清单

服务入口用 uvicorn 启动,开发阶段开 reload,部署阶段务必关掉。下面的依赖清单直接写入 requirements.txt,保证答辩换一台机器也能一键复现。

uvicorn app.main:app --host 0.0.0.0 --port 8000 --reload

requirements.txt 至少包含以下内容。版本号不必锁死到小版本,但 torch 和 opencv 建议固定主版本,这两个库的大版本升级经常带来接口不兼容问题。

fastapi==0.110.0 uvicorn==0.27.0 torch==2.1.0 opencv-python==4.9.0.80 numpy==1.26.4 python-multipart==0.0.9

如果用 vscode 写代码,vscode python 环境配置这一关一定要提前处理好。在项目根目录建.venv,然后conda activate face后让 vscode 选择 face 环境作为解释器,避免代码在系统 Python 和虚拟环境之间来回切换导致 opencv 装错地方。接口写完可以用 requests 快速验证注册和签到流程。

import base64 import requests img = base64.b64encode(open("test.jpg", "rb").read()).decode() r = requests.post("http://127.0.0.1:8000/api/v1/register", json={"user_id": "001", "name": "张三", "image_base64": img}) print(r.json())

这段脚本模拟了注册请求,返回里能看到新用户的 embedding 是否成功写入。签到接口的测试逻辑完全一样,只是路径换成 checkin。两个请求都通了,整个系统的最小闭环就成立了。

5. 部署细节与答辩验证:让深度学习签到系统稳定跑满一学期

5.1 答辩前必看的三项指标与文档说明写法

答辩老师不会看训练曲线,但一定会问三个问题:准确率多高、阈值怎么定、遇到相似脸怎么办。这三个问题全部可以用一张指标表回答。写一个小脚本扫描验证集,输出不同阈值下的 FAR、FRR 和准确率。

阈值FAR(误识率)FRR(拒识率)准确率
0.400.5%3.2%96.5%
0.450.1%5.0%95.0%
0.500.0%8.5%91.8%

表格里的数值来自一份 50 人规模验证集。答辩讲解时报准率和误识率的取舍,远比报一个孤立的模型精度有说服力。文档说明部分重点写三块:数据来源与预处理流程、模型结构图与损失函数说明、接口文档和部署命令。代码注释不用面面俱到,但上述三个部分必须详写,这是题目标注“文档说明”的得分点。

5.2 增量注册与数据备份:长期运行的三件事

系统要跑满一个学期,三件事必须提前做好。第一,新同学注册只需要调 register 接口写入向量,不需要重训,但要写一个独立脚本定期重建内存中的向量矩阵,重启服务时从数据库重新加载。第二,阈值会随着摄像头位置调整而变化,建议每隔两周用留存样本重新跑一遍指标表。第三,SQLite 文件和模型权重是唯一需要备份的数据,用系统定时任务每天打包一次即可。

提示:人脸识别签到系统的成败不在模型而在工程。注册照和签到照的光照差异一旦过大,再好的 ArcFace 也会失效。部署时固定摄像头位置和角度,比调整任何超参数都有效。

最后补一个具体操作。如果出现误签到或测试污染,管理员需要按日期清除记录,直接执行:

UPDATE checkin_records SET status = 0 WHERE check_time LIKE '2025-06-11%';

再在签到接口里过滤掉 status 为 0 的记录即可恢复干净状态。这样整套系统从模型训练、服务部署到日常维护就形成了一个可解释、可验证的完整闭环。

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

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

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

立即咨询