☰
基于深度学习的人脸识别签到系统:MTCNN+FaceNet实战解析
2026/10/10 14:11:12 网站建设 项目流程

简介:这是一份面向计算机类毕业生的深度学习实战项目,围绕人脸识别与签到场景,提供从后台管理、人脸数据入库到识别签到功能的完整实现,适合需要快速搭建演示系统或参考系统设计的本科/专科学生。压缩包共27个文件,约101.47MB,以Python源码、HTML模板、SQLite数据库及配置文档为主,并包含依赖清单、数据库迁移脚本和管理员初始化入口,解压后按说明即可还原可运行环境。资源内包含人脸注册、签到记录、用户管理、前端页面与识别模块,能帮助梳理完整业务链路。项目已有人脸识别模型与后台界面,技术栈涉及Flask、数据库和深度学习推理,可直接作为毕业设计原型扩展。已有404人浏览学习,对正在选题或中期开发的同学具有较高参考价值,既能了解整体架构,也能复用核心代码完成功能演示与答辩准备。

1. 基于深度学习的人脸识别签到系统:毕业设计里少见的落地型题目

教室里 30 个人点名要花掉一分半,这个场景让“基于深度学习的人脸识别签到系统”成了毕业设计里少见的落地型题目。它要解决的事看着简单:把摄像头拍到的脸编码成高维向量,和注册库里的特征做比对,命中就写入一条签到记录。真正动手才发现,卡住你的往往不是模型,而是数据采集、图像预处理和阈值标定这条流水线。这套资源适合三类人:拿它当毕设框架的学生、给实验室做离线刷脸考勤的开发者、想弄清人脸从像素到高维向量全过程的入门者。整个系统不依赖云端、不烧训练卡,一台带 CPU 的笔记本就能跑起来,这决定了它天然适合在校园网环境里演示和答辩。

2. 技术选型:为什么是 MTCNN+FaceNet,而不是 Dlib 或现成门禁 SDK

2.1 四段式管线:检测、对齐、特征提取、比对

人脸识别签到系统之所以让人感觉“玄学”,是因为很多人把整条链路当成了一个黑匣子。实际上从摄像头画面到签到成功,中间是四段完全独立的流程,每一段都有独立的输入和输出。

第一段是人脸检测,解决“脸在哪”的问题。常用的 MTCNN 是三级联网络,PNet 负责在整张图上快速扫出候选框,RNet 对候选框做一次精筛,ONet 输出最终的人脸框和五个关键点坐标。第二段是人脸对齐,拿到左眼、右眼、鼻尖、左嘴角、右嘴角这五个坐标后,把脸部区域做仿射变换,旋转到统一姿态。第三段是特征提取,对齐后的人脸图缩放成 160×160,送入 FaceNet 的 InceptionResnetV1 网络,最终输出一个 512 维的向量。第四段是相似度比对,计算当前向量与注册库中每个向量的距离,距离小于阈值就判定为同一个人。

这就是“人脸识别图像进入神经网络到输出高维度向量”的完整路径。你可以看到,深度学习在这里只负责第二段和第三段,第一段用 MTCNN 也是深度网络,只有第四段是纯数学。理清这个结构之后,后面所有调参和排错都有了解释坐标。

2.2 选型对比:开源深度学习方案、Dlib、现成 SDK 三者的取舍

毕设答辩最怕被问“方案为什么这么定”。拿这张对比表去说,比背概念要有说服力。

方案精度水平部署与离线能力答辩可讲性
Dlib HOG + 线性分类器依赖正脸,侧脸和遮挡基本失效纯 CPU 可跑,离线完全可用手工特征可讲,但天花板低
MTCNN + FaceNet公开人脸数据集上表现稳定,对侧脸和弱光容忍度更高离线可用,CPU 能跑推理,GPU 只是加速深度网络结构、损失函数、向量度量都是考点
人脸识别门禁机 / 厂商 SDK商用级,但闭源通常要联网授权,存在年费黑匣子,参数不可见,答辩容易卡壳

从折中角度看,MTCNN+FaceNet 是校园场景里最划算的组合。Dlib 不是不能用,但它的 HOG 特征对姿态变化敏感,签到场景里学生侧脸看一眼摄像头,漏检率马上上来。商用 SDK 精度高,但你没法在论文里写清楚“内部发生了什么”,这恰恰是毕设评分的关键权重。

2.3 训练还是微调:预训练权重怎么落地

