☰
基于RetinaFace与ArcFace的人脸识别考勤系统实战:从技术选型到工程落地
2026/10/2 3:45:54 网站建设 项目流程

简介:这份资源是面向高校计算机相关专业学生的毕业设计完整项目包,主题为基于深度学习的人脸识别考勤系统,适合正在准备毕设、希望将深度学习与计算机视觉落地为可运行系统的学习者。项目以Python为主要开发语言,涵盖模型训练、图像预处理、人脸检测、特征提取与匹配、用户界面及后端接口等环节,并配有数据库配置与项目文档,构成一套从算法到系统的整体方案。压缩包共49个文件,约13.15MB,其中17个py脚本承担模型构建、训练与推理逻辑,9个pyc为编译缓存,7张jpg与1张png用于效果展示或测试图像,另有4个md文档、3个xml配置及h5模型权重文件等,目录结构清晰,便于按模块查阅。目前已有231人学习,读者可借此理解FaceNet类网络、三元组损失、LFW评测等关键知识点,并参考完整代码与说明完成自己的毕设实现与排错。

1. 从一张.zip说起:人脸识别考勤系统到底难在哪

很多同学拿到「基于深度学习的人脸识别考勤系统」这个毕业设计题目时,第一反应是去网上找一个开源项目,把.zip解压、装好依赖、跑通demo,然后截图写论文。但真正动手之后会发现,demo能跑和系统能用之间,隔着一整套工程问题:摄像头采集到的人脸角度不对怎么办、同一个人换了发型识别率骤降怎么办、考勤记录怎么和识别结果绑定、多人同时出现在画面里怎么处理。这些才是答辩老师真正会追问的点。

这个题目的本质,是构建一条从「人脸检测 → 人脸对齐 → 特征提取 → 特征比对 → 考勤记录」的完整链路。深度学习在其中承担的核心角色是特征提取——把一张人脸图片映射成一个高维向量,使得同一个人的向量距离近、不同人的向量距离远。听起来简单,但选什么模型、用什么损失函数、阈值怎么定、底库怎么管理,每一步都有讲究。

这篇文章面向的是正在做这个毕业设计、或者想用这个方向做实战项目的同学。我会按实际落地顺序,把技术选型、环境搭建、核心代码、考勤逻辑、踩坑排查全部讲清楚。新手可以跟着步骤复现,熟手可以重点关注参数边界和工程化建议。

2. 技术选型:为什么我最终选了RetinaFace + ArcFace这条路线

2.1 人脸识别系统的四段式流水线

一个人脸识别考勤系统,无论用什么模型,流水线都可以拆成四段:

第一段是人脸检测,从摄像头画面中找到人脸的位置,输出边界框和关键点。第二段是人脸对齐,根据关键点把人脸旋转、缩放到标准姿态,消除角度和尺度的影响。第三段是特征提取,把对齐后的人脸图片输入深度网络,得到一个固定维度的特征向量(通常是512维)。第四段是特征比对与考勤判定,计算当前人脸特征与底库中已注册特征的相似度,超过阈值则判定为同一人,写入考勤记录。

这四段中,检测和对齐决定了输入质量,特征提取决定了识别上限,比对策略决定了实际体验。很多同学只关注第三段的模型选择,忽略了检测和对齐的重要性,结果就是模型本身很强但系统识别率很低。

2.2 检测模型选型:RetinaFace vs MTCNN vs YOLO-face

常见的人脸检测方案有三个梯队。MTCNN是最经典的多任务级联网络,速度慢但在CPU上也能跑,适合对实时性要求不高的场景。RetinaFace是近年来的主流选择,单阶段检测,精度高,支持五点关键点输出,有轻量版(MobileNet主干)和重量版(ResNet主干)可选。YOLO-face系列则是把人脸检测当作通用目标检测来做,速度快,适合边缘部署。

我一般会推荐RetinaFace的MobileNet版本作为默认选择。原因是:它同时输出检测框和五个关键点(双眼、鼻尖、左右嘴角),这对后续对齐非常关键;模型体积小,推理速度快,在普通笔记本的CPU上也能达到可用帧率;开源实现成熟,InsightFace项目里直接可用。

