PyQt+FaceNet人脸识别考勤系统实战指南
简介本资源是一套完整的毕业设计项目面向计算机、人工智能及相关专业本科生解决高校学生课堂考勤中人工登记效率低、代签漏洞多等实际问题。系统以前端PyQt构建交互式桌面界面后端集成FaceNet深度学习模型实现高精度人脸识别支持人脸采集、特征提取、实时比对与考勤记录全流程。压缩包共55个文件含23个Python核心逻辑脚本如face_recognition.py、facenet.py、12个Qt Designer生成的.ui界面文件及对应.py绑定代码、3个SQLite数据库文件StudentFaceDB.db等支撑数据持久化另有npy模型参数、png界面素材与README.md说明文档整体仅1.98MB轻量易部署。目前已有119人学习下载提供从UI布局、数据库设计、FaceNet嵌入向量计算到SQLite事务处理的完整工程实践路径特别适合AI桌面应用跨领域学习者掌握模型调用、前后端协同与真实场景落地能力。1. 为什么用 PyQT FaceNet 做考勤系统比直接调 OpenCVHaar 或 Dlib 更稳这不是一个“又一个”人脸识别 Demo。它解决的是高校毕设里最真实的断层前端界面卡在 PyQt Designer 拖控件不会联动逻辑后端模型卡在 FaceNet 训练完不会导出嵌入向量部署时发现摄像头采集帧率一掉、人脸对齐就漂移、余弦相似度阈值调三天还是误识别——最后答辩被问“你这个系统真能跑满 50 人教室吗”当场哑火。这个项目标题里的三个关键词其实是三层硬约束PyQT 是唯一能交到答辩现场的可执行桌面程序载体不是网页、不是命令行FaceNet 是当时2020–2023在小样本、跨光照场景下识别率最扛打的开源方案比原始 CNN 分类器泛化强比 LBPH/PCA 更抗遮挡考勤系统是落地闭环——不是“检测识别”而是“谁在什么时间出现在哪台设备前”要存表、要查重、要导出 Excel、要防代打卡活体检测虽未明写但 FaceNet 的 triplet loss 天然排斥打印照片攻击。我带过 7 届毕设90% 翻车点不在模型精度而在PyQt 信号槽没绑对线程、FaceNet 特征向量没做 L2 归一化、考勤记录没加时间戳原子写入。这篇笔记不讲论文公式只拆你打包 zip 后双击运行前必须亲手敲、亲手测、亲手改的那 23 个关键节点。2. 从零搭起 PyQT 界面不是拖控件而是用代码定义 UI 生命周期2.1 主窗口结构为什么必须继承 QMainWindow 而非 QWidget很多同学用 Qt Designer 拖出一个 QWidget 当主窗结果发现菜单栏加不上、状态栏不显示、中心部件无法动态替换——这是根本性选型错误。QMainWindow 提供了完整的桌面应用骨架菜单栏File/Export、工具栏Start Camera、状态栏显示当前识别 ID、中心部件QStackedWidget 切换登录/考勤/统计页。而 QWidget 只是“一块画布”强行往上堆功能会失控。# main_window.py from PyQt5.QtWidgets import QMainWindow, QStackedWidget, QAction, QStatusBar from PyQt5.QtCore import Qt class AttendanceSystem(QMainWindow): def __init__(self): super().__init__() self.setWindowTitle(学生人脸识别考勤系统) self.setGeometry(100, 100, 1200, 800) # 1. 创建中心堆叠窗口用于切换页面 self.stacked_widget QStackedWidget() self.setCentralWidget(self.stacked_widget) # 2. 初始化各页面LoginPage, AttendancePage, ReportPage self.login_page LoginPage() self.attendance_page AttendancePage() self.report_page ReportPage() self.stacked_widget.addWidget(self.login_page) self.stacked_widget.addWidget(self.attendance_page) self.stacked_widget.addWidget(self.report_page) # 3. 构建菜单栏 self._setup_menu() # 4. 状态栏用于实时反馈 self.statusBar QStatusBar() self.setStatusBar(self.statusBar) def _setup_menu(self): menubar self.menuBar() file_menu menubar.addMenu(文件) export_action QAction(导出考勤记录, self) export_action.triggered.connect(self.export_attendance) file_menu.addAction(export_action)提示QStackedWidget是本系统 UI 架构核心。所有页面切换如登录成功跳转考勤页都通过self.stacked_widget.setCurrentIndex(1)实现避免新建窗口导致资源泄漏。不要用show()弹窗那是新手陷阱。2.2 摄像头实时预览用 QTimer 驱动而非 while True否则 GUI 卡死PyQt 的事件循环和 OpenCV 的cv2.VideoCapture.read()天然冲突。若在主线程里写while cap.isOpened(): ret, frame cap.read()GUI 完全冻结——因为 PyQt 的app.exec_()被阻塞。正确做法是用QTimer每 33ms约 30fps触发一帧采集并在子线程中做人脸检测与对齐主线程只负责刷新 QLabel。# attendance_page.py from PyQt5.QtCore import QTimer, Qt, QThread, pyqtSignal from PyQt5.QtGui import QImage, QPixmap import cv2 import numpy as np class VideoThread(QThread): # 自定义信号用于将处理后的图像传回主线程 change_pixmap_signal pyqtSignal(np.ndarray) def __init__(self, camera_id0): super().__init__() self.camera_id camera_id self._run_flag True def run(self): cap cv2.VideoCapture(self.camera_id) # 设置分辨率提升清晰度关键默认 640x480 太糊 cap.set(cv2.CAP_PROP_FRAME_WIDTH, 1280) cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 720) while self._run_flag: ret, frame cap.read() if ret: # 这里可插入轻量级预处理灰度、直方图均衡提升低光效果 gray cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) frame cv2.equalizeHist(gray) if len(frame.shape) 2 else frame self.change_pixmap_signal.emit(frame) cap.release() def stop(self): self._run_flag False self.wait() class AttendancePage(QWidget): def __init__(self): super().__init__() self.video_thread None self.label QLabel(self) self.label.setAlignment(Qt.AlignCenter) layout QVBoxLayout() layout.addWidget(self.label) self.setLayout(layout) # 启动视频线程 self.start_camera() def start_camera(self): self.video_thread VideoThread() self.video_thread.change_pixmap_signal.connect(self.update_image) self.video_thread.start() def update_image(self, cv_img): 将 OpenCV 图像转为 QPixmap 显示 rgb_image cv2.cvtColor(cv_img, cv2.COLOR_BGR2RGB) h, w, ch rgb_image.shape bytes_per_line ch * w convert_to_Qt_format QImage(rgb_image.data, w, h, bytes_per_line, QImage.Format_RGB888) p convert_to_Qt_format.scaled(1024, 768, Qt.KeepAspectRatio) self.label.setPixmap(QPixmap.fromImage(p))参数说明cap.set(cv2.CAP_PROP_FRAME_WIDTH, 1280)是血泪经验——很多 USB 摄像头默认输出 640x480FaceNet 对齐模块MTCNN 或 dlib在小图上极易漏检侧脸。1280x720 是平衡帧率与精度的甜点分辨率QTimer被QThread替代因 QTimer 在高负载下易丢帧而 QThread 可控性更强。2.3 考勤按钮与状态反馈用 QPushButton QStatusBar 实现用户可感知的交互闭环考勤不是“点一下就完事”。用户需要明确知道摄像头是否就绪是否检测到人脸是否已提取特征是否匹配成功是否已写入数据库这些状态必须实时反馈否则用户会狂点按钮导致重复记录。# attendance_page.py续 def __init__(self): # ... 上文代码 self.btn_attend QPushButton(开始考勤, self) self.btn_attend.clicked.connect(self.on_attend_click) self.btn_attend.setStyleSheet(background-color: #4CAF50; color: white; font-size: 16px;) layout.addWidget(self.btn_attend) # ... 布局代码 def on_attend_click(self): if not hasattr(self, current_frame) or self.current_frame is None: self.statusBar.showMessage(⚠️ 摄像头未启动或无画面请检查设备) return # 1. 检测人脸调用 MTCNN boxes, probs self.mtcnn.detect(self.current_frame) if boxes is None or len(boxes) 0: self.statusBar.showMessage( 未检测到人脸请正对镜头) return # 2. 对齐并提取特征FaceNet aligned self.mtcnn.align(self.current_frame, boxes[0]) # 取置信度最高的人脸 if aligned is None: self.statusBar.showMessage(❌ 人脸对齐失败请保持正面、无遮挡) return embedding self.facenet_model(aligned.unsqueeze(0)) # 输出 128 维向量 embedding F.normalize(embedding, p2, dim1) # 必须 L2 归一化 # 3. 匹配最近邻余弦相似度 0.6 才算识别成功 scores torch.nn.functional.cosine_similarity(embedding, self.known_embeddings, dim1) best_idx torch.argmax(scores).item() best_score scores[best_idx].item() if best_score 0.6: self.statusBar.showMessage(f 识别失败相似度 {best_score:.3f} 0.6请重试) return student_id self.student_ids[best_idx] timestamp datetime.now().strftime(%Y-%m-%d %H:%M:%S) # 4. 写入 SQLite带事务防并发写入 try: conn sqlite3.connect(attendance.db) cursor conn.cursor() cursor.execute( INSERT INTO records (student_id, timestamp, device_id) VALUES (?, ?, ?), (student_id, timestamp, DESKTOP-001) ) conn.commit() conn.close() self.statusBar.showMessage(f✅ {student_id} 已考勤成功{timestamp}) except Exception as e: self.statusBar.showMessage(f 写入失败{str(e)})注意F.normalize(embedding, p2, dim1)是 FaceNet 匹配的生死线。未归一化的向量余弦相似度计算会失效导致“张三的脸匹配李四的向量得分更高”。这是 80% 毕设生调试三天没发现的玄学 bug。3. FaceNet 模型落地不是下载 pretrain 就完事而是重构推理链路3.1 模型选型为什么用 Inception ResNet v1 而非原始 FaceNet 论文结构FaceNet 原论文Schroff et al., 2015用的是自研的 ZFNet 变体但实际工程中几乎没人复现——参数量大、训练慢、显存吃紧。主流选择是Inception ResNet v1来自torchvision.models或facenet-pytorch库它在 LFW 数据集上达到 99.65% 准确率且支持torch.jit.trace导出为 TorchScript便于部署。# facenet_model.py import torch import torch.nn as nn from facenet_pytorch import InceptionResnetV1 class FaceNetWrapper(nn.Module): def __init__(self, pretrainedvggface2): super().__init__() # 加载预训练权重vggface2 比 casia-webface 更鲁棒 self.model InceptionResnetV1(pretrainedpretrained).eval() # 冻结所有参数微调需另开分支 for param in self.model.parameters(): param.requires_grad False def forward(self, x): # x shape: (B, 3, 160, 160)FaceNet 输入固定尺寸 return self.model(x) # 使用示例 facenet FaceNetWrapper().to(cuda if torch.cuda.is_available() else cpu) # 注意输入必须是 [0,1] 归一化后的 tensor非 uint8 transform transforms.Compose([ transforms.Resize(160), transforms.ToTensor(), # 自动除以 255 transforms.Normalize(mean[0.5, 0.5, 0.5], std[0.5, 0.5, 0.5]) ])参数说明pretrainedvggface2是关键。它在 2.6M 张人脸图像上预训练比casia-webface50 万张更适应真实教室场景戴眼镜、侧光、口罩遮挡。transforms.Normalize(mean[0.5,0.5,0.5], std[0.5,0.5,0.5])是 FaceNet 训练时的数据增强配置必须严格一致否则特征向量漂移。3.2 人脸对齐MTCNN 为何比 OpenCV Haar 更可靠三个对齐参数必须手调Haar 分类器在侧脸、低头、强光下漏检率超 40%而 MTCNNZhang et al., 2016是三阶段级联网络P-Net 粗检、R-Net 精修、O-Net 输出 5 个关键点。但它的默认参数在教室场景下仍需调整参数默认值教室场景推荐值作用keep_allFalseTrue返回所有人脸框防多人同时入镜漏记min_face_size2040过滤小尺寸人脸教室距离远40px 人脸不可靠thresholds[0.6, 0.7, 0.7][0.5, 0.6, 0.6]降低 P/R/O-Net 置信度阈值提升召回# face_aligner.py from facenet_pytorch import MTCNN import torch class FaceAligner: def __init__(self, image_size160, margin20): self.mtcnn MTCNN( image_sizeimage_size, marginmargin, min_face_size40, # 关键 thresholds[0.5, 0.6, 0.6], # 关键 keep_allTrue, # 关键 devicecuda if torch.cuda.is_available() else cpu ) def detect_and_align(self, img): img: PIL.Image 或 numpy array (H,W,3) return: list of aligned faces (tensor), list of bounding boxes try: # MTCNN 输入必须是 PIL.Image if isinstance(img, np.ndarray): img Image.fromarray(cv2.cvtColor(img, cv2.COLOR_BGR2RGB)) # detect 返回 (boxes, probs, landmarks) boxes, probs, landmarks self.mtcnn.detect(img, landmarksTrue) if boxes is None: return [], [] # align 返回 list of tensors (1,3,160,160) aligned_faces self.mtcnn.align(img, boxes) return aligned_faces, boxes except Exception as e: print(fMTCNN 处理异常: {e}) return [], [] # 使用 aligner FaceAligner() aligned_list, boxes aligner.detect_and_align(frame) # frame 是 cv2 读取的 BGR 图 if aligned_list: embedding facenet(aligned_list[0].unsqueeze(0)) # 取第一个最高置信度人脸避坑self.mtcnn.align()内部会做仿射变换但若输入img是np.ndarray且未转PIL.Image会静默返回None。这是 debug 时最隐蔽的坑——控制台无报错但aligned_list为空后续全线崩。3.3 特征库构建不是把所有人脸图喂进去而是用 triplet loss 微调 L2 归一化固化毕设常见误区直接用facenet(model(img))提取全校 200 张照片的 embedding存成.npy文件。问题在于——预训练模型在 VGGFace2 上学的是“全球名人脸”而你的学生数据有统一着装、相似发型、教室背景干扰直接用会导致类内距离 类间距离。正确路径收集每个学生 5~10 张不同角度/光照的人脸图必须含正脸侧脸用 triplet loss 微调最后两层Inception ResNet v1 的last_linear和last_bn微调后用验证集生成 embedding 并 L2 归一化存为known_embeddings.pt# train_triplet.py from torch.utils.data import DataLoader, Dataset from facenet_pytorch import InceptionResnetV1 import torch.nn.functional as F class TripletDataset(Dataset): def __init__(self, root_dir, transformNone): # 构建三元组anchor某学生正脸、positive同学生侧脸、negative另一学生正脸 self.triplets self._generate_triplets(root_dir) self.transform transform def _generate_triplets(self, root_dir): # 伪代码遍历每个学生文件夹随机采样 anchor/positive再随机选 negative pass def __getitem__(self, idx): anchor_path, pos_path, neg_path self.triplets[idx] anchor self.transform(Image.open(anchor_path)) pos self.transform(Image.open(pos_path)) neg self.transform(Image.open(neg_path)) return anchor, pos, neg def train_triplet(model, dataloader, epochs10): optimizer torch.optim.Adam(model.last_linear.parameters(), lr0.001) scheduler torch.optim.lr_scheduler.StepLR(optimizer, step_size5, gamma0.5) for epoch in range(epochs): for anchor, pos, neg in dataloader: anchor_emb model(anchor.to(cuda)) pos_emb model(pos.to(cuda)) neg_emb model(neg.to(cuda)) # triplet loss: max(0, ||a-p||² - ||a-n||² margin) ap_dist F.pairwise_distance(anchor_emb, pos_emb, p2) an_dist F.pairwise_distance(anchor_emb, neg_emb, p2) loss torch.mean(torch.clamp(ap_dist - an_dist 0.2, min0.0)) optimizer.zero_grad() loss.backward() optimizer.step() scheduler.step() print(fEpoch {epoch}, Loss: {loss.item():.4f}) # 微调后保存 torch.save(model.state_dict(), facenet_finetuned.pth)参数说明margin0.2是 triplet loss 的核心超参。太小0.05导致模型不收敛太大0.5导致梯度爆炸。教室场景推荐 0.15~0.25p2表示欧氏距离与 FaceNet 原论文一致。4. 考勤业务闭环SQLite 存储设计 防代打卡机制 Excel 导出4.1 数据库表结构为什么用复合主键而非 auto-increment考勤系统的核心矛盾是同一学生一天可多次打卡早/午/晚但同一时段不能重复记录。若用id INTEGER PRIMARY KEY AUTOINCREMENT无法阻止“张三在 8:00 连续点 5 次按钮生成 5 条记录”。解决方案用(student_id, date, period)作联合主键period字段标识“早/午/晚/其他”。-- attendance.db CREATE TABLE students ( student_id TEXT PRIMARY KEY, name TEXT NOT NULL, class TEXT NOT NULL, photo_path TEXT ); CREATE TABLE records ( student_id TEXT NOT NULL, timestamp DATETIME NOT NULL, device_id TEXT NOT NULL, -- 复合主键防止同一学生同一天同一时段重复打卡 CONSTRAINT pk_record PRIMARY KEY (student_id, DATE(timestamp), strftime(%H, timestamp)/4) ); -- 注strftime(%H, timestamp)/4 将小时分组为 0-50-4点、6-116-11点等粗略划分早/午/晚# database.py import sqlite3 from datetime import datetime def insert_attendance(student_id: str, device_id: str DESKTOP-001): conn sqlite3.connect(attendance.db) cursor conn.cursor() # 获取今日日期和当前小时段早6-11午12-16晚17-22 now datetime.now() hour now.hour if 6 hour 11: period 早 elif 12 hour 16: period 午 elif 17 hour 22: period 晚 else: period 其他 try: cursor.execute( INSERT INTO records (student_id, timestamp, device_id) VALUES (?, ?, ?), (student_id, now.strftime(%Y-%m-%d %H:%M:%S), device_id) ) conn.commit() return True, f{student_id} {period}间考勤成功 except sqlite3.IntegrityError as e: if UNIQUE constraint failed in str(e): return False, f{student_id} 今日{period}间已考勤不可重复 else: return False, f数据库错误{str(e)} finally: conn.close()注意SQLite 的ON CONFLICT IGNORE不适用于复合主键冲突的友好提示必须捕获IntegrityError并解析错误信息。这是毕设答辩时老师最爱问的“你怎么防止代打卡”的底层答案。4.2 防代打卡用活体检测补丁而非重写整个 FaceNetFaceNet 本身不防照片攻击但加一个轻量级活体检测模块即可。不必上 Eye Blink 或 3D Depth用瞳孔反光Pupil Reflection 微表情帧差就够用# liveness_detector.py import cv2 import numpy as np def detect_liveness(face_roi): face_roi: (h,w,3) numpy array已对齐的人脸区域 return: bool, True 为活体 # 1. 瞳孔反光检测在眼部区域找高亮小圆斑手机屏幕/灯光反射 gray cv2.cvtColor(face_roi, cv2.COLOR_BGR2GRAY) eyes_roi face_roi[80:120, 40:120] # 粗略裁剪眼部区域 eye_gray cv2.cvtColor(eyes_roi, cv2.COLOR_BGR2GRAY) # 找最亮点反光 _, thresh cv2.threshold(eye_gray, 200, 255, cv2.THRESH_BINARY) contours, _ cv2.findContours(thresh, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) if len(contours) 0: return False # 取最大轮廓假设是反光点 largest_contour max(contours, keycv2.contourArea) (x, y), radius cv2.minEnclosingCircle(largest_contour) # 反光点直径应在 2~6px过大是噪点过小不可见 if 2 radius 6: # 2. 微表情帧差连续 3 帧眼部区域像素方差 阈值 # 此处省略帧缓存逻辑实际需维护 ring buffer return True return False # 在考勤主流程中调用 if not detect_liveness(aligned_face.cpu().numpy().transpose(1,2,0)): self.statusBar.showMessage(⚠️ 活体检测未通过请勿使用照片) return提示此模块耗时 15ms不影响 30fps 流程。它不追求 100% 防伪专业攻击仍可破但能拦截 95% 的手机相册照片代打卡且无需额外硬件——这才是毕设尺度下的务实方案。4.3 Excel 导出用 openpyxl 直接写入而非 pandas.to_excel避免依赖冲突毕设交付物要求“双击 exe 即可运行”而pandas依赖numpy、openpyxl、xlrd极易因版本冲突导致打包失败。直接用openpyxl写入更可控# export_report.py from openpyxl import Workbook from openpyxl.styles import Font, Alignment, PatternFill from datetime import datetime import sqlite3 def export_to_excel(date_filter: str None): date_filter: 2024-05-20 或 None导出全部 conn sqlite3.connect(attendance.db) cursor conn.cursor() if date_filter: cursor.execute( SELECT s.name, s.class, r.timestamp, r.device_id FROM records r JOIN students s ON r.student_id s.student_id WHERE DATE(r.timestamp) ? ORDER BY r.timestamp , (date_filter,)) else: cursor.execute( SELECT s.name, s.class, r.timestamp, r.device_id FROM records r JOIN students s ON r.student_id s.student_id ORDER BY r.timestamp ) rows cursor.fetchall() conn.close() wb Workbook() ws wb.active ws.title 考勤记录 # 表头 headers [姓名, 班级, 考勤时间, 设备ID] for col, header in enumerate(headers, 1): cell ws.cell(row1, columncol, valueheader) cell.font Font(boldTrue) cell.alignment Alignment(horizontalcenter) cell.fill PatternFill(start_colorDCE6F1, end_colorDCE6F1, fill_typesolid) # 数据 for row_idx, row in enumerate(rows, 2): for col_idx, value in enumerate(row, 1): ws.cell(rowrow_idx, columncol_idx, valuevalue) # 列宽自适应 for col in ws.columns: max_length 0 for cell in col: try: if len(str(cell.value)) max_length: max_length len(str(cell.value)) except: pass adjusted_width min(max_length 2, 50) ws.column_dimensions[col[0].column_letter].width adjusted_width filename f考勤记录_{datetime.now().strftime(%Y%m%d_%H%M%S)}.xlsx wb.save(filename) return filename避坑openpyxl不支持.xls格式必须用.xlsx写入前务必关闭数据库连接否则 Windows 下会报“database is locked”列宽计算中min(..., 50)防止 Excel 崩溃超长字段如路径名。5. 避坑指南PyQTFaceNet 考勤系统 5 大翻车现场与后悔药5.1 现象PyInstaller 打包后程序闪退cmd 窗口一闪而过原因PyQt5 与 PyInstaller 兼容性问题缺失Qt5Core.dll或opengl32sw.dllFaceNet 模型路径在打包后变成相对路径错误。解决打包命令加--add-binary C:/path/to/Qt5Core.dll;.Windows或--add-binary /usr/lib/libQt5Core.dylib:.macOS模型加载改用sys._MEIPASSimport sys import os def get_resource_path(relative_path): if getattr(sys, frozen, False): base_path sys._MEIPASS else: base_path os.path.abspath(.) return os.path.join(base_path, relative_path) # 加载模型 model_path get_resource_path(models/facenet_finetuned.pth)5.2 现象MTCNN 检测到人脸但 FaceNet 提取 embedding 全为 nan原因输入图像未归一化到 [0,1]或transforms.ToTensor()前图像 dtype 是uint16某些工业相机。解决强制转换if img.dtype np.uint16: img (img / 256).astype(np.uint8) # 缩放到 uint8 tensor_img transform(Image.fromarray(img))5.3 现象考勤记录写入 SQLite 成功但重启程序后数据消失原因数据库路径写成相对路径attendance.dbPyInstaller 打包后工作目录变为临时解压目录每次启动新建空库。解决固定数据库路径import os DB_PATH os.path.join(os.path.dirname(sys.executable), attendance.db) \ if getattr(sys, frozen, False) else attendance.db5.4 现象多线程下QLabel.setPixmap()报QObject::setParent: Cannot set parent原因QPixmap对象在子线程创建却在主线程调用setPixmap()违反 Qt 跨线程对象规则。解决只传递numpy.ndarray在主线程内转QPixmap# 子线程 emit self.change_pixmap_signal.emit(cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)) # 主线程接收后转换 def update_image(self, rgb_array): h, w, ch rgb_array.shape qt_img QImage(rgb_array.data, w, h, w * ch, QImage.Format_RGB888) self.label.setPixmap(QPixmap.fromImage(qt_img))5.5 现象FaceNet 特征向量余弦相似度始终在 0.3~0.5 波动无法达到 0.6原因未对 embedding 做 L2 归一化或transforms.Normalize的 mean/std 与预训练模型不一致。解决检查归一化embedding F.normalize(embedding, p2, dim1)检查 transformtransforms.Normalize(mean[0.5,0.5,0.5], std[0.5,0.5,0.5])FaceNet vggface2 要求打印embedding[0][:5]查看数值范围应为 [-1,1]非 [0,100]6. 最后一公里用 conda 环境固化 打包技巧让毕设一次过审6.1 环境固化为什么不用 pip freeze而用 conda env exportpip freeze会列出所有依赖包括wheel、setuptools等构建工具而毕设只需运行时依赖。conda 环境导出可精准锁定平台相关包如pytorch的 cuda 版本# 创建专用环境 conda create -n attendance python3.8 conda activate attendance # 安装核心包按此顺序 conda install pytorch torchvision cpuonly -c pytorch # 先装 torch避免版本冲突 pip install facenet-pytorch opencv-python PyQt5 openpyxl # 导出环境生成 environment.yml conda env export environment.ymlenvironment.yml关键片段name: attendance channels: - pytorch - defaults dependencies: - python3.8.10 - pytorch1.12.1py38_cpu - torchvision0.13.1py38_cpu - facenet-pytorch2.5.1 - opencv4.5.5 - pyqt5.15.6 - openpyxl3.0.10教训我曾见学生用pip install torch装了 CUDA 版但答辩电脑无 GPU程序启动即报CUDA out of memory。用cpuonly渠道安装确保环境可移植。6.2 PyInstaller 打包三步走策略debug → console → noconsole不要一上来就--onefile --noconsole。按顺序验证Debug 模式pyinstaller --debugall main.py生成main.exe和main-debug.log查看报错位置Console 模式pyinstaller --console main.py保留 cmd 窗口方便看到print日志和异常 tracebackNoconsole 模式确认无报错后加--noconsole --onefile --iconicon.ico关键参数说明--add-data models;models打包模型文件夹Windows 用;macOS/Linux 用:--hidden-importPyQt5.sip显式导入 sip 模块防 PyQt5 导入失败--exclude-moduletkinter排除 tkinterPyQt5 不需要6.3 答辩演示 checklist5 分钟内让老师信服你真做出来了别一上来就讲“我用了 FaceNet 和 PyQT”老师只想看结果。按此流程演示|本文还有配套的精品资源点击获取