QT—-基于QT的人脸考勤系统

相关源码

相关的代码更新请参考提交记录,我做了详细的备注,方便观察代码变化
github与提交记录
b站视频参考
[环境安装文件](链接:https://pan.baidu.com/s/1U5C-4hE3DfYEAemMD6pPGQ
提取码:6vfp)

1 编译opencv库

1.1 下载源代码

源代码下载地址:https://github.com/opencv/opencv

第三方库下载地址:https://github.com/opencv/opencv_contrib

cmake下载地址:https://cmake.org/download/

都下载4.5.2版本的,把两个zip都解压,windows的第一个库是直接exe解压的

1.2 qt编译opencv

使用qt打开项目,源代码opencv/sources/CMakeLIsts.txt

file

使用MinGW64构建项目,稍等一会,等qt吧资源都加载完

file

点击项目,搜索ms取消这两个安装项目,将第三方库的人脸识别模块加入,==注意debug为release版本,所有操作都是在release版本上执行的,图片还没更改==

file

修改安装路径

file

修改下边构建步骤里边 为install,取消勾选all,最后执行Cmake

file

1.3 执行Cmake一直卡着data: Download: face_landmark_model.dat

发现一直卡着,这个face_landmark_model.dat下载不下来

file

采取手动下载的方式

https://raw.githubusercontent.com/opencv/opencv_3rdparty/8afa57abc8229d611c4937165d20e2a2d9fc5a12/face_landmark_model.dat

放到opencv的路径下,并且把没下载完的文件的前缀也加上,替换,再次执行Cmake成功

file

完毕后回到编辑点击启动,更改构建模式为release,运行

file

2 编译SeetaFace2代码

2.1 遇到报错By not providing "FindOpenCV.cmake" in CMAKE_MODULE_PATH this project has

下载代码:https://github.com/seetafaceengine/SeetaFace2

依旧是打开CmakeList文件,release模式,遇到报错

file

打开CmakeList文件添加上这一句,加上你刚刚安装的opcv的路径,注意斜杠的位置

set(OpenCV_DIR E:/Environment/opencv452)

file

修改安装路径,opencv路径等等,更上边类似,执行cmake,执行

file

2.2遇到报错Model missing

执行后遇到报错模型丢失

file

下载模型,把四个模型都下载了,放入生成release的文件夹

https://github.com/seetafaceengine/SeetaFace2?tab=readme-ov-file

file

file

再次报错,没有图片,找张人脸图片放入

file

file

3 测试两个环境能否使用

3.1 配置环境变量

找到系统环境变量PATH,添加两个库的bin文件夹

file

新建一个qt项目,在配置文件pro里添加上两个库的路径,并且添加上lib。这样在引入头文件就不会报错

#添加头文件
INCLUDEPATH += E:\Environment\opencv452\include
INCLUDEPATH += E:\Environment\opencv452\include\opencv2
INCLUDEPATH += E:\Environment\SeetaFace2\include
INCLUDEPATH += E:\Environment\SeetaFace2\include\seeta

#添加库
LIBS += E:\Environment\opencv452\x64\mingw\lib\libopencv*
LIBS += E:\Environment\SeetaFace2\lib\libSeeta*

file

#include "mainwindow.h"

#include <QApplication>
#include <opencv.hpp>
#include <FaceDetector.h>

using namespace cv;
using namespace seeta::v2;

int main(int argc, char *argv[])
{
    QApplication a(argc, argv);
    MainWindow w;
    w.show();

    cv::namedWindow("fram");
    Mat mt = imread("E:/CPP_Study/QT/QTCode/opencvSeetaface/1.jpg");
    imshow("fram",mt);
    cv::waitKey(10);

    seeta::ModelSetting::Device device = seeta::ModelSetting::CPU ;
    int id = 0;
    seeta::ModelSetting FD_model("E:/Environment/SeetaFace2/bin/model/fd_2_00.dat",device ,id);
    seeta::FaceDetector FD(FD_model);

    return a.exec();
}

4客户端设计

4.1 考勤界面设计

按照以下界面搭建窗口,后边是样式表代码,实现功能就行,美化后边再说

file

QWidget#widget
{
background-color: rgb(124, 124, 124);
}

QWidget#widget_3
{
background-color: rgb(94, 94, 94);
}

QWidget#widget_2
{
background-color: rgba(180, 180, 180, 63);
}

QLabel#lb_2
{
font:25 16pt "微软雅黑 Light";
border:none;
color:rgb(255,255,255);
}
/*设置姓名等*/
QLabel[name = "key"]
{
background-color:#20232A;
font:20 12pt"微软雅黑 Light";
color:#808183;
}
/*设置姓名的值等*/
QLabel[name = "value"]
{
background-color:#20232A;
font:20 12pt"微软雅黑 Light";
color:#ffffff;
}
/*设置识别成功人脸的圆形*/
QLabel#lb_headpic
{
background-color:#20232A;
border-radius:70px;
}
/*设置标题*/
QLabel#lb_title
{
font:20 20pt"微软雅黑 Bold";
color:#ffffff;
}

4.2 qt连接摄像头并显示

首先在添加一个label,覆盖住左边那一块的区域,右击放到后面,不会挡住认证成功的显示

file

在.pro文件里导入我们之前写好的lib和头文件件路径。使用opencv读取摄像头数据,使用定时器事件来采集数据,需要将OpenCV 的 Mat 格式(BGR)转换为 Qt 的 QImage 格式(RGB)。

#ifndef FACEATTENDENCE_H
#define FACEATTENDENCE_H

#include <QMainWindow>
#include <opencv.hpp>

using namespace cv;
using namespace std;

QT_BEGIN_NAMESPACE
namespace Ui {
class FaceAttendence;
}
QT_END_NAMESPACE

