简介基于Python与FaceNet的课堂学生签到系统毕业设计资源包面向计算机相关专业本科生和需要完成人脸识别类课题的研究者可帮助解决课堂点名效率低、代签等问题包内含完整Python源码、数据集及详细文档源码已经本地编译可运行评审分达95分以上经助教审定难度适中适合学习与二次开发。资源包共21个文件以Python脚本为主15个py包含人脸检测模块、摄像头调用模块、GUI界面逻辑和Facenet识别核心代码另有4个pyc编译文件、1个ttf字体文件和1个gif演示动画压缩包约40MB代码结构清晰目录模块划分明确可对照文档快速理解检测、识别、签到记录全流程。目前已有152人学习下载配套的详细文档和数据集能为毕业设计答辩、课程实训提供完整参考具备较强的实践价值。1. 为什么课堂签到系统会盯上FaceNet这份源码能直接落地的部分人脸识别签到这件事这几年在高校里已经从“演示级”变成了“真能用”的需求。老师不想挨个点名学生不想被代签而传统点名和刷卡又很难杜绝替答到。我拆过不少课堂签到相关的毕设项目最常见的翻车点有两个一是用人脸检测当人脸识别换个背景就乱认二是模型训练流程写得不完整拿到源码也跑不出效果。这次拆的这份“基于PythonFaceNet的人脸检测识别的课堂学生签到系统源码数据集详细文档”属于能直接拿去做二次开发的完整工程不是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 tensorflow2.10.0 pip install opencv-python4.8.0.74 pip install numpy1.24.3 pip install mtcnn0.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_size160): 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, keylambda 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_okTrue) 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, keylambda x: x[confidence])意思是如果一张合影里有多个学生只取置信度最高、面积最大的人脸作为目标这个策略是为了后续建库时避免误抓别人。第二个是margin扩展我按20%设置因为MTCNN返回的框有时候会紧贴人脸直接裁剪会把额头或下巴切掉FaceNet的输入要求人脸完整居中边缘丢失会导致特征向量偏移。target_size160是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_ratio0.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_pathmodels/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, namefacenet) self.sess tf.compat.v1.Session(graphself.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, axis0) 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_dictfeed_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 Normalizationphase_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), axis0) 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, threshold0.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.3TF2.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写入冲突的解决办法是所有写操作放进同一个线程队列里顺序执行并且设置timeout30。如果并发量确实大换成MySQL是更稳妥的做法但对课堂签到场景来说SQLite足够了。这个问题的本质在于流程边界没有设计好修改逻辑比换数据库更根本。6. 验证模型效果与精度提升用数据说话而不是凭感觉拿到源码跑通之后第一件应该做的事不是拿摄像头随机测而是建一个独立的验证集来量化模型能力。验证集至少要包含每个已注册学生2到3张未参与建库的照片以及10张以上非本班人员的照片。统计三个指标已注册学生的识别成功率、未注册人员的拒绝成功率、平均响应时间。这套指标同时是毕设答辩时的核心论据。import numpy as np from sklearn.metrics import confusion_matrix def evaluate(face_db, test_data, threshold0.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结果连续三天都有外班学生成功“签到”成本班同学从那以后我每次做参数调优都强制自己跑一遍完整验证集观察阈值调整对正负样本两端的真实影响而不是只看单个人的识别结果。希望帮到你。本文还有配套的精品资源点击获取