资讯动态

基于MTCNN与FaceNet的人脸识别签到系统:从选型到部署的工程实践

发布时间:2026/9/29 5:13:47 来源:尧图企业网站定制
简介这份资源是面向高校学生与深度学习入门者的毕业设计完整项目围绕人脸识别签到场景展开解决从算法调用到Web系统落地的一整套实现问题。压缩包共27个文件约101.47MB以8个Python源码为核心配合7个HTML页面模板、SQLite数据库、CSS样式、mako迁移脚本及字体文件等覆盖后端逻辑、前端界面与数据持久化各环节。项目内置管理员初始化与数据库升级脚本附带依赖清单与说明文档便于快速搭建运行环境。目前已有404人学习下载说明其在同类毕设选题中具备一定参考价值。读者可从中获得人脸注册与识别签到的主流程代码、用户管理页面、数据库迁移方案以及字体转图等辅助工具适合作为毕设模板、课程设计参考或深度学习工程化练手素材帮助理解模型调用与Web系统整合的完整思路。1. 从一张签到表到一套能跑的人脸识别系统每年毕业季计算机毕业设计选题里总有一类题目经久不衰基于深度学习的人脸识别签到系统。它听起来像是把两个成熟技术拼在一起但真正动手做的人会发现从调用一个开源模型到让系统在教室门口稳定签到中间隔着一堆工程细节。人脸识别签到系统的核心链路其实很清晰摄像头采集画面人脸检测框出位置人脸识别模型提取特征并与底库比对比对成功则写入签到记录。这条链路里深度学习负责的是检测和识别两个环节签到系统负责的是业务逻辑、数据存储和交互界面。适合做这个题目的是计算机、软件工程、电子信息类专业的本科毕业生也适合想用一个人脸识别项目入门深度学习部署的开发者。难点不在模型本身而在于如何把模型推理、数据库、前端界面串成一条能稳定运行的流水线。下面按实际落地顺序把选型、实现、参数和踩坑一次讲清楚。2. 人脸识别签到系统的技术选型为什么是 MTCNN FaceNet 而不是端到端大模型2.1 检测与识别为什么要拆成两段很多人第一反应是找一个端到端的人脸识别模型输入图片直接输出身份。这种方案在学术数据集上精度很高但在毕业设计场景里往往翻车。原因在于签到场景的人脸是动态的学生走进教室、低头、侧脸、光线变化端到端模型对未对齐的人脸非常敏感。常见做法是把任务拆成检测和识别两段。检测负责回答“人脸在哪”识别负责回答“这是谁”。检测阶段用 MTCNN 或 RetinaFace识别阶段用 FaceNet 或 ArcFace。拆开的好处是每一段都可以单独调试和替换检测框不准时调检测阈值识别不准时换识别模型或调距离阈值不会牵一发动全身。MTCNN 由三个阶段网络组成P-Net 快速生成候选框R-Net 过滤掉大量误检O-Net 输出最终边界框和五个关键点。它的优势是轻量、对侧脸和遮挡有一定鲁棒性在 CPU 上也能跑到接近实时。FaceNet 的核心是三元组损失训练出的 128 维或 512 维嵌入向量同一个人的人脸向量距离小不同人距离大。签到系统只需要计算当前人脸向量与底库中每个向量的欧氏距离取最小值判断是否小于阈值即可。这个阈值就是签到系统的“后悔药”设得太松会认错人设得太紧会频繁拒识。2.2 环境配置与依赖安装毕业设计最怕环境配不起来。我一般建议用 Python 3.8 到 3.10太新的版本某些深度学习库轮子不全。下面是一套经过验证的依赖组合直接抄作业即可。# 创建虚拟环境避免污染系统 Python python -m venv face_env source face_env/bin/activate # Windows 用 face_env\Scripts\activate # 安装核心依赖 pip install torch1.13.1 torchvision0.14.1 --index-url https://download.pytorch.org/whl/cpu pip install facenet-pytorch2.5.2 pip install opencv-python4.8.1.78 pip install numpy1.24.3 pip install flask2.3.3 pip install flask-sqlalchemy3.0.5这里选 CPU 版 PyTorch 是因为毕业设计演示环境不一定有 GPUMTCNN FaceNet 在 CPU 上单张人脸推理大约 200 到 400 毫秒签到场景完全够用。facenet-pytorch 这个库把 MTCNN 和 InceptionResnetV1 封装好了省去自己写网络结构的麻烦。Flask 用来做后端接口和简单的管理页面SQLAlchemy 负责 SQLite 数据库操作。如果学校要求用 MySQL把连接字符串换掉即可代码逻辑不变。2.3 底库构建人脸注册的完整流程签到系统必须先有底库。底库就是每个学生的人脸特征向量和姓名的对应关系。注册流程分三步采集、检测对齐、提取特征入库。import cv2 import torch import numpy as np from facenet_pytorch import MTCNN, InceptionResnetV1 from PIL import Image # 初始化检测和识别模型 device torch.device(cpu) mtcnn MTCNN(keep_allTrue, devicedevice) resnet InceptionResnetV1(pretrainedvggface2).eval().to(device) def register_face(image_path, name, db_session): 注册单张人脸到数据库 image_path: 人脸照片路径 name: 学生姓名 db_session: SQLAlchemy 会话对象 img Image.open(image_path).convert(RGB) # 检测人脸框和关键点 boxes, probs, landmarks mtcnn.detect(img, landmarksTrue) if boxes is None or len(boxes) 0: return {ok: False, msg: 未检测到人脸} if len(boxes) 1: return {ok: False, msg: 检测到多张人脸请使用单人照片} # 根据检测框裁剪并对齐人脸 face mtcnn(img) if face is None: return {ok: False, msg: 人脸对齐失败} # 提取 512 维特征向量 with torch.no_grad(): embedding resnet(face.unsqueeze(0)).squeeze().numpy() # 归一化方便后续用欧氏距离比较 embedding embedding / np.linalg.norm(embedding) # 写入数据库 record FaceRecord(namename, embeddingembedding.tobytes()) db_session.add(record) db_session.commit() return {ok: True, msg: f{name} 注册成功}这段代码的关键点有三个。第一keep_allTrue让 MTCNN 检测所有人脸注册时如果检测到多张就拒绝避免把别人的脸注册进去。第二mtcnn(img)返回的是对齐并裁剪后的 224x224 人脸张量FaceNet 要求输入就是这个人脸区域不要直接把原图丢给 ResNet。第三特征向量做了 L2 归一化这样两张人脸的欧氏距离范围在 0 到 2 之间阈值设定有明确的物理意义。底库中每个人存一条记录embedding 字段用二进制存储512 个 float32 占 2048 字节一千个学生也才 2MBSQLite 完全扛得住。2.4 签到比对距离阈值怎么定签到时的比对逻辑是检测当前画面中的人脸提取特征与底库中所有向量计算欧氏距离取最小距离。如果最小距离小于阈值判定为同一个人写入签到记录否则提示未注册。def recognize_face(frame, db_session, threshold0.9): 对一帧画面进行人脸识别 frame: OpenCV BGR 图像 threshold: 欧氏距离阈值越小越严格 img Image.fromarray(cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)) boxes, probs mtcnn.detect(img) if boxes is None: return [] results [] faces mtcnn(img) # 返回对齐后的人脸张量 if faces is None: return [] # 查询底库所有记录 records db_session.query(FaceRecord).all() if not records: return [] # 构建底库矩阵 db_embeddings np.stack([ np.frombuffer(r.embedding, dtypenp.float32) for r in records ]) db_names [r.name for r in records] with torch.no_grad(): embeddings resnet(faces).numpy() # 逐张人脸比对 for i, emb in enumerate(embeddings): emb emb / np.linalg.norm(emb) distances np.linalg.norm(db_embeddings - emb, axis1) min_idx np.argmin(distances) min_dist distances[min_idx] if min_dist threshold: results.append({ name: db_names[min_idx], distance: float(min_dist), box: boxes[i].tolist() }) else: results.append({ name: 未知, distance: float(min_dist), box: boxes[i].tolist() }) return results阈值 0.9 是 FaceNet 在 VGGFace2 预训练权重下的经验值。实际部署时建议用一批已知身份的照片做 ROC 曲线找到等错误率对应的距离。如果底库只有几十个人阈值可以放到 1.0 到 1.1如果底库上千人建议收紧到 0.8 左右因为底库越大撞脸的概率越高。这个参数没有绝对标准必须用自己的数据校准。我一般会留一个管理接口允许在页面上动态调整阈值方便演示时根据现场光线快速修正。3. 从摄像头到签到记录把推理链路串成可运行的服务3.1 视频流采集与帧采样策略签到系统不需要对每一帧都做推理。摄像头 30 帧每秒如果每帧都跑 MTCNN FaceNetCPU 直接满载而且同一个人会被反复签到。常见做法是每隔 N 帧取一帧做推理N 取 5 到 10。同时加一个去重逻辑同一个人如果在 30 秒内已经签到过不再重复写入。import time import cv2 from collections import defaultdict class AttendanceService: def __init__(self, db_session, threshold0.9, frame_interval5, cooldown30): self.db db_session self.threshold threshold self.frame_interval frame_interval self.cooldown cooldown self.last_seen defaultdict(float) # 记录每个人上次签到时间 self.frame_count 0 def process_frame(self, frame): 处理单帧返回识别结果列表 self.frame_count 1 # 跳帧降低 CPU 负载 if self.frame_count % self.frame_interval ! 0: return [] results recognize_face(frame, self.db, self.threshold) valid [] now time.time() for r in results: if r[name] 未知: continue # 冷却期内不重复签到 if now - self.last_seen[r[name]] self.cooldown: continue self.last_seen[r[name]] now # 写入签到记录 record AttendanceRecord(namer[name], timestampnow) self.db.add(record) self.db.commit() valid.append(r) return validframe_interval5意味着每 5 帧推理一次30 帧的摄像头实际推理频率是 6 次每秒CPU 占用能控制在 60% 左右。cooldown30是冷却时间防止同一个人站在摄像头前被连续签到。这两个参数要根据机器性能调整如果 CPU 较弱把 frame_interval 调到 10冷却时间保持 30 秒不变。注意last_seen用字典存在内存里服务重启会丢失生产环境应该把冷却状态也写进数据库或 Redis但毕业设计演示场景内存字典足够。3.2 Flask 接口与前端页面最小实现后端提供两个核心接口一个用于接收摄像头帧并返回识别结果一个用于查询签到记录。前端用一个简单的 HTML 页面调摄像头通过 WebSocket 或定时 POST 发送帧。from flask import Flask, request, jsonify, render_template import base64 import numpy as np import cv2 app Flask(__name__) service AttendanceService(db_session) app.route(/api/recognize, methods[POST]) def api_recognize(): 接收 base64 图片返回识别结果 data request.json.get(image, ) if not data: return jsonify({ok: False, msg: 缺少图片数据}) # 去掉 base64 前缀 if , in data: data data.split(,)[1] img_bytes base64.b64decode(data) img_array np.frombuffer(img_bytes, dtypenp.uint8) frame cv2.imdecode(img_array, cv2.IMREAD_COLOR) if frame is None: return jsonify({ok: False, msg: 图片解码失败}) results service.process_frame(frame) return jsonify({ok: True, results: results}) app.route(/api/records) def api_records(): 查询今日签到记录 records db_session.query(AttendanceRecord).order_by( AttendanceRecord.timestamp.desc() ).limit(100).all() return jsonify([{ name: r.name, time: time.strftime(%Y-%m-%d %H:%M:%S, time.localtime(r.timestamp)) } for r in records])前端页面用navigator.mediaDevices.getUserMedia获取摄像头画到 canvas 上每隔 200 毫秒转成 base64 发给后端。这个间隔对应 5 帧一次推理和上面的 frame_interval 配合。注意浏览器要求 HTTPS 或 localhost 才能调摄像头本地演示用 localhost 没问题如果要在局域网其他机器访问需要自签证书或部署到有 HTTPS 的服务器。签到记录页面直接轮询/api/records每 3 秒刷新一次展示姓名和时间。3.3 数据库表结构设计两张表就够人脸底库表和签到记录表。底库表存姓名和特征向量签到记录表存姓名和时间戳。如果要做多课程签到再加一个课程表和外键。CREATE TABLE face_record ( id INTEGER PRIMARY KEY AUTOINCREMENT, name VARCHAR(50) NOT NULL, embedding BLOB NOT NULL, created_at DATETIME DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE attendance_record ( id INTEGER PRIMARY KEY AUTOINCREMENT, name VARCHAR(50) NOT NULL, timestamp FLOAT NOT NULL, course_id INTEGER DEFAULT 0 ); CREATE INDEX idx_attendance_name ON attendance_record(name); CREATE INDEX idx_attendance_time ON attendance_record(timestamp);embedding 用 BLOB 存二进制比存 JSON 数组省空间也更快。timestamp 用浮点数存 Unix 时间戳方便排序和范围查询。索引加在 name 和 timestamp 上查询某个人某天的签到记录时不用全表扫描。如果底库要支持一个人多张照片把 face_record 的主键改成自增name 上不加唯一约束即可识别时取距离最小的那条记录。3.4 活体检测的轻量替代方案照片翻拍是签到系统最常见的攻击方式。毕业设计不要求工业级活体检测但至少要有基本防御。最简单的做法是要求眨眼或张嘴用 MTCNN 的五个关键点计算眼睛纵横比。连续几帧内眼睛纵横比从大到小再变大判定为眨眼。def eye_aspect_ratio(landmarks): 计算眼睛纵横比landmarks 为 MTCNN 输出的五个关键点 # landmarks 顺序左眼、右眼、鼻子、左嘴角、右嘴角 left_eye landmarks[0] right_eye landmarks[1] # 简化计算用眼睛区域的宽高比近似 left_ear abs(left_eye[1] - left_eye[3]) / (abs(left_eye[0] - left_eye[2]) 1e-6) right_ear abs(right_eye[1] - right_eye[3]) / (abs(right_eye[0] - right_eye[2]) 1e-6) return (left_ear right_ear) / 2.0实际使用时维护一个长度为 10 的队列记录连续帧的 EAR 值。如果队列中最大值与最小值之差超过 0.15且最小值出现在中间位置判定为一次眨眼。这个逻辑对正面人脸有效侧脸时关键点不准EAR 会波动所以活体检测只在识别成功后触发不通过则拒绝签到。更严格的方案是用红外摄像头或 3D 结构光但毕业设计用普通 RGB 摄像头加眨眼检测已经能挡住大部分照片攻击。4. 避坑与排查人脸识别签到系统最容易翻车的五个地方4.1 检测不到人脸框时有时无现象摄像头画面里明明有人MTCNN 返回的 boxes 是 None或者框的位置飘忽不定。原因通常是光照不足或人脸角度过大。MTCNN 在暗光下召回率明显下降侧脸超过 45 度也容易漏检。解决方法是加一个简单的图像预处理先做直方图均衡化再送检测。如果现场光线实在差考虑换 RetinaFace它对暗光和侧脸的鲁棒性更好但推理速度慢一倍。另一个常见原因是输入图像尺寸太大MTCNN 内部会缩放但缩放比例不合适时小脸会丢。建议把摄像头分辨率设成 640x480不要用 1080p大分辨率对检测精度没有本质提升反而拖慢速度。4.2 同一个人被识别成不同名字现象底库里有张三但签到记录里出现了李四或者同一个人今天签到显示张三明天显示王五。原因几乎都是阈值设得太松。FaceNet 在 VGGFace2 上预训练时不同人之间的最小距离可能低到 0.7 左右如果阈值设成 1.1撞脸概率很高。解决方法是收紧阈值到 0.8 到 0.9同时检查底库照片质量。如果底库里某个人只有一张模糊照片他的特征向量本身就不稳定识别时容易漂移。建议每个人注册 3 到 5 张不同角度的照片取平均向量或保留多条记录取最小距离。另外注册时一定要用 MTCNN 对齐后的人脸不要直接把原图裁剪区域丢给 FaceNet对齐与否对特征质量影响很大。4.3 CPU 占用 100%画面卡顿现象程序跑起来后风扇狂转摄像头画面延迟好几秒签到响应慢。原因是每帧都跑了 MTCNN 和 FaceNet没有跳帧。解决方法是加 frame_interval每 5 到 10 帧推理一次。另外检查是否在循环里反复创建 MTCNN 和 ResNet 对象这两个模型初始化很耗时必须放在循环外面只创建一次。还有一个隐藏坑PyTorch 默认使用所有 CPU 核心在 4 核机器上跑推理会把其他进程也拖慢。可以在代码开头加torch.set_num_threads(2)限制推理线程数留出核心给 Flask 和摄像头采集。4.4 数据库写入冲突导致签到丢失现象多人同时签到偶尔有人的记录没写进去或者 Flask 报数据库锁错误。原因是 SQLite 默认的并发写入能力弱多个请求同时 commit 会锁表。解决方法是把数据库操作串行化用一个全局锁或者队列。简单做法是在 Flask 里用threading.Lock()包住 commit 操作。如果并发量真的很大换 PostgreSQL 或 MySQL但毕业设计场景一般每天几十到几百次签到SQLite 加锁完全够用。另一个容易忽略的点是 embedding 字段用 BLOB 存储时不同数据库驱动的字节序可能不一致从 SQLite 迁移到 MySQL 时要注意重新注册底库。4.5 浏览器调摄像头失败现象前端页面提示getUserMedia is not defined或NotAllowedError。原因有三个一是页面不是通过 HTTPS 或 localhost 访问的浏览器安全策略禁止非安全上下文调摄像头二是用户拒绝了摄像头权限三是页面在 iframe 里iframe 没有 allow 属性。解决方法是本地演示用http://localhost:5000访问局域网访问时用ngrok或自签证书起 HTTPS。权限问题需要用户在浏览器地址栏手动允许代码里可以加navigator.mediaDevices.getUserMedia({video: true})的 catch 分支提示用户检查权限。iframe 场景在毕业设计里少见但如果用 Vue 或 React 开发注意开发服务器的 host 配置。5. 让签到系统更稳的几个进阶技巧5.1 用特征向量平均提升底库稳定性一个人注册多张照片时不要只存一条记录。把多张照片的特征向量取平均再归一化能显著降低单张照片质量差带来的漂移。具体做法是注册时对同一个人采集 3 到 5 张分别提取特征求平均后存一条记录。如果担心平均会模糊个体差异也可以保留多条记录识别时取所有记录中的最小距离。我一般用平均向量因为底库小的时候平均更稳底库大时多记录更灵活。平均向量的代码很简单def average_embedding(embeddings): 对多张人脸的特征向量取平均并归一化 stacked np.stack(embeddings) mean_emb np.mean(stacked, axis0) return mean_emb / np.linalg.norm(mean_emb)注意平均前每个向量都要先归一化否则模长大的向量会主导平均结果。这个技巧在底库只有几十个人时效果最明显能把误识率降低一个数量级。5.2 用签到时间窗口过滤重复记录冷却时间只能防同一个人短时间内重复签到但如果一个人上午签到了下午又出现在摄像头前冷却时间已经过了会再签一次。如果业务要求每天只签一次需要在写入前查当天是否已有记录。查询用 timestamp 范围过滤比字符串比较日期更高效。import datetime def already_signed_today(db_session, name): 检查某人今天是否已签到 today_start datetime.datetime.now().replace( hour0, minute0, second0, microsecond0 ).timestamp() count db_session.query(AttendanceRecord).filter( AttendanceRecord.name name, AttendanceRecord.timestamp today_start ).count() return count 0这个查询走 timestamp 索引一千条记录里查一次不到 1 毫秒。如果要做多课程签到把 course_id 也加进过滤条件。注意时间戳用本地时区的零点如果服务器时区不对签到日期会错位部署时用timedatectl确认时区。5.3 用日志和截图定位识别失败签到系统出问题时光看数据库记录很难定位是检测失败还是识别失败。建议在识别流程里加日志和截图检测到人脸但识别为未知时把当前帧保存到logs/unknown/目录文件名带时间戳。这样事后可以翻看是哪些人脸被拒了是光线问题还是底库缺失。日志用 Python 的 logging 模块按天切分文件。import logging import os logging.basicConfig( levellogging.INFO, format%(asctime)s %(levelname)s %(message)s, handlers[ logging.FileHandler(logs/attendance.log, encodingutf-8), logging.StreamHandler() ] ) def save_unknown_face(frame, distance): 保存未识别的人脸截图 os.makedirs(logs/unknown, exist_okTrue) ts time.strftime(%Y%m%d_%H%M%S) path flogs/unknown/{ts}_{distance:.3f}.jpg cv2.imwrite(path, frame) logging.info(f未知人脸已保存: {path}, 最小距离: {distance:.3f})这个习惯在调试阈值时特别有用。把 unknown 目录里的截图拿出来手动算一下它们与底库的距离分布就能判断阈值是该放宽还是收紧。我一般会跑一周的签到数据统计真实签到和未知人脸的距离直方图两条曲线的交叉点就是最优阈值。这个做法比拍脑袋定 0.9 靠谱得多。5.4 模型量化让 CPU 推理再快一倍如果演示机器没有 GPUPyTorch 的动态量化能把 FaceNet 的推理速度提升 30% 到 50%精度损失很小。量化把模型权重从 float32 转成 int8内存占用也降到四分之一。import torch.quantization # 对 ResNet 做动态量化 resnet_quantized torch.quantization.quantize_dynamic( resnet, {torch.nn.Linear, torch.nn.Conv2d}, dtypetorch.qint8 ) resnet_quantized.eval()量化后的模型用法和原来一样直接替换resnet变量即可。注意 MTCNN 不建议量化它的三个子网络对精度敏感量化后漏检率会上升。只量化 FaceNet 的 ResNet 部分整体速度提升明显识别距离分布基本不变。如果量化后距离整体偏移重新校准一次阈值就行。5.5 用签到热力图验证系统覆盖最后一个小技巧把签到记录按小时聚合画一张热力图看看哪个时间段签到人数最多、哪个时间段识别失败率最高。热力图用 matplotlib 或前端 ECharts 都能画。如果发现某个时间段失败率异常高大概率是那个时间段光线变化剧烈比如下午阳光直射摄像头。调整摄像头角度或加遮光罩就能解决。这个验证方法比看日志更直观也方便在毕业答辩时展示系统运行情况。我习惯在答辩前跑三天完整数据把热力图和识别率曲线放进 PPT比单纯说“系统能跑”有说服力得多。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