简介这是一份基于QT实现的人脸识别考勤管理系统源码工程适合正在做C/Qt课设、毕设或入门人脸识别应用开发的读者。系统拆分为员工打卡端Armface和管理员管理端AdminFace员工端通过按钮调起摄像头进行人脸识别打卡同时实时显示时间并用定时器轮询通知表展示管理员发布的最新公告管理端则对应完成后台管理与数据维护。资源包共294个文件包含42个cpp、26个hpp源码8个ui界面描述文件以及pro工程、qrc资源配置、xml配置、py脚本、db数据库等覆盖界面布局、业务逻辑与存储配置172个jpg和23个png多为界面截图或人脸样本素材docx与txt有助于快速理解项目结构。压缩包约18.82MB已有939人学习下载。目录组织清晰从人脸检测、特征训练到打卡记录均能对照代码排查能帮助读者快速掌握QT下摄像头调用、定时器、数据库访问和模块化界面设计的完整思路。1. 人脸识别考勤最大风险不在识别率上把基于QT的人脸识别考勤系统部署到办公室环境后会发现出问题最多的不是模型认不出人而是摄像头启动失败、时间显示与考勤机本地时间错位、定时轮询通知卡住UI线程、以及打包后运行环境缺组件。这套系统的两个模块员工打卡端 Armface 和管理员端 AdminFace核心链路其实很短打开摄像头、提取人脸、LBPH分类、写考勤记录。但它跨了 Qt 事件循环、OpenCV 视频帧处理、SQLite 数据同步三套机制任何一层没接好识别率再高也白搭。本文按真实代码结构逐层拆带参数和坑。2. 人脸特征与关键点LBPH 参数和 Facemark 训练拆解2.1 LBPH 不是深度模型参数对结果的影响比想象大源码里有一组lbph_faces.cpp这是训练 LBPH 人脸识别器的核心。LBPHLocal Binary Patterns Histograms先把人脸图划分成若干网格在每个单元格内计算局部二值模式的直方图再把所有单元格的直方图串联成整张人脸的特征向量。它不依赖 GPU也不吃显存在工控机上跑也很流畅这是它被选作 QT 考勤端识别算法的原因。cv::Ptrcv::face::LBPHFaceRecognizer model cv::face::LBPHFaceRecognizer::create(1, 8, 8, 8, 120.0); model-train(faceImages, labels);create的五个参数依次是邻居半径 radius、参与比较的邻域点数 neighbors、水平方向格子数 grid_x、垂直方向格子数 grid_y、置信度阈值 threshold。默认值是(1, 8, 8, 8, DBL_MAX)如果不改第五个参数predict永远不会因为置信度过高而拒绝识别结果就是随便一张陌生脸都会落到某个员工名下。真实考勤场景必须把阈值调小我一般设在 100~130具体要看摄像头到人脸的距离和现场光照。想要更严格就在predict之后对置信度再做一次拦截。int label -1; double confidence 0.0; model-predict(grayFace, label, confidence); if (confidence 120.0) { qDebug() reject unknown face, confidence: confidence; return; }这里必须注意LBPH 返回的置信度是特征向量之间的距离数值越小代表越匹配所以上述判断是“大于阈值则拒绝”方向写反的话所有员工都会被拦在外面。新人在这块最容易出问题。2.2 Facemark 的 LBF 与 AAM关键点训练要选对文件facemarkLBF.cpp、facemarkAAM.cpp、trainFacemark.cpp、regtree.cpp这几个文件都在处理关键点训练目的是给后续对齐提供坐标。LBFLocal Binary Features和 AAMActive Appearance Model思路完全不同。AAM 需要先建立形状模型再拟合纹理对初始位置极其敏感轮廓初始化偏了就容易发散参数调整成本高。LBF 用回归树直接学像素差到关键点偏移量的映射训练快对光照变化更稳。在考勤系统里关键点只用来做人脸对齐没必要上 AAM。LBF 模型的训练入口大致如下cv::face::FacemarkLBF::Params params; params.n_landmarks 68; params.initShape_n 10; params.n_means 1000; cv::Ptrcv::face::Facemark facemark cv::face::FacemarkLBF::create(params); facemark-training(images, landmarks);n_landmarks决定关键点数量。68 点是完整面部轮廓加眉毛、眼睛、鼻子、嘴巴标注成本高如果只是给 LBPH 提供对齐后的裁剪区域5 点或 68 点都可以。initShape_n是均值形状初始化的候选数量n_means控制训练时使用的均值形状数量。样本量不足时68 点模型反而容易过拟合我踩过这个坑之后内部工具默认改成 68 点但要求每张图人工复核少于 300 张样本就直接降级用 5 点。2.3 训练样本的裁剪与对齐lbph_faces.cpp在喂数据之前人脸区域必须先对齐。常见做法是借助 Facemark 检测双眼坐标做仿射变换再 resize 到固定尺寸例如 92×112。这一步对识别率的影响比调 LBPH 参数更明显。cv::Mat aligned alignFace(gray, leftEye, rightEye, 92, 112);alignFace内部用cv::getRotationMatrix2D计算旋转矩阵再用cv::warpAffine执行变换。头部偏转超过正负 15 度时LBPH 的纹理直方图变化很大基本无法识别。另外样本不能全来自同一段视频否则 LBPH 会把背景光照一起学进去换一个工位就识别失败。我一般给每个员工采集 20~30 张照片覆盖早中晚三个时段训练完立刻用没有参与训练的照片做回测达不到 95% 就继续补样本。3. 员工端 Armface摄像头取流、QTimer 定时与打卡判定3.1 show_time() 与 QTimer 的协作Armface 界面上方实时显示系统时间源码里对应show_time()。实现上不是用 while 循环刷新 label而是用QTimer每 1000ms 触发一次。原因很直接Qt 界面更新必须回到主线程的事件循环用死循环会把窗口消息全部阻塞界面假死最后用户只能强杀进程。QTimer *clockTimer new QTimer(this); connect(clockTimer, QTimer::timeout, this, ClockWindow::show_time); clockTimer-start(1000);槽函数内部的写法是取系统时间再格式化到 QLabelvoid ClockWindow::show_time() { QDateTime now QDateTime::currentDateTime(); ui-labelTime-setText(now.toString(yyyy-MM-dd HH:mm:ss dddd)); }需要注意一个细节QDateTime::currentDateTime()返回的是本机时区时间。如果跑在没做过时间同步的工控机上显示的秒数会和标准时间差很多考勤记录也就跟着错。部署时最好在程序启动时校正一次系统时间或者从管理员端拿服务器时间。start(1000)并不是严格意义上的每秒整点触发槽函数执行本身有耗时会造成少量漂移但显示用的时间直接取系统时间不用自己做补偿系统时钟自己会走准。3.2 摄像头采集OpenCV VideoCapture 与 Qt 事件循环共存员工端点“打卡”按钮后打开摄像头源码对应按钮触发的打开摄像头操作。这个环节最大的坑是不能沿用控制台程序的写法比如cv::waitKey(30)。Qt 程序里一调用 waitKey整个事件循环被阻塞界面停住窗口无法响应。正确的做法是把视频帧读取放进 QTimer 定期触发或者放到独立采集线程。QTimer *cameraTimer new QTimer(this); connect(cameraTimer, QTimer::timeout, this, ClockWindow::grabFrame); cameraTimer-start(33); // 约 30 FPSvoid ClockWindow::grabFrame() { cv::Mat frame; if (!cap.read(frame)) { qDebug() camera read failed; return; } cv::Mat gray; cv::cvtColor(frame, gray, cv::COLOR_BGR2GRAY); // detect face, then crop and predict }cap是cv::VideoCapture对象构造时传摄像头索引通常 0 表示默认摄像头。如果设备被其他程序占用cap.isOpened()会返回 false界面上必须给出提示不能静默失败。识别也不需要每一帧都跑30 FPS 全量推理对 CPU 占用太高实践中我改成每 6~8 帧做一次完整检测识别剩下的帧只做预览预览画面用cv::Mat转QImage后贴到 QLabel 上。cv::cvtColor(frame, rgb, cv::COLOR_BGR2RGB); QImage img(rgb.data, rgb.cols, rgb.rows, static_castint(rgb.step), QImage::Format_RGB888); ui-labelCamera-setPixmap( QPixmap::fromImage(img.scaled(ui-labelCamera-size(), Qt::KeepAspectRatio)));这段转换要注意rgb.step必须显式传给 QImage 构造函数因为 OpenCV Mat 存在行对齐填充不传 stride 会出现图像错位。3.3 打卡判定与重复打卡拦截识别成功后要写考勤记录这里的关键问题是防止同一员工在短时间内重复打卡。数据库里至少保留employee_id, clock_time字段插入前先查一下该员工最后一次打卡时间。QSqlQuery query(QSqlDatabase::database(attendance)); query.prepare( SELECT clock_time FROM attendance WHERE employee_id ? ORDER BY clock_time DESC LIMIT 1); query.addBindValue(employeeId); query.exec(); if (query.next()) { QDateTime last query.value(0).toDateTime(); if (last.secsTo(QDateTime::currentDateTime()) 60) { return; // 同一个人 60 秒内不重复记录 } }阈值按考勤制度调。办公室上下班场景60 秒足够如果是门禁闸机识别通过后门要开合时间太短容易出现误判。更严格的做法是记录当天第一条和最后一条分别标记上班和下班但这套逻辑放管理员端做二次处理更合适员工端保持简单。4. 管理员端 AdminFace考勤记录同步与通知轮询实现4.1 run_message() 定时轮询通知表管理员端发布的公告员工端通过定时器访问通知表获取对应源码里的run_message()。员工端不去主动监听数据库变更而是每隔几秒查一次有新公告就把标题和内容更新到公告栏。QTimer *noticeTimer new QTimer(this); connect(noticeTimer, QTimer::timeout, this, ClockWindow::run_message); noticeTimer-start(5000);void ClockWindow::run_message() { QSqlQuery query(QSqlDatabase::database(attendance)); query.exec( SELECT title, content, publish_time FROM notice WHERE status 1 ORDER BY publish_time DESC LIMIT 1); if (query.next()) { QString title query.value(0).toString(); QString content query.value(1).toString(); ui-labelNotice-setText(QString(%1\n%2).arg(title, content)); } }轮询间隔影响数据库压力。单机 SQLite 场景 5 秒一次没问题但如果换成网络数据库且员工端有几十台建议把间隔放大到 15 秒查询条件不要再用ORDER BY publish_time DESC LIMIT 1改成记录上次已读的自增 id只捞增量数据。query.prepare( SELECT title, content FROM notice WHERE id ? AND status 1 ORDER BY id ASC); query.addBindValue(lastNoticeId);4.2 考勤汇总查询与权限区分AdminFace 不只展示记录还要管员工信息、发公告、导出考勤。数据库至少拆三张表employee、attendance、notice。表关键字段说明employeeid, name, face_feature_path, statusstatus 标记离职识别时过滤attendanceid, employee_id, clock_time, typetype 区分上班/下班noticeid, title, content, publish_time, statusstatus1 表示生效中管理员按日期汇总考勤时SQL 要处理当天首次和末次打卡SELECT employee_id, MIN(clock_time) AS first_clock, MAX(clock_time) AS last_clock FROM attendance WHERE clock_time 2025-01-01 AND clock_time 2025-01-02 GROUP BY employee_id;这段 SQL 把一天内的多次打卡压缩成两个时间点迟到、早退的判断交给程序处理。注意如果员工中途外出再次刷脸MAX(clock_time)取的是最后一次返回时间审批逻辑里要允许管理员手动修正单条记录否则把外出时间当成下班时间就会误判。4.3 两端数据库并发与数据一致性员工端和管理员端共用同一个数据库最大的隐患是同时写入。SQLite 支持多进程读但写锁是全局的管理员端在更新员工照片特征时员工端正好写入打卡记录就会报database is locked。常见做法是每个连接设置 busy_timeout让短暂锁冲突自动重试。QSqlDatabase db QSqlDatabase::addDatabase(QSQLITE, attendance_conn); db.setConnectOptions(QSQLITE_BUSY_TIMEOUT3000);3 秒的忙等待能覆盖绝大多数瞬时冲突。如果部署规模再大就要换 MySQL 或 PostgreSQLQt 代码里只需把addDatabase的驱动字符串改掉上层 SQL 基本不动这是这个项目在数据层比较舒服的地方。5. 部署这关Runtime 环境与 Qt 插件路径的常见报错5.1 运行时报错先查三件事考勤系统发布后最常见的三个报错我在项目里反复遇到。第一个是qt_qpa_platform_plugin_path相关错误提示某个d:\qt\5.15.2\msvc2019_64路径下找不到插件。原因通常是QApplication找不到 platforms 插件目录直接把QT_QPA_PLATFORM_PLUGIN_PATH指向发布目录下的platforms文件夹即可路径不能有中文和空格。第二个是缺少 WebView2 Runtime。Qt 5.15 里部分组件依赖 WebView2 运行库如果程序没有用到 QWebEngine可以直接排除相关 DLL如果确实依赖需要在目标机器安装 WebView2 Runtime不能靠手动拷 dll安装器会写入注册表路径。第三个是 OpenCV 的face模块模型文件缺失。LBPH 是代码内训练不需要额外模型但 Facemark 的 LBF 模型需要lbfmodel.yaml这类文件。发布时把模型放到可执行文件同级的 models 目录用QCoreApplication::applicationDirPath()拼绝对路径加载。5.2 windeployqt 补完还要手动加 OpenCV DLLWindows 下windeployqt只部署 Qt 自身的运行库OpenCV 的opencv_core455.dll、opencv_face455.dll、opencv_videoio455.dll要手动复制。发布前做一次冒烟测试拔掉开发环境路径在干净环境里依次执行“登录 → 打开摄像头 → 刷脸打卡 → 管理员查询记录”。漏 DLL 时程序要么弹0xc000007b要么直接没反应用 Dependencies.exe 打开主程序就能看到未解析的依赖项。最后提一个优化技巧识别成功后的写库操作不必放在摄像头采集线程里同步执行把识别结果员工 id、时间戳抛进队列由独立线程批量写入 SQLite打卡响应会明显变快。Qt 里用信号槽把结果抛到主线程的数据库连接执行注意每个线程只能用自己的 QSqlDatabase 连接跨线程共用同一个实例会触发随机崩溃。本文还有配套的精品资源点击获取