class FaceAttendence : public QMainWindow
{
    Q_OBJECT

public:
    FaceAttendence(QWidget *parent = nullptr);
    ~FaceAttendence();

    //定时器事件
    void timerEvent(QTimerEvent *e);

private:
    Ui::FaceAttendence *ui;

    //摄像头
    VideoCapture cap;
};
#endif // FACEATTENDENCE_H

#include "faceattendence.h"
#include "ui_faceattendence.h"

FaceAttendence::FaceAttendence(QWidget *parent)
    : QMainWindow(parent)
    , ui(new Ui::FaceAttendence)
{
    ui->setupUi(this);

    //打开摄像头
    cap.open(0);//如果是linux的话,dev/video
    //启动定时器,每多少毫秒采集一次
    startTimer(1);
}

FaceAttendence::~FaceAttendence()
{
    delete ui;
}

void FaceAttendence::timerEvent(QTimerEvent *e)
{
    //采集数据
    Mat srcImage;
    if(cap.grab())
    {
        cap.read(srcImage);
        // 水平镜像图像
        cv::flip(srcImage, srcImage, 1);
    }
    //没有数据返回
    if(srcImage.data == NULL)
    {
        return;
    }
    // 将 OpenCV 的 Mat 格式(BGR)转换为 Qt 的 QImage 格式(RGB)
    cvtColor(srcImage, srcImage, COLOR_BGRA2RGB);

    // 将 Mat 数据转换为 QImage
    QImage image(srcImage.data, srcImage.cols, srcImage.rows, srcImage.step1(), QImage::Format_RGB888);

    // 将 QImage 转换为 QPixmap
    QPixmap mmp = QPixmap::fromImage(image);

    // 将 QPixmap 显示在 QLabel 控件上
    ui->lb_camera->setPixmap(mmp);
}

4.3 opencv识别人脸

识别采用级联分类器CascadeClassifier,首先定义分类器cv::CascadeClassifier cascade;,然后导入我们的安装的opencv的模型(在etc文件夹下)cascade.load("E:/Environment/opencv452/etc/haarcascades/haarcascade_frontalface_alt2.xml");

在采集数据后直接开始检测。detectMultiScale的参数

第一个参数为待检测的图片,对类型没有要求。

第二个参数为输出的参数,表示检测到的目标位置信息,是一个vector,每个元素都是一个Rect类型矩形,表示检测到的目标在原始图像中的位置和大小。

第三个参数为表示图像缩放比例的参数,

其他参数默认即可

//检测人脸
    std::vector<Rect> faceRects;
    cascade.detectMultiScale(grayImage,faceRects,1.1,3);
    if(faceRects.size()>0)
    {
        Rect rect = faceRects.at(0);//第一个人脸的矩形框
        //绘制矩形框
        rectangle(srcImage,rect,Scalar(0,0,255));
    }

为了提高检测速度,可以把图像转换为灰度图进行检测

//把图像转为灰度图,提高处理速度
    Mat grayImage;
    cvtColor(srcImage,grayImage,COLOR_BGR2GRAY);

file

移动检测的圆形代码也很简单,调用label->move

//移动圆形检测框
        ui->lb_traceFace->move(rect.x-rect.width/2,rect.y-rect.height/2);

//没有检测到人脸 移动圆形检测框到中心
        ui->lb_traceFace->move(100,60);

4.4 网络相关

首先在pro配置文件里添加上network模块QT += core gui network,增加头文件,使用定时器来定期检查是否断开连接,使用三个槽函数来检测,开始,断开连接。

#include <QTcpSocket>
#include <QTimer>

//创建网络前套字,定时器
    QTcpSocket m_socket;
    QTimer m_timer;

    void timer_connect();
    void stop_connnect();
    void start_connect();

在构造函数里搭建信号的连接,断开时开始连接,连接上停止定时器,我连接的是我自己的服务器,阿里云可以免费领。

   //QTcpSocket当断开连接的时候disconnected信号,连接成功会发送connected
    connect(&m_socket,&QTcpSocket::disconnected,this,&FaceAttendence::start_connect);
    connect(&m_socket,&QTcpSocket::connected,this,&FaceAttendence::stop_connnect);

    //定时器到时间,连接服务器
    connect(&m_timer,&QTimer::timeout,this,&FaceAttendence::timer_connect);
    //启动定时器,每5s连接一次直到成功
    m_timer.start(5000);

void FaceAttendence::timer_connect()
{
    //连接服务器
    m_socket.connectToHost("你想要连接的ip",22);
}

void FaceAttendence::stop_connnect()
{
    //停止计时器
    m_timer.stop();
    QMessageBox::information(nullptr,"信息","连接成功");
}

void FaceAttendence::start_connect()
{
    //5秒连接
    m_timer.start(5000);
    QMessageBox::information(nullptr,"信息","服务器断开");
}

5 考勤终端服务器搭建

5.1 服务器搭建读取数据

新建一个服务器的qt项目,搭建ui,放置一个label,方便等会展示图片

file

导入头文件,定义两个槽函数,一个连接成功,一个读取客户端发送的数据

#ifndef ATTENDANCEWINDOW_H
#define ATTENDANCEWINDOW_H

#include <QMainWindow>
#include <QTcpSocket>
#include <QTcpServer>

QT_BEGIN_NAMESPACE
namespace Ui {
class AttendanceWindow;
}
QT_END_NAMESPACE

class AttendanceWindow : public QMainWindow
{
    Q_OBJECT

public:
    AttendanceWindow(QWidget *parent = nullptr);
    ~AttendanceWindow();

public slots:

    // 接受客户端连接的槽函数
    void accept_client();
    // 读取数据的槽函数
    void read_data();

private:
    Ui::AttendanceWindow *ui;

    // TCP服务器对象
    QTcpServer m_server;

    // 与客户端通信的套接字对象
    QTcpSocket *m_socket;
};
#endif // ATTENDANCEWINDOW_H