注意:如果答辩环境没有GPU,不要选ResNet主干的重型模型,推理速度会让你在演示时非常尴尬。

2.3 特征提取模型选型:ArcFace为什么成为默认答案

特征提取是人脸识别的核心。早期方案用Softmax分类训练,但泛化能力差——训练集里的人识别很准,没见过的面孔就翻车。后来出现了 triplet loss、center loss、cosface、arcface 等一系列度量学习方案。

ArcFace(Additive Angular Margin Loss)的核心思想是在角度空间里给同类特征加一个margin,使得类内更紧凑、类间更分散。它的数学形式不复杂,但效果在多个公开评测集上长期领先。更重要的是,InsightFace提供了基于ArcFace训练好的模型权重,直接拿来做推理就行,不需要自己从头训练。

对于毕业设计来说,自己训练一个ArcFace模型是不现实的——需要百万级人脸数据、多卡GPU、几天训练时间。合理做法是:用预训练模型做特征提取,在自己的小规模数据集上验证效果,论文里写清楚用的是预训练权重加自建底库的方案。

2.4 底库管理与比对策略

底库就是已注册人员的特征向量集合。每个人员注册时,采集若干张人脸图片,提取特征后取平均或保留多条记录。比对时,计算当前特征与底库中所有特征的余弦相似度,取最大值,如果超过阈值则判定为同一人。

这里有个工程细节:底库规模小的时候(几十人),暴力比对完全够用;底库上百人以后,可以考虑用FAISS做向量检索加速。但毕业设计场景下,几十到一两百人的规模,numpy矩阵运算就足够了。

阈值设定是个经验活。ArcFace在LFW数据集上推荐的余弦相似度阈值大约是0.4-0.5,但实际场景中受光照、角度、摄像头质量影响很大。我的建议是:先用0.45作为初始值,然后在自己的测试集上画ROC曲线,根据业务需求调整——考勤场景宁可误拒(要求重新刷脸)也不要误识(把别人记成你)。

3. 环境搭建与最小可跑通demo

3.1 深度学习环境配置:避开CUDA版本地狱

环境配置是第一个拦路虎。常见做法是用Anaconda创建虚拟环境,然后安装PyTorch和InsightFace。这里最大的坑是CUDA版本、PyTorch版本、onnxruntime版本之间的兼容性。

截至我写这篇文章时,比较稳妥的组合是:Python 3.8-3.10、PyTorch 1.13或2.0、CUDA 11.7或11.8、onnxruntime-gpu 1.15以上。如果你没有NVIDIA GPU,直接用CPU版本的onnxruntime也行,RetinaFace-MobileNet在CPU上单张推理大约50-100ms,考勤场景完全够用。

# 创建虚拟环境 conda create -n face_attendance python=3.9 -y conda activate face_attendance # 安装PyTorch(根据你的CUDA版本选择,这里以CUDA 11.8为例) pip install torch torchvision --index-url https://download.pytorch.org/whl/cu118 # 安装InsightFace和依赖 pip install insightface onnxruntime-gpu opencv-python numpy pandas # 如果没有GPU,把onnxruntime-gpu换成onnxruntime # pip install insightface onnxruntime opencv-python numpy pandas

这段命令的逻辑是:先建一个干净的虚拟环境,避免和系统Python或其他项目的包冲突。PyTorch的安装源要指定官方wheel地址,否则pip会从PyPI拉CPU版本。InsightFace自带模型下载逻辑,首次运行时会自动下载RetinaFace和ArcFace的onnx模型文件。

参数说明:--index-url指定PyTorch的wheel源,cu118对应CUDA 11.8。如果你的CUDA是11.7,把cu118改成cu117。如果不确定CUDA版本,运行nvcc --version查看。

3.2 用InsightFace跑通检测+识别的最小代码

环境装好后,先用一段最小代码验证整条链路能跑通。这段代码做三件事:加载模型、检测一张图片中的人脸、提取特征并比对。

