简介:面向计算机专业毕业设计及课程实践的Python人脸识别考勤系统项目包,基于深度学习FaceNet算法完成人脸特征提取与身份比对,覆盖人脸录入、人脸识别、考勤管理、课堂管理、班级管理、日志管理等功能模块,适用于正在准备毕设、课程设计或期末大作业的学生及需要项目实战练习的学习者。资源压缩包共含2000个文件,以Python源码为主体(py文件1957个),另含PDF论文文档、HTML/CSS/JS前端资源、配置文件与说明文档等,整体大小84.31MB。已有698人学习下载,项目经过严格调试,下载即可直接运行。除完整源码外,还提供配套毕业论文等参考材料,目录结构清晰,便于快速理解FaceNet算法与考勤业务逻辑,可直接作为毕业设计交付内容,也可作为深度学习人脸识别实战的优质范例。
1. 基于深度学习的人脸识别考勤系统是什么:给本科毕设的真实工作量预估
基于深度学习的人脸识别考勤系统,简单说就是摄像头拍一张人脸,系统判断你是谁,然后替你完成打卡。它在本科毕业设计里属于性价比很高的选题:深度学习的技术点足够新,MTCNN人脸检测、FaceNet特征嵌入、迁移学习这些概念每一个都能单独写一节;系统本身没有复杂业务黑匣子,数据流清晰,代码量控制在两千行左右,一个人花两到三个月能完成。适合有Python基础、想走视觉方向、手上没有真实企业数据的本科生。这篇笔记按我实际做这类系统的路径展开,把技术选型、环境配置、代码骨架、踩坑记录和论文写作一次讲完,目标是让你照着走能跑出一个可演示的完整项目。
2. 技术选型与核心原理:为什么MTCNN+FaceNet是毕业设计的最优组合
2.1 人脸识别考勤的技术链路:从摄像头到打卡记录的完整路径
人脸识别考勤系统表面上是个"摄像头加数据库"的小工程,但拆开看,算法侧有四个必须串起来的环节:人脸检测、人脸对齐、特征提取、特征比对。检测解决"脸在哪里",输出矩形框坐标;对齐解决"脸是否摆正",通过眼鼻嘴关键点做仿射变换,把倾斜和侧脸统一转成标准正脸;特征提取是核心,负责把一张160×160的图片映射成固定维度的向量,同一个人不同照片的向量距离近,不同人的向量距离远;比对则计算现场向量和库中向量的距离,低于阈值就判定为同一人。这四个环节串得好不好,直接决定系统在教室、实验室这类开放场景里的可用性。
很多人一开始会跳过对齐,直接对检测框做识别。这个偷懒会在答辩演示时暴露得很彻底:抬头看投影的学生、侧头讨论的同学都会被误判。我的建议是第一天就把对齐写进流程,它带来的准确率提升在室内场景下通常有5到10个百分点。
再加上一层业务逻辑,就是考勤侧的事:身份判定后,判断当前是否在考勤时间窗内、是否已经打过卡、是否迟到,最后写入数据库并生成报表。算法侧和业务侧的分界要清晰,否则调试时你分不清到底是模型认错了人,还是打卡逻辑写错了。我在开发时把这两侧拆成两个独立的Python包,算法包只暴露recognize(face_tensor)这样一个函数,业务包只负责读写数据库,联调时出问题能快速定位到层。
2.2 方案选型:传统OpenCV、云端API与本地深度学习的对比
做毕设的人脸识别方案选择,基本逃不开三档。第一档是OpenCV自带的Haar级联检测器加LBPH人脸识别器,传统机器学习路线,任何电脑都能跑,但准确率在教室这类多人密集、姿态随意的场景里勉强及格,光照一变就大面积漏检。第二档是调云端API,识别精度很高,但需要联网、按次计费,答辩现场一旦断网就变成纯PPT项目,而且论文里很难写出你自己的技术工作。第三档就是本地跑深度学习模型,完全离线、零成本,这也是多数做这个题目的同学最终选择的路线。
在本地深度学习路线里,我用过且推荐的是MTCNN做人脸检测,FaceNet家族里的InceptionResnetV1做人脸特征提取。MTCNN是一个三级级联卷积网络,先快速筛出候选框,再逐步精修,输出人脸框、置信度和五个关键点,对侧脸、遮挡、暗光都有不错的鲁棒性;FaceNet的思路是端到端学习一个嵌入空间,用三元组损失让同类距离近、异类距离远,vggface2预训练版本在LFW数据集上的人脸验证准确率超过99%,够用。ArcFace在精度上确实更胜一筹,但训练代码、数据集要求都更重,调试周期明显拉长,对本科毕设属于杀鸡用牛刀。
| 方案 | 离线 | 识别精度 | 实现成本 | 适合场景 |
|---|---|---|---|---|
| OpenCV Haar+LBPH | 是 | 中低 | 很低 | 光线稳定的入门demo |
| 云端API | 否 | 高 | 低但付费 | 联网演示 |
| MTCNN+FaceNet | 是 | 高 | 中 | 本科毕设首选 |
| ArcFace | 是 | 更高 | 高 | 研究型毕设 |
顺带一提,人脸识别门禁机上跑的算法也大多是这个路线,只是工程上多了活体检测和硬件交互。你把考勤系统的识别部分做扎实了,往门禁场景迁移只是换一个输入源的事。
2.3 系统架构:模块划分、数据流与数据库设计
一个可交付的人脸识别考勤系统,我习惯按五个模块拆:摄像头采集模块、人脸处理模块(检测+对齐+特征提取)、身份比对模块、考勤业务模块、数据展示模块。模块之间用文件或数据库解耦,人脸特征库存成pickle文件,考勤记录存SQLite,摄像头画面由主循环驱动,界面轮询数据库刷新记录。这样每个模块都能独立测试,单人开发也不会把自己绕晕。
数据流是:摄像头逐帧读取,MTCNN检测出人脸框和关键点,对齐裁切后送入InceptionResnetV1得到128维向量,与已入库向量逐一算欧氏距离,最小距离低于阈值则判定身份,再查考勤表判断冷却期和考勤窗口,最后写入打卡记录。这条链路每一步都有明确的输入输出,论文里的系统设计章节直接照着画数据流图,评审老师一眼就能看出系统是完整的。
数据库设计上,我用了最基础的两张表,一张存学生信息,一张存考勤记录:
CREATE TABLE student ( student_id TEXT PRIMARY KEY, name TEXT NOT NULL, class_name TEXT, enroll_year INTEGER ); CREATE TABLE attendance ( id INTEGER PRIMARY KEY AUTOINCREMENT, student_id TEXT NOT NULL, check_time TEXT NOT NULL, status TEXT DEFAULT '正常', FOREIGN KEY (student_id) REFERENCES student(student_id) );注意attendance表用自增id做主键,student_id只做外键关联。这样即使某天删除了一条误打卡记录,也不会影响其他记录的主键稳定性。status字段预留了'迟到''早退'的扩展空间,后面写报表统计时直接按status分组计数,不需要改表结构。
3. 环境搭建与数据准备:三天内跑通最小系统的完整流程
3.1 Python、PyTorch与CUDA的版本匹配
人脸识别的深度学习环境,最核心的依赖是PyTorch。我建议用conda建一个独立环境,Python版本按你安装教程里常见的3.8或3.9选,PyTorch装哪个CUDA构建版取决于你的显卡驱动。查驱动支持CUDA版本最稳的命令是nvidia-smi,看Driver Version下面那行CUDA Version,PyTorch对应装小于等于这个数字的CUDA构建即可。
# 创建环境并激活 conda create -n face_attendance python=3.8 conda activate face_attendance # 安装PyTorch,cu118表示CUDA 11.8版本 # 如果驱动版本老,改成 cu113 或 cu121 对应的命令 pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu118 # 安装人脸识别相关库 pip install facenet-pytorch opencv-python pandas numpy pillow # 验证PyTorch能否调用GPU python -c "import torch; print(torch.cuda.is_available()); print(torch.cuda.get_device_name(0))"安装完成后那两行验证输出,第一行如果是True说明PyTorch识别到了显卡,第二行会打印显卡型号;如果第一行是False,先别继续往下走,去检查驱动版本和PyTorch的CUDA构建是否匹配,这个坑在后面专门有一节展开。没有NVIDIA显卡也不要紧,MTCNN和FaceNet在CPU上也能跑,只是识别一帧从30毫秒变成300毫秒,演示时能感觉到明显卡顿。
facenet-pytorch这个库是本文方案的核心依赖,它把MTCNN和InceptionResnetV1都封装好了,接口比直接调dlib友好得多。新手上路最容易卡在pip安装上,报错多半是缺少Visual C++编译工具链,去微软官网装一下就能解决。
3.2 数据集制作:自采人脸照片的采集规范与预处理
数据集是整个系统的地基。用别人的公开人脸照片集做考勤系统,答辩时被问"这些人是谁?怎么进你的系统?"会直接卡住。正确做法是采集同班同学或室友的照片,每人30到50张,拍一段30秒的视频再抽帧,效率远高于让人对着摄像头一张张拍。
# extract_frames.py # 从视频中按固定间隔抽帧,快速积累一个学生的训练照片 import cv2 import os video_path = 'videos/2024001.mp4' # 以学号命名视频文件 save_dir = 'dataset/raw/2024001' # 原始照片保存目录 os.makedirs(save_dir, exist_ok=True) cap = cv2.VideoCapture(video_path) frame_count = 0 saved_count = 0 while True: ret, frame = cap.read() if not ret: break frame_count += 1 # 每5帧保存一张,30秒视频约能抽出180张原始帧 if frame_count % 5 == 0: out_path = os.path.join(save_dir, f'frame_{frame_count:04d}.jpg') cv2.imwrite(out_path, frame) saved_count += 1 cap.release() print(f'视频共有 {frame_count} 帧,保存 {saved_count} 张')抽出来的原始帧包含大量背景,直接送去训练会引入噪声。下一步用MTCNN裁出人脸并统一缩放,这一步我习惯单独跑一个脚本,核心参数是margin设为20像素、统一缩放到160×160。margin是为了让裁切的人脸包含一点额头和下巴,避免关键点贴边;160×160是InceptionResnetV1的标准输入尺寸。如果连续多张检测不到人脸,说明视频里人像太小,拍视频时让人脸占画面三分之一以上。
3.3 迁移学习:用预训练模型微调自己的分类器
考勤场景需要把每个学生当成一个类别。FaceNet预训练模型已经能提取通用人脸特征,但为了让你自己的数据集上同一个人更聚拢,可以在预训练模型最后接一个全连接分类层,用交叉熵损失微调。这就是迁移学习,也是论文里很好写的技术点。
# train_classifier.py # 迁移学习微调:冻结骨干网络,只训练最后的分类层 import torch from facenet_pytorch import InceptionResnetV1 from torch.utils.data import Dataset, DataLoader from torchvision import transforms from PIL import Image import os class FaceDataset(Dataset): """读取dataset/aligned下的学生照片,每个文件夹一人""" def __init__(self, root): self.classes = sorted(os.listdir(root)) self.class_to_idx = {c: i for i, c in enumerate(self.classes)} self.samples = [] for cls in self.classes: cls_dir = os.path.join(root, cls) for name in os.listdir(cls_dir): self.samples.append((os.path.join(cls_dir, name), self.class_to_idx[cls])) def __len__(self): return len(self.samples) def __getitem__(self, idx): path, label = self.samples[idx] img = Image.open(path).convert('RGB') img = transforms.ToTensor()(img) # 归一化到[0,1] return img, label dataset = FaceDataset('dataset/aligned') dataloader = DataLoader(dataset, batch_size=16, shuffle=True) # 加载预训练模型,替换最后一层为类别数 model = InceptionResnetV1(pretrained='vggface2').eval() model.logits = torch.nn.Linear(512, len(dataset.classes)) model.train() # 只更新logits层参数,冻结其他所有层 for param in model.parameters(): param.requires_grad = False for param in model.logits.parameters(): param.requires_grad = True criterion = torch.nn.CrossEntropyLoss() optimizer = torch.optim.Adam(model.logits.parameters(), lr=0.001) for epoch in range(15): total_loss = 0.0 for imgs, labels in dataloader: optimizer.zero_grad() outputs = model(imgs) loss = criterion(outputs, labels) loss.backward() optimizer.step() total_loss += loss.item() print(f'Epoch {epoch+1}/{15} Loss: {total_loss/len(dataloader):.4f}') torch.save(model.state_dict(), 'models/student_classifier.pt') print('模型已保存到 models/student_classifier.pt')这里有个容易踩的版本差异:facenet-pytorch在旧版本里最后的全连接层叫last_linear,新版本改成了logits。如果你用last_linear却报KeyError,改成logits即可。冻结骨干只训分类层,是因为vggface2预训练模型已经学到足够好的人脸特征,你手里几十张照片微调整个网络很容易过拟合。如果loss降到2.0就不动了,可以解冻最后两个Block用更小的学习率再训,但本科毕设一般不需要做到这一步。
4. 核心代码实现:人脸检测、特征入库与实时考勤主循环
4.1 MTCNN人脸检测与对齐:三个关键参数决定检测质量
MTCNN在facenet-pytorch里用起来很简单,但参数影响很大。image_size控制输出图片边长,margin控制裁切边距,keep_all控制是否返回多张人脸。在考勤场景中我推荐image_size=160、margin=20、keep_all=False,检测到多张脸时只取置信度最高的那个。
from facenet_pytorch import MTCNN import cv2 # 初始化MTCNN,有GPU自动用GPU device = 'cuda' mtcnn = MTCNN( image_size=160, # 输出图片边长,FaceNet标准输入 margin=20, # 人脸框外扩像素,防止裁掉额头 min_face_size=40, # 小于40像素的人脸直接忽略,过滤远处路人 keep_all=False, # 只返回置信度最高的人脸 device=device ) frame = cv2.imread('test.jpg') frame_rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) # 细粒度检测:拿到框、关键点和置信度 boxes, probs, landmarks = mtcnn.detect(frame_rgb, landmarks=True) if boxes is not None: for i, box in enumerate(boxes): x1, y1, x2, y2 = [int(v) for v in box] cv2.rectangle(frame, (x1, y1), (x2, y2), (0, 255, 0), 2) cv2.putText(frame, f'{probs[i]:.2f}', (x1, y1-10), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 255, 0), 2) cv2.imwrite('detect_result.jpg', frame)detect方法返回三个值:boxes是[N,4]的框数组,probs是置信度数组,landmarks是[N,5,2]的关键点坐标。注意坐标体系是RGB图像上的,直接用OpenCV的BGR图画框不受影响,因为坐标不依赖通道顺序。真正送入特征提取网络时,用mtcnn(img)而不是mtcnn.detect(img),前者直接返回对齐并裁切好的tensor,一步到位。
4.2 特征入库:把每个学生的照片转成128维向量
人脸特征库是系统的心脏。每个学生注册时,把他几十张照片分别过一遍InceptionResnetV1,得到几十个128维向量,取平均作为这个人的人脸特征。平均这一步能显著平滑照片间的光线和表情差异,是我调试后确认性价比最高的技巧。
# register_faces.py # 把数据集全部入库为128维向量,保存为pickle文件 import os import pickle import numpy as np import torch from PIL import Image from facenet_pytorch import MTCNN, InceptionResnetV1 device = 'cuda' if torch.cuda.is_available() else 'cpu' mtcnn = MTCNN(image_size=160, margin=20, device=device) resnet = InceptionResnetV1(pretrained='vggface2').eval().to(device) face_db = {} data_root = 'dataset/aligned' for student_id in sorted(os.listdir(data_root)): student_dir = os.path.join(data_root, student_id) embeddings = [] for img_name in os.listdir(student_dir): img_path = os.path.join(student_dir, img_name) img = Image.open(img_path).convert('RGB') # mtcnn()返回对齐后的3x160x160 tensor,无法检测则返回None face = mtcnn(img) if face is None: continue face = face.unsqueeze(0).to(device) with torch.no_grad(): emb = resnet(face).cpu().numpy()[0] embeddings.append(emb) if len(embeddings) >= 5: # 多张照片取平均,降低单张照片光线、表情的影响 face_db[student_id] = np.mean(embeddings, axis=0) print(f'{student_id}: 入库 {len(embeddings)} 张,特征维度 {face_db[student_id].shape}') else: print(f'{student_id}: 有效照片不足5张,跳过') with open('face_db.pkl', 'wb') as f: pickle.dump(face_db, f) print(f'入库完成,共 {len(face_db)} 人')这个脚本有两个必须注意的点。第一,resnet必须处于eval模式,否则BatchNorm在推理时会产生随机性,导致同一张照片每次提取的向量都不一样,识别阶段就会出现明明是同一个人却比对失败。第二,只保留至少5张有效照片的学生,照片太少时平均向量不稳定,考勤阶段会频繁误判。文件输出用pickle保存字典,加载时一行代码,对入门阶段最友好。
4.3 实时考勤主循环:阈值、去重与迟到判定的代码骨架
实时环节把前面的模块全部串起来。主循环做的事情:读取摄像头帧,MTCNN检测人脸,FaceNet提取向量,与库中向量算距离,距离小于阈值判定身份,检查是否在冷却期,写入SQLite。这里最需要调的是THRESHOLD,它控制系统的严格程度。
# attendance_main.py import cv2 import pickle import numpy as np import torch from datetime import datetime from facenet_pytorch import MTCNN, InceptionResnetV1 import sqlite3 # 加载人脸特征库 with open('face_db.pkl', 'rb') as f: face_db = pickle.load(f) device = 'cuda' if torch.cuda.is_available() else 'cpu' mtcnn = MTCNN(image_size=160, margin=20, device=device) resnet = InceptionResnetV1(pretrained='vggface2').eval().to(device) # 打开SQLite,自动建表 conn = sqlite3.connect('attendance.db') conn.execute('''CREATE TABLE IF NOT EXISTS attendance ( id INTEGER PRIMARY KEY AUTOINCREMENT, student_id TEXT NOT NULL, check_time TEXT NOT NULL, status TEXT NOT NULL DEFAULT '正常' )''') # 两个核心参数:距离阈值和打卡冷却时间 THRESHOLD = 0.85 # 欧氏距离阈值,越小越严格 COOLDOWN_SECONDS = 60 # 同一人最短打卡间隔,防止重复打卡 last_check = {} # student_id -> datetime def recognize(face_tensor): """输入对齐后的tensor,返回(学号, 距离)或(None, 距离)""" with torch.no_grad(): emb = resnet(face_tensor.unsqueeze(0)).cpu().numpy()[0] best_id = None best_dist = float('inf') for sid, db_emb in face_db.items(): dist = np.linalg.norm(emb - db_emb) if dist < best_dist: best_dist = dist best_id = sid if best_dist <= THRESHOLD: return best_id, best_dist return None, best_dist cap = cv2.VideoCapture(0) frame_skip = 0 while True: ret, frame = cap.read() if not ret: break frame_skip += 1 if frame_skip % 3 != 0: # 每3帧处理一次,降低CPU占用 continue boxes, probs = mtcnn.detect(frame, landmarks=False) if boxes is None: cv2.imshow('Attendance', frame) if cv2.waitKey(1) & 0xFF == ord('q'): break continue for box in boxes: x1, y1, x2, y2 = [int(v) for v in box] face = mtcnn(frame) # 再次调用,获取对齐后的tensor if face is None: continue face = face.to(device) student_id, dist = recognize(face) now = datetime.now() if student_id: last = last_check.get(student_id) if last is None or (now - last).total_seconds() > COOLDOWN_SECONDS: conn.execute( 'INSERT INTO attendance (student_id, check_time, status) VALUES (?, ?, ?)', (student_id, now.strftime('%Y-%m-%d %H:%M:%S'), '正常') ) conn.commit() last_check[student_id] = now print(f'[{now}] {student_id} 打卡成功,距离 {dist:.3f}') cv2.rectangle(frame, (x1, y1), (x2, y2), (0, 255, 0), 2) cv2.putText(frame, f'{student_id} {dist:.2f}', (x1, y1-10), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 255, 0), 2) else: cv2.rectangle(frame, (x1, y1), (x2, y2), (0, 0, 255), 2) cv2.putText(frame, f'unknown {dist:.2f}', (x1, y1-10), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 0, 255), 2) cv2.imshow('Attendance', frame) if cv2.waitKey(1) & 0xFF == ord('q'): break cap.release() cv2.destroyAllWindows() conn.close()这段主循环里有三个容易出问题的设计点。第一,mtcnn.detect和mtcnn(frame)被调用了两次,前者拿坐标画框,后者拿对齐后的tensor送识别,一次检测一次对齐,逻辑清晰但每帧多花一次检测时间,真觉得卡顿就把处理间隔改成每5帧一次。第二,冷却时间的判断必须在写入数据库之前,否则同一张脸在镜头前多站两秒就会写好几条记录。第三,SQL写入用参数化方式,不要字符串拼接,一是防止SQL注入,二是避免学号里带特殊字符导致语句报错。
实际交付时,建议把界面从OpenCV的HighGUI换到Tkinter或PyQt5,把考勤记录、学生管理、报表导出画进同一个窗口,这就是完整的考勤系统界面了。第5.4节会讲界面和识别线程的关系。
5. 避坑与排查:让人脸识别考勤系统稳定运行的五个关键问题
5.1 问题:nvidia-smi显示CUDA可用,但PyTorch不调用GPU
现象:按教程装了CUDA版PyTorch,torch.cuda.is_available()却返回False,代码全程跑在CPU上,识别一帧要一两秒。
原因:九成是PyTorch的CUDA构建版本和显卡驱动版本不匹配。nvidia-smi里的CUDA Version是驱动能支持的最高版本,PyTorch要求驱动版本不低于它编译时对应的CUDA版本。还有种情况是conda默认频道装成了CPU版本。
解决:先运行python -c "import torch; print(torch.version)",看版本字符串里有没有cu后缀,比如2.1.0+cu118。没有cu后缀就卸载重装,安装时用官方whl源,不用conda默认源。装完再验证一次。如果驱动版本太老,去显卡厂商官网更新驱动,这是这套毕设里最花时间但必须解决的问题。
5.2 问题:同一个学生,换个角度就识别失败
现象:验证时正对镜头,距离0.5识别通过;侧头15度,距离跳到1.2,被判定为陌生人。
原因:注册时用的照片全是正脸,特征库里只有正脸的平均向量,现场出现侧脸、低头时,特征偏移量超过阈值。另外注册照片太少也会放大这个效应。
解决:一是注册时故意混入侧脸、微低头、不同光线的照片,让平均向量更贴近真实分布。二是把欧氏距离阈值从0.85放宽到1.0左右,同时接受误识率升高的代价,但要用现场场景反复验证。三是检查预处理是否真的做了对齐,侧脸没对齐直接裁切,特征偏移当然大。这个问题的本质是特征分布和阈值之间的平衡,论文实验部分如果把这组不同阈值下的准确率对比列出来,反而是加分项。
5.3 问题:考勤记录重复写入,一分钟内同一人被打十几次卡
现象:演示时人站在摄像头前不动,数据库里刷出十几条记录。
原因:去重逻辑写错了位置。我调试早期版本时,把冷却判断放在写入之后而不是之前,导致第一条记录写进去后,还没来得及更新last_check,第二条又进来了。
解决:把冷却时间判断放到数据库写入之前。每次识别成功后,先查last_check字典里上次打卡时间,间隔小于COOLDOWN_SECONDS就直接跳过写入。记住是先判断后写入,不要写完再更新冷却时间。COOLDOWN_SECONDS设为60到300秒,考勤场景一般不允许一分钟内重复打卡。另外,同一帧画面里多张脸也能分别触发打卡逻辑,所以循环前记得把同一个人在同一帧里重复出现的框去重。
5.4 问题:GUI界面卡死,窗口显示未响应
现象:点击系统界面后窗口转圈几秒,系统提示未响应,然后整个程序被强制关闭。
原因:人脸识别是计算密集操作,CPU上处理一帧要好几百毫秒。在GUI主线程里直接跑识别循环,界面的事件循环被阻塞,系统就判定程序无响应。这是初学者最常见的问题:代码逻辑没问题,但线程模型错了。
解决:把识别循环放进单独的工作线程,GUI主线程只负责显示和接收事件。用Python标准库的threading就可以:
import threading import tkinter as tk # 界面主线程 root = tk.Tk() # 识别线程 recognition_thread = threading.Thread(target=recognition_loop, daemon=True) recognition_thread.start() root.mainloop()recognition_loop里跑第4.3节的主循环,识别到身份后用队列把结果发给界面线程刷新显示。daemon=True保证关闭窗口后线程自动退出,不会残留后台进程。这一改,界面卡死的概率基本归零。
5.5 问题:把系统打包成exe后,模型文件路径加载失败
现象:在PyCharm里运行正常,用PyInstaller打包后在别的电脑打开,提示找不到模型文件、找不到face_db.pkl。
原因:PyInstaller打包时默认不把非py文件包含进去,而且运行时的工作目录和开发时不一定相同。模型权重、pkl文件这些资源文件不会被自动打包,用相对路径在打包后也容易失效。
解决:在spec文件里用datas参数把模型和pkl文件加进去,代码里用sys._MEIPASS取打包后的临时解压目录,拼上资源文件路径:
import sys, os def resource_path(relative_path): """兼容开发态和打包态的资源路径定位""" if hasattr(sys, '_MEIPASS'): return os.path.join(sys._MEIPASS, relative_path) return os.path.join(os.path.dirname(os.path.abspath(__file__)), relative_path) face_db_path = resource_path('face_db.pkl') model_path = resource_path('models/student_classifier.pt')这个resource_path函数是所有打包工具的通用惯例。开发时它指向源码目录,打包后指向PyInstaller解压的临时目录。以后凡是代码里加载模型、加载图片、加载配置文件的地方,统一走resource_path,就不会出现开发能用、打包不能用的问题。
6. 论文写作与答辩打磨:从能跑到高分的关键一步
代码和系统跑通只是毕设的一半,另一半在论文和答辩演示。本科毕设论文最看重的是做了什么、为什么这么做、效果如何。系统设计章节就按第2章的数据流展开,把每个模块的输入输出写清楚,配上系统架构图和时序图,这一章基本就稳了。实验章节不要只放一张"识别率95%"的截图,要设计三组对比:不同阈值下的准确率和误识率曲线、注册照片数量对识别距离的影响、CPU与GPU环境下的帧率对比,三张表撑起实验分析。
答辩现场演示是最容易翻车的地方。我的血泪经验是:提前录一段识别视频作为备用,万一现场摄像头驱动有问题、光线太暗或机器过热,播放录好的演示视频一样能讲清楚系统逻辑。现场演示时,让提前注册好的同学正对摄像头,保持人脸在画面中间,等待两三秒,不要着急点鼠标。如果识别失败,不要慌,自然地演示手动补卡功能,这反而说明系统设计了兜底方案。
答辩评委高频问题就三个方向:人脸特征是怎么表示的、阈值为什么设这么大、系统怎么防代打卡。前两个在论文第二章都有答案,第三个建议回答"当前版本靠冷却时间去重和人工复核,后续可以加活体检测",既讲清了边界又有扩展思路。
最后分享一个我写论文时的习惯:把系统架构图、数据库表结构、核心伪代码、实验数据表四样东西整理成一页PPT放第一页。答辩开场三十秒,评委看到的是一个结构完整、有实验数据的毕设,而不是一个只演示了能识别人的demo。希望这篇笔记能帮你把整个系统从头到尾搭通,也祝你答辩顺利。
本文还有配套的精品资源,点击获取