这套资源里不包含从头训练的步骤,原因很现实。FaceNet 这类模型在 LFW 这类大规模人脸数据集上已经收敛得足够好,用你自己的 50 张班级照片从头训练,效果只会更差。预训练给出的是通用人脸空间的分布规律,你不需要重新学“眼睛长在哪”,你只需要把已有的特征空间用到自己的班级上。

如果确实想微调,正确的做法是冻结 InceptionResnetV1 的前面几层,只在后面全连接层上用你自己的注册数据做三元组损失训练。但要注意,毕设场景里数据量往往只有几十个人、几百张图,微调非常容易过拟合,导致本地测试全过、现场换台摄像头就翻车。我的建议是:先直接加载pretrained="vggface2"的权重做特征提取,跑通整条链路,把签到准确率测出来,再决定要不要微调。这个顺序能让你把变量控制住,出了问题也知道往哪查。

3. 环境配置与人脸数据准备:把地基先夯到这个程度再谈识别

3.1 运行环境与依赖安装

我在 Ubuntu 20.04 和 Windows 11 上都跑过这套流程,结论是 Linux 下少一些环境冲突,但 Windows 完全能复现。Python 用 3.8 到 3.10 都行,核心依赖其实只有四个:PyTorch、facenet-pytorch、OpenCV、NumPy。

# 基于 CUDA 11.8 安装 PyTorch,CPU 机器去掉 --index-url 参数 pip install torch torchvision --index-url https://download.pytorch.org/whl/cu118 pip install facenet-pytorch opencv-python numpy pillow

facenet-pytorch 这个库把 MTCNN 和 InceptionResnetV1 都封装好了,你不需要自己搭网络结构。注意 PyTorch 版本别太新,2.6 以上有时候会和旧版 CUDA 驱动起冲突,安装完先跑一段推理验证。环境变量这块,Windows 用户建议在系统设置里配好 CUDA 路径,Ubuntu 用户检查nvidia-smi和nvcc -V的版本是否匹配,不匹配时会自动落回 CPU,速度慢但不报错,很多同学在这种“静默降级”里浪费了几个小时。

3.2 注册照片的数据采集标准

人脸库的质量直接决定签到准确率,这一步偷懒后面全得还回来。先看目录组织方式,每个注册人一个文件夹,文件夹名就是能用来展示的姓名或学号。

faces/ ├── zhang_san/ │ ├── frame_01.jpg │ ├── frame_02.jpg │ └── frame_03.jpg ├── li_si/ │ ├── frame_01.jpg │ └── frame_02.jpg

采集注册照片时,常见做法是直接用 OpenCV 打开摄像头,每 3 帧保存一帧到对应文件夹。

import cv2 import os cap = cv2.VideoCapture(0) cap.set(cv2.CAP_PROP_FRAME_WIDTH, 640) cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 480) save_dir = "faces/zhang_san" os.makedirs(save_dir, exist_ok=True) count = 0 while count < 30: ret, frame = cap.read() if not ret: continue if count % 3 == 0: cv2.imwrite(os.path.join(save_dir, f"frame_{count:02d}.jpg"), frame) count += 1 cv2.imshow("capture", frame) if cv2.waitKey(1) & 0xFF == ord("q"): break cap.release() cv2.destroyAllWindows()

这段代码的核心逻辑是控制采样间隔。摄像头默认 30 帧每秒,如果每一帧都保存,相邻帧几乎一样,等于浪费存储和后面的计算时间。每 3 帧保存一次,20 秒能攒出约 10 张有效照片,足够提取稳定特征。采集时让人轻微转动头部,左右各偏 15 度,这样后续对侧脸的匹配会宽容很多。cv2.imshow 那段是为了让你确认光线是否过暗,画面里人脸轮廓不清晰时,重新打光再采,不要指望模型帮你补光。

3.3 把照片变成 512 维特征库

采集完原始照片后,用 MTCNN 检测并对齐,再进入 InceptionResnetV1 提取特征,最后把每个人的多张照片特征取平均,存成一个本地特征库文件。