首先打开服务器的监听功能指定端口,当客服端连接到服务器后连接槽函数accept_client,获取与客户端通信的套接字连接槽函数read_data,并且当套接字准备好读取数据时,从套接字中读取所有数据。

#include "attendancewindow.h"
#include "ui_attendancewindow.h"

AttendanceWindow::AttendanceWindow(QWidget *parent)
    : QMainWindow(parent)
    , ui(new Ui::AttendanceWindow)
{
    ui->setupUi(this);

    // 当有客户端连接时,触发 newConnection 信号
    // 并将其连接到 accept_client 槽函数
    connect(&m_server,&QTcpServer::newConnection,this,&AttendanceWindow::accept_client);
    // 监听指定的IP地址和端口,启动TCP服务器
    m_server.listen(QHostAddress::Any,9999);
}

AttendanceWindow::~AttendanceWindow()
{
    delete ui;
}

void AttendanceWindow::accept_client()
{
    //获取与客户端通信的套接字
    m_socket = m_server.nextPendingConnection();
    // 当套接字准备好读取数据时,触发 readyRead 信号
    // 并将其连接到 read_data 槽函数
    connect(m_socket,&QTcpSocket::readyRead,this,&AttendanceWindow::read_data);
}

void AttendanceWindow::read_data()
{
    // 从套接字中读取所有数据
    QString msg = m_socket->readAll();
    qDebug()<<msg;
}

最后打开测试工具,客户端模式连接,发送数据,服务器端已经接收到了

file

6 人脸数据发送和接受

6.1 客户端人脸数据发送

需要将考勤客户端捕捉到的数据发送给服务器,不能以图片的形式发送,需要使用QByteArry,使用imencode编码数据

bool imencode(const string& ext, InputArray img, vector<uchar>& buf, const vector<int>& params = vector<int>());

ext:指定要使用的图像格式的文件扩展名,例如 “.jpg”、“.png”。

img:输入的图像,可以是Mat对象。

buf:输出的字节流,即编码后的图像数据将被存储在这个向量中。

params:可选参数,用于指定编码选项。例如,对于JPEG格式,可以通过指定params来设置图像质量等参数。

返回值是一个布尔值,表示编码是否成功。如果成功,返回true;否则,返回false。

代码添加在人脸检测成功的判断里

 //把Mat数据转化为QbyteArry,编码成jpg格式
        vector<uchar> buf;
        cv::imencode(".jpg",srcImage,buf);
        QByteArray byte((const char*)buf.data(),buf.size());

        // 获取数据大小
        quint64 backsize = byte.size();
        // 创建用于发送数据的 QByteArray 对象
        QByteArray sendData;

        // 创建 QDataStream 对象,用于将数据写入 sendData
        QDataStream stream(&sendData, QIODevice::WriteOnly);

        // 设置 QDataStream 的版本
        stream.setVersion(QDataStream::Qt_6_4);

        // 将数据大小和字节数据写入 sendData
        stream << backsize << byte;

        // 将数据发送给客户端
        m_socket.write(sendData);

6.2 服务器接受人脸数据

首先创建接收流,判断是否有足够的数据接收。当 bsize 为零时,表示尚未读取数据大小。如果可读数据不足以读取数据大于 (sizeof(bsize)),则表示数据不足,需要等待更多数据。在读取数据大小之后,需要检查套接字中是否有足够的数据读取图像数据。再次使用 m_socket->bytesAvailable() 函数获取可读数据的字节数。如果可读数据不足以读取 bsize 个字节,则表示数据不足,需要等待更多数据。进行两次if检查可以避免不必要的读取操作,提高效率,避免数据溢出。

void AttendanceWindow::read_data()
{
    // 创建 QDataStream 对象,并设置其版本为 Qt_6_4
    QDataStream stream(m_socket);
    stream.setVersion(QDataStream::Qt_6_4);

    // 如果 bsize 为零,则检查套接字中是否有足够的数据读取数据大小
    if (bsize == 0) {
        if (m_socket->bytesAvailable() < (qint64)sizeof(bsize)) {
            // 数据不足,返回
            return;
        }
        // 读取数据大小并将其赋予 bsize
        stream >> bsize;
    }

    // 检查套接字中是否有足够的数据读取图像数据
    if (m_socket->bytesAvailable() < bsize) {
        // 数据不足,返回
        return;
    }

    // 读取图像数据并将其赋予 data
    QByteArray data;
    stream >> data;

    // 将 bsize 重置为零
    bsize = 0;

    // 如果 data 为空,则返回
    if (data.size() == 0) {
        return;
    }
    // 使用 loadFromData() 函数从 data 中加载图像
    QPixmap m_mp;
    if (!m_mp.loadFromData(data, "jpg")) {
        // 加载图像失败,可能是数据损坏或格式错误
        return;
    }

    // 将图像缩放到 QLabel 控件的大小
    m_mp = m_mp.scaled(ui->lb_pic->size());

    // 将图像显示在 QLabel 控件上
    ui->lb_pic->setPixmap(m_mp);
}

运行客户端和服务器后没有数据,排查发现接收的stream>>data,data的size是0,也就是没有数据,发现我已开始吧bsize定义为了quint8位了修改为quint64解决了问题。

qint8 是一个 8 位的整型数据类型,其范围远小于 bsize 所需的存储空间。

当读取数据大小时,QDataStream 会将数据转换为 qint64 类型,而 qint8 无法容纳 qint64 类型的值,会导致数据溢出。

即使没有数据溢出,qint8 也无法存储足够大的数据大小,导致无法正确读取图像数据。

file

7 人脸识别模块封装–注册和登陆

新建一个c++class,继承自QObject,因为封装的功能不需要界面引入头文件FaceEngine.h,报错显示没有。从seeta2的源代码里搜索,复制放到我们的编译目录。

