
简介一套完整的基于深度学习的人脸识别签到系统毕业设计项目面向计算机相关专业学生、毕业设计开发者可有效解决传统点名签到效率低、易代签、记录易丢失等问题。项目涵盖人脸注册、特征提取、活体检测、识别签到等核心流程并集成Web管理端实现用户管理、签到记录查询等功能。压缩包共27个文件以8个Python源码文件、7个HTML模板、4个dat数据文件及SQLite数据库为主体另含依赖清单、配置文件与字体资源压缩后约101.47MB。已有405人学习下载内含完整的环境部署说明、数据库迁移命令、管理员初始化脚本可快速创建虚拟环境并一键运行前后端分层设计清晰便于二次开发与课程设计答辩展示。通过深入阅读源码可掌握OpenCV人脸检测、深度学习模型推理与Flask框架结合的实现思路具有较高的实战参考价值。1. 基于深度学习的人脸识别签到系统不只是个毕业设计人脸识别考勤听起来是“识别一下打个卡”的小事但真把一个基于深度学习的人脸识别签到系统从训练跑到落地上线中间全是暗坑门口逆光拍不清、戴个口罩全员掉线、换了个摄像头角度识别率能掉一半。这套以“毕业设计”为定位的项目表面上是交差用的课题实际代码把模型训练、特征入库、签到记录、阈值调优这几件事全部串起来了。无论你是要完成一个毕业课题还是想在团队内部做一套轻量考勤工具这套系统的架构思路和核心实现都值得照着搬一遍。这篇笔记就带你拆开它从技术选型到运行改造一条路走完。2. 系统架构与核心技术选型从摄像头到考勤记录的完整链路2.1 技术选型为什么是深度学习而不是传统方法传统人脸识别方案比如基于LBPH或EigenFace的老路子对角度、光线、遮挡极其敏感。在签到这种真实场景下光线一变准确率掉得飞快更不用说换副眼镜换个发型。深度学习方案靠大规模人脸数据集预训练的卷积神经网络把一张人脸映射成一个高维特征向量在姿态和光照变化下的鲁棒性明显更强。技术栈选型上这套系统的核心依赖如下表模块技术选型选择理由后端框架Flask轻量、接口开发快适合中小规模系统深度学习框架PyTorch动态图调试方便生态成熟人脸检测RetinaFace自带五关键点输出检测框精度高特征提取ArcFace角度间隔损失开集识别效果好推理加速onnxruntimeCPU推理比PyTorch原生快不少不依赖GPU数据库SQLite / MySQL开发用SQLite部署切MySQL前端原生HTML JavaScript canvas不引入前端框架聚焦核心逻辑为什么用Flask而不是Django这个问题每次都要解释一遍。Django自带admin后台、ORM和中间件体系功能全但重签到系统只需要注册、签到、查询三个接口Flask的灵活性和低复杂度更合适。刚入门的开发者看Flask代码能快速理解请求处理流程后面改起来也没有额外学习成本。ArcFace的选型也是同理。现在人脸识别模型很多FaceNet、CosFace、ArcFace各有侧重。ArcFace通过角度间隔在特征空间里拉大类间距离在公开测试集上的表现稳定InsightFace官方提供的预训练权重直接可以用不必从零开始训练这步省了非常多时间。2.2 整体架构前端页面、后端服务与模型推理如何协作这套系统的整体架构分成三层浏览器端负责摄像头采集和画面预览后端服务负责请求路由和业务逻辑模型推理层负责从图像里找脸和提取特征。三层之间通过HTTP接口通信前端不需要接触任何深度学习代码。数据流的完整链路是这样用户点击“签到”浏览器调用getUserMedia打开摄像头视频流实时显示在页面上。用户摆正姿势点击“拍照”前端用canvas截取当前帧压缩成JPEG后通过FormData发送到后端。后端收到图片后先交给推理服务做人脸检测。检测不到人脸就返回提示检测到就裁剪出人脸区域做仿射对齐。对齐后的图像送入特征提取模型输出一个512维的归一化特征向量。后端拿这个特征向量去数据库里逐条比对计算余弦相似度。最高分超过阈值就判定为同一个人写入签到记录否则返回识别失败。模块划分上后端拆成两个核心service这个设计直接影响后面二次开发的成本FaceService封装所有AI能力对外只暴露一个方法detect_and_embed(image)输入一张图片输出人脸是否找到和对应的特征向量。AttendanceService负责所有数据库操作包括注册人脸、比对识别、记录签到、查询历史。这样拆的好处是以后换人脸算法只用改FaceService签到业务一行都不用动。我后面做门禁改造的时候感触很深模型是“换脸”最容易的部分真正费时的是把业务和模型耦合在一起的代码。2.3 数据库设计用户表、考勤表与特征存储考勤系统的表结构很简单核心就是用户表和签到记录表。这次我用SQLite来做开发库生产环境平滑切MySQL建表脚本如下CREATE TABLE user ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL UNIQUE, feature BLOB NOT NULL, created_at DATETIME DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE attendance ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL, sign_time DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (user_id) REFERENCES user(id) ); CREATE INDEX idx_attendance_user_time ON attendance(user_id, sign_time);最后一个索引很关键签到查询频繁用user_id sign_time做过滤没有索引的话数据量大了以后每次签到都要全表扫一遍。我在第一次测试时没加这个索引500条记录还不明显等导入2000条历史数据后重复签到检查从2毫秒涨到150毫秒加了索引直接回到个位数毫秒。为什么特征要存BLOB而不是直接存文本原因有三第一512维float数组用二进制存储体积最小每个人512乘以4约等于2KB一千人才2MB第二numpy数组转bytes可以零拷贝快速还原用np.frombuffer一秒读几千条没压力第三避免JSON序列化把小数精度损耗掉虽然0.0001的精度变化对余弦相似度影响不大但做工程时省一点是一点。再来看注册和签到两条流程注册流程用户上传一张正脸照片 → 后端检测人脸并提取特征检测不到人脸直接返回错误 → 检查姓名是否重复注册 → 把特征以BLOB形式插入用户表。签到流程摄像头截帧上传 → 提取特征 → 全表循环计算相似度 → 最高分低于阈值则识别失败 → 高于阈值但今天已签到则返回“今日已签到” → 高于阈值且未签到则插入考勤记录。这套流程的优点是简单可靠缺点是全表扫描的性能瓶颈。注册人数到几千人以后每次签到要算几千次余弦相似度实测耗时在18到30毫秒波动考虑到签到场景的并发量这个开销完全可以接受。真到了上万人再考虑引入向量检索。3. 核心模块拆解人脸检测、特征提取与签到逻辑实现3.1 人脸检测定位RetinaFace的关键点输出与后处理人脸检测是整条识别链路的第一个环节选RetinaFace是因为它同时输出检测框和五个关键点坐标关键点一是用来判断人脸朝向二是做对齐仿射变换。代码实现不复杂import cv2 from retinaface import RetinaFace def detect_face(img): # img 是 BGR 格式的 numpy 数组 faces RetinaFace.detect_faces(img) if not faces: return None, None # 只取置信度最高的一张脸 key list(faces.keys())[0] face faces[key] box face[facial_area] # [x1, y1, x2, y2] landmarks face[landmarks] # 五个关键点的字典 score face[score] return box, landmarks返回的faces是一个字典key是face_1这样的字符串。facial_area是检测框的坐标列表landmarks包含left_eye、right_eye、nose、mouth_left、mouth_right五个关键点每个点都是[x, y]。这里只取第一个检测结果默认签到场景画面里只有一个人。如果画面里可能有多人就得遍历所有检测框逐一截取并识别那个逻辑就得从dict.values()循环处理。有个参数容易忽略RetinaFace.detect_faces默认的置信度阈值在0.9左右实际用下来0.8更合理。摄像头距离人两米左右时小脸框的置信度天然偏低阈值定太高会导致漏检。我调到0.8之后教室后排的签到成功率从70%升到了92%。3.2 特征提取ArcFace的512维向量与归一化细节检测到人脸框后把框内区域裁剪出来对齐后送入特征提取模型。实际项目中我直接用InsightFace封装好的接口它内部把检测、对齐和特征提取全串好了比自己手工拼要稳import insightface import numpy as np # buffalo_l 包含检测、关键点、特征提取全套模型 model insightface.app.FaceAnalysis(namebuffalo_l) model.prepare(ctx_id0, det_size(640, 640)) def get_embedding(img): faces model.get(img) if len(faces) 0: return None face faces[0] # normed_embedding 是已经归一化的512维向量 return face.normed_embedding.astype(np.float32) def cosine_similarity(a, b): # 两个向量都归一化后点积等于余弦相似度 return float(np.dot(a, b))两点值得说明normed_embedding已经做了L2归一化省去了手工归一化的步骤这样做的好处是后续比对只需要一次点积不用再做除法astype(np.float32)是为了控制内存体积float64虽然精度高但对余弦相似度没有任何帮助存储和比对耗时都翻倍。预处理还有一个细节model.prepare(ctx_id0, det_size(640, 640))里的det_size控制检测分辨率。分辨率越高小脸检测越准但推理耗时也越长。我试过(1920, 1080)的输入单次推理慢了一倍而识别率几乎没有提升反而因为图片过大产生了大量不必要的计算。3.3 签到业务识别成功后的防重复与时间逻辑识别通过之后核心业务逻辑反而更考验工程细节。防重复签到和跨天判断是这部分的两个坑直接看代码from flask import Flask, request, jsonify import sqlite3 import datetime import numpy as np app Flask(__name__) app.route(/sign, methods[POST]) def sign(): file request.files[image] img file.read() embedding get_embedding_from_bytes(img) if embedding is None: return jsonify({code: 400, msg: 未检测到人脸}) conn sqlite3.connect(attendance.db) cur conn.cursor() # 全表扫描逐条比对特征 cur.execute(SELECT id, name, feature FROM user) rows cur.fetchall() best_match None best_score -1 for uid, name, feat_blob in rows: feat np.frombuffer(feat_blob, dtypenp.float32) score cosine_similarity(embedding, feat) if score best_score: best_score score best_match (uid, name) if best_score 0.6: conn.close() return jsonify({code: 401, msg: 识别失败}) today datetime.date.today().isoformat() cur.execute( SELECT COUNT(*) FROM attendance WHERE user_id? AND sign_time?, (best_match[0], today) ) if cur.fetchone()[0] 0: conn.close() return jsonify({code: 403, msg: 今日已签到}) cur.execute(INSERT INTO attendance(user_id) VALUES(?), (best_match[0],)) conn.commit() conn.close() return jsonify({code: 200, msg: f签到成功{best_match[1]}})这段代码有三个关键设计。第一np.frombuffer直接把BLOB还原成numpy数组比用pickle先反序列化一堆对象快得多。第二查重复签到用的是sign_time today这样即使签到记录里存的是带时分秒的时间戳也能正确覆盖当天数据。第三事务提交是在确认没有重复签到之后才完成的把数据库锁的持有时间压到最短。3.4 前端摄像头调用getUserMedia与canvas截帧前端这块核心代码只有几十行但有一个安全限制最容易踩const video document.getElementById(video); navigator.mediaDevices.getUserMedia({ video: { width: 640, height: 480 } }).then(stream { video.srcObject stream; }).catch(err { console.error(摄像头权限被拒绝, err); }); function captureAndUpload() { const canvas document.getElementById(canvas); canvas.width 640; canvas.height 480; const ctx canvas.getContext(2d); ctx.drawImage(video, 0, 0, 640, 480); canvas.toBlob(blob { const formData new FormData(); formData.append(image, blob, capture.jpg); fetch(/sign, { method: POST, body: formData }).then(res res.json()) .then(data alert(data.msg)); }, image/jpeg, 0.9); }canvas.toBlob的第三个参数是压缩质量0.9时图片体积大概60KB识别精度基本无损。想省流量可以调到0.8约40KB识别效果差距不大。真正要命的是getUserMedia必须运行在HTTPS或者localhost环境否则浏览器会直接拒绝报NotAllowedError。本地调试用127.0.0.1没问题一旦部署到公网服务器上不配置SSL证书摄像头永远打不开。这个坑卡了很多人。4. 避坑指南训练数据、模型精度与并发签到的常见问题4.1 现象注册时明明能识别真实签到却总是失败原因是注册阶段和签到阶段的数据分布差异太大。注册时通常人在镜头正中光线均匀表情自然真实签到场景里可能出现侧脸、逆光、低头看手机这些情况模型提取出的特征和注册时相差很远。解决方法是注册阶段就采集多角度图片。至少三张正脸、左右各偏15度每张之间隔两秒拍摄让表情略有变化。同时签到端做一个简单活体检测让用户眨眼或者缓缓转头一方面挡掉用照片打卡的情况另一方面也把极端角度下质量太差的帧过滤掉。代码上在拿到视频流后连续判断三帧人脸框位置是否连续变化变化过小就当成静态照片拒绝。4.2 现象服务器上模型越跑越慢内存还暴涨前因后果是这样的InsightFace默认加载全套模型包括检测、关键点、特征提取内存占用接近2GB。如果服务器没有独立显卡模型被迫走CPU推理一次识别耗时1到2秒并发一多接口直接卡死。解决方案是收缩检测尺寸。model.prepare(ctx_id-1, det_size(320, 320))可以让精度略降但速度提升四倍左右。另外后端要限制并发推理数量我一般用一个ThreadPoolExecutor(max_workers2)模型推理串行化让每次请求的响应时间稳定在800毫秒内。from concurrent.futures import ThreadPoolExecutor inference_pool ThreadPoolExecutor(max_workers2) def handle_sign(request): future inference_pool.submit(run_inference, request.files[image].read()) embedding future.result(timeout5)4.3 现象多人同时签到报database is locked原因很简单SQLite默认只允许一个写连接两个请求同时执行INSERT时第二个请求会抛OperationalError: database is locked。解决思路有两个一是换MySQL但从毕设体量来说有点杀鸡用牛刀二是加一个全局写锁把插入操作串行化。import threading write_lock threading.Lock() app.route(/sign, methods[POST]) def sign(): # 前面的识别逻辑省略 with write_lock: conn sqlite3.connect(attendance.db) cur conn.cursor() cur.execute(INSERT INTO attendance(user_id) VALUES(?), (uid,)) conn.commit() conn.close()注意这里的lock必须是全局对象不能放在函数内部否则多个线程各持一把锁等于没锁。另外不要在多个线程里各自创建SQLite连接并同时写库就算加了锁也会偶发死锁统一用一个连接配合锁是最稳的。4.4 现象模型路径在Windows下诡异报错模型路径写死成Linux风格是毕设项目里的老演员了比如/home/user/model/arcface.onnx代码拷到Windows电脑上直接运行报FileNotFoundError。解决方法是不要用绝对路径在项目根目录建一个config.py用os.path.dirname(__file__)拼路径import os BASE_DIR os.path.dirname(os.path.abspath(__file__)) MODEL_DIR os.path.join(BASE_DIR, models) MODEL_PATH os.path.join(MODEL_DIR, arcface.onnx)这个习惯不仅是Windows和LInux通用问题也决定了模型文件能不能随项目打包迁移。下次换电脑跑项目不用再改任何路径相关代码。4.5 现象相似度阈值怎么调都不对有人说0.5好有人说0.7好其实阈值只跟你的模型和数据分布有关。直接用默认阈值容易上瘾但真上线后误识别率可能高得离谱。正确做法是自己统计阈值分布。准备50对同一个人不同角度的照片再准备50对不同人的照片跑一遍特征提取和相似度计算画出两组分数分布曲线取两个峰谷中间的位置作为阈值。我这套系统的阈值最终定在0.6因为同人最小分数0.58异人最大分数0.44中间隔了0.140.6正落在安全区。如果你的数据分布不同这个值要重新标定。5. 项目运行与部署从数据集准备到上线签到的实操步骤5.1 环境准备与依赖安装顺序整套系统基于Python 3.9开发深度学习框架用PyTorch。依赖安装的顺序很重要顺序错了会装到CPU版PyTorchGPU白瞎# 先装CUDA版PyTorch pip install torch torchvision --index-url https://download.pytorch.org/whl/cu118 # 再装人脸识别和推理库 pip install insightface onnxruntime # 最后装Web框架和工具库 pip install flask flask-cors numpy opencv-python如果服务器没有NVIDIA显卡把第一行的cu118换成cpu即可。有个血泪经验如果你先装了insightface再装torchpip自动解析依赖时会下载CPU版torch之后就算你重新装CUDA版也容易版本冲突。所以顺序必须固定先torch后insightface。5.2 数据集准备与模型微调可选预训练模型在通用人脸识别上已经够用但如果你要识别的是公司内部人员或者同班同学通用模型在特定人脸上的精度可能不够。微调这一步能显著改善效果数据集目录规范如下dataset/ class1_name/ img1.jpg img2.jpg class2_name/ img1.jpg img2.jpg每个文件夹代表一个人文件夹名就是识别结果里的姓名图片建议用摄像头采集的真实照片每类至少10张正脸和侧脸混合。用PyTorch的torchvision.datasets.ImageFolder可以直接读这个结构省去自己写数据加载器。微调的核心是损失函数用ArcFace的ArcfaceLoss学习率从0.001开始在验证集上观察loss下降一批后降到1e-4。验证集里必须包含真实签到场景的摄像头截图不能只用干净的网图验证否则微调出来的模型在真实环境中还是不稳定。我用这种方式把内部人员识别率从91%提到了97%。5.3 启动服务与测试项目入口是run.py启动后监听8000端口测试流程用命令行配合curl最直观# 启动服务 python run.py # 注册人脸传一张包含正脸的图片 curl -X POST \ -F name张三 \ -F image./test_data/zhangsan.jpg \ http://localhost:8000/register # 识别签到传一张摄像头截图 curl -X POST \ -F image./test_data/capture.jpg \ http://localhost:8000/sign注册接口返回{code: 200}表示特征已入库签到接口返回{code: 200, msg: 签到成功张三}表示识别通过。如果返回401优先检查图片是否包含清晰人脸再检查阈值配置。用curl先把接口跑通再对接前端页面能省下很多联调时排查问题的力气。5.4 生产环境部署三处必改的工程点代码在本地跑通和线上稳定运行之间隔了三个必须处理的问题。第一SQLite换成MySQL解决并发写锁问题第二Flask开发服务器换成gunicorn解决HTTP并发能力问题第三模型常驻一个独立进程避免每次请求都重新加载模型导致响应延迟。# gunicorn 启动4个worker进程 gunicorn -w 4 -b 0.0.0.0:8000 run:app这里有个细节-w 4不要理解成越多越好每个worker进程都会拷贝一份模型到内存4个worker就是4份2GB内存直接变8GB。一般2个worker够用如果服务器内存有限用1个worker配合线程模式更省资源。6. 二次开发方向从签到系统到通用人脸识别平台6.1 识别与业务解耦从考勤改造成门禁最自然的改法是把“识别成功即签到”替换成“识别成功即开门”。在AttendanceService之外加一个AccessService把核心判断逻辑抽出来识别成功后调用外设模块。比如在某ARM开发板上接一个继电器import serial ser serial.Serial(/dev/ttyUSB0, 9600) def open_door(): ser.write(b1) # 高电平触发继电器 time.sleep(1) ser.write(b0)核心思路是FaceService只回答“这个人是谁”业务层决定“接下来干什么”。换一个场景就重写业务层识别链路不动。这也是整个项目最值得保留的架构设计。6.2 性能优化用特征向量缓存替代全库扫描注册人数超过3000人后全表扫描的延迟开始明显。可以用BallTree把所有特征加载进内存做近邻检索from sklearn.neighbors import BallTree # 启动时加载全部特征 features load_all_features() # shape: (N, 512) tree BallTree(features, metriccosine) # 识别时 dist, idx tree.query([new_feature], k1) threshold 0.4 # BallTree的cosine距离与相似度相反注意这里的metriccosine返回的是距离值距离越小越相似和之前余弦相似度的方向相反。另外注册接口每次插入新用户后要重建树虽然重建有开销但注册频率低这个成本完全可接受。6.3 端侧部署替换轻量移动端模型如果要把签到系统跑到平板上现在的InsightFace全套模型太重。现成方案是换成MobileFaceNet或SFace量化成int8后模型体积降到几MB在ARM处理器上也能保持实时的推理速度。替换模型这个动作恰好验证了架构设计的合理性只要服务接口不变输入是图片输出是特征向量内部模型随意替换。把FaceService的模型权文件换掉改几行加载配置业务层完全不用动。我后来接平板方案时整个替换过程不到半小时。做完这套系统我最大的收获不是某个模型怎么调参而是认识到识别精度只是系统里很小的一块拼图。真正的难点在工程链路图像采集质量、特征存储格式、相似度阈值、并发控制、数据库锁任何一环出问题都会让你误以为是模型不行。从那以后我每次搭人脸识别项目都强制先跑通一条最小链路——拍一张照、提取特征、比对、写入记录——再往上叠需求。这个习惯帮我少走了很多弯路希望也能帮到你。本文还有配套的精品资源点击获取