import torch import os import pickle import numpy as np from PIL import Image from facenet_pytorch import MTCNN, InceptionResnetV1 device = torch.device("cuda" if torch.cuda.is_available() else "cpu") mtcnn = MTCNN(image_size=160, margin=20, min_face_size=60, thresholds=[0.6, 0.7, 0.7], factor=0.709, device=device) resnet = InceptionResnetV1(pretrained="vggface2").eval().to(device) names, features = [], [] for person in sorted(os.listdir("faces")): dir_path = os.path.join("faces", person) if not os.path.isdir(dir_path): continue embed_list = [] for img_name in os.listdir(dir_path): img_path = os.path.join(dir_path, img_name) img = Image.open(img_path).convert("RGB") face = mtcnn(img) if face is None: print(f"[skip] {img_path} 未检测到人脸") continue with torch.no_grad(): embed = resnet(face.unsqueeze(0)).cpu().numpy()[0] embed_list.append(embed) if len(embed_list) > 0: names.append(person) features.append(np.mean(embed_list, axis=0)) with open("face_db.pkl", "wb") as f: pickle.dump({"names": names, "features": np.array(features)}, f) print(f"特征库生成完成:{len(names)} 人,特征维度 {features[0].shape[0]}")

这里有几个参数需要解释。image_size=160是 FaceNet 的标准输入尺寸,别改成 224 或其他值,否则特征分布会偏移。margin=20是在检测框外围再扩一圈,避免脸的边缘信息被裁剪掉。thresholds=[0.6, 0.7, 0.7]是 MTCNN 三个网络各自的置信度门槛,第一次跑通了先别动它。特征取平均是一个很实用的降噪手段:单张照片可能因为眨眼、光影产生异常特征,5 张照片平均之后,异常方向会被抵消一部分。

需要特别留意的坑点是:网上很多 FaceNet 教程说输出是 128 维向量,那说的是原始论文里的 Inception v1 版本,而 facenet-pytorch 实现的 InceptionResnetV1 输出 512 维。这个差异不影响使用,但你在论文里写“特征向量 128 维”,实验结果又是 512 维,答辩时会很难看。以实际代码打印出来的维度为准。

4. 核心实现:从摄像头画面到签到记录的完整闭环

4.1 实时检测与对齐:不要重复加载模型

签到主程序的第一个常见败笔,是在每帧图像上都重新初始化 MTCNN 和 FaceNet 模型。模型初始化大约耗时几百毫秒,看似不多,但每帧都执行会让摄像头画面卡成一秒一帧。正确做法是启动时加载一次模型,视频循环里只做前向推理。

import cv2 import torch from facenet_pytorch import MTCNN, InceptionResnetV1 device = torch.device("cuda" if torch.cuda.is_available() else "cpu") mtcnn = MTCNN(image_size=160, margin=20, min_face_size=60, thresholds=[0.6, 0.7, 0.7], factor=0.709, device=device) resnet = InceptionResnetV1(pretrained="vggface2").eval().to(device) cap = cv2.VideoCapture(0) cap.set(cv2.CAP_PROP_FRAME_WIDTH, 640) cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 480) while True: ret, frame = cap.read() if not ret: break rgb_frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) boxes, probs = mtcnn.detect(rgb_frame) if boxes is not None: for box in boxes: x1, y1, x2, y2 = [int(v) for v in box] cv2.rectangle(frame, (x1, y1), (x2, y2), (0, 255, 0), 2) cv2.imshow("attendance", frame) if cv2.waitKey(1) & 0xFF == ord("q"): break cap.release() cv2.destroyAllWindows()

这段代码先用 BGR 转 RGB,因为 OpenCV 读进来的是 BGR 通道顺序,而 MTCNN 内部按 RGB 训练。mtcnn.detect只返回边界框和置信度,不返回关键点,所以用它做纯检测是最快的。如果你接下来还要提取特征,走mtcnn(frame)会同时完成检测、对齐和裁剪,直接返回一个 160×160 的张量,省掉手动仿射变换的步骤。

4.2 特征比对:欧氏距离阈值与余弦相似度的选择

拿到实时人脸特征后,与特征库比对。两种度量方式在实际项目里都有人用:欧氏距离描述向量空间中的绝对距离,余弦相似度描述方向差异。对 512 维的人脸特征,我的实测感受是欧氏距离更稳定,因为它同时包含了方向和幅值信息,而余弦相似度对特征幅值不敏感,容易把两张“方向一致但表情差异大”的脸误判成同一个人。

import numpy as np import pickle with open("face_db.pkl", "rb") as f: db = pickle.load(f) db_names = db["names"] db_features = db["features"] # shape: (N, 512) def match_face(query_feat, threshold=0.9): distances = np.linalg.norm(db_features - query_feat, axis=1) min_idx = np.argmin(distances) if distances[min_idx] < threshold: return db_names[min_idx], distances[min_idx] return None, distances[min_idx]

