尧图精选

人脸识别签到系统开发:从ArcFace模型选型到完整工程实现

🕒 发布时间:2026/9/11 16:16:59 📁 来源:尧图网络
简介面向计算机专业学生的毕业设计与人脸识别实战这套基于深度学习的人脸识别签到系统提供了完整源码与使用指南项目经导师指导并获评审高分难度适中适合作为课程设计、毕业设计或项目练习的参考。资源压缩包共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 跑 DemoRetinaFace精度高、带关键点模型体积较大GPU 服务器YOLOv5-Face速度快、支持批量依赖训练数据质量实时视频流签到我一般会选 RetinaFace 的 MobileNet 版本它在精度和速度之间比较均衡在 CPU 上处理一帧约 80msGPU 上可以跑到 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, scale1.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), flagscv2.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, threshold0.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(maxsize10) 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)把采集和计算分到两个线程即使模型推理偶尔变慢也不会阻塞摄像头的读取避免画面卡死。maxsize10限制队列长度防止内存被未处理的帧占满。签到系统不是视频分析系统不需要每帧都处理所以队列里积压的帧直接丢弃是正确策略。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, dtypenp.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, axis0) # 归一化 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, threshold0.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, keylambda 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_version11 ) # 加载 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_workers10) 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 压缩阈值是否在现场重新校验过数据库索引是否齐全摄像头画面是否有人流遮挡。这四个点都确认后这个系统才能从“能演示”变成“能天天用”。验证完成后把系统的签到统计页面打开对比现场人数和记录条数差距在合理范围内就可以正式投入使用。后续如果发现误识率升高优先怀疑摄像头角度变化而不是模型退化重新拍一组现场照片测试阈值即可。本文还有配套的精品资源点击获取
上一篇/下一篇内容由系统自动关联 返回资讯列表 →