file

定义一个人脸注册槽函数,一个人脸识别槽函数,注册和识别成功都返回人脸ID。一开始初始化人脸引擎对象,需要传入三个模型。首先将传入的opencv mat图像转换为SeetaImageData。然后就是直接调用引擎的register和query,注册后把人脸数据保存到数据库。
SeetaImageData是一个结构体,直接一个个赋值就行
file

#ifndef QFACEOBJECT_H
#define QFACEOBJECT_H

#include <QObject>
#include <seeta/FaceDatabase.h>
#include <seeta/FaceEngine.h>
#include <opencv.hpp>
using namespace cv;
using namespace seeta;

//人脸数据存储,人脸检测,人脸识别
class QFaceObject : public QObject
{
    Q_OBJECT
public:

    explicit QFaceObject(QObject *parent = nullptr);

    ~QFaceObject();

public slots:
    // 人脸注册函数,用于将人脸图像注册到人脸数据库中
    // 参数:faceImage - OpenCV Mat 对象,代表人脸图像
    // 返回值:int64_t,注册成功返回人脸 ID,失败返回 -1
    int64_t face_register(cv::Mat &faceImage);

    // 人脸识别函数,用于识别给定图像中的人脸
    // 参数:faceImage - OpenCV Mat 对象,代表人脸图像
    // 返回值:int,识别成功返回人脸 ID,失败返回 -1
    int face_query(cv::Mat &faceImage);

signals:

private:
    // seeta 人脸引擎指针
    seeta::FaceEngine *fengineptr;
};

#endif // QFACEOBJECT_H
#include "qfaceobject.h"

QFaceObject::QFaceObject(QObject *parent)
    : QObject{parent}
{
    // 初始化 seeta 人脸引擎设置
    seeta::ModelSetting FDmodel("E:/Environment/SeetaFace2/bin/model/fd_2_00.dat",seeta::ModelSetting::GPU,0);
    seeta::ModelSetting PDmodel("E:/Environment/SeetaFace2/bin/model/pd_2_00_pts5.dat",seeta::ModelSetting::GPU,0);
    seeta::ModelSetting FRmodel("E:/Environment/SeetaFace2/bin/model/fr_2_10.dat",seeta::ModelSetting::GPU,0);
    // 创建 seeta 人脸引擎对象
    this->fengineptr = new seeta::FaceEngine(FDmodel,PDmodel,FRmodel);
}

QFaceObject::~QFaceObject()
{
    delete fengineptr;
}

int64_t QFaceObject::face_register(Mat &faceImage)
{
    // 转换 OpenCV Mat 数据到 seeta::SeetaImageData 结构体,数据转换
    SeetaImageData image;
    image.data = faceImage.data;
    image.channels = faceImage.channels();
    image.height = faceImage.rows;
    image.width = faceImage.cols;

    // 调用人脸引擎进行人脸注册
    int64_t faceID = this->fengineptr->Register(image);
    if (faceID >= 0) {
        // 注册成功后保存人脸数据库
        this->fengineptr->Save("./face.db");
    }
    return faceID;
}

int QFaceObject::face_query(Mat &faceImage)
{
    // 转换 OpenCV Mat 数据到 seeta::SeetaImageData 结构体
    SeetaImageData image;
    image.data = faceImage.data;
    image.channels = faceImage.channels();
    image.height = faceImage.rows;
    image.width = faceImage.cols;

    // 声明相似度变量
    float similaty = 0;

    // 调用人脸引擎进行人脸识别
    int64_t faceID = this->fengineptr->Query(image, &similaty);
    return faceID;
}

8 人脸数据库搭建

8.1 表格设计

人脸数据和faceid的绑定是引擎帮我们做好的,我们需要在自己的数据库让Faceid和人的相关信息进行绑定

file

我们需要搭建这样的表格
file

8.2 代码编写

在服务器的.pro文件中添加上sql模块,使用代码来创建表格,也可以先用navicat生成表格来使用

file

代码原理。首先连接上sqlite数据库驱动,创建一个数据库的db文件(会放在debug生成的文件夹下),用sql语句创建两个table表格并且设置标题和数据格式


#include "attendancewindow.h"

#include <QApplication>
#include <QSqlDatabase>
#include <QSqlError>
#include <QSqlQuery>

int main(int argc, char *argv[])
{
    QApplication a(argc, argv);

    // 连接到 SQLite 数据库
    QSqlDatabase db = QSqlDatabase::addDatabase("QSQLITE");
    // 设置数据库文件名
    db.setDatabaseName("server.db");

    // 检查数据库连接是否成功
    if(!db.open())
    {
        qDebug()<< db.lastError().text();
        return -1;  // 程序退出并返回错误代码
    }

    // 创建员工信息表 (if not exists: 如果不存在)
    QString createstr  = "create table if not exists employee(employeeID integer primary key autoincrement, name text , sex text,"
                        "birthday text, address text, phone text, faceID integer unique, headfile text)";

    // 创建 SQL 查询对象
    QSqlQuery query;
    if(!query.exec(createstr))
    {
         qDebug()<< query.lastError().text();
    }

     // 创建考勤记录表 (if not exists: 如果不存在则创建)
    createstr  = "create table if not exists attendance(attendenceID integer primary key autoincrement, employeeID integer,"
                        "attendanceTime TimeStamp NOT NULL DEFAULT(datetime('now','localtime')))";

    if(!query.exec(createstr))
    {
        qDebug()<< query.lastError().text();
    }

    AttendanceWindow w;
    w.show();
    return a.exec();
}

用navicat来查看,两个表里都有标题了,表格就创建完毕

file

9 注册页面设计

新建一个qt界面设计师类,widget(方面我们嵌入到主界面),按照下边简易搭建一个界面

file

