简介:基于Python与FaceNet的课堂学生签到系统毕业设计资源包,面向计算机相关专业本科生和需要完成人脸识别类课题的研究者,可帮助解决课堂点名效率低、代签等问题;包内含完整Python源码、数据集及详细文档,源码已经本地编译可运行,评审分达95分以上,经助教审定,难度适中,适合学习与二次开发。资源包共21个文件,以Python脚本为主(15个py),包含人脸检测模块、摄像头调用模块、GUI界面逻辑和Facenet识别核心代码,另有4个pyc编译文件、1个ttf字体文件和1个gif演示动画,压缩包约40MB,代码结构清晰,目录模块划分明确,可对照文档快速理解检测、识别、签到记录全流程。目前已有152人学习下载,配套的详细文档和数据集能为毕业设计答辩、课程实训提供完整参考,具备较强的实践价值。
1. 为什么课堂签到系统会盯上FaceNet:这份源码能直接落地的部分
人脸识别签到这件事,这几年在高校里已经从“演示级”变成了“真能用”的需求。老师不想挨个点名,学生不想被代签,而传统点名和刷卡又很难杜绝替答到。我拆过不少课堂签到相关的毕设项目,最常见的翻车点有两个:一是用人脸检测当人脸识别,换个背景就乱认;二是模型训练流程写得不完整,拿到源码也跑不出效果。这次拆的这份“基于Python+FaceNet的人脸检测+识别的课堂学生签到系统源码+数据集+详细文档”,属于能直接拿去做二次开发的完整工程,不是Demo。它的核心逻辑是人脸检测负责“从画面里把人找出来”,FaceNet负责“把人认出来”,两条链路分开处理,识别部分用深度学习特征而不是简单模板匹配。适合正在做毕业设计的学生,也适合需要快速搭一个本地人脸识别原型的开发者。下面我按“架构选型、数据准备、特征提取、部署避坑、精度验证”这五块来拆,每一步都会给出能直接抄的代码和参数。
2. 系统整体设计与FaceNet选型:架构、环境与目录结构
2.1 为什么选FaceNet而不是传统特征或ArcFace
人脸识别方案不少,常见的有三种路线:传统LBP/HAAR特征、Dlib的深度人脸模型、FaceNet和ArcFace这类度量学习模型。LBP那套只适合固定环境下的简单比对,光照一变就废,课堂上教室窗户多,光线变化大,不建议选。Dlib的模型虽然轻量,但对侧脸和戴眼镜的支持一般,而且特征区分度不够细。FaceNet的核心是一个将人脸图像映射到128维欧几里得空间的深度卷积网络,它用三元组损失来训练,让同一个人的两张脸在空间中距离更近、不同人的脸距离更远。这个128维向量就是“人脸指纹”,后续识别全部在这个向量空间里做距离计算,好处是人脸库更新时不需要重训模型,只需要对新学生重新提取一次特征存入数据库即可。
这份源码走的正是这条路线:检测部分用MTCNN或OpenCV的级联检测器框出人脸,识别部分用FaceNet提取128维特征,然后用余弦相似度或欧氏距离判断是不是同一个人。相比ArcFace还要准备大型训练集和复杂的损失函数调参,FaceNet在中小规模班级签到场景下性价比更高,预训练模型公开且稳定,训练和部署成本都可控。这也是我推荐毕设选它的原因:论文能写清楚原理,代码能跑得动,并且真实场景的精度足够用。
2.2 工程目录结构与运行环境
拿到源码包后,先看目录结构,不要急着运行。合格的工程会按功能把代码拆分清楚,而不是一个main.py塞几千行。常见的划分方式如下:
face_signin_system/ ├── data/ │ ├── raw_faces/ # 原始采集的学生照片 │ ├── aligned_faces/ # 对齐后的人脸图 │ └── embeddings/ # 每人提取出的128维特征向量 ├── models/ │ ├── mtcnn/ # 人脸检测模型 │ └── facenet/ # FaceNet预训练模型权重 ├── src/ │ ├── face_detector.py # 人脸检测模块 │ ├── face_embedder.py # FaceNet特征提取模块 │ ├── recognize.py # 识别与比对模块 │ ├── signin.py # 签到业务逻辑 │ └── database.py # 签到记录存储 ├── datasets/ # 开源人脸数据集 / 自建学生人脸库 ├── docs/ # 毕业设计文档和操作手册 └── main.py # 主程序入口先确认Python环境。这个项目依赖TensorFlow、OpenCV、NumPy、MTCNN这几个核心库,我建议用虚拟环境安装,避免把系统Python搞乱。安装命令如下:
python -m venv venv source venv/bin/activate # Windows下执行 venv\Scripts\activate pip install tensorflow==2.10.0 pip install opencv-python==4.8.0.74 pip install numpy==1.24.3 pip install mtcnn==0.1.1 pip install Pillow scikit-learnTensorFlow的版本选择有一个注意点:FaceNet的预训练权重大多是基于TF1或TF2早期版本导出的,直接用新版TF2.x加载经常报AttributeError或者saved_model格式不兼容。我一般会选择TensorFlow 2.10这个版本,兼容性相对较好,既能加载TF1保存的.pb模型,也支持Keras自定义层。如果你电脑没有NVIDIA显卡,直接装tensorflow-cpu版本,识别速度虽然慢一些,但签到场景每秒处理1到2帧足够。装好后在Python交互环境里验证一下:
import tensorflow as tf from mtcnn import MTCNN import cv2 print("TensorFlow:", tf.__version__) print("OpenCV:", cv2.__version__) detector = MTCNN() print("MTCNN loaded:", detector is not None)能打印出版本号且MTCNN正常实例化,说明环境基础没问题。如果mtcnn导入报错,通常是NumPy版本太高导致类型定义冲突,把NumPy降到1.24.3即可。这一步卡住的人很多,属于环境层面的第一个坑,后面避坑章节会详细展开。
2.3 签到业务流程的数据走向
搞清楚工程结构之后,需要理解这个系统的数据流是怎么走的。整个签到流程可以拆成四步:
- 录入阶段:采集每个学生的1到5张人脸照片,经过检测和对齐后存入
aligned_faces/目录,然后提取128维特征向量保存到embeddings/。 - 识别阶段:摄像头或图片进入系统后,先做人脸检测,把框出来的人脸裁剪并缩放到FaceNet要求的输入尺寸(通常是160x160),再提取特征向量。
- 比对阶段:将当前特征向量与数据库中每个学生的特征向量计算相似度,取最高分且超过设定阈值的作为识别结果。
- 记录阶段:将识别到的学生ID、时间、课程信息写入SQLite或MySQL,生成签到表。
我对这个流程的评价是:分工合理,哪里出了问题排查范围很清晰。检测不准就调检测模块,识别不准就调特征比对模块,不会互相干扰。这也是这份源码值得下载的原因之一,它把一个完整系统拆成了可替换的模块,毕设答辩时你可以分别展示每一层的实现细节。
3. 人脸检测与数据预处理:MTCNN对齐、人脸库构建与三个关键参数
3.1 MTCNN检测与对齐的代码实现
人脸检测是整个识别链路的起点,检测框的位置直接决定后续特征提取的质量。如果脸都没框准,FaceNet提取的特征就会失真,识别准确率断崖式下降。这份源码用的是MTCNN,它会返回人脸框坐标以及左右眼的置信度,我们可以利用这些信息做人脸对齐。
下面这段代码是从单张照片中检测人脸并对齐保存,是构建学生人脸库的第一步:
import cv2 import numpy as np from mtcnn import MTCNN from PIL import Image import os detector = MTCNN() def detect_and_align(image_path, output_path, target_size=160): img = cv2.cvtColor(cv2.imread(image_path), cv2.COLOR_BGR2RGB) results = detector.detect_faces(img) if len(results) == 0: print(f"[WARN] 未检测到人脸: {image_path}") return False # 取最大的人脸框,避免把背景小脸当主目标 box = max(results, key=lambda x: x['confidence'])['box'] x, y, w, h = box # 向外扩展20%,防止额头和下巴被截掉 margin = int(max(w, h) * 0.2) x1 = max(0, x - margin) y1 = max(0, y - margin) x2 = min(img.shape[1], x + w + margin) y2 = min(img.shape[0], y + h + margin) face = img[y1:y2, x1:x2] face = cv2.resize(face, (target_size, target_size)) cv2.imwrite(output_path, cv2.cvtColor(face, cv2.COLOR_RGB2BGR)) return True # 批量处理 data/raw_faces/ 下每个学生的照片 for student_dir in os.listdir("data/raw_faces"): student_path = os.path.join("data/raw_faces", student_dir) if not os.path.isdir(student_path): continue out_dir = os.path.join("data/aligned_faces", student_dir) os.makedirs(out_dir, exist_ok=True) for img_name in os.listdir(student_path): detect_and_align( os.path.join(student_path, img_name), os.path.join(out_dir, img_name) )这段代码里有两个值得注意的参数。第一个是max(results, key=lambda x: x['confidence']),意思是如果一张合影里有多个学生,只取置信度最高、面积最大的人脸作为目标,这个策略是为了后续建库时避免误抓别人。第二个是margin扩展,我按20%设置,因为MTCNN返回的框有时候会紧贴人脸,直接裁剪会把额头或下巴切掉,FaceNet的输入要求人脸完整居中,边缘丢失会导致特征向量偏移。target_size=160是FaceNet预训练模型的标准输入尺寸,不能随意改成224或者128,除非你打算从头训练模型。
3.2 训练集与测试集划分的人脸库构建规范
人脸库的构建质量决定了这个系统能用多久。我见过很多毕设项目脸上准确率很高,换一批同学来测试就崩了,原因是建库照片太少,每个人只有一张,而且光线、角度都差不多,模型学的其实是“这张照片”而不是“这个人”。
合理的建库规范是:每个学生至少准备3到5张照片,覆盖正脸、左右侧脸、抬头低头各一张,最好在不同光照条件下采集。这样可以保证特征向量覆盖该学生的主要外观变化。同时要把照片按学生姓名或学号分文件夹存放,目录名就是标签名,后续提取特征时直接用文件夹名映射学生ID。
人脸库的划分也建议按学生维度而不是图片维度。假设班级有40人,按每人4张照片计算,应该随机抽30人做“注册库”,剩下10人作为“陌生人测试”或“识别测试”。这样能验证一个关键能力:系统对没注册过的人要能拒绝,而不是随便拉一个人进来都匹配成某个学生。课堂签到场景里,外班学生混进来代签是必须拦住的情况。
3.3 光照、姿态与图像质量的三个关键参数
人脸识别的玄学大多出在图像质量上,而图像质量可以通过参数控制。根据我的实操经验,有三个参数对识别率影响最大:光照归一化、人脸最小尺寸和清晰度过滤。
光照问题最有效的手段是在提取特征前做标准化的预处理,包括灰度化、直方图均衡化、像素值归一化到[-1, 1]区间。FaceNet自带的预处理函数通常会做最后一步,但直方图均衡化需要自己加,尤其光源偏黄或偏蓝的教室环境,均衡化能显著降低色偏干扰。
人脸最小尺寸决定系统能识别多远的距离。MTCNN默认能检出很小的脸,但小脸送到FaceNet里信息量不足,识别率很低。我是这样处理的:
def is_valid_face(box, frame_width, frame_height, min_face_ratio=0.08): x, y, w, h = box face_area_ratio = (w * h) / (frame_width * frame_height) # 人脸面积小于画面8%时直接丢弃,避免小脸误识别 if face_area_ratio < min_face_ratio: return False # 清晰度判断:拉普拉斯方差低于阈值则判定为模糊 gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) laplacian_var = cv2.Laplacian(gray, cv2.CV_64F).var() return laplacian_var > 50min_face_ratio设为0.08意味着学生坐在教室后排时可能检测不到,但如果学生距离摄像头2到3米内,这个值是可以接受的。清晰度阈值50是经验值,教室正常光照下清晰人脸拉普拉斯方差一般在80到200之间,低于30的基本是运动模糊或摄像头失焦。这两个参数宁可严格一些,也不要为了“能识别”而放低标准,否则签到系统会频繁出现错误匹配。
4. 人脸识别核心:FaceNet特征提取、相似度计算与阈值设置
4.1 加载预训练模型与特征提取代码
FaceNet模型本身不做分类,它输出的是一个128维的特征向量。你可以把它理解成一个高度抽象的人脸编码器,同一个人的两张不同照片编码出的向量应该足够接近,不同人的向量应该远离。作为使用者,我们直接加载预训练权重并前向推理即可,不需要重新训练。
常见的加载方式有两种:使用keras_facenet库,或者直接用TensorFlow加载.pb文件。这份源码的文档里推荐的是后一种,因为不依赖第三方封装,模型路径和输入输出节点都可以自己控制。加载和提取特征的代码如下:
import tensorflow as tf import numpy as np import cv2 class FaceEmbedder: def __init__(self, model_path="models/facenet/facenet.pb"): self.graph = tf.compat.v1.Graph() with self.graph.as_default(): with tf.compat.v1.gfile.FastGFile(model_path, 'rb') as f: graph_def = tf.compat.v1.GraphDef() graph_def.ParseFromString(f.read()) tf.import_graph_def(graph_def, name="facenet") self.sess = tf.compat.v1.Session(graph=self.graph) self.input_op = self.graph.get_tensor_by_name("facenet/input:0") self.output_op = self.graph.get_tensor_by_name("facenet/embeddings:0") self.phase_train_op = self.graph.get_tensor_by_name("facenet/phase_train:0") def preprocess(self, face_img): # 将BGR转RGB,缩放到160x160,像素归一化 rgb = cv2.cvtColor(face_img, cv2.COLOR_BGR2RGB) resized = cv2.resize(rgb, (160, 160)) normalized = (resized - 127.5) / 127.5 return np.expand_dims(normalized, axis=0) def get_embedding(self, face_img): processed = self.preprocess(face_img) feed_dict = { self.input_op: processed, self.phase_train_op: False } emb = self.sess.run(self.output_op, feed_dict=feed_dict) return emb[0] # 128维向量 embedder = FaceEmbedder() img = cv2.imread("data/aligned_faces/student_001/photo1.jpg") feature = embedder.get_embedding(img) print("特征维度:", feature.shape, "L2范数:", np.linalg.norm(feature))这段代码中有一个很多人容易忽略的参数:phase_train_op。FaceNet在训练时使用Batch Normalization,phase_train必须设置为False,否则推理时BN层会使用当前batch的统计量而不是训练集的统计量,导致特征漂移。我见过有人直接把phase_train设成True,结果同一个人的两张照片余弦相似度只有0.3。另外(resized - 127.5) / 127.5这一步是ImageNet常见的[-1, 1]归一化,FaceNet预训练权重就是在这个分布下训练的,不能省略。
4.2 注册学生特征入库的批量处理
识别不是每次现场比对,而是提前把全班学生的特征向量算好存入数据库,识别时只需将当前帧的特征和库里所有向量逐一计算距离。这个“离线提取、在线比对”的设计非常关键,它保证了签到时的响应速度。
建库代码如下:
import os import pickle embedder = FaceEmbedder() face_db = {} # {student_id: list_of_128dim_vectors} stu_root = "data/aligned_faces" for stu_name in os.listdir(stu_root): stu_path = os.path.join(stu_root, stu_name) if not os.path.isdir(stu_path): continue vectors = [] for img_name in os.listdir(stu_path): img_path = os.path.join(stu_path, img_name) face = cv2.imread(img_path) if face is None: continue vec = embedder.get_embedding(face) vectors.append(vec) if vectors: # 同一个人多张照片的特征取平均,减少单张照片的噪声 face_db[stu_name] = np.mean(np.array(vectors), axis=0) with open("data/embeddings/face_db.pkl", "wb") as f: pickle.dump(face_db, f) print("入库学生数:", len(face_db))特征取平均这一步是提升识别稳定性的关键技巧。每个人存了3到5张照片,每张照片提取的向量都会因为表情、角度和光线有细微差别,直接保存每一张会导致比对时命中率不稳定。取平均后得到的向量更接近这个人在特征空间中的“质心”,误识和漏识的概率都会降低。但要注意平均前确认所有向量已经L2归一化,否则简单的向量平均会被模长更大的那一个主导,得到错误的方向。
4.3 相似度计算与阈值选择的数学逻辑和实验方法
特征向量建好后,剩下的事情就是距离度量。FaceNet论文里用的是欧氏距离,但实际工程中余弦相似度更容易解释,因为它和阈值的关系更直观:两个向量完全相同时相似度是1.0,完全不相关时趋近0。实践中两种度量都有人用,我更推荐余弦相似度,因为识别阈值可以按“90%相似才算同一个人”这种直观口径来调。
from sklearn.preprocessing import normalize import numpy as np def cosine_similarity(v1, v2): v1_n = v1 / np.linalg.norm(v1) v2_n = v2 / np.linalg.norm(v2) return np.dot(v1_n, v2_n) def recognize(embedding, face_db, threshold=0.8): best_id = None best_score = -1.0 for stu_id, stu_vec in face_db.items(): score = cosine_similarity(embedding, stu_vec) if score > best_score: best_score = score best_id = stu_id if best_score >= threshold: return best_id, best_score else: return None, best_score # 低于阈值视为陌生人阈值0.8是一个起点,不是一个定值。它需要在你的实际数据集上实验确定:取一批已注册学生的照片作为正样本,计算同类相似度分布;再取一批非本班学生照片作为负样本,计算异类相似度分布。两个分布的中位数之间就是阈值的选择区间。如果正样本相似度中位数是0.92,负样本中位数是0.55,那0.8就是安全位置。如果负样本干扰大,相似度中位数到了0.75,就要把阈值提高到0.84以上。阈值设低会导致代签的人被误识别成班里同学,设高会导致学生本人签到失败,这个平衡只能靠实测数据来校准。
5. 部署与使用中的常见问题排查:现象、原因、解决方案
5.1 环境依赖冲突导致模型加载失败
现象:运行FaceEmbedder类时报错,包含AttributeError: module 'tensorflow' has no attribute 'gfile'或saved_model相关错误,有时还会出现TypeError: Cannot convert a symbolic Keras input/output to a numpy array。
原因:FaceNet的.pb模型大多在TensorFlow 1.x时代保存,tf.gfile在TF 2.0后被移除,新版API不兼容。另外TensorFlow 2.11以后移除了部分compat.v1接口,强行运行会出现不可预知的异常。
解决:把TensorFlow固定为2.10.0版本,同时NumPy固定为1.24.3(TF2.10默认编译基于这个版本)。如果还报错,检查是否安装了多个TF版本导致路径冲突,pip list | grep tensorflow确认只有一个版本。加载.pb文件时注意name=""参数,如果模型保存时已经带facenet前缀,就要在get_tensor_by_name里写完整路径。我一般第一次加载前会用dir(self.graph)检查节点名,避免凭记忆写错。
5.2 同一个人识别出两个不同学生
现象:用学生A的注册照片测试,系统返回的结果是学生B,而且置信度评分很高。重新用现场拍摄的照片测试,结果又变成学生C。
原因:注册照片和测试照片的特征差异太大,很可能注册时用了不同光照条件下拍的侧脸照,或者学生对齐不完整导致特征向量偏离。还有一种常见情况是不同学生的照片没有做像素值归一化,导致模型输入的数值分布不一致。
解决:检查aligned_faces目录下图片的对齐效果,肉眼看一下每张照片人脸是否居中、是否有被裁剪的边缘。然后统一预处理管线:所有图片必须经过同一个detect_and_align函数处理后再提取特征。最后检查phase_train是否设置为False。解决后重新提取库特征,不要沿用旧的pkl文件,否则仍然可能带上错误数据的残留影响。
5.3 摄像头识别卡顿严重
现象:调用摄像头实时识别时,画面帧率掉到每秒2到3帧,学生走到镜头前要等好几秒才有结果,体验接近不可用。
原因:识别主循环里每帧都运行了完整检测和特征提取,CPU模式下160x160的FaceNet推理单帧需要500毫秒左右,加上MTCNN的检测耗时就会更慢。另一个原因是摄像头帧为1080P,检测时缩放和遍历耗时移交给了CPU。
解决:首先把输入帧缩放为640较合适,这可以大幅缩短MTCNN耗时;另外视频流是连续帧,同一个学生在连续帧中的位置变化不大,不需要每一帧都做识别。我会做一个间隔逻辑:每5帧只处理1帧作为识别帧,并且只有当检测到新的人脸框且该框中心位置和上一帧已识别位置距离超过50像素时才重新提取特征。这样能将帧率提升到10帧每秒以上,且不会漏检快速进入画面的学生。再加一层缓存,同一个学生10秒内重复出现不会重复记录签到。
5.4 学生本人被判定为陌生人
现象:识别时best_score低于阈值,学生明明站在摄像头前但系统提示未识别,日志里显示相似度只有0.65左右。
原因:阈值设得过高是直接原因,但更深层的原因是注册照片和现场照片的特征差异太大。常见诱因包括学生戴了口罩、换了发型、眼镜起雾,或者摄像头位置偏低导致仰拍角度过大。口罩遮挡导致面部特征缺失时,FaceNet能提取的有效区域只剩眉眼,这种情况下相似度普遍下降0.15到0.25。
解决:不要把阈值无脑往下调,而是为这类特殊情况做二次匹配。我给这份源码补充了一个策略:当最高相似度低于阈值但高于某个下限(比如0.6)时,触发活体检测和语音确认,学生报一次学号后人工放行,同时更新该学生当前状态下的特征缓存。另外建库时额外采集几张不同状态下的照片(戴口罩、侧脸、低头),会明显提升鲁棒性。
5.5 签到记录重复和数据库写入失败
现象:同一个学生离开教室再回来,系统再次识别成功,数据库里生成了两条签到记录。偶尔还会出现database is locked错误,页面卡住。
原因:重复记录是因为系统没有检查这个学生今天是否已经签过到,或者签到记录只在内存里做了一次,没有落库。database is locked是SQLite在高并发写入时的典型问题,多个线程同时写入同一个库文件时会互相阻塞。
解决:签到逻辑设计为“每个学生每天限签一次”,写入前先按学生ID和日期查询记录,存在则跳过。SQLite写入冲突的解决办法是:所有写操作放进同一个线程队列里顺序执行,并且设置timeout=30。如果并发量确实大,换成MySQL是更稳妥的做法,但对课堂签到场景来说SQLite足够了。这个问题的本质在于流程边界没有设计好,修改逻辑比换数据库更根本。
6. 验证模型效果与精度提升:用数据说话而不是凭感觉
拿到源码跑通之后,第一件应该做的事不是拿摄像头随机测,而是建一个独立的验证集来量化模型能力。验证集至少要包含每个已注册学生2到3张未参与建库的照片,以及10张以上非本班人员的照片。统计三个指标:已注册学生的识别成功率、未注册人员的拒绝成功率、平均响应时间。这套指标同时是毕设答辩时的核心论据。
import numpy as np from sklearn.metrics import confusion_matrix def evaluate(face_db, test_data, threshold=0.8): # test_data: [(image, label), ...] label为None表示陌生人 preds, labels = [], [] for img, label in test_data: emb = embedder.get_embedding(img) stu_id, score = recognize(emb, face_db, threshold) preds.append(stu_id if stu_id is not None else "unknown") labels.append(label if label is not None else "unknown") cm = confusion_matrix(labels, preds) return cm跑一次验证集后,如果发现某个学生频繁失败,就返回去检查他的注册照片质量,而不是调低阈值。调低阈值只会让陌生人混进来的概率增加,属于饮鸩止渴。如果识别速度达标但准确率差,优先检查MTCNN是否把五官对齐到同一位置,这是影响FaceNet特征质量的最关键因素。我曾经为了追求识别率把阈值从0.8降到0.65,结果连续三天都有外班学生成功“签到”成本班同学,从那以后我每次做参数调优都强制自己跑一遍完整验证集,观察阈值调整对正负样本两端的真实影响,而不是只看单个人的识别结果。希望帮到你。
本文还有配套的精品资源,点击获取