import cv2 import numpy as np from insightface.app import FaceAnalysis # 初始化FaceAnalysis,指定检测和识别模型 # buffalo_l 是InsightFace提供的一套预训练模型包 app = FaceAnalysis(name='buffalo_l', providers=['CUDAExecutionProvider', 'CPUExecutionProvider']) app.prepare(ctx_id=0, det_size=(640, 640)) # 读取两张测试图片 img1 = cv2.imread('person_a_1.jpg') img2 = cv2.imread('person_a_2.jpg') img3 = cv2.imread('person_b_1.jpg') # 检测人脸并提取特征 faces1 = app.get(img1) faces2 = app.get(img2) faces3 = app.get(img3) # 每张图取第一张人脸的特征向量 emb1 = faces1[0].embedding emb2 = faces2[0].embedding emb3 = faces3[0].embedding # 计算余弦相似度 def cosine_similarity(a, b): return np.dot(a, b) / (np.linalg.norm(a) * np.linalg.norm(b)) sim_same = cosine_similarity(emb1, emb2) # 同一个人的两张照片 sim_diff = cosine_similarity(emb1, emb3) # 不同人 print(f"同一人相似度: {sim_same:.4f}") print(f"不同人相似度: {sim_diff:.4f}")

逻辑说明:FaceAnalysis是InsightFace的高层封装,name='buffalo_l'指定使用哪套预训练模型包。app.get()一次性完成检测、对齐、特征提取,返回的Face对象包含bbox(边界框)、kps(关键点)、embedding(512维特征向量)。余弦相似度的计算就是两个向量点积除以模长乘积,结果在-1到1之间,越接近1越相似。

参数说明:det_size=(640, 640)是检测网络的输入尺寸,越大检测小脸的能力越强但速度越慢。ctx_id=0指定用第一块GPU,如果只有CPU就设为-1。providers列表里CUDA在前CPU在后,表示优先用GPU推理。

正常情况下,同一人的相似度应该在0.5以上,不同人应该在0.3以下。如果你的结果不符合这个预期,先检查图片质量——模糊、侧脸、遮挡都会显著拉低相似度。

3.3 摄像头实时采集与人脸注册流程

验证完静态图片,接下来接入摄像头做实时采集。注册流程是:让被注册人面对摄像头,采集多张人脸,提取特征后存入底库。

import cv2 import numpy as np import pickle from insightface.app import FaceAnalysis app = FaceAnalysis(name='buffalo_l', providers=['CUDAExecutionProvider', 'CPUExecutionProvider']) app.prepare(ctx_id=0, det_size=(640, 640)) # 底库:{姓名: [特征向量列表]} face_db = {} def register_person(name, num_samples=5): """采集num_samples张人脸,取平均特征存入底库""" cap = cv2.VideoCapture(0) embeddings = [] while len(embeddings) < num_samples: ret, frame = cap.read() if not ret: break faces = app.get(frame) if len(faces) == 1: # 画面中只有一张人脸时才采集 emb = faces[0].embedding embeddings.append(emb) # 画框提示 box = faces[0].bbox.astype(int) cv2.rectangle(frame, (box[0], box[1]), (box[2], box[3]), (0, 255, 0), 2) cv2.putText(frame, f"Captured {len(embeddings)}/{num_samples}", (10, 30), cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 255, 0), 2) cv2.imshow('Register', frame) if cv2.waitKey(1) & 0xFF == ord('q'): break cap.release() cv2.destroyAllWindows() if embeddings: # 取平均特征作为该人员的底库特征 avg_emb = np.mean(embeddings, axis=0) # 归一化 avg_emb = avg_emb / np.linalg.norm(avg_emb) face_db[name] = avg_emb print(f"{name} 注册成功,采集了{len(embeddings)}张人脸") else: print("注册失败,未采集到有效人脸") # 注册一个人员 register_person("张三", num_samples=5) # 保存底库到文件 with open('face_db.pkl', 'wb') as f: pickle.dump(face_db, f)