开始右击按钮转到槽,编写逻辑

9.1 重置按钮

全部清空数据即可

void register_ui::on_btn_clear_clicked()
{
    //重置内容
    ui->le_nickname->clear();
    ui->le_birthday->setDate(QDate::currentDate());
    ui->le_phone->clear();
    ui->le_adress->clear();
    ui->le_path->clear();
    ui->lb_pic->clear();
}

9.2 添加头像按钮

使用文件对话框打开图片,==图片的名字和路径一定要是英文的,不然opencv在保存的时候会报错==

void register_ui::on_btn_addhead_clicked()
{
    //通过文件对话框选中文件
    QString filepath = QFileDialog::getOpenFileName(this);
    ui->le_path->setText(filepath);

    //显示图片,在图片标签中显示缩放后的图片
    QPixmap mmp(filepath);
    mmp = mmp.scaledToWidth(ui->lb_pic->width());
    // mmp = mmp.scaledToHeight(ui->lb_pic->height());
    ui->lb_pic->setPixmap(mmp);
    // // 设置图片在标签中居中显示
    // ui->lb_pic->setAlignment(Qt::AlignCenter);
}

9.3 注册按钮

通过照片,结合faceobject模块的到faceID;把个人信息储存到employee表,保存头像;吧各个数据更新到数据库,注册成功

void register_ui::on_btn_register_clicked()
{
    //1、通过照片,结合faceobject模块的到faceID
    QFaceObject faceobject;
    cv::Mat image = cv::imread(ui->le_path->text().toUtf8().data());
    int faceID = faceobject.face_register(image);
    //2、把个人信息储存到employee表
    QSqlTableModel model;
    model.setTable("employee");

    //把头像保存到一个路径下
    QString headfile = QString("./data/%1.jpg").arg(faceID);
    cv::imwrite(headfile.toStdString(),image);
    QSqlRecord record = model.record();

     // 设置记录的各个字段值
    record.setValue("name",ui->le_nickname->text());
    record.setValue("sex",ui->btn_man->isChecked()?"男":"女");
    record.setValue("birthday",ui->le_birthday->text());
    record.setValue("address",ui->le_adress->text());
    record.setValue("phone",ui->le_phone->text());
    record.setValue("faceID",faceID);
    record.setValue("headfile",headfile);

    //3、提示注册成功
    // 插入记录到模型
    bool ret = model.insertRecord(0,record);
    if(ret)
    {
        // 提交所有的挂起的更改到数据库
        model.submitAll();
        QMessageBox::information(nullptr,"信息","注册成功");
    }
    else
    {
        QMessageBox::information(nullptr,"信息","注册失败,头像重复或信息错误");
    }
}

但是此时发现注册第二张人脸的时候及后边时都无法注册,排查发现faceID一直是0,应为faceID不能重复所以发生了错误。我们在一开始启动的时候引擎检索现在的人脸数据库,这样faceID就不会重复。在QfaceObject的构造函数中加上这样依据导入现有的face数据库fengineptr->Load("./face.db");

9.4 打开摄像头按钮

定义一个定时器事件来捕捉数据

 //定时器事件
    void timerEvent(QTimerEvent *e);

    int timerID;  // 定时器 ID

// OpenCV 的视频捕获对象,用于访问摄像头
    cv::VideoCapture cap;

编写按钮的逻辑,打开时开恰摄像头没启动定时器,这样就会一直采集数据,同时更改按钮

void register_ui::on_btn_opencamera_clicked()
{
    // 检查按钮文本,如果当前为“打开摄像头”
    if(ui->btn_opencamera->text() == "打开摄像头")
    {
        // 打开摄像头
        if(cap.open(0)) // 尝试打开摄像头设备(设备编号为0)
        {
            ui->btn_opencamera->setText("关闭摄像头"); // 设置按钮文本为“关闭摄像头”
            timerID = startTimer(100); // 启动定时器,以每100毫秒的间隔捕获摄像头图像
        }
    }
    else // 如果当前按钮文本不是“打开摄像头”,即为“关闭摄像头”
    {
        killTimer(timerID); // 停止定时器
        ui->btn_opencamera->setText("打开摄像头"); // 设置按钮文本为“打开摄像头”
        // 关闭摄像头
        cap.release(); // 释放摄像头资源
    }
}

获取摄像头跟客户端打开摄像头一致

void register_ui::timerEvent(QTimerEvent *e)
{
    // 获取摄像头数据
    cv::Mat srcImage;
    if(cap.isOpened()) // 如果摄像头成功打开
    {
        cap >> srcImage; // 从摄像头读取图像帧
        if(srcImage.data == nullptr) return; // 如果图像数据为空,则返回
    }

    // 将图像格式转换为Qt能够处理的格式
    cv::Mat outImage;
    cv::cvtColor(srcImage, outImage, cv::COLOR_BGR2RGB); // 将图像从BGR格式转换为RGB格式(OpenCV默认的颜色通道顺序与Qt不同)
    cv::flip(outImage, outImage, 1); // 水平翻转图像,因为摄像头的图像可能是镜像的

    QImage image(outImage.data, outImage.cols, outImage.rows, outImage.step1(), QImage::Format_RGB888); // 创建Qt的QImage对象

    // 在Qt界面上显示图像
    QPixmap mmp = QPixmap::fromImage(image); // 将QImage转换为QPixmap
    mmp = mmp.scaledToWidth(ui->lb_pic->width()); // 根据标签的宽度缩放图像
    ui->lb_pic->setPixmap(mmp); // 将图像显示在标签上
}

启动时并且没有数据给到srcImage,并且qDebug()<<cap.isOpened();啥也不显示,就是这个函数没被调用。排查发现timerEvent少打一个e,这是qt规定好的定时器事件,不能输错。

9.5 拍照按钮

