简介:面向本科、高职毕业设计的人脸识别签到系统完整项目包,基于Python与深度学习技术实现,适合计算机相关专业学生用于课程设计、毕业设计或工程实践。资源将系统源码、Web前端页面、人脸识别模型数据、SQLite数据库及部署说明整合为一体,共27个文件,压缩包约102.26MB。其中Python源码覆盖Flask应用、人脸识别算法、数据库迁移等核心模块;HTML模板与CSS样式构成管理后台界面;dat与sqlite文件用于存储人脸特征模型和签到记录;另有requirements.txt等依赖清单与说明文档,可辅助快速搭建环境、启动系统。已有4467人学习,项目目录结构清晰,包含完整的初始化与运行配置说明,适合在此基础上二次开发或作为论文实验素材。整套代码结构规范,对人脸检测、特征提取、Web交互等环节均有体现,是理解深度学习落地流程与Web系统开发的优质参考。
1. 基于深度学习的人脸识别签到系统:先搞清楚它在解决什么问题
公司考勤和教室签到现在越来越常见的是人脸识别方案。传统刷卡会代签,指纹在冬季识别失败率很高,而基于深度学习的人脸识别签到系统的核心思路并不神秘:每张人脸经过卷积神经网络被压缩成一个高维特征向量,签到变成了“当前人脸特征和人脸库中谁的距离最近”的比对问题。这个方向常用于毕业设计、企业内部考勤和课堂签到,但很多人做出来只能算 demo——本地跑通几张图片,一接摄像头就各种翻车,要么识别慢,要么误识别。这篇笔记会按落地顺序讲清楚模型选型、活体检测、识别链路、数据准备与训练参数、常见坑和验证方法,让新手能复现一套能上线的系统。
2. 人脸识别签到系统的技术选型:模型、活体检测与部署架构怎么定
2.1 识别模型选型:ArcFace、MobileFaceNet与FaceNet的取舍
人脸识别签到系统最核心的部分是识别模型。常见做法是用预训练的 ArcFace 或 FaceNet 模型把对齐后的人脸映射为 128 维或 512 维的特征向量,再通过向量距离判断是不是同一个人。ArcFace 在 Softmax 基础上引入加性角度间隔(angular margin),训练时要求同一个人的特征向量在角度空间里靠得更近,不同人的特征向量离得更远,这个 margin 对类间可分性的提升非常直观。
选型时主要看两个维度:精度和部署成本。如果签到系统跑在 GPU 服务器上,用 ResNet50 或 ResNet100 做 backbone 的 ArcFace 模型精度最高,单张人脸特征提取在主流 GPU 上大概几毫秒到十几毫秒,完全够用。如果方案是人脸识别门禁机这类一体机,算力受限,通常选择 MobileFaceNet 作为 backbone,参数量不到 1M,在 CPU 上也能跑,LFW 准确率仍能到 99% 以上。我的实际经验是:室内签到场景光照稳定、人脸角度变化小,MobileFaceNet 已经足够,不需要硬上大模型。
| 模型组合 | 特征维度 | 部署环境 | 适用场景 |
|---|---|---|---|
| ResNet100 + ArcFace | 512 | GPU 服务器 | 精度优先,人数多、环境复杂 |
| MobileFaceNet + ArcFace | 128 或 512 | CPU / 嵌入式 | 签到一体机、低成本方案 |
| FaceNet(Inception ResNet) | 128 | GPU / CPU | 快速原型,精度不如 ArcFace |
有人会在选型时纠结“哪个模型排名第一”,但这里有一个容易忽略的事实:签到系统的整体精度不只取决于模型,还取决于注册照片质量、阈值设置和活体检测策略。模型哪怕提升 0.1% 的准确率,对用户体验的影响远不如把阈值调错来得大。
2.2 活体检测:为什么只靠识别模型一定会被照片骗过
签到场景最大的威胁不是识别不准,而是伪造攻击。一张同事的工牌照、一段录好的视频,就能绕过只做特征比对的系统。所以落地时几乎都要加活体检测模块。常见方案分两种:一是 RGB 活体检测,用 CNN 直接从普通摄像头图像判断当前画面是否为真实人脸,优点是不加硬件成本,缺点是对光线和屏幕反光敏感;二是红外 / 深度活体,需要专用摄像头,但红外图像下屏幕基本是黑的,伪造攻击天然失效。
一个务实的策略是分层防御:正常签到只做 RGB 活体检测加人脸识别;检测到可疑情况(如连续失败、特征分数卡在阈值边缘)时,临时要求用户做点头或眨眼动作,用动作验证兜底。我在实际项目里经常用这条组合拳,既能控制硬件成本,又不会在最容易被攻击的环节裸奔。
提示:RGB 活体检测的模型要定期用现场设备录制的新数据做验证。摄像头更换、安装位置变化、门口光线调整,都会让原来的活体模型评估指标失真。
2.3 部署架构:识别服务独立部署还是嵌入终端
签到系统的整体架构一般分三块:摄像头采集端、识别服务、签到数据库。常见做法有两种。第一种把识别服务独立部署在一台带 GPU 的服务器上,摄像头终端通过 RTSP 或 HTTP 把帧传到识别服务,服务返回识别结果,再写入数据库。第二种把所有逻辑封装进一台嵌入式一体机,终端直接输出签到结果。
这两种架构的取舍点在于升级和响应速度。识别服务独立部署,模型更新、活体策略调整都可以在服务端完成,终端几乎不用动;缺点是每次识别都要经过网络,一条教学楼网络波动就可能影响签到。嵌入式一体机响应快、不依赖网络,但后续升级模型要刷机,调试也不方便。我一般建议先做独立服务:成本低、好调试,后期如果要做门禁一体机,再把服务端模型导出成 ONNX 放到终端上。深度学习环境配置也可以统一在服务端做,省去每台终端重复装 CUDA、PyTorch 的麻烦。
3. 从视频帧到签到记录:人脸检测、特征提取与签到判断的完整链路
3.1 人脸检测与关键点对齐:为什么必须先把脸转正
识别模型不是拿到一张图就输出的。它期望的输入是“对齐后的人脸”——两只眼睛水平、人脸居中、固定尺寸。这张图是否对齐,直接决定了特征向量的质量。常见的人脸检测模型有 MTCNN 和 RetinaFace。MTCNN 轻量,在 CPU 上也能跑,适合简单场景;RetinaFace 精度更高,对角度、遮挡、远近变化更鲁棒,签到场景我更推荐 RetinaFace。
检测模型输出人脸框的同时会给出五个关键点(左右眼、鼻尖、左右嘴角),我们用这五个点做仿射变换,把脸转正并裁剪到模型要求的输入尺寸。下面是基于 OpenCV 的对齐代码:
import cv2 import numpy as np def align_face(image, keypoints, output_size=(112, 112)): # keypoints顺序: 左眼、右眼、鼻尖、左嘴角、右嘴角 left_eye = keypoints[0] right_eye = keypoints[1] # 根据左右眼连线计算旋转角度,把脸转水平 delta_x = right_eye[0] - left_eye[0] delta_y = right_eye[1] - left_eye[1] angle = np.degrees(np.arctan2(delta_y, delta_x)) # 以两眼中心为旋转中心 center = ((left_eye[0] + right_eye[0]) // 2, (left_eye[1] + right_eye[1]) // 2) rot_mat = cv2.getRotationMatrix2D(center, angle, 1.0) rotated = cv2.warpAffine(image, rot_mat, (image.shape[1], image.shape[0])) # 简化裁剪:以两眼中心为中心取112x112区域 x = center[0] - output_size[0] // 2 y = center[1] - output_size[1] // 2 aligned = rotated[y:y + output_size[1], x:x + output_size[0]] # 如果越界,先灰度填充再裁剪 if aligned.shape[0] != output_size[1] or aligned.shape[1] != output_size[0]: canvas = np.zeros((output_size[1], output_size[0], 3), dtype=np.uint8) canvas[:aligned.shape[0], :aligned.shape[1]] = aligned aligned = canvas return aligned这段代码做了什么:先用左右眼坐标算旋转角,再用仿射变换把整个人脸旋转到水平方向,最后以两眼中心为基准裁剪出 112x112 的区域。ArcFace 训练时用的是 112x112,如果换用其他模型,这里要跟着改。参数说明里最需要注意的是 keypoints 顺序,不同检测模型输出的关键点顺序不一样,顺序错了脸就是歪的;另一个细节是旋转后的图像边缘可能会露出黑色区域,极端角度下需要用填充或重新检测兜底。
实际工程里裁剪窗口不会像我这样固定取两眼中心往四周各 56 像素,而是会结合人脸框和关键点动态计算。人脸在画面里偏大或偏小时,固定窗口会导致漏掉部分额头或下巴,这会直接影响后续特征提取的稳定性。常见做法是把人脸框向外扩 1.2 到 1.5 倍再缩放,确保完整人脸都在输入图像里。
3.2 特征提取:ONNX Runtime 加载 ArcFace 模型输出 512 维向量
对齐好的人脸图接下来送入特征提取模型。工程上最省事的方式是把 PyTorch 或 PaddlePaddle 模型导出成 ONNX,再用 ONNX Runtime 推理。这样部署时不需要安装整套深度学习框架,只依赖 onnxruntime 一个包,环境配置简单很多。下面是一个完整的特征提取类:
import onnxruntime as ort import cv2 import numpy as np class FaceEncoder: def __init__(self, onnx_path, input_size=(112, 112)): self.sess = ort.InferenceSession( onnx_path, providers=['CUDAExecutionProvider', 'CPUExecutionProvider'] ) self.input_name = self.sess.get_inputs()[0].name self.input_size = input_size def preprocess(self, img): # ArcFace标准预处理:BGR转RGB、resize到112x112、归一化到[-1, 1] img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) img = cv2.resize(img, self.input_size) img = img.astype(np.float32) / 127.5 - 1.0 # HWC -> CHW,增加batch维 img = np.transpose(img, (2, 0, 1)) return np.expand_dims(img, axis=0) def get_embedding(self, aligned_face): blob = self.preprocess(aligned_face) feat = self.sess.run(None, {self.input_name: blob})[0] feat = feat.flatten() # L2归一化,让特征向量模长为1,后续可直接用点积算余弦相似度 norm = np.linalg.norm(feat) if norm > 0: feat = feat / norm return feat逻辑说明:先做通道转换,ONNX 模型一般按 RGB 训练,OpenCV 读进来是 BGR,顺序不转的话特征分布会偏。再归一化到 [-1, 1],这是 ArcFace 训练时的输入范围;有人统一用 ImageNet 的 mean/std 做了标准化,跟训练不一致,特征就乱了。最后一个关键步骤是 L2 归一化,它让每个特征向量的模长变成 1,两个向量点积的结果就是余弦相似度,省去单独算 cosine 的步骤。
参数说明一句话总结:providers 里把 CUDA 放在前面,有 GPU 时优先用 GPU;没有 GPU 时 ONNX Runtime 会自动落到 CPU。输入尺寸必须与模型训练时的输入一致,ArcFace 常见的是 112x112,人脸识别领域还有不少模型用 96x96 或 128x128,换模型就要同步改这里。
3.3 签到逻辑:阈值、去重和异常回退
识别特征拿到手后,签到逻辑就是和人脸底库比对。底库可以简单理解成一个 dict:人员 ID 到特征向量的映射。几百人的量级,直接在内存里算点积就很快;如果到了几千人、几万人,再换成 FAISS 一类的向量索引,但签到系统一般用不上。下面是一段可直接改造的签到判断逻辑:
from datetime import datetime, timedelta def check_in(embedding, face_db, records, threshold=0.45, cooldown=timedelta(hours=24)): # 遍历底库,找余弦相似度最高的人 best_person = None best_score = -1.0 for person_id, db_emb in face_db.items(): score = float(np.dot(embedding, db_emb)) if score > best_score: best_score = score best_person = person_id # 低于阈值:认为是陌生人 if best_score < threshold: return {"status": "unknown", "score": best_score} # 时间窗去重:防止反复刷脸重复签到 last = records.get(best_person) now = datetime.now() if last is not None and now - last < cooldown: return {"status": "duplicate", "person": best_person, "score": best_score} # 通过所有检查,记录签到时间 records[best_person] = now return {"status": "ok", "person": best_person, "score": best_score}参数说明:threshold 决定“这是不是同一个人”,取值一般在 0.3 到 0.6 之间。调高阈值,误识别少但漏识别多;调低则相反。我不会拍脑袋定值,而是用验证集把 0.25 到 0.65 每个值得 ROC 都算一遍,取等错误率附近再上下浮动 0.03 作为业务阈值。cooldown 是去重窗口,公司考勤往往设成一天一次,课堂签到有时候要求“课前 15 分钟签到”,这里按业务调整即可。
多帧确认是另一个容易被忽视的细节:不要拿单帧结果直接写库。常见做法是连续 3 到 5 帧都识别为同一个人、且得分都过阈值,才判定签到成功。这样能过滤掉偶发的检测抖动和特征波动,代价是签到响应会多几百毫秒,对体验几乎没有影响。
4. 数据准备与模型训练:人脸底库怎么建,ArcFace 训练参数怎么调
4.1 人脸底库建设:一个人注册几张照片才够
签到系统上线前,第一步是给每个人注册人脸。最常见的问题是采集太少,只拍一张还站在同一个位置同一角度,特征多样性严重不足。我给的建议是最少 5 张,分别覆盖正面、左右偏转 15 度以内、轻微抬头低头、戴眼镜和不戴眼镜。注意不要从一张照片里裁几个区域充当多张,那是同一样本的数据增强,对特征多样性没有本质帮助。
采集完的照片要先清洗一遍。把模糊的、过曝的、闭眼的、人脸检测都检不出来的样本直接删掉。一个很实用的检查手段是:把注册照片全部跑一遍特征提取,对同一个人算两两相似度,如果某一对明显偏低,多半是那张照片质量有问题,可以补拍或删除。
数据增强也是一个方向。如果每个人只有 5 张照片,训练时可以用水平翻转、轻微旋转、颜色抖动来扩充。但测试集评估时不能用增强后的图片,否则指标虚高。这条原则处理起来很机械,但做错的人很多,尤其是毕设和课程设计里。
4.2 训练与微调:ArcFace 的 s、margin 和其他关键参数
自建数据量大的时候,可以自己从零训练 ArcFace。数据一般用公开数据集(CASIA-WebFace 或 MS1M)预训练,再用自己的人脸数据微调。ArcFace 损失函数有两个核心超参数:s 是特征缩放因子,论文里设 64,控制 softmax 输出的温度;m 是角度间隔,常见设 0.5。m 调太大会导致难以收敛,太小则类间区分度不够。
优化器一般用 SGD,momentum=0.9,weight_decay=5e-4,初始学习率 0.1,在预设的 epoch 处衰减,比如分别在 8、14、20 个 epoch 时除以 10。如果做微调,学习率要降到 0.001 甚至更低,防止破坏预训练模型已经学到的底层特征。ArcFace 损失函数在 PyTorch 里实现并不复杂:
import torch import torch.nn as nn import torch.nn.functional as F class ArcFaceLoss(nn.Module): def __init__(self, s=64.0, m=0.5): super().__init__() self.s = s self.m = m self.cos_m = torch.cos(torch.tensor(m)) self.sin_m = torch.sin(torch.tensor(m)) self.threshold = torch.cos(torch.tensor(torch.pi - m)) def forward(self, cosine, label): # cosine: (N, C),来自权重归一化和特征归一化后的内积 cos_theta = cosine.clamp(-1.0 + 1e-7, 1.0 - 1e-7) # 根据cos值算加角度间隔后的cos sin_theta = torch.sqrt(1.0 - cos_theta ** 2) cos_theta_m = cos_theta * self.cos_m - sin_theta * self.sin_m # 当cos_theta接近π时,改用线性近似避免数值不稳定 cos_theta_m = torch.where( cos_theta > self.threshold, cos_theta_m, cos_theta - 2 * self.m ) one_hot = F.one_hot(label, num_classes=cosine.shape[1]).float() output = one_hot * cos_theta_m + (1 - one_hot) * cos_theta return F.cross_entropy(self.s * output, label)逻辑说明:forward 的输入不是 logits,而是特征向量和分类层权重各自 L2 归一化后的内积(即余弦相似度)。对目标类别,我们把它在角度空间加上 m,再把缩放因子 s 乘回去,最后过交叉熵。训练的时候要监控两类指标:类内距离应该持续下降,类间距离应该持续上升;如果 loss 在降但类间距离也在降,往往是学习率偏大或者 m 不够,特征没有真正被拉开。
参数说明:s 和 m 不要同时乱调。s 控制温度,影响收敛速度;m 控制类间间隔,影响最终精度。实际操作中先用标准值(s=64, m=0.5)跑通流程,再重点调 m,从 0.1 慢慢升到 0.5,每轮看验证集准确率。我自己踩过 m 一上来就设 0.8 导致 loss 不收敛的坑,后来用这种 warmup 的方式才解决。
4.3 评估方法:LFW 指标、自建验证集与阈值选择
模型训练完怎么看效果?先在 LFW 上测,达到 99% 以上说明模型基础能力没问题。但 LFW 的 99% 不代表现场签到能复现这个数字——LFW 是干净的、对齐好的、光照难度受控的图片,而现场摄像头画面会有运动模糊、背光和畸变,误识别率可能高出几个数量级。
所以我的习惯是训练完立刻建一个自建验证集:在签到场景用同样的摄像头录一段含多人出入的视频,人工标注“第几秒到第几秒是谁”,抽帧后组成正样本对(同一个人)和负样本对(不同人)。几十个人、每人十张图就够用了,重点是这些样本和真实运行环境的分布一致。用这个验证集画 ROC 曲线,从曲线上找等错误率对应的阈值,再把阈值往严格方向调一点点(比如加 0.02 到 0.05),作为签到系统的默认阈值。
为什么要往严格调?因为签到场景漏识别一次可以重新刷脸,误识别一次却是把别人的签到记到账上,代价完全不同。阈值这个参数是整套系统里最需要根据现场重测的,不要指望一个阈值走天下。
5. 深度学习人脸识别签到系统的 5 个常见坑与排查清单
5.1 坑一:戴口罩、戴眼镜时识别率骤降
现象:同事戴上口罩后基本认不出来;戴眼镜的人摘下眼镜后识别失败率明显上升。原因:ArcFace 为主的预训练模型训练数据里遮挡样本占比很低,模型没有学到“通过上半张脸也能判断身份”的能力;眼镜反光则会在特征提取阶段引入局部噪声。解决:第一种思路是用对遮挡鲁棒的训练方式,比如 Partial FC 论文里提出的方法,在训练时随机 mask 掉一部分特征,让模型学会用剩余部分做判断;第二种更接地气,注册底库时给戴眼镜的人单独存一套眼镜版特征,识别时先匹配同类型特征。第二种实施成本低很多,效果立竿见影。
5.2 坑二:同一人多次识别特征距离波动大
现象:同一个人站在同一个位置,前后两次算出的特征余弦相似度只有 0.5,勉强过阈值。排查顺序:先确认 L2 归一化有没有做——没做归一化时点积结果受特征模长影响,模长分布不稳就导致分数剧烈波动;再看预处理细节,特别是 resize 尺寸和归一化方式,如果训练时用的是除以 127.5 减 1,推理时用了 0 到 1 的归一化,特征分布完全对不上;最后看检测对齐是否稳定,人脸检测框在相邻帧之间抖动超过 10 个像素时,裁出来的图内容变化很大,特征自然漂移。我的习惯是把每帧的人脸框坐标和关键点坐标打出来,如果框在跳,优先修检测和对齐,而不是怀疑模型。
5.3 坑三:GPU 显存占用很高但推理延迟没降
现象:部署到 GPU 服务器后,显存占了几个 G,但单张人脸推理还是几十毫秒,和 CPU 差不多。原因:最常见的是模型没走 TensorRT,ONNX Runtime 的 CUDAExecutionProvider 虽然有 GPU 加速,但相比 TensorRT 仍少了很多算子融合和显存复用优化;另一个原因是没有开启 FP16,在支持的 GPU 上 FP16 推理比 FP32 快 2 到 3 倍。解决:用 TensorRT 把 ONNX 模型转成 FP16 engine。签到场景一次只需要识别一个人脸,batch size 设 1 就好,不需要为大 batch 优化。
5.4 坑四:RGB 活体检测被高清屏幕绕过
现象:用一台高分辨率手机屏幕翻拍照片,活体检测把它当成了真人签到成功。原因:RGB 活体学到的最强特征往往是屏幕的摩尔纹、颜色偏差和反光,当屏幕分辨率足够高时这些特征变得很弱,模型就分不清了。解决:最可靠的是加随机动作验证,让用户按提示完成眨眼、点头或张嘴,同时用关键点跟踪判断动作是否真实发生;预算允许的话直接上红外加 RGB 双摄,红外图里普通屏幕是纯黑的,彩色照片和打印照片在红外下也完全不一样,攻击难度指数级上升。这也是我的血泪经验:一套看着检测率很高的 RGB 活体,上线第二天就被一张打印照片破了。
5.5 坑五:签到了但数据库没有记录
现象:识别成功、界面上也显示“签到成功”,但后台查不到记录。排查:先用日志定位是在哪个环节丢的。绝大多数原因是识别服务和签到服务之间的 HTTP 回调失败,网络超时或者异常被吞掉。解决:别让识别服务只返回结果不管落库,把落库放到识别服务内部,先写一条 pending 状态的记录,再异步更新为 confirmed;或者干脆在识别服务里直接写数据库,少一个中间环节就少一类故障。另一个容易出问题的是时间字段,本地时间跨天和统计不同步会直接搞乱考勤报表,建议统一存 UTC 时间戳,展示时再转时区。
6. 让签到系统跑得更稳:模型量化、批量注册与端到端验收技巧
模型训练完、逻辑跑通后,还有几件工程上的事情能让系统从“能跑”变成“好用”。
第一是模型量化。如果用 GPU 部署,把 ONNX 模型用 TensorRT 转成 FP16 engine,单张人脸推理延迟通常能从几十毫秒降到几毫秒,显存占用也会降。没有 GPU 的嵌入式终端,可以尝试把模型量化成 INT8,代价是准确率会有小幅波动,量完后用自建验证集重测一遍阈值,别直接用原来的阈值。
第二是批量注册。上百人的底库不能靠一张张手填,做一个简单的批量导入脚本:读取 CSV(姓名、工号、照片路径),逐张做人脸检测、对齐、特征提取,写成 npy 文件或者存进数据库。脚本跑完打出一份注册失败名单,把没检测到人脸或特征异常的样本剔除。
第三是端到端验收。上线前录一段模拟真实考勤的视频,多人轮流进出、有人戴帽子、有一张照片伪造攻击,跑一遍完整流程,记录三个指标:正确识别率、误识别率、平均签到延迟。其中误识别率是红线,宁可多几次漏识别重新刷脸,也不要出现 A 签到了 B 账上的情况。
这个方案值不值得做?我的判断是:深度学习人脸识别签到系统在“识别”这件事上技术已经非常成熟,真正的工程难点在数据、阈值和活体这三件事上,而不是模型本身。把 ArcFace、RetinaFace 这一套跑稳,把底库管好,把阈值校准好,这套系统就能稳定服务几百人规模的使用。我自己每次部署新场景都要重新录一段视频重测阈值,因为每台摄像头的色彩、视角、摆放位置都会影响特征分布。哪怕测试时一切正常,也要留一个人工复核的入口,防止极端情况下把陌生人当成签到人员放进来。这套习惯帮我避开了不少尴尬,希望这篇笔记也能帮你少走点弯路。
本文还有配套的精品资源,点击获取