逻辑说明:注册时要求画面中只有一张人脸,避免误采集。采集多张后取平均特征,可以降低单张图片质量波动的影响。归一化是为了后续余弦相似度计算方便——归一化后的向量点积就等于余弦相似度。

参数说明:num_samples=5是采集张数,太少不够稳定,太多用户体验差,5张是经验值。len(faces) == 1这个条件确保画面中只有一个人,如果多人同时出现就跳过当前帧。

提示:底库文件建议用pickle保存,简单直接。如果底库规模大,可以改用numpy的.npy格式或SQLite数据库。

4. 考勤逻辑实现:从识别结果到考勤记录

4.1 识别比对与阈值判定

识别流程是注册流程的逆操作:从摄像头读取帧,检测人脸,提取特征,与底库中所有特征比对,找最大相似度。

import cv2 import numpy as np import pickle import datetime from insightface.app import FaceAnalysis app = FaceAnalysis(name='buffalo_l', providers=['CUDAExecutionProvider', 'CPUExecutionProvider']) app.prepare(ctx_id=0, det_size=(640, 640)) # 加载底库 with open('face_db.pkl', 'rb') as f: face_db = pickle.load(f) # 考勤记录 attendance_records = [] # 相似度阈值 THRESHOLD = 0.45 def recognize(frame): """识别画面中的人脸,返回(姓名, 相似度)列表""" faces = app.get(frame) results = [] for face in faces: emb = face.embedding emb = emb / np.linalg.norm(emb) # 归一化 best_name = "未知" best_score = 0.0 for name, db_emb in face_db.items(): score = np.dot(emb, db_emb) # 归一化向量的点积=余弦相似度 if score > best_score: best_score = score best_name = name if best_score < THRESHOLD: best_name = "未知" results.append((best_name, best_score, face.bbox.astype(int))) return results def mark_attendance(name): """记录考勤,同一天同一人只记一次""" today = datetime.date.today().isoformat() for record in attendance_records: if record['name'] == name and record['date'] == today: return False # 今天已记录 attendance_records.append({ 'name': name, 'date': today, 'time': datetime.datetime.now().strftime('%H:%M:%S') }) return True # 实时识别主循环 cap = cv2.VideoCapture(0) while True: ret, frame = cap.read() if not ret: break results = recognize(frame) for name, score, bbox in results: color = (0, 255, 0) if name != "未知" else (0, 0, 255) cv2.rectangle(frame, (bbox[0], bbox[1]), (bbox[2], bbox[3]), color, 2) label = f"{name} ({score:.2f})" cv2.putText(frame, label, (bbox[0], bbox[1]-10), cv2.FONT_HERSHEY_SIMPLEX, 0.8, color, 2) if name != "未知": if mark_attendance(name): print(f"[考勤] {name} 已签到") cv2.imshow('Attendance', frame) if cv2.waitKey(1) & 0xFF == ord('q'): break cap.release() cv2.destroyAllWindows()

逻辑说明:recognize函数对画面中每张人脸提取特征,与底库中所有特征计算余弦相似度,取最高分。如果最高分低于阈值,判定为未知。mark_attendance函数做去重——同一天同一人只记录一次,避免摄像头持续识别导致重复签到。

参数说明:THRESHOLD = 0.45是判定阈值,这个值需要根据实际场景调整。阈值越高越严格,误识率降低但拒识率升高。考勤场景建议在0.4-0.5之间。

4.2 考勤记录存储与导出

考勤记录需要持久化存储,方便后续查询和导出。简单场景用CSV就够了,复杂一点可以用SQLite。

import csv import os def save_attendance_to_csv(records, filename='attendance.csv'): """将考勤记录追加写入CSV文件""" file_exists = os.path.isfile(filename) with open(filename, 'a', newline='', encoding='utf-8') as f: writer = csv.DictWriter(f, fieldnames=['name', 'date', 'time']) if not file_exists: writer.writeheader() writer.writerows(records) print(f"已保存 {len(records)} 条考勤记录到 {filename}") # 程序退出前保存 save_attendance_to_csv(attendance_records)