拍照保存图像,使用该图像进行注册。需要把定时器事件里的srcImage变为全局变量拿来接受图片,这样按按钮就能保存图片。

void register_ui::on_btn_shoot_clicked()
{
    //保存数据
    QString headfile = QString("./data/%1.jpg").arg(ui->le_nickname->text().toUtf8().toBase64());
    cv::imwrite(headfile.toStdString(),srcImage);
    killTimer(timerID); // 停止定时器
    ui->btn_opencamera->setText("打开摄像头"); // 设置按钮文本为“打开摄像头”
    // 关闭摄像头
    cap.release(); // 释放摄像头资源
    QMessageBox::information(nullptr,"信息","拍照成功");
}

10 客户端发送数据,服务器识别人脸id

首先在服务器端处理人脸数据识别,在read_data函数里,客服端的数据传到服务器后进行人脸识别

// 识别人脸
    // 定义一个 OpenCV 的 Mat 对象,用于存储人脸图像
    cv::Mat faceImage;

    // 定义一个无符号字符型向量 decode,用于存储解码后的数据
    std::vector<uchar> decode;

    // 调整 decode 的大小为与 data 相同
    decode.resize(data.size());

    // 将数据从 data 拷贝到 decode 中
    memcpy(decode.data(), data.data(), data.size());

    // 对解码后的数据进行颜色图像格式的解码,得到人脸图像
    faceImage = cv::imdecode(decode, cv::IMREAD_COLOR);

    // 使用 face_query 函数查询人脸,获取人脸ID
    int faceID = m_faceobject.face_query(faceImage);

    // 输出人脸ID信息
    qDebug() << faceID;

识别效果,坤坤识别出来了,丁真也识别出来了

file

file

10.1 识别人脸id同时从数据库里提取个人信息

在上边代码的下边继续添加从数据库中提取数据。使用faceID从数据库中过滤数据,只选中一条,把这个数据拿出打包为json格式发送

// -----------------从数据库中提取数据------------------
    // 给模型设置过滤器
    model.setFilter(QString("faceID=%1").arg(faceID));
    model.select();

    // 检查是否成功提取到一条记录
    if (model.rowCount() == 1)
    {
        // 获取第一条记录
        QSqlRecord record = model.record(0);

        // 构建要发送的 JSON 消息
        QString sdmsg = QString("{\"employeeID\":\"%1\",\"name\":\"%2\",\"address\":\"%3\",\"time\":\"%4\"}")
                            .arg(record.value("employeeID").toString())    // 员工ID
                            .arg(record.value("name").toString())          // 姓名
                            .arg(record.value("address").toString())       // 地址
                            .arg(QDateTime::currentDateTime().toString("yyyy-MM-dd hh:mm:ss")); // 当前时间

        // 发送数据
        m_socket->write(sdmsg.toUtf8());
    }

在客户端我们需要新建一个槽函数,接受数据是显示一下

 //接受json数据槽函数连接
    connect(&m_socket,&QTcpSocket::readyRead,this,&FaceAttendence::receive_data);

// 接收数据槽函数
void FaceAttendence::receive_data()
{
    // 接受数据并展示
    QString msg = m_socket.readAll(); // 读取所有接收到的数据
    qDebug() << msg; // 输出接收到的数据到调试输出
}

11 优化发送资源消耗

11.1 客户端优化发送次数

通过设置一个次数标签flag_onepersion来减少发送次数。
当 faceRects.size() > 0 时,表示检测到了人脸。此时会执行相应的处理,比如移动检测框等。
flag_onepersion 的初值为 0,表示初始状态下没有检测到人脸。
如果连续检测到人脸(即 faceRects.size() > 0),flag_onepersion 会逐步增加,表示连续检测到人脸的次数。
当 flag_onepersion 达到一定值(大于 2)时,才会执行发送人脸图像的操作。这样做可以确保在连续检测到人脸一定次数之后再发送图像,避免了因为瞬间的干扰导致频繁发送图像的情况。
一旦发送了人脸图像后,会将 flag_onepersion 重置为 -2,以便下一次检测到人脸时再次触发发送操作,相当于检测到的是同一个人的时候后就不在发送,只有他移动了脱离了检测区域才会进行下一次检测。
如果在一帧图像中没有检测到人脸(即 faceRects.size() == 0),则会将 flag_onepersion 重置为 0,表示重新开始检测人脸。

修改客户端中timerEvent事件的代码

//检测人脸
    std::vector<Rect> faceRects;
    cascade.detectMultiScale(grayImage,faceRects,1.1,3);
    if(faceRects.size()>0 && flag_onepersion >=0)
    {
        Rect rect = faceRects.at(0);//第一个人脸的矩形框
        //绘制矩形框
        // rectangle(srcImage,rect,Scalar(0,0,255));

        //移动圆形检测框
        ui->lb_traceFace->move(rect.x-rect.width/2,rect.y-rect.height/2);

        if(flag_onepersion > 2)
        {
            //把Mat数据转化为QbyteArry,编码成jpg格式
            vector<uchar> buf;
            cv::imencode(".jpg",srcImage,buf);
            QByteArray byte((const char*)buf.data(),buf.size());

            // 获取数据大小
            quint64 backsize = byte.size();
            // 创建用于发送数据的 QByteArray 对象
            QByteArray sendData;

            // 创建 QDataStream 对象,用于将数据写入 sendData
            QDataStream stream(&sendData, QIODevice::WriteOnly);

            // 设置 QDataStream 的版本
            stream.setVersion(QDataStream::Qt_6_4);

            // 将数据大小和字节数据写入 sendData
            stream << backsize << byte;

            // 将数据发送给客户端
            m_socket.write(sendData);
            flag_onepersion = -2;
        }
        flag_onepersion++;
    }
    if(faceRects.size() == 0)
    {
        //没有检测到人脸 移动圆形检测框到中心
        ui->lb_traceFace->move(100,60);
        flag_onepersion = 0;
    }