阈值 0.9 是一个需要手动标定的起点值。特征空间里,同一人不同照片的欧氏距离通常在 0.5 到 0.8 之间,不同人之间通常在 1.2 以上。标定方法我在第 6 章细说,这里先记住一个原则:宁可阈值松一点漏掉几个人,也不要太紧把陌生人放进来。签到系统的业务逻辑里,漏签可以补一次手动签到,误签却是数据问题,后者的麻烦大得多。

4.3 签到记录与防重复:用时间窗口而不是按帧写库

签到记录写入数据库这一步,最容易出现的毛病是一秒内识别成功多次,数据库里堆满重复签到。解决思路是给每个人加一个时间窗口,两次签到记录间隔小于某个秒数时,视为重复操作。

import sqlite3 import datetime conn = sqlite3.connect("attendance.db") conn.execute(""" CREATE TABLE IF NOT EXISTS records ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, created_at TEXT NOT NULL ) """) last_sign_time = {} def sign_in(name, cooldown=60): now = datetime.datetime.now() last = last_sign_time.get(name) if last and (now - last).total_seconds() < cooldown: return False, "已在签到窗口内,勿重复操作" conn.execute( "INSERT INTO records (name, created_at) VALUES (?, ?)", (name, now.strftime("%Y-%m-%d %H:%M:%S")), ) conn.commit() last_sign_time[name] = now return True, f"{name} 签到成功"

这里的cooldown=60表示同一人 60 秒内只算一次签到,适合课堂点名场景。如果你做的是实验室门禁,应该把 cooldown 改成识别到一次就关闭签到窗口,等此人离开摄像头视野再重新开放。SQLite 足够撑起几百人的签到场景,不需要引入 MySQL。注意last_sign_time是内存字典,程序重启后会丢失,对毕设演示没有影响,生产环境要换数据库表来记录。

5. 避坑:人脸识别签到系统最容易翻车的五个环节

5.1 现象:新摄像头换上去,签到率断崖式下降

原因:注册照片用的是笔记本内置摄像头,分辨率低、视角窄;答辩现场换成了实验室的高清外接摄像头,分辨率变高之后,MTCNN 的min_face_size=60含义发生了变化。同样一张脸,在 640×480 画面里占 80 像素,在 1920×1080 画面里占 240 像素,检测框位置、裁剪比例都变了,特征分布自然偏移。

解决:换摄像头之后,重新用注册脚本采集一遍全班级的人脸库,不要沿用旧库。如果不想重新采集,就在主程序里对每一帧先做 resize,统一到 640×480 再做检测。从那以后我每次换设备,都强制走一遍回归测试:找 10 个人各拍 3 张,过一遍离线回放脚本,确认准确率不低于 90% 再上线。

5.2 现象:训练时特征提取正常,摄像头实时识别时几乎全不匹配

原因:训练脚本里mtcnn(img)接收的是 PIL Image,而实际签到程序里传给mtcnn的是torch.Tensor或 NumPy 数组,预处理路径不一致。最常见的是颜色空间问题,OpenCV 读出来是 BGR,直接送入模型,人脸看起来是蓝绿色的,特征完全错乱。

解决:统一入口。签到程序里先cv2.cvtColor(frame, cv2.COLOR_BGR2RGB),再转成 PIL Image 后送入mtcnn。如果你用torchvision.transforms做预处理,也要检查是不是做了同样的通道变换。排错手段很简单:把实时画面中裁剪出的人脸保存到本地,用训练脚本的读图方式重新走一遍特征提取,比对两次特征向量的距离,距离小于 0.3 就说明预处理一致性没问题。

5.3 现象:同一人两张注册照,特征提取结果差异极大

原因:注册照片有低头、仰头或者大笑的表情。MTCNN 对齐依赖五个关键点,极端角度下关键点会偏,导致对齐后的人脸区域差异大。多张照片特征取平均虽然能缓解,但架不住某张照片在深度学习推理时因为运动模糊产生了噪声向量。

解决:采集注册照片时,去掉低头、捂脸、大笑、侧脸超过 30 度的帧。另外在特征提取脚本里加一个过滤逻辑:先算出所有照片特征的均值,再把离均值最远的那张剔除,重新计算平均特征。这是动手深度学习实践里常用的“均值去噪法”,虽然朴素,但对小数据集非常有效。

