
简介这是一套面向计算机专业本科生及课程设计、毕业设计学习者的Python人脸识别考勤系统实战源码聚焦于图像处理、GUI开发与业务逻辑集成解决传统人工签到效率低、易代签等实际管理问题。资源共44个文件含12个核心Python源码如MainWindow.py、face_model.py、CamShow.py、10个XML级联分类器文件含frontalface_default/alt2/eye_tree等多场景检测模型、2个PyQt5界面UI文件及16个已编译pyc文件辅以requirements.txt依赖说明和README.md项目文档整体压缩包仅681KB轻量易部署。已有263人学习下载代码全程配备中文详细注释覆盖Haar人脸检测、LBPH特征提取、SQLite考勤记录存储、datetime时间校验及PyQt5信号槽交互等关键实现目录结构分层清晰data/ui/code等模块明确便于理解人脸识别全流程与工程化落地细节。1. 这不是“调个 face_recognition 就能跑”的玩具系统它用 OpenCV 做实时人脸检测与特征提取PyQt5 构建可交互的考勤界面Python 负责逻辑调度与数据持久化——适合中小型企业门禁、教室点名、会议签到等真实场景落地要求开发者理解图像预处理链路、模型轻量化约束、GUI 线程安全机制而非仅会 pip install 后改几行 demo。很多初学者拿到“人脸识别考勤系统”源码第一反应是解压、pip install、python main.py——然后卡在摄像头打不开、识别率低、界面卡死、数据库写入失败。根本原因在于这个标题里的三个技术栈OpenCV、PyQt5、Python各自承担不可替代的硬性角色且存在强耦合依赖。OpenCV 不只是“读图识脸”它要完成灰度化、直方图均衡、Haar 或 LBP 检测器加载、ROI 截取、关键点归一化PyQt5 不只是“画按钮弹窗”它必须用 QThread 隔离视频采集线程与 GUI 主线程用 QPixmap 实时渲染帧用 QStandardItemModel 绑定考勤记录表格Python 则需组织完整的业务流人脸注册阶段的样本对齐与特征向量存储非简单存图片识别阶段的余弦相似度阈值判定与防重复签到逻辑以及 SQLite 的事务写入与时间戳校验。本系统不依赖云端 API所有计算在本地完成因此对 OpenCV 版本兼容性、摄像头驱动稳定性、PyQt5 事件循环阻塞点极为敏感。如果你正被“cv2.VideoCapture(0) 返回 None”、“QPixmap cannot be used outside GUI thread”或“人脸匹配 score 波动大导致误签”困扰这篇就是为你写的实操指南。2. 用 OpenCV 在本地跑通人脸检测与特征提取的最小闭环从 VideoCapture 到 embedding 向量生成2.1 为什么不用 face_recognition 库选 OpenCV LBPH 的底层逻辑与轻量化优势face_recognition库虽封装简洁但其底层依赖 dlib 的 HOG SVM 检测器和 ResNet-34 编码器在无 GPU 的办公 PC 或树莓派上推理延迟常超 800ms/帧无法满足考勤场景下“秒级响应”要求。而本系统采用 OpenCV 原生cv2.CascadeClassifier加载haarcascade_frontalface_default.xml进行检测配合cv2.face.LBPHFaceRecognizer_create()训练识别模型核心优势在于检测耗时稳定在 15~35ms/帧i5-8250U Logitech C920LBPH 模型训练仅需每张人脸 5~10 张样本内存占用 2MB全程纯 C 实现无 Python GIL 阻塞可与 PyQt5 多线程无缝协同支持动态调整scaleFactor1.1和minNeighbors5参数平衡检出率与误报率。提示haarcascade_frontalface_default.xml文件必须从 OpenCV 官方 GitHub 仓库data/目录下载如https://github.com/opencv/opencv/blob/master/data/haarcascades/haarcascade_frontalface_default.xml不能用网上随意打包的修改版否则在侧脸、弱光、戴眼镜场景下漏检率飙升。2.2 实现人脸检测与 ROI 截取带防抖与尺寸校验的健壮代码以下代码段是系统中camera_handler.py的核心逻辑直接决定后续识别准确率import cv2 import numpy as np class FaceDetector: def __init__(self, cascade_pathhaarcascade_frontalface_default.xml): self.detector cv2.CascadeClassifier(cascade_path) # 初始化历史坐标用于平滑防抖 self.last_rect None self.smooth_factor 0.3 def detect_and_crop(self, frame): gray cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) # 直方图均衡增强对比度提升弱光下检出率 gray cv2.equalizeHist(gray) # 检测参数scaleFactor 控制缩放步长minNeighbors 控制邻域合并强度 faces self.detector.detectMultiScale( gray, scaleFactor1.15, # 每次缩放 15%比默认 1.1 更快收敛 minNeighbors6, # 需至少 6 个邻近检测框才确认为人脸降低误报 minSize(60, 60), # 过滤过小区域排除噪点 flagscv2.CASCADE_SCALE_IMAGE ) if len(faces) 0: return None, None # 取最大人脸通常为主人脸并做平滑处理 largest_face max(faces, keylambda x: x[2] * x[3]) x, y, w, h largest_face # 平滑坐标避免帧间跳变尤其 USB 摄像头易抖动 if self.last_rect is not None: x int(self.last_rect[0] * (1 - self.smooth_factor) x * self.smooth_factor) y int(self.last_rect[1] * (1 - self.smooth_factor) y * self.smooth_factor) w int(self.last_rect[2] * (1 - self.smooth_factor) w * self.smooth_factor) h int(self.last_rect[3] * (1 - self.smooth_factor) h * self.smooth_factor) self.last_rect (x, y, w, h) # 扩展 ROI 区域增加上下左右各 20% 边距确保完整人脸 margin_x int(w * 0.2) margin_y int(h * 0.2) x max(0, x - margin_x) y max(0, y - margin_y) w min(frame.shape[1] - x, w 2 * margin_x) h min(frame.shape[0] - y, h 2 * margin_y) # 截取并归一化为 128x128LBPH 模型输入标准尺寸 roi frame[y:yh, x:xw] roi_resized cv2.resize(roi, (128, 128)) return roi_resized, (x, y, w, h) # 使用示例 cap cv2.VideoCapture(0) detector FaceDetector() while True: ret, frame cap.read() if not ret: break roi, rect detector.detect_and_crop(frame) if roi is not None: cv2.rectangle(frame, (rect[0], rect[1]), (rect[0]rect[2], rect[1]rect[3]), (0,255,0), 2) cv2.imshow(Detected, roi) # 实时查看截取效果 cv2.imshow(Frame, frame) if cv2.waitKey(1) 0xFF ord(q): break cap.release() cv2.destroyAllWindows()参数说明与调试要点scaleFactor1.15值越大检测越快但可能漏检小脸1.05更准但慢 3 倍考勤场景推荐1.1~1.18minNeighbors6值越大越保守减少误报但侧脸可能被过滤若环境光线均匀可设为4minSize(60,60)必须大于摄像头分辨率的 1/10否则易把阴影当人脸ROI 扩展边距margin_x/y不加此步会导致 LBPH 训练时边缘信息丢失识别鲁棒性下降 40%cv2.equalizeHist()对低照度环境如会议室关灯提升显著但强光下可能过曝可加开关控制。2.3 训练 LBPH 模型并生成 embedding本地化部署的关键一步LBPHLocal Binary Patterns Histograms模型不生成传统意义上的 128D embedding 向量而是为每张人脸生成一个整数标签label和对应的直方图特征histogram。系统通过cv2.face.LBPHFaceRecognizer_create()实现其训练流程如下import cv2 import os import numpy as np def train_lbph_model(data_dirdataset/): data_dir 结构示例 dataset/ └── user_001/ ├── 1.jpg ├── 2.jpg └── ... └── user_002/ ├── 1.jpg └── ... recognizer cv2.face.LBPHFaceRecognizer_create( radius1, # LBP 算子半径1 对应 3x3 邻域增大则更鲁棒但计算量升 neighbors8, # LBP 邻居点数8 是标准值不可改 grid_x8, # 直方图网格横向划分数影响特征粒度 grid_y8, # 直方图网格纵向划分数grid_x * grid_y 64 个 bin threshold100 # 匹配阈值单位为距离越小越严格考勤建议 80~120 ) faces [] ids [] label_map {} # {name: id} current_id 0 for person_dir in os.listdir(data_dir): person_path os.path.join(data_dir, person_dir) if not os.path.isdir(person_path): continue label_name person_dir if label_name not in label_map: label_map[label_name] current_id current_id 1 for img_file in os.listdir(person_path): if not img_file.lower().endswith((.jpg, .jpeg, .png)): continue img_path os.path.join(person_path, img_file) img cv2.imread(img_path, cv2.IMREAD_GRAYSCALE) if img is None: continue # 必须 resize 到训练尺寸与 detect_and_crop 输出一致 img_resized cv2.resize(img, (128, 128)) faces.append(img_resized) ids.append(label_map[label_name]) if len(faces) 0: raise ValueError(No valid training images found!) recognizer.train(faces, np.array(ids)) recognizer.write(lbph_model.yml) # 保存模型文件 return recognizer, label_map # 训练调用 recognizer, name_to_id train_lbph_model(dataset/) print(fTrained on {len(name_to_id)} persons)关键参数与生产注意事项threshold100这是 LBPH 的核心判别阈值。值越小要求匹配度越高如70时戴口罩几乎无法识别值越大越宽松130时双胞胎易混淆。考勤系统建议初始设90上线后根据误识率动态下调grid_xgrid_y864-bin 直方图在 CPU 上推理最快16x16虽精度略高但耗时翻倍不推荐必须保证训练图与检测图尺寸一致若detect_and_crop输出128x128训练图也必须cv2.resize(..., (128,128))否则recognizer.predict()报错或结果随机label_map必须持久化如存为 JSON因为predict()返回的是整数 ID需映射回真实姓名。3. 用 PyQt5 构建线程安全的考勤界面解决视频卡顿、按钮无响应、数据不同步三大痛点3.1 GUI 主窗口结构设计QMainWindow QTabWidget QStatusBar 的工业级布局系统主界面采用标准QMainWindow顶部QMenuBar提供“注册新员工”、“查看考勤记录”、“系统设置”菜单中央区域为QTabWidget含两个 Tab实时签到 Tab左侧QLabel显示摄像头画面setPixmap()右侧QTableWidget动态刷新签到记录姓名、时间、状态人员管理 TabQTreeView展示人员目录QListView显示该人员已注册照片缩略图底部QPushButton触发批量注册。状态栏QStatusBar实时显示当前摄像头帧率FPS、已连接设备数、最近一次识别结果如“张三 | 置信度 92.3% | 已签到”。这种布局符合 Windows/macOS 用户习惯且QTabWidget自动管理焦点切换避免多窗口堆叠。# main_window.py from PyQt5.QtWidgets import QMainWindow, QTabWidget, QLabel, QTableWidget, QStatusBar from PyQt5.QtCore import Qt class AttendanceMainWindow(QMainWindow): def __init__(self): super().__init__() self.setWindowTitle(智能考勤系统 v1.2) self.setGeometry(100, 100, 1200, 700) # 创建 Tab 区域 self.tabs QTabWidget() self.setCentralWidget(self.tabs) # Tab 1: 实时签到 self.sign_tab QWidget() self.tabs.addTab(self.sign_tab, 实时签到) self._setup_sign_tab() # Tab 2: 人员管理 self.manage_tab QWidget() self.tabs.addTab(self.manage_tab, 人员管理) self._setup_manage_tab() # 状态栏 self.statusBar QStatusBar() self.setStatusBar(self.statusBar) self.statusBar.showMessage(就绪 | 摄像头未启动) def _setup_sign_tab(self): layout QVBoxLayout(self.sign_tab) # 左侧视频显示区 self.video_label QLabel() self.video_label.setAlignment(Qt.AlignCenter) self.video_label.setMinimumSize(640, 480) layout.addWidget(self.video_label) # 右侧签到记录表 self.record_table QTableWidget() self.record_table.setColumnCount(4) self.record_table.setHorizontalHeaderLabels([姓名, 工号, 签到时间, 状态]) self.record_table.horizontalHeader().setStretchLastSection(True) layout.addWidget(self.record_table)3.2 视频采集线程隔离QThread moveToThread() 避免 GUI 冻结PyQt5 的 GUI 必须运行在主线程而 OpenCV 的cap.read()是阻塞操作。若直接在QTimer.timeout中调用会导致界面卡死。正确做法是创建独立QThread子类将摄像头采集逻辑移入其中并通过信号pyqtSignal将帧数据传回主线程更新QLabel# camera_thread.py from PyQt5.QtCore import QThread, pyqtSignal import cv2 class CameraThread(QThread): frame_ready pyqtSignal(object) # 发送 numpy array error_occurred pyqtSignal(str) def __init__(self, camera_id0): super().__init__() self.camera_id camera_id self.running False def run(self): self.running True cap cv2.VideoCapture(self.camera_id) if not cap.isOpened(): self.error_occurred.emit(f无法打开摄像头 {self.camera_id}) return # 设置分辨率避免默认 640x480 导致识别精度下降 cap.set(cv2.CAP_PROP_FRAME_WIDTH, 1280) cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 720) while self.running: ret, frame cap.read() if not ret: self.error_occurred.emit(摄像头读取失败) break # 发送帧注意不能直接发送 cv2.imshow必须 emit self.frame_ready.emit(frame) cap.release() def stop(self): self.running False self.wait()主线程中启动与连接# 在 main_window.py 的 __init__ 中 self.camera_thread CameraThread(0) self.camera_thread.frame_ready.connect(self.update_video_frame) self.camera_thread.error_occurred.connect(self.show_camera_error) self.camera_thread.start() def update_video_frame(self, frame): # BGR - RGB - QImage - QPixmap rgb_image cv2.cvtColor(frame, 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(self.video_label.size(), Qt.KeepAspectRatio) self.video_label.setPixmap(QPixmap.fromImage(p)) def show_camera_error(self, msg): self.statusBar.showMessage(f摄像头错误: {msg}) self.camera_thread.stop()注意moveToThread()方式已被弃用QThread子类重写run()是唯一安全方案frame_ready.emit(frame)中frame是 numpy array无需深拷贝PyQt 信号自动处理内存。3.3 考勤记录表格的实时绑定与防重复机制QTableWidget不支持 Model/View 架构但可通过insertRow()setItem()实现高效更新。关键在于同一人在 30 秒内不得重复签到需维护一个last_sign_time字典from datetime import datetime, timedelta class AttendanceManager: def __init__(self): self.last_sign_time {} # {name: datetime} self.cooldown_seconds 30 def can_sign(self, name): now datetime.now() if name in self.last_sign_time: delta now - self.last_sign_time[name] return delta.total_seconds() self.cooldown_seconds return True def record_sign(self, name, status已签到): if not self.can_sign(name): return False self.last_sign_time[name] datetime.now() # 插入表格首行 row self.record_table.rowCount() self.record_table.insertRow(0) self.record_table.setItem(0, 0, QTableWidgetItem(name)) self.record_table.setItem(0, 1, QTableWidgetItem(self.get_employee_id(name))) self.record_table.setItem(0, 2, QTableWidgetItem(datetime.now().strftime(%Y-%m-%d %H:%M:%S))) self.record_table.setItem(0, 3, QTableWidgetItem(status)) return True性能优化点insertRow(0)而非append确保最新记录在顶部get_employee_id()应从 SQLite 查询缓存避免每次查库last_sign_time字典内存占用极小比 SQLite 查询快 100 倍适合高频判断。4. SQLite 数据持久化与考勤逻辑落地从单次识别到完整业务流闭环4.1 数据库表结构设计兼顾查询效率与扩展性系统使用 SQLite零配置、单文件、ACID 事务核心表attendance_records与employees表名字段类型约束说明employeesidINTEGER PRIMARY KEY自增员工唯一标识nameTEXT NOT NULLUNIQUE姓名用于 LBPH labelemployee_idTEXTINDEX工号业务主键departmentTEXT部门created_atTIMESTAMP DEFAULT CURRENT_TIMESTAMP注册时间attendance_recordsidINTEGER PRIMARY KEY自增记录IDemployee_idTEXT NOT NULLFOREIGN KEY关联 employees.employee_idsign_timeTIMESTAMP DEFAULT CURRENT_TIMESTAMPINDEX签到时间device_idTEXT设备编号支持多终端statusTEXT DEFAULT successsuccess/failed/duplicate建表 SQLCREATE TABLE IF NOT EXISTS employees ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL UNIQUE, employee_id TEXT NOT NULL, department TEXT, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE IF NOT EXISTS attendance_records ( id INTEGER PRIMARY KEY AUTOINCREMENT, employee_id TEXT NOT NULL, sign_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP, device_id TEXT DEFAULT main_terminal, status TEXT DEFAULT success, FOREIGN KEY (employee_id) REFERENCES employees(employee_id) ); -- 为高频查询字段建索引 CREATE INDEX IF NOT EXISTS idx_emp_id ON employees(employee_id); CREATE INDEX IF NOT EXISTS idx_sign_time ON attendance_records(sign_time); CREATE INDEX IF NOT EXISTS idx_emp_sign ON attendance_records(employee_id, sign_time);4.2 识别结果写入数据库的原子操作事务与异常兜底考勤不是简单 insert需保证识别成功 → 查员工表 → 写记录表 → 更新状态栏任一环节失败如网络中断、磁盘满→ 回滚事务提示用户重复签到 →statusduplicate不计入统计。import sqlite3 from datetime import datetime class DatabaseManager: def __init__(self, db_pathattendance.db): self.db_path db_path self.init_db() def init_db(self): conn sqlite3.connect(self.db_path) cursor conn.cursor() # 执行上述建表 SQL cursor.executescript( CREATE TABLE IF NOT EXISTS employees (...); CREATE TABLE IF NOT EXISTS attendance_records (...); CREATE INDEX IF NOT EXISTS idx_emp_id ON employees(employee_id); ) conn.commit() conn.close() def record_attendance(self, employee_id, statussuccess): try: conn sqlite3.connect(self.db_path) conn.execute(BEGIN TRANSACTION) # 显式开启事务 # 检查员工是否存在防止伪造 employee_id cursor conn.execute(SELECT name FROM employees WHERE employee_id ?, (employee_id,)) result cursor.fetchone() if not result: raise ValueError(f员工 {employee_id} 不存在) # 插入考勤记录 conn.execute( INSERT INTO attendance_records (employee_id, status) VALUES (?, ?), (employee_id, status) ) conn.commit() return True except Exception as e: conn.rollback() print(f数据库写入失败: {e}) return False finally: conn.close() # 在识别回调中调用 def on_recognition_success(self, name, confidence): emp_id self.get_employee_id_by_name(name) # 从 label_map 或 DB 查询 if self.db_manager.record_attendance(emp_id): self.statusBar.showMessage(f{name} | 置信度 {confidence:.1f}% | 已签到) self.attendance_mgr.record_sign(name) # 更新 UI 表格 else: self.statusBar.showMessage(数据库写入失败请检查磁盘空间)4.3 考勤统计报表生成用原生 SQL 实现日报/月报导出不依赖 Pandas用 SQLite 的strftime和GROUP BY直接生成统计def generate_daily_report(self, date_strtoday): date_str 格式: 2024-05-20 或 today if date_str today: date_str datetime.now().strftime(%Y-%m-%d) conn sqlite3.connect(attendance.db) cursor conn.cursor() # 查询当日各部门签到人数 cursor.execute( SELECT e.department, COUNT(*) as count, COUNT(CASE WHEN ar.status success THEN 1 END) as success_count FROM attendance_records ar JOIN employees e ON ar.employee_id e.employee_id WHERE DATE(ar.sign_time) ? GROUP BY e.department ORDER BY count DESC , (date_str,)) rows cursor.fetchall() conn.close() # 生成 CSV 字符串可直接写入文件 csv_lines [部门,总人次,有效签到] for dept, total, success in rows: csv_lines.append(f{dept},{total},{success}) return \n.join(csv_lines) # 导出示例 report_csv self.db_manager.generate_daily_report(2024-05-20) with open(daily_report_20240520.csv, w, encodingutf-8) as f: f.write(report_csv)SQL 关键点DATE(ar.sign_time) ?SQLite 的DATE()函数提取日期部分比LIKE 2024-05-20%更高效COUNT(CASE WHEN ...)一行 SQL 完成条件计数避免多次查询GROUP BY e.department按部门聚合支撑管理视图。5. 系统级调优与排错解决摄像头初始化失败、识别率波动、PyQt5 打包后黑屏三大高频问题5.1 摄像头初始化失败的根因定位与修复清单当cv2.VideoCapture(0)返回None或cap.isOpened() False按以下顺序排查检查项命令/操作预期结果修复方案设备权限ls -l /dev/video*Linuxsudo usermod -aG video $USER/dev/video0权限含video组重启终端或newgrp video驱动冲突lsmod | grep uvcvideoLinuxdmesg | tail -20uvcvideo模块已加载若有bcm2835冲突sudo modprobe -r bcm2835-v4l2; sudo modprobe uvcvideoWindows 设备管理器查看“照相机”是否黄色感叹号正常无警告右键更新驱动 → “浏览我的电脑” → “让我从列表选择” → “USB 视频设备”OpenCV 版本兼容性python -c import cv2; print(cv2.__version__); cv2.getBuildInformation()输出版本号 ≥ 4.5.0含videoio: YES (MSMF: YES)升级pip install --upgrade opencv-python-headless无 GUI 依赖提示在 PyInstaller 打包后黑屏90% 是因cv2.VideoCapture未找到 backend。解决方案在spec文件中添加--add-binary C:/path/to/opencv_ffmpeg*.dll;.或强制指定 backendcap cv2.VideoCapture(0, cv2.CAP_DSHOW)Windows。5.2 识别率波动的三大物理层优化LBPH 对光照、角度、遮挡敏感单纯调阈值治标不治本问题现象根因解决方案弱光下识别率骤降灰度图信噪比低在detect_and_crop中启用cv2.createCLAHE(clipLimit2.0, tileGridSize(8,8))替代equalizeHistCLACHE 局部自适应效果更稳侧脸/低头识别失败Haar 检测器只支持正脸增加cv2.face.createFisherFaceRecognizer()作为备用模型其对姿态变化鲁棒性更高但训练样本需 ≥20 张/人多人同框时误识别detectMultiScale返回多个 face取最大者逻辑失效改为遍历所有检测框对每个 ROI 分别predict()取最高置信度结果代码中max(faces, key...)替换为for face in faces:循环5.3 PyQt5 打包后界面黑屏/按钮失灵的终极修复使用pyinstaller --onefile --windowed main.py打包后常见问题现象原因修复命令启动后主窗口黑屏缺少 Qt 平台插件pyinstaller --add-binary C:/Python39/Lib/site-packages/PyQt5/Qt/plugins/platforms;platforms --onefile main.pyQLabel 不显示视频QPixmap 构造时路径错误确保QImage构造前frame不为None加if frame is not None:判断按钮点击无响应信号未正确连接检查self.btn.clicked.connect(self.on_click)是否在__init__中执行而非if __name__ __main__:块内验证打包完整性运行生成的 exe 后打开任务管理器 → 查看“详细信息”页 → 找到进程 → 右键“打开文件位置” → 确认platforms/目录存在且含qwindows.dll若仍黑屏在 exe 同目录创建debug.log在main.py开头加import logging; logging.basicConfig(filenamedebug.log, levellogging.INFO)捕获初始化日志。5.4 考勤系统上线前必做的 5 项压力测试不要跳过这五步它们直接决定系统在真实场景中的可用性测试项方法合格标准工具连续运行稳定性启动系统摄像头持续采集 8 小时内存占用 500MBCPU 40%无 crashhtopLinux/ 任务管理器Win并发识别能力用 2 台手机同时对准摄像头模拟双人靠近两人均被正确识别无 ID 混淆人工观察record_table断网容错拔掉网线执行签到本地 SQLite 写入成功状态栏显示“离线签到”查看attendance.db文件大小是否增长低照度鲁棒性关闭室内灯仅靠窗自然光识别率 ≥ 85%100 次测试中成功 ≥85 次录制视频回放统计数据库写入峰值模拟 50 人集中签到脚本循环调用record_attendance无事务超时平均写入延迟 200mssqlite3 attendance.db EXPLAIN QUERY PLAN SELECT ...最终交付时将lbph_model.yml、attendance.db、dataset/三个目录与 exe 同级放置即可脱离开发环境独立运行。记住考勤系统的价值不在算法多炫酷而在 365 天每天 8 点准时稳定工作——这正是 OpenCV 底层控制、PyQt5 线程隔离、SQLite 事务保障共同达成的工程结果。本文还有配套的精品资源点击获取