11.2 服务器端使用线程优化人脸查询

这句代码资源消耗很大
// 使用 face_query 函数查询人脸,获取人脸ID
int faceID = m_faceobject.face_query(faceImage);
我们在构造函数里创建线程,把QfaceObject的对象放入线程,但是在线程中不能直接调用对象的函数,需要用信号来触发

 //-----------------创建线程-----------------
    QThread *thread;
    //把QfaceObject的对象移动到线程中执行
    m_faceobject.moveToThread(thread);
    //启动线程
    thread->start();
    //绑定查询槽函数
    connect(this,&AttendanceWindow::query,&m_faceobject,&QFaceObject::face_query);

在头文件里定义一个信号,在cpp里要查询发送信号,替换原来的查询void query(cv::Mat &image);

file

但是此时faceid无法得到,这部分逻辑有点绕。查询的时候会跳到faceobject的face_query槽函数里,我们在里定义一个发送的信号send_faceid,同时将faceID传入,判断人脸的置信度;send_faceid跟AttendanceWindow的recevice_faceID槽函数绑定,得到faceid,并且进行后边处理从数据库中提取数据并发送给客户端。

int QFaceObject::face_query(Mat &faceImage)
{
    // 转换 OpenCV Mat 数据到 seeta::SeetaImageData 结构体
    SeetaImageData image;
    image.data = faceImage.data;
    image.channels = faceImage.channels();
    image.height = faceImage.rows;
    image.width = faceImage.cols;

    // 声明相似度变量
    float similaty = 0;

    // 调用人脸引擎进行人脸识别
    int64_t faceID = this->fengineptr->Query(image, &similaty);

    //如果人脸可信度大于0.85,发送faceid信号
    if(similaty>=0.85)
    {
        emit send_faceid(faceID);
    }
    else
    {
        emit send_faceid(-1);
    }
    return faceID;
}

void AttendanceWindow::recevice_faceID(qint64 faceID)
{
    // 输出人脸ID信息
    qDebug() <<"face:::" <<faceID;

    //没有检测到人脸,也得发送空的数据
    if(faceID < 0)
    {
        QString sdmsg = QString("{\"employeeID\":,\"name\":,\"address\":,\"time\":}");
        // 发送数据
        m_socket->write(sdmsg.toUtf8());
    }
    // -----------------从数据库中提取数据------------------
    // 给模型设置过滤器
    model.setFilter(QString("faceID=%1").arg(faceID));
    model.select();

    // 检查是否成功提取到一条记录
    if (model.rowCount() == 1)
    {
        // 获取第一条记录
        QSqlRecord record = model.record(0);

        // 构建要发送的 JSON 消息
        QString sdmsg = QString("{\"employeeID\":\"%1\",\"name\":\"%2\",\"address\":\"%3\",\"time\":\"%4\"}")
                            .arg(record.value("employeeID").toString())    // 员工ID
                            .arg(record.value("name").toString())          // 姓名
                            .arg(record.value("address").toString())       // 地址
                            .arg(QDateTime::currentDateTime().toString("yyyy-MM-dd hh:mm:ss")); // 当前时间

        // 发送数据
        m_socket->write(sdmsg.toUtf8());
    }

}

12 客户端JSON数据解码

#include <QJsonDocument>
#include <QJsonObject>
#include <QJsonParseError>

// 接收数据槽函数
void FaceAttendence::receive_data()
{
    //{employeeID:%1, name : %2,address:%3,time:%4}
    // 接受数据并展示
    QByteArray array = m_socket.readAll(); // 读取所有接收到的数据

    // JSON解析
    QJsonParseError err;
    QJsonDocument doc = QJsonDocument::fromJson(array, &err);

    // 检查JSON解析错误
    if (err.error != QJsonParseError::NoError)
    {
        qDebug() << "Json格式错误";
        return;
    }

    // 获取JSON对象
    QJsonObject obj = doc.object();

    // 获取员工ID、姓名、时间和地址信息
    QString employeeID = obj.value("employeeID").toString(); // 员工ID
    QString name = obj.value("name").toString();             // 姓名
    QString timestr = obj.value("time").toString();          // 时间字符串
    QString address = obj.value("address").toString();       // 地址

    // 更新界面显示
    ui->lb_employeeID->setText(employeeID);  // 员工ID标签
    ui->lb_nickname->setText(name);          // 姓名标签
    ui->lb_address->setText(address);        // 地址标签
    ui->lb_time->setText(timestr);           // 时间标签
}

file

显示认证成功和头像,认证成功的话很简单就是show跟hide。显示头像我们在检测到人脸时把人脸图像保存下来,后边添加到label上。

//检测到人脸发送数据时保存一下这个人脸的图片
//保存终端显示的人脸数据
            cv::imwrite("./cache.jpg",srcImage);

//receive_data接受数据显示信息时把头像也加上
 //-----------------显示头像------------------
    ui->lb_headpic->setStyleSheet("border-radius:70px; border-image: url(./cache.jpg);");    
//显示认证成功
    ui->wg_success->show();

file

13 考勤信息

13.1 界面搭建

按照下图搭建一个界面

file

新建一个指针,查询时判断按钮选中的哪个表,把表格里的内容进行展示

void selectwindow::on_btn_check_clicked()
{
    if(ui->btn_employee->isChecked())
    {
        model->setTable("employee");
    }
    else
    {
        model->setTable("attendance");
    }
    //查询数据
    model->select();
    ui->tv_message->setModel(model);
}

13.2 考勤数据录入

我们只需要在attendcewindow。Cpp里的recevice_faceID函数里在他要构建姓名,打开时间的JSON时,这些数据写入数据库。数据库模型model的默认表已经设置为员工表我们不修改他,直接用insert语句