5.4 现象:光线稍暗就检测不到人脸,程序直接跳过

原因:MTCNN 的min_face_size=60是按像素算的,但它对低照度图像的鲁棒性有限。教室后排光线不足是常态,画面里人脸区域对比度低,PNet 的置信度压线不上,直接整体漏检。

解决:检测前对灰度图做直方图均衡化,或者让用户手动调高摄像头曝光。更靠谱的是把min_face_size从 60 调到 40,让模型去检测更小更模糊的人脸区域。但要注意,这个参数调低之后误检率也会上升,所以thresholds的第二个数0.7要同步调高到0.75,过滤掉那些“勉强算脸”的候选框。参数之间是联动关系,改一个就要观察另一个。

5.5 现象:Windows 下运行时报 MTCNN 线程错误

原因:facenet-pytorch 的 MTCNN 内部用了torch.multiprocessing,在 Windows 上进程启动机制和 Linux 不同,容易触发RuntimeError。这个报错在 PyTorch 1.13 到 2.x 版本都有用户遇到。

解决:在主程序入口加上if __name__ == "__main__":保护,并把 MTCNN 的初始化放在主进程里。如果你在 Jupyter Notebook 里跑,改用torch.set_num_threads(1)或者把 MTCNN 的device显式设为cpu,能绕过一部分多线程崩溃。Windows 上跑深度学习项目,血泪经验是:不要用 Jupyter 跑视频流,写成一个独立的.py脚本,从命令行运行,问题少一半。

6. 进阶验证:把自己的班级数据测成一张准确率报告

6.1 离线回放:让每一次调参都有数字依据

现场拿摄像头测试的重复性太差,没办法回归。正确的验证方式是收集一段班级同学进出教室的视频,离线逐帧跑检测和比对,统计“实际出现次数”和“成功识别次数”。这个回放脚本本身只有 40 行,放在毕设里就是最好的实验章节素材。

import cv2 import numpy as np import torch from facenet_pytorch import MTCNN, InceptionResnetV1 cap = cv2.VideoCapture("test_clip.mp4") mtcnn = MTCNN(image_size=160, margin=20, min_face_size=60, thresholds=[0.6, 0.7, 0.7]) resnet = InceptionResnetV1(pretrained="vggface2").eval() total_frames = 0 detected_frames = 0 while cap.isOpened(): ret, frame = cap.read() if not ret: break total_frames += 1 rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) boxes, _ = mtcnn.detect(rgb) if boxes is not None and len(boxes) > 0: detected_frames += 1 cap.release() print(f"检测率:{detected_frames / total_frames * 100:.2f}%")

这块的指标要拆开看。检测率只反映第一阶段,识别准确率还要叠加特征比对结果。建议指标表格做成两行:一行是“视频中有人脸的帧数中被正确检测到的比例”,另一行是“被检测到的人脸中特征匹配正确的比例”。前者反映 MTCNN 的阈值设置,后者反映欧氏距离阈值的合理性,两个指标互不干扰,答辩时讲起来也清楚。

6.2 特征空间可视化:让人脸向量“看得见”

512 维向量没法直接展示,但可以用 PCA 降维到二维,把特征库里每个注册人以及测试集里的人脸向量画在一张散点图上。如果同一人的点聚在一起、不同人的点相互分离,就说明特征提取是有效的,这个可视化图放答辩 PPT 里比任何文字都直观。

import pickle import numpy as np from sklearn.decomposition import PCA import matplotlib.pyplot as plt with open("face_db.pkl", "rb") as f: db = pickle.load(f) features = db["features"] names = db["names"] pca = PCA(n_components=2) coords = pca.fit_transform(features) plt.figure(figsize=(10, 8)) for i, name in enumerate(names): plt.scatter(coords[i, 0], coords[i, 1], label=name) plt.xlabel("PC1") plt.ylabel("PC2") plt.legend() plt.savefig("feature_space.png", dpi=150)

如果你把特征库里不同人的点 P 出来之后发现重叠严重,先别怀疑模型能力,检查一下是不是注册照片采集时大家坐得太近、背景雷同,或者有几张照片拍糊了。特征可视化是排查这类问题的利器,比看准确率数字直观得多。从那以后我每次给人脸识别项目做效果评估,都先跑一遍 PCA 图,确认点云分布没问题再调阈值,这个习惯帮我少走了很多弯路。希望帮到你。

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

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

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

立即咨询