逻辑说明:用追加模式写入CSV,首次写入时自动创建表头。encoding='utf-8'确保中文姓名不乱码。

参数说明:fieldnames定义了CSV的列,可以根据需要增加「部门」「学号」等字段。

4.3 多人同时识别的处理策略

当画面中出现多张人脸时,上面的代码会逐一识别并分别记录考勤。但实际场景中可能遇到一些问题:远处的人脸太小导致特征质量差、侧脸导致相似度偏低、多人同时移动导致帧间识别结果不稳定。

我的处理策略是:对每张人脸,连续多帧识别结果一致才确认考勤。这样可以过滤掉偶发的误识别。

from collections import defaultdict # 跟踪每个人脸的连续识别结果 track_history = defaultdict(list) CONFIRM_FRAMES = 3 # 连续3帧识别为同一人才确认 def recognize_with_tracking(frame): faces = app.get(frame) confirmed = [] for i, face in enumerate(faces): emb = face.embedding emb = emb / np.linalg.norm(emb) best_name = "未知" best_score = 0.0 for name, db_emb in face_db.items(): score = np.dot(emb, db_emb) if score > best_score: best_score = score best_name = name if best_score < THRESHOLD: best_name = "未知" # 用边界框中心点做简单跟踪 bbox = face.bbox.astype(int) center = ((bbox[0]+bbox[2])//2, (bbox[1]+bbox[3])//2) track_key = f"{center[0]//50}_{center[1]//50}" # 粗粒度位置作为跟踪ID track_history[track_key].append(best_name) if len(track_history[track_key]) > CONFIRM_FRAMES: track_history[track_key].pop(0) # 连续CONFIRM_FRAMES帧结果一致才确认 if len(track_history[track_key]) == CONFIRM_FRAMES: if len(set(track_history[track_key])) == 1 and best_name != "未知": confirmed.append((best_name, best_score, bbox)) return confirmed

逻辑说明:用边界框中心点的粗粒度位置作为跟踪ID,记录每个人脸位置最近几帧的识别结果。只有连续多帧结果一致且不是「未知」时才确认考勤。这能有效过滤掉偶发的误识别。

参数说明:CONFIRM_FRAMES = 3是确认帧数,越大越稳定但响应越慢。center[0]//50是位置量化粒度,50像素为一个格子,根据摄像头分辨率和场景调整。

5. 避坑与排查:那些答辩前让我熬夜的问题

5.1 检测不到人脸:先查图片通道再查模型输入尺寸

现象:摄像头画面明明有人,但app.get()返回空列表。

原因通常有三个:一是OpenCV读取的图片是BGR格式,而某些模型期望RGB,虽然InsightFace内部做了转换,但如果你自己预处理过图片可能搞乱通道;二是图片分辨率太高或太低,RetinaFace的det_size设的是640x640,如果原图是4K,缩放后小脸可能丢失;三是光照太暗或过曝,检测网络置信度低于默认阈值。

解决:先用cv2.imshow确认图片能正常显示,排除通道问题。然后把det_size调大,比如(1280, 1280),看能否检测到。如果还不行,用直方图均衡化预处理图片。

5.2 同一人相似度忽高忽低:检查对齐关键点是否稳定

现象:同一个人,有时候相似度0.6,有时候只有0.3。

原因:RetinaFace输出的五个关键点在某些角度下不稳定,导致对齐后的人脸姿态差异大。特别是侧脸超过30度时,关键点定位误差明显增大。

解决:在注册和识别时都加一个姿态过滤——如果关键点构成的头部姿态角超过阈值,提示用户正对摄像头。简单做法是计算左右眼关键点的水平距离和垂直距离之比,判断是否侧脸。

5.3 底库特征没归一化导致比对结果异常

现象:余弦相似度计算出来大于1或小于-1。

原因:特征向量没有归一化,直接用了点积。余弦相似度的定义是点积除以模长乘积,只有归一化后的向量点积才等于余弦相似度。

