news 2026/9/11 16:16:58

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
人脸识别签到系统开发:从ArcFace模型选型到完整工程实现

简介:面向计算机专业学生的毕业设计与人脸识别实战,这套基于深度学习的人脸识别签到系统提供了完整源码与使用指南,项目经导师指导并获评审高分,难度适中,适合作为课程设计、毕业设计或项目练习的参考。资源压缩包共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:特征缩放因子,一般取 64
  • m:角度边际,常见取 0.5

这个设计让模型在训练时不仅要求分类正确,还要求特征在角度上足够靠近类中心。对签到系统来说,这意味着同一张脸在不同光线、不同角度下提取出的特征向量距离更近,误识率更低。实际使用时我们并不需要自己从零训练,开源的 ArcFace 预训练模型在 LFW 上已经能到 99% 以上的准确率,直接拿来做人脸编码足够用。

选择 ArcFace 还有一个现实理由:它输出的 512 维特征向量可以提前计算并存入数据库,签到时只需要计算当前帧的特征向量,再与库里存好的向量做余弦相似度比对,不需要每次都过一遍分类头。这种“预抽取 + 实时比对”的结构,正是人脸识别签到系统能够低延迟运行的关键。

2.2 人脸检测环节的轻量化方案

特征提取之前必须先找到人脸在哪。检测方案常见的有三条路:

方案优点缺点适用场景
MTCNN轻量、部署简单极端角度和遮挡表现一般CPU 跑 Demo
RetinaFace精度高、带关键点模型体积较大GPU 服务器
YOLOv5-Face速度快、支持批量依赖训练数据质量实时视频流签到

我一般会选 RetinaFace 的 MobileNet 版本,它在精度和速度之间比较均衡,在 CPU 上处理一帧约 80ms,GPU 上可以跑到 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, scale=1.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), flags=cv2.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, threshold=0.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(maxsize=10) 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)

把采集和计算分到两个线程,即使模型推理偶尔变慢,也不会阻塞摄像头的读取,避免画面卡死。maxsize=10限制队列长度,防止内存被未处理的帧占满。签到系统不是视频分析系统,不需要每帧都处理,所以队列里积压的帧直接丢弃是正确策略。

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);

featureBLOB存储比用文本存数组要高效得多。读取时直接np.frombuffer(blob, dtype=np.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, axis=0) # 归一化 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, threshold=0.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, key=lambda r: r['similarity']) return jsonify({"status": "success", "name": top["name"], "similarity": round(top["similarity"], 4)}) else: return jsonify({"status": "fail", "message": "未识别到有效人脸"})

后端在base64解码后要先np.frombuffercv2.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_version=11 ) # 加载 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_workers=10) 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 压缩,阈值是否在现场重新校验过,数据库索引是否齐全,摄像头画面是否有人流遮挡。这四个点都确认后,这个系统才能从“能演示”变成“能天天用”。

验证完成后,把系统的签到统计页面打开,对比现场人数和记录条数,差距在合理范围内就可以正式投入使用。后续如果发现误识率升高,优先怀疑摄像头角度变化而不是模型退化,重新拍一组现场照片测试阈值即可。

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

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/11 16:12:25

Scala课程设计实战:基于滑动窗口与线性回归的交通拥堵预测

简介&#xff1a;一份基于Scala的交通拥堵预测课程设计源码&#xff0c;主要面向计算机相关专业学生、教师以及正在完成课设或大作业的开发者。项目以交通拥堵预测为业务场景&#xff0c;整合Scala编程、数据处理与数据库设计相关知识点&#xff0c;经导师指导评审获得高分&…

作者头像 李华
网站建设 2026/9/11 16:11:58

RP2040 RTC寄存器深度解析:SETUP/IRQ/INTF裸机控制实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/11 16:10:19

G-Helper CPU降压教程:-25mV降温15℃

G-Helper CPU降压教程&#xff1a;-25mV降温15℃ 【免费下载链接】g-helper Lightweight Armoury Crate alternative for Asus laptops with nearly the same functionality. Works with ROG Zephyrus, Flow, TUF, Strix, Scar, ProArt, Vivobook, Zenbook, Expertbook, ROG Al…

作者头像 李华
网站建设 2026/9/11 16:09:56

YOLO多版本工程化实践:SpringBoot驱动的安全锥检测系统

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华