
简介本资源是一套面向计算机专业本科生的毕业设计级学生人脸识别考勤系统融合PyQt桌面开发与FaceNet深度学习模型解决高校课堂/实验室场景下的无感化、高精度考勤管理需求。压缩包共55个文件含23个Python核心模块如face_recognition.py、facenet.py、Sqlite_UI.py、12个Qt Designer生成的.ui界面文件及对应.py绑定逻辑、3个.npy人脸特征数据库、2个SQLite考勤数据库StudentFaceDB.db与StudentCheckWorkDB.db以及README.md和.gitignore等工程规范文件整体仅1.98MB轻量易部署。已有119人学习下载适合希望实践AIGUI跨栈开发的学习者。读者可直接运行完整GUI应用掌握FaceNet嵌入向量生成与欧氏距离匹配逻辑、PyQt多窗口交互设计、SQLite本地人脸库管理、摄像头实时采集与对齐align/目录、以及从数据预处理到模型推理的端到端落地流程。1. 这不是又一个“调用 OpenCV 画个框”的考勤 Demo它用 FaceNet 做特征比对、PyQt 做真交互、SQLite 做本地持久化能跑在没 GPU 的笔记本上毕业答辩前一周还能改考勤逻辑你见过太多“人脸识别考勤系统”毕设——打开摄像头OpenCV detect 一下face_recognition 库算个欧式距离界面是 tkinter 拼的三个按钮数据存在 csv 里答辩时一换光照就失灵。这个项目不是。它把 FaceNet 的嵌入embedding训练和推理拆成可复现的两阶段先用预训练的 Inception-ResNet-v1 权重提取 128 维人脸特征再用余弦相似度做比对不是简单阈值硬判所有模型加载、图像预处理、数据库写入都封装进 PyQt 的 QObject 子类里避免 GUI 线程卡死SQLite 表结构带last_attendance_time和status字段支持补签、缺勤标记、按周导出 Excel最关键的是——它不依赖 CUDACPU 推理单帧耗时稳定在 320ms±15msi5-8250U 测试连宿舍台式机都能跑满考勤流程。适合需要交源码、要现场演示、且导师会问“为什么不用 MTCNN 做检测”“余弦相似度和欧氏距离区别在哪”的工科本科生。如果你的毕设还卡在“怎么让窗口不假死”或者被问到“特征向量维度怎么来的”答不上来这份资源就是你的后悔药。2. FaceNet 不是黑匣子从预训练权重加载到 128 维嵌入生成每一步都可控可调试FaceNet 的核心价值不在“识别”而在“度量学习”——它让同一个人不同角度的照片在特征空间里靠得近不同人的照片离得远。这个项目没自己训 FaceNet而是直接加载 Google 官方发布的20180402-114300版本 Inception-ResNet-v1 冻结权重.pb格式这是经过 LFW 99.6% 准确率验证的成熟模型。但直接 load pb 文件会踩坑TensorFlow 1.x 的 graph_def 加载方式在 TF 2.x 默认 eager mode 下会报FailedPreconditionError。解决方案是显式启用 v1 兼容模式并手动构建 session。2.1 加载 .pb 模型并提取 embedding 的完整链路import tensorflow as tf import numpy as np from scipy import spatial # 必须在 import tensorflow 后立即启用 TF 1.x 兼容模式 tf.compat.v1.disable_v2_behavior() def load_facenet_model(model_path): 加载 FaceNet .pb 模型返回 inference function with tf.compat.v1.gfile.GFile(model_path, rb) as f: graph_def tf.compat.v1.GraphDef() graph_def.ParseFromString(f.read()) with tf.compat.v1.Graph().as_default() as graph: tf.compat.v1.import_graph_def(graph_def, name) # 获取输入输出 tensor 名称关键不能猜 images_placeholder graph.get_tensor_by_name(input:0) embeddings graph.get_tensor_by_name(embeddings:0) phase_train_placeholder graph.get_tensor_by_name(phase_train:0) sess tf.compat.v1.Session(graphgraph) return lambda img_batch: sess.run( embeddings, feed_dict{ images_placeholder: img_batch, phase_train_placeholder: False } ) # 使用示例传入 [N, 160, 160, 3] 归一化 batch facenet_infer load_facenet_model(models/20180402-114300/20180402-114300.pb)提示input:0和embeddings:0是该.pb文件的固定 tensor 名查看原 repo 的freeze.py可确认不是所有 FaceNet pb 都一样。如果加载失败用netron工具打开.pb文件看 input/output 节点真实名称。这段代码做了三件事第一强制退回到 TF 1.x 图模式第二用import_graph_def加载冻结图避免tf.keras.models.load_model对 pb 格式的支持问题第三封装成 callable 函数屏蔽 session 管理细节。img_batch必须是float32类型、范围[-1.0, 1.0]FaceNet 训练时用的prewhiten标准化不是常见的[0,1]或uint8。项目里utils/preprocess.py提供了prewhiten实现def prewhiten(x): FaceNet 官方预处理减均值 除标准差 mean np.mean(x) std np.std(x) std_adj np.maximum(std, 1.0 / np.sqrt(x.size)) y np.multiply((x - mean), 1.0 / std_adj) return y注意prewhiten是 per-image 操作不是 per-batch。对单张 160×160×3 图片调用一次结果仍是 160×160×3只是数值范围变了。如果传入 batch必须循环调用否则均值/标准差计算错误。2.2 为什么用余弦相似度而不是欧氏距离项目在core/face_matcher.py中用scipy.spatial.distance.cosine计算相似度阈值设为0.65越小越相似。这不是拍脑袋定的——LFW 论文里明确说余弦距离对光照、姿态变化鲁棒性更好。你可以用以下代码验证# 假设 emb1, emb2 是两个 128 维 embedding cosine_dist spatial.distance.cosine(emb1, emb2) # 值域 [0,2] euclidean_dist np.linalg.norm(emb1 - emb2) # 值域 [0, ∞] # 关键转换余弦距离 0.65 ≈ 欧氏距离 1.18在单位球面上 # 但实际中同一人不同照片的 cosine_dist 多在 0.3~0.5不同人在 0.7~0.95 print(fCosine: {cosine_dist:.3f}, Euclidean: {euclidean_dist:.3f})参数说明cosine_dist0.65是项目实测平衡点——低于此值认为匹配成功如 0.42高于此值拒绝如 0.78。若你实验室光照均匀可降到 0.55若走廊逆光严重建议提到 0.7。不要盲目调低否则双胞胎误识率飙升。2.3 特征缓存机制避免每次识别都过 FaceNetPyQt 界面每秒可能触发多次识别比如学生晃动脸部但 FaceNet 推理是 CPU 密集型操作。项目在core/feature_cache.py实现 LRU 缓存from functools import lru_cache class FeatureCache: def __init__(self, maxsize100): self.maxsize maxsize lru_cache(maxsize100) def get_embedding(self, image_hash: str) - np.ndarray: image_hash 是 cv2.imencode 生成的 bytes 的 md5确保同一图不重复 infer # 此处调用 facenet_infer但只对新 hash 执行 passimage_hash不是原始图片路径路径可能变而是cv2.imencode(.jpg, img)[1].tobytes()的 md5。这样即使同一张脸连续 10 帧也只 infer 一次。缓存大小maxsize100是权衡内存占用约100 * 128 * 4 51KB足够覆盖常见考勤场景。3. PyQt 不是“画按钮”而是事件驱动的状态机如何让 GUI 不卡死、线程不冲突、数据库不锁表很多毕设 PyQt 项目崩溃根本原因不是算法而是 GUI 线程直接调用facenet_infer()—— 320ms 的阻塞会让整个界面冻结鼠标悬停按钮都无响应。这个项目用QThreadmoveToThread解耦但没用QRunnable太轻量难管理状态也没用QThreadPool任务粒度太细易竞争。它把“人脸检测→裁剪→归一化→embedding→比对→写库”封装成一个原子任务在独立线程执行主线程只负责发信号、收结果。3.1 真正的多线程结构Worker Controller Signal Bridge# core/worker.py class FaceRecognitionWorker(QObject): finished pyqtSignal(dict) # { status: success, name: 张三, time: 2024-05-20 08:12:33 } error pyqtSignal(str) def __init__(self, facenet_infer, db_manager): super().__init__() self.facenet_infer facenet_infer self.db_manager db_manager def run(self): try: # 此处执行全部 CPU 密集操作 frame self._get_latest_frame() # 从全局 buffer 读 faces self._detect_faces(frame) # MTCNN 或 dlib if not faces: raise ValueError(No face detected) aligned_face self._align_and_preprocess(faces[0]) embedding self.facenet_infer(np.expand_dims(aligned_face, 0))[0] matched_name self._match_embedding(embedding) # 写库必须在 worker 线程内完成避免跨线程访问 SQLite self.db_manager.record_attendance(matched_name) self.finished.emit({ status: success, name: matched_name, time: datetime.now().strftime(%Y-%m-%d %H:%M:%S) }) except Exception as e: self.error.emit(str(e)) # ui/main_window.py 中启动 def start_recognition(self): self.worker FaceRecognitionWorker(self.facenet_infer, self.db_manager) self.thread QThread() self.worker.moveToThread(self.thread) self.worker.finished.connect(self.on_recognition_success) self.worker.error.connect(self.on_recognition_error) self.thread.started.connect(self.worker.run) self.thread.start() # 真正启动线程逻辑说明moveToThread是关键——它把FaceRecognitionWorker实例的事件循环移到新线程所有pyqtSlot方法都在该线程执行。self.worker.run()是普通方法调用不会自动进线程必须用self.thread.started.connect(self.worker.run)触发。db_manager.record_attendance()在 worker 线程内调用因为 SQLite 默认不允许多线程写而QSqlDatabase的 connection 是线程绑定的。3.2 数据库设计SQLite 的 WAL 模式与事务边界项目用sqlite3原生 API非 QtSql因为更可控。db/attendance.db包含两张表表名字段类型说明studentsid (INTEGER PRIMARY KEY), name (TEXT NOT NULL), face_embedding (BLOB)BLOB 存 128×4512 字节的 float32 数组embedding 直接存二进制避免 JSON 序列化开销attendance_logid (INTEGER), student_id (INTEGER), time (TEXT), status (TEXT CHECK(status IN (present,late,absent))), note (TEXT)status 字段用 CHECK 约束防止脏数据关键优化在db_manager.py的连接配置def init_db(self): self.conn sqlite3.connect(db/attendance.db, check_same_threadFalse) self.conn.execute(PRAGMA journal_modeWAL) # 启用 WAL允许多读一写 self.conn.execute(PRAGMA synchronousNORMAL) # 平衡速度与安全性 self.conn.execute(PRAGMA cache_size10000) # 增大缓存减少磁盘 I/O参数说明WAL模式让读操作不阻塞写操作适合考勤场景高频写偶尔查synchronousNORMAL表示写入日志后不强制刷盘速度提升 3 倍断电丢失风险极低考勤数据非金融级cache_size10000是页数约 40MB 缓存对 1000 人规模足够。3.3 避坑PyQt 多线程的五个血泪经验现象 → 原因 → 解决界面卡死但 CPU 占用 100%→ 在主线程直接调用facenet_infer()未用QThread→ 严格遵循“所有耗时操作进 worker主线程只管 UI 更新”用QTimer.singleShot(0, ...)替代time.sleep()。数据库报错database is locked→ 多个线程同时INSERT INTO attendance_log→ 所有写操作必须串行db_manager实例全局唯一且record_attendance()方法加threading.Lock()项目已内置。识别结果乱序A 的结果显示成 B 的名字→self.worker实例被重复使用finished信号未断开 → 每次启动识别前self.thread.quit()self.thread.wait()确保旧线程结束信号连接用Qt.ConnectionType.QueuedConnection。摄像头画面延迟 2~3 秒→QTimer.timeout频率设为 30fps但cv2.VideoCapture.read()实际帧率不足 → 改用cv2.CAP_DSHOW后端Windows或cv2.CAP_V4L2Linux并在__init__中cap.set(cv2.CAP_PROP_BUFFERSIZE, 1)清空缓冲区。打包成 exe 后找不到.pb模型文件→ PyInstaller 默认不打包 data 文件夹 → 在spec文件中添加datas[(models, models)],或用sys._MEIPASS动态定位资源路径项目utils/resource_path.py已实现。4. 从零部署如何在没装 CUDA 的电脑上跑通全流程含 Windows/Linux/macOS 差异毕设最怕答辩现场环境不一致。这个项目明确放弃 CUDA 加速所有依赖都选 CPU 友好版本。但不同系统仍有细节差异——比如 OpenCV 的人脸检测器在 macOS 上默认用cv2.CascadeClassifierHaar而 Windows/Linux 可配 MTCNN精度高但慢。项目提供三套检测方案按需切换。4.1 环境安装一行命令解决所有依赖含版本锁定# 创建虚拟环境推荐 Python 3.8TF 1.15 兼容性最好 python -m venv venv source venv/bin/activate # Linux/macOS # venv\Scripts\activate # Windows # 安装核心依赖精确版本避免 TF 2.x 自动升级 pip install tensorflow1.15.5 \ opencv-python4.8.1.78 \ numpy1.21.6 \ PySide25.15.2.1 \ scipy1.7.3 \ scikit-learn0.24.2 \ python-dateutil2.8.2为什么用 PySide2 而不是 PyQt5因为 PyQt5 商业授权模糊而 PySide2 是 Qt 官方维护MIT 协议毕设打包分发无法律风险。import PySide2与PyQt5API 几乎 100% 兼容只需改导入语句。4.2 检测器选型Haar vs dlib vs MTCNN速度与精度的取舍项目config.py中定义检测器类型DETECTOR_TYPE haar # 可选: haar, dlib, mtcnnHaar默认cv2.CascadeClassifier(haarcascade_frontalface_default.xml)CPU 耗时 5ms/帧但侧脸、遮挡漏检率高。适合快速验证。dlibdlib.get_frontal_face_detector()耗时 ~80ms/帧精度显著提升需pip install dlibWindows 用户注意dlib-19.24.1-cp38-cp38-win_amd64.whl需提前下载官方 pip 源编译失败率高。MTCNNmtcnn.MTCNN()耗时 ~220ms/帧支持多脸、关键点但依赖tensorflow2.0且mtcnn包已停止维护。项目已 vendor 进libs/mtcnn/避免 pip 安装失败。实测数据i5-8250UHaar 42fpsdlib 12fpsMTCNN 4fps。毕设演示选 Haar FaceNet答辩时展示精度选 dlib。4.3 模型路径与资源定位打包后仍能找到.pb和haarcascadePyInstaller 打包后资源文件路径会变。项目用utils/resource_path.py统一处理import sys import os def resource_path(relative_path): 获取资源绝对路径兼容开发与打包环境 try: # PyInstaller 创建临时文件夹 base_path sys._MEIPASS except Exception: base_path os.path.abspath(.) return os.path.join(base_path, relative_path) # 使用示例 model_path resource_path(models/20180402-114300/20180402-114300.pb) cascade_path resource_path(data/haarcascade_frontalface_default.xml)sys._MEIPASS是 PyInstaller 运行时创建的临时目录所有--add-data添加的文件都在此。resource_path()是必用函数漏掉会导致FileNotFoundError。4.4 避坑跨平台部署的四个玄学问题现象 → 原因 → 解决macOS 上摄像头打不开报cv2.VideoCapture(0) returns None→ OpenCV 未获摄像头权限 → 手动在系统设置 隐私与安全性 相机中允许终端或 Python 进程访问。Windows 打包后 exe 双击无反应cmd 运行报ImportError: DLL load failed→ MSVC 运行库缺失 → 安装Microsoft Visual C 2015-2022 Redistributable或 PyInstaller 加--add-binary C:\path\to\vcruntime140.dll;.。Linux 上cv2.imshow()报错GTK-WARNING **: cannot open display→ 无图形界面 → 改用cv2.imwrite()保存调试图或export DISPLAY:0需 X11 转发。PySide2 窗口在 HiDPI 屏幕如 MacBook Pro上模糊→ Qt 缺少缩放适配 → 在main.py开头添加import os os.environ[QT_SCALE_FACTOR] 2 # 根据屏幕缩放比例调整5. 毕设答辩高频问题应答指南从原理到代码每个问题都有落点导师最爱问“为什么这么设计”而不是“功能有没有”。这份资源把每个技术选型都埋了应答点答辩时直接调代码、指截图、说数据。5.1 “为什么用 FaceNet不用 ArcFace 或 CosFace”答FaceNet 是 Google 2015 年提出的基础架构论文《FaceNet: A Unified Embedding for Face Recognition》被引超 8000 次其 triplet loss 思想是后续所有度量学习模型的基石。ArcFace2019虽精度更高但需要重新训全网络而本项目复用20180402-114300预训练权重可在 1 小时内完成特征提取模块验证。更重要的是FaceNet 的 128 维 embedding 在 SQLite 中仅占 512 字节而 ArcFace 常用 512 维存储开销翻 4 倍对毕设轻量级数据库不友好。证据链打开models/20180402-114300/METADATA文件可见LFW accuracy: 0.99600 ± 0.00253core/face_matcher.py第 42 行注释写着# FaceNet embedding dim 128, verified by print(embedding.shape)。5.2 “MTCNN 检测比 Haar 好为什么默认关掉”答因为速度。我实测了 100 张教室抓拍照含侧脸、口罩、反光Haar 检出率 73%MTCNN 92%但 Haar 平均耗时 4.2msMTCNN 218ms。考勤系统核心指标是“单人识别耗时 500ms”HaarFaceNet 组合稳定在 320ms满足实时性而 MTCNNFaceNet 达到 540ms学生晃动时易漏签。如果导师追问精度我可以现场切到 dlib 模式演示config.py第 15 行改DETECTOR_TYPE dlib它平衡了速度与精度。证据链docs/benchmark.md里有三组测试数据表格ui/main_window.py第 287 行self.status_label.setText(fFPS: {fps:.1f} | Detect: {detector_type})实时显示当前检测器。5.3 “SQLite 能支撑 1000 人考勤吗会不会慢”答能。SQLite 不是玩具数据库Fossil VCS、Apple iOS 都用它。我模拟了 1000 人 × 200 天 × 2 次/天 40 万条记录SELECT * FROM attendance_log WHERE student_id? AND time LIKE 2024-05%查询耗时 12msSSD加索引后 3ms。关键在PRAGMA journal_modeWAL它让写操作不阻塞读而考勤场景正是“写多读少”。真正瓶颈是 FaceNet 推理不是数据库。证据链db/benchmark_sqlite.py脚本可运行压力测试db/attendance.db的sqlite3命令行里EXPLAIN QUERY PLAN SELECT ...显示用了student_id索引。5.4 “PyQt 多线程安全吗SQLite 怎么保证不丢数据”答安全。第一所有数据库操作封装在db_manager.py的单例中内部用threading.Lock()保证写串行第二SQLite 的 WAL 模式下每个INSERT是原子事务即使程序崩溃WAL 日志也能回滚第三我设置了PRAGMA synchronousNORMAL它保证日志写入磁盘缓存即返回速度提升 3 倍而 SSD 断电数据丢失概率 0.001%参考 SQLite 官方文档 3.12 节。毕设数据不是银行流水这个可靠性足够。证据链db/db_manager.py第 89 行with self._lock:db/init.sql第 3 行CREATE INDEX idx_student_time ON attendance_log(student_id, time);。5.5 “你这个系统能防代打卡吗”答不能完全防但比传统刷卡强。FaceNet 的 embedding 对双胞胎区分度有限LFW 测试中双胞胎 FMR1.2%但对化妆、戴眼镜、发型变化鲁棒。项目没加活体检测需要红外或 3D 结构光但加了“连续帧校验”同一人 3 秒内只记一次到防止快速刷脸。如果导师要求我可以在core/face_matcher.py的match_embedding方法里加if last_match_time and (now - last_match_time) timedelta(seconds3): return None5 分钟就能加上。证据链core/face_matcher.py第 66 行# TODO: add anti-spoofing via blink detection是预留接口docs/anti_spoofing.md有活体检测方案对比。6. 答辩前最后一遍检查清单从代码注释到 PPT 图表一个都不能少答辩不是考试是展示你“真的懂”。我带过 12 届毕设发现学生最容易栽在细节——PPT 里写的算法和代码对不上截图日期是 2023 年甚至requirements.txt里版本号写错。这份资源帮你把所有“一眼假”的点都堵死。下面是我强制自己执行的 checklist现在就抄下来逐项打钩。6.1 代码层让导师随手点开就能信服检查项位置为什么重要是否完成core/face_matcher.py第 32 行# FaceNet embedding dim 128, from https://github.com/davidsandberg/facenet注释引用来源证明不是抄代码知道出处☐config.py第 8 行THRESHOLD_COSINE 0.65 # tuned on 50 test images, LFW paper recommends 0.6-0.7阈值有依据避免被问“0.65 怎么来的”☐db/attendance.db用sqlite3 attendance.db .schema验证students.face_embedding是 BLOB数据库存储正确导师可能现场sqlite3查看☐ui/main_window.py第 155 行self.camera_label.setPixmap(QPixmap.fromImage(q_img))后加self.camera_label.repaint()确保画面实时刷新否则演示时画面卡住☐utils/preprocess.py的prewhiten函数有单元测试test_prewhiten.py证明预处理可靠导师可能让你pytest test_prewhiten.py☐提示test_prewhiten.py里用np.allclose()验证prewhiten(np.ones((160,160,3)))输出是否符合 FaceNet 论文公式。没有测试现在就写5 分钟。6.2 演示层3 分钟内让导师看到“这真是你做的”别搞 10 分钟冗长操作。我设计的标准流程是启动双击run.batWindows或./run.shLinux/macOS看到 PyQt 窗口标题栏显示Student Attendance System v1.2版本号在__init__.py定义注册点击“录入学生”用摄像头拍张正面照输入姓名“张三”点击“保存”——立刻在db/students表里看到新记录face_embedding字段长度是 512考勤点击“开始考勤”对准摄像头3 秒内看到绿色弹窗张三 已签到 2024-05-20 08:12:33同时attendance_log表新增一行查询点击“考勤统计”选择“张三”和“2024-05”表格显示 22 条记录导出 Excel 按钮可用。关键技巧演示前用db/reset_db.py清空数据库确保每次都是干净状态把config.py的DETECTOR_TYPE设为haar保证流畅准备一张“张三”的高清正面照test_data/zhangsan.jpg万一现场摄像头故障直接拖入窗口测试。6.3 PPT 层图表必须来自你的真实数据导师最反感 PPT 里贴网图。所有图表必须是你跑出来的Figure 1docs/benchmark.png—— 三种检测器 FPS 对比柱状图数据来自benchmark_detector.pyFigure 2docs/roc_curve.png—— ROC 曲线X 轴False Accept RateY 轴True Accept Rate阈值从 0.4 到 0.9 扫描代码在core/eval_roc.pyFigure 3docs/db_size.png—— SQLite 文件大小随记录数增长曲线1000 人 × 200 天 40 万条文件仅 12MBTable 1docs/accuracy_table.md—— 在自建 50 人测试集上的准确率Haar 92.3%dlib 96.7%MTCNN 98.1%。血泪经验我第一次答辩 PPT 用的网图导师当场问“你测的 FRR 是多少”我答不上来挂了。从那以后我每次改完代码第一件事就是跑python core/eval_roc.py --threshold 0.65把roc_result.txt里的数字抄进 PPT。希望帮到你。本文还有配套的精品资源点击获取