解决:注册和识别时都对特征做L2归一化。emb = emb / np.linalg.norm(emb)这一行不能省。

5.4 摄像头帧率太低导致画面卡顿

现象:实时识别时画面一卡一卡的,体验很差。

原因:每帧都做检测+识别,GPU或CPU算力不够。RetinaFace-MobileNet单帧推理约30-50ms,ArcFace约10-20ms,加起来每帧60-70ms,理论上能到14fps左右。但如果图片分辨率高、检测尺寸大,时间会成倍增加。

解决:降低det_size到(320, 320),或者每隔一帧做一次检测,中间帧复用上一帧的检测结果只做识别。还可以把摄像头分辨率从1080p降到720p。

5.5 注册时采集的人脸质量差导致底库污染

现象:注册时没注意,采集到了模糊或侧脸图片,导致该人员后续识别率一直很低。

原因:注册流程没有质量检查,来者不拒。

解决:注册时加质量过滤——检测置信度低于0.9的不采、人脸尺寸小于80x80的不采、关键点姿态角过大的不采。宁可让用户多试几次,也不要存入低质量特征。

6. 进阶技巧:用FAISS加速比对与多帧融合提升稳定性

底库规模超过200人以后,每次识别都要遍历所有底库特征做点积,虽然numpy很快,但仍有优化空间。FAISS是Facebook开源的向量检索库,可以把比对复杂度从O(n)降到近似O(log n)。

import faiss import numpy as np # 假设底库有N个人,每人一个512维特征 names = list(face_db.keys()) embeddings = np.array([face_db[name] for name in names]).astype('float32') # 构建FAISS索引,使用内积(归一化向量的内积=余弦相似度) dim = embeddings.shape[1] index = faiss.IndexFlatIP(dim) # IP = Inner Product index.add(embeddings) def recognize_faiss(emb, top_k=1): """用FAISS检索最相似的人""" emb = emb / np.linalg.norm(emb) query = emb.astype('float32').reshape(1, -1) scores, indices = index.search(query, top_k) best_idx = indices[0][0] best_score = scores[0][0] if best_score < THRESHOLD: return "未知", best_score return names[best_idx], best_score

逻辑说明:IndexFlatIP是精确内积索引,适合底库规模几千以内的场景。如果底库上万,可以改用IndexIVFFlat做近似检索。归一化后的向量内积就是余弦相似度,所以直接用IP索引即可。

参数说明:top_k=1只取最相似的一个,如果需要返回多个候选可以调大。THRESHOLD与前面一致。

另一个提升稳定性的技巧是多帧融合。单帧识别受光照、表情、运动模糊影响大,可以对同一个人连续多帧的特征取平均后再比对。

def recognize_multi_frame(frames, max_frames=5): """对连续多帧的人脸特征取平均后比对""" all_embeddings = [] for frame in frames[:max_frames]: faces = app.get(frame) if len(faces) == 1: emb = faces[0].embedding emb = emb / np.linalg.norm(emb) all_embeddings.append(emb) if not all_embeddings: return "未知", 0.0 # 平均特征再归一化 avg_emb = np.mean(all_embeddings, axis=0) avg_emb = avg_emb / np.linalg.norm(avg_emb) return recognize_faiss(avg_emb)

逻辑说明:采集连续多帧中只有单张人脸的帧,提取特征后取平均。平均可以抵消单帧的随机噪声,提升相似度的稳定性。注意平均后要重新归一化。

参数说明:max_frames=5是最多融合帧数,太多会引入运动模糊的坏帧。实际使用时可以配合质量筛选,只保留检测置信度高的帧。

最后说一个我在实际项目中养成的习惯:每次调整阈值或更换模型后,一定用同一套测试集跑一遍完整的注册-识别流程,记录误识率和拒识率。不要凭感觉调参,数据不会骗人。测试集至少包含每个人的5张不同角度、不同光照的照片,以及若干张未注册人员的照片用于测试误识。

希望帮到你。

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

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

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

立即咨询