//--------------把数据写入考勤表------------
        //时间是系统默认生成不需要加入
        QString insertstr = QString("INSERT INTO attendance(employeeID,name) VALUES (%1,'%2')")
                                .arg(record.value("employeeID").toString())
                                .arg(record.value("name").toString());

        qDebug()<<insertstr;
        QSqlQuery query;
        if(query.exec(insertstr))
        {
            // 数据插入数据库发送正确数据
            m_socket->write(sdmsg.toUtf8());
        }
        else
        {
            QString sdmsg = QString("{\"employeeID\":\"\",\"name\":\"\",\"address\":\"\",\"time\":\"\"}");
            // 发送数据
            m_socket->write(sdmsg.toUtf8());
        }

file

14 增加语言播报功能

qt6.4.3没有语言播报的功能,因此换成了5.15,重新将opencv和seeta编译,在使用时只需要引入头文件,定义即可

//增加语音播报
        QTextToSpeech * m_speech = new QTextToSpeech();
        QString saystr =QString("%1,打卡成功!").arg(ui->lb_nickname->text());
        m_speech->say(saystr);

至此本教程以完结

如果觉得本文对您有所帮助,可以支持下博主,—分也是缘。

评论

  1. 阿尼
    已编辑
    11 月前
    2024-3-17 20:03:49

    大佬的论坛老牛逼了

    • 博主
      阿尼
      11 月前
      2024-3-17 22:37:44

      都是抄的,我太菜了

  2. 1111
    11 月前
    2024-4-02 9:50:10

    太强了太强了

  3. 黒开
    10 月前
    2024-4-16 18:52:50

    大佬 你能帮我看看为什么我打不开摄像头吗

    • 博主
      黒开
      10 月前
      2024-4-21 17:20:19

      你发下报错啥的看看

  4. LaoSong
    10 月前
    2024-4-25 19:21:55

    连接摄像头那一步直接异常退出了,也不报错,怎么办啊

    • 博主
      LaoSong
      10 月前
      2024-4-26 10:50:17

      你一步步注释一下,看看是哪里出问题了

  5. tyndall
    10 月前
    2024-4-28 0:55:05

    博主太赞了 !刚好最近在学这个小项目 对我太有帮助了 配合你这个一起学事半功倍 逻辑也很清晰!!! 博主还有其他推荐的嵌入式linuxqt项目吗?

    • 博主
      tyndall
      10 月前
      2024-4-28 15:00:16

      都是b站找的,自己搭着玩

  6. 帕斯尚尔
    9 月前
    2024-5-19 11:33:29

    博主,新开始弄可以跳过编译,直接用文章开头的环境安装文件吗

    • 博主
      帕斯尚尔
      9 月前
      2024-6-07 13:44:23

      应该来说,你把环境变量加上就能用,但最好还是自己编译

  7. 123
    6 月前
    2024-8-19 14:18:57

    写的太好了

  8. 123
    6 月前
    2024-8-19 14:20:06

    写的太好了

  9. hello
    6 月前
    2024-8-19 18:50:59

    佬,从github下载的模型放入realse/model文件下,继续运行还是显示modle missing咋办

    • 博主
      hello
      6 月前
      2024-8-24 9:44:01

      你看看路径啥的对不对,把所有模型都放进去

  10. 加我13760473654
    6 月前
    2024-8-28 9:24:36

    怎么能联系上你啊 我有一些需求想联系你一下 opencv 显卡的问题

    • 博主
      加我13760473654
      6 月前
      2024-9-01 15:49:28

      我大概率解决不了,多百度,opencv和显卡的各种版本都要对应上,你可以csdn搜一下

  11. qingf
    5 月前
    2024-10-08 21:22:49

    大佬,服务端程序异常退出,不知道什么问题

    • 博主
      qingf
      5 月前
      2024-10-09 16:08:59

      一步步注释回到你没崩的代码,查找问题

  12. qingf
    5 月前
    2024-10-08 21:24:44

    大佬,服务端程序异常退出,不知道什么问题呀

  13. 周浩东
    3 月前
    2024-11-21 12:16:04

    大佬为什么我在github下载的SeetaFace2文件中找不到include这个文件呀

    • 博主
      周浩东
      3 月前
      2024-11-27 17:12:04

      你把代码编译完才会生成include吧,有点忘了

  14. 周浩东
    已编辑
    3 月前
    2024-11-21 12:55:11

    .

  15. zjy
    2 月前
    2024-12-12 15:22:40

    请问下,下载源码的三个文件好像是用不了了吗

    • 博主
      zjy
      2 周前
      2025-2-13 9:59:53

      能用啊

  16. zjy
    2 月前
    2024-12-12 15:25:49

    请问下,下载源码的三个文件好像是用不了了吗

  17. MK
    2 月前
    2025-1-05 14:15:54

    大佬为什么我运行之后会报错没有gobject-2.0-0.dll等乱七八糟的dll文件啊

    • 博主
      MK
      2 周前
      2025-2-13 10:02:54

      你百度看看是啥问题

  18. MK
    2 月前
    2025-1-05 14:17:32

    大佬为什么我运行之后会报错没有gobject-2.0-0.dll等乱七八糟的dll文件啊

  19. D
    2 月前
    2025-1-09 16:30:11

    大佬,为什么用QT打开CMakeList.txt的时候会没有合适的工具包啊

    • 博主
      D
      2 周前
      2025-2-13 10:07:00

      我没遇到过这个问题

  20. D
    2 月前
    2025-1-09 16:34:06

    大佬,为什么用QT打开CMakeList.txt的时候会没有合适的工具包啊

  21. D
    2 月前
    2025-1-09 16:38:47

    大佬,为什么用QT打开CMakeList.txt的时候会没有合适的工具包啊

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇

超多性价比流量卡,扫码查看

这将关闭于 20