基于人脸识别的考勤系统

开源项目学习过程及优化记录。 

https://gitee.com/capybara_ting/face-attendance.git 



1. windows下环境编译

1.1opencv源码编译(MinGW)

step1.Qt配置opencv源码的CMakeLIsts.txt

        使用Qt打开opencv-4.5.2目录下的CMakeLIsts.txt

(我在这里遇到的配置错误:Qt配置CMake出错-CSDN博客 )

step2. 点击项目,构建配置

         0'debug改为release版本,

         1’添加第三方库中人脸识别模块,

        2'取消MSMF两个模块的安装. 

        3'选择安装路径(设置opencv编译输出目录).

        4‘配置编译 

step3.编译成功后,指定的Install目录下得到生成文件 :

 可以将编译得到的install文件夹下的文件移动至理想目录。

step4. 测试opencv

1'添加opencv环境变量

我将编译得到的opencv环境文件移动至D盘Environment目录下,故添加的环境变量如下:

2'代码测试

--在pro文件中添加库文件和其include路径

--添加头文件,读取显示图片

1.2 SeedaFace2编译

step1.同样打开SeetaFace源码的CMakeLists.txt配置文件 

出现以下报错0:

step2.根据报错提示,在 CMakeLists.txt配置文件 中添加Opencv环境地址

step3.点击项目,构建配置(修改安装路径,opencv路径等等),执行cmake,执行 

 报错1:找不到模型

 step4.根据报错,下载训练好的模型,放入生成的release/bin/文件目录下

GitHub - seetafaceengine/SeetaFace2: SeetaFace 2: open source, full stack face recognization toolkit.

报错2: 找不到图片1.jpg

step5.根据报错,在 生成的release/bin/文件目录下存入1.jpg图片。

 运行,指定install目录下生成seetaface文件

step6. 测试SeetaFace 

1' 添加SeetaFace环境变量

 

2‘代码测试 

--在pro文件中添加库文件和其include路径

 这里我遇到了异常终止问题,于是我将opencv与seetaface的环境变量加入在该项目的系统环境变量配置中,后解决异常终止问题.(为什么呢?)

2. Ubuntu下环境编译

 创建ubantu虚拟机->导入opencv,seedaFace2源码文件(可以以共享文件的方式从本机导入)

->sudo apt install 安装gcc g++, cmake-gui工具,

经cp,unzip等命令后准备以下所需环境源码文件

2.1 opencv编译 

step1.在opencv-4.5.2文件夹下新建build目录,以存放稍后编译的文件.

 step2.cmake-gui图形化配置(..指明源码在bulid的上一层目录)

 cmake配置

 

 配置完成后,bulid目录下生成以下文件

step3.cmake编译 (-j4表示以4线程编译)

step4. sudo make install 安装

 编译完成后,设置的install目录/opt/opencv4-pc下生成了以下文件。

2.2 SeetaFace2编译

步骤和Opencv编译类似

step1. SeetaFace2文件下新建bulid目录

step2. cmake- gui图像化配置

step3.cmake编译

step4. sudo make install 安装

完成编译后,查看目录/opt/opencv4-pc下生成的库/lib/,头文件/include/以及生成的可执行文件/bin/。

 2.3 linux平台安装Qt

step1.Qt安装

官网下载 

https://download.qt.io/

 在共享目录中获取本机下载好的的执行文件,运行:

默认安装目录(也可根据需要修改) 

 安装组件选择

 step2.linux Qt环境下测试opencv,SeedaFace2

代码:

 报错:make编译过程找不到-libGL库

解决:
编译过程找不到库文件 -lxxx-CSDN博客icon-default.png?t=O83Ahttps://blog.csdn.net/qq_43855258/article/details/140444521?csdn_share_tail=%7B%22type%22%3A%22blog%22%2C%22rType%22%3A%22article%22%2C%22rId%22%3A%22140444521%22%2C%22source%22%3A%22qq_43855258%22%7D

 make 编译成功!

 3.系统客户端搭建

3.1 客户端UI设计

3.2 摄像头实时显示

         本模块通过startTime定时器,每间隔100ms调用一次timerEvent函数,通过重载timerEvent函数实现周期读取摄像头数据。读取的数据存放在Mat数据类型的变量中,故需先通道QImage实现类型转换,再由QPixmap显示在Qlabel中。

API:cv::VideoCapture  QTimerEvent

  • cv::VideoCapture 视频的读操作由VideoCapture类完成

VideoCapture类-CSDN博客icon-default.png?t=O83Ahttps://blog.csdn.net/qq_43855258/article/details/140452260?csdn_share_tail=%7B%22type%22%3A%22blog%22%2C%22rType%22%3A%22article%22%2C%22rId%22%3A%22140452260%22%2C%22source%22%3A%22qq_43855258%22%7D

  • QTimerEvent

启动计时器并返回一个计时器标识符,如果启动失败则返回零。
计时器事件将每毫秒发生一次,直到调用killTimer()。
如果标识符返回interval为0,则计时器事件在每次没有更多窗口系统事件需要处理时发生一次。
当计时器事件发生时,使用QTimerEvent事件参数类调用虚拟timerEvent()函数。
重载此函数以获取计时器事件。

如果有多个计时器在运行,QTimerEvent::timerId()可以用来找出哪个计时器被激活了。 

QT定时任务- timerEvent事件使用以及和QTimer 定时器的使用区别-CSDN博客icon-default.png?t=O83Ahttps://blog.csdn.net/corefunction/article/details/116586227

代码实现:

attendanceClient.h

#ifndef ATTENDANCECLIENT_H
#define ATTENDANCECLIENT_H

#include <QMainWindow>
#include <opencv.hpp>
using namespace cv;
using namespace std;

QT_BEGIN_NAMESPACE
namespace Ui { class AttendanceClient; }
QT_END_NAMESPACE

class AttendanceClient : public QMainWindow
{
    Q_OBJECT

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

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

private:
    Ui::AttendanceClient *ui;

    VideoCapture cameraCap; // 摄像头
};
#endif // ATTENDANCECLIENT_H

attendanceClient.cpp

#include "attendanceclient.h"
#include "ui_attendanceclient.h"

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

    //打开摄像头0(初始化)
    cameraCap.open(0); // linux下为:dev/videoshe'bei'hao
    //启动定时器事件100ms
    startTimer(100);
}

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

void AttendanceClient::timerEvent(QTimerEvent *event)
{
    // 采集数据
    Mat srcImage;
    if(cameraCap.grab())    // 获取摄像头
    {
        cameraCap.read(srcImage);   //读取一帧数据
    }
    if(srcImage.data == nullptr) return;

    // opencv Mat数据格式(BGR)-> QImage数据格式(RGB)
    cvtColor(srcImage, srcImage, COLOR_BGR2RGB);
    QImage image(srcImage.data, srcImage.cols, srcImage.rows, srcImage.step1(), QImage::Format_RGB888);
    QPixmap mmp = QPixmap::fromImage(image);
    ui->videoShowLb->setPixmap(mmp);

}

3.3 opencv实现人脸检测

        通过opencv库中的预训练好的级联分类器CascadeClassifier实现人脸检测,并根据检测返回的位置信息,移动检测框。

代码实现:

step1.级联分类器实例化对象

private:
    cv::CascadeClassifier faceDetect;

step2.导入级联分类器文件

faceDetect.load("E:/project/Environment/opencv452_MinGW64/etc/haarcascades/haarcascade_frontalface_alt2.xml");

step3.分类器实现目标人脸检测

    Mat grayImage;
    vector<Rect> faceRects;
    cvtColor(srcImage, grayImage, COLOR_BGR2GRAY);  // BGR->GRAY 加速检测
    faceDetect.detectMultiScale(grayImage, faceRects); // 级联分类器

step4.QLabel空间根据检测的人脸矩形框移动

 if(faceRects.size()>0){
        Rect rect = faceRects.at(0); //第一个人脸矩形框
        //rectangle(srcImage, rect, Scalar(0, 255, 0));
        ui->headPicLb->setGeometry(rect.x, rect.y, rect.width, rect.height);
    }
 else ui->headPicLb->move(90, 20);

API:cv::CascadeClassifier  

  • cv::CascadeClassifier

        级联分类器是由一系列弱分类器(如 Haar 特征, Local Binary Patterns (LBP) 特征、HOG特征)组成的强分类器。每个弱分类器逐步过滤掉非目标区域,减少误检率,同时尽可能保持高召回率。

CV_WRAP bool load( const String& filename );

 /** @brief Loads a classifier from a file.

    @param filename Name of the file from which the classifier is loaded. The file may contain an old HAAR classifier trained by the haartraining application or a new cascade classifier trained by the traincascade application.
     */

 CV_WRAP void detectMultiScale( InputArray image,
                          CV_OUT std::vector<Rect>& objects,
                          double scaleFactor = 1.1,
                          int minNeighbors = 3, int flags = 0,
                          Size minSize = Size(),
                          Size maxSize = Size() );

  /** @brief Detects objects of different sizes in the input image. The detected objects are returned as a list
    of rectangles.

    @param image Matrix of the type CV_8U containing an image where objects are detected.
    @param objects Vector of rectangles where each rectangle contains the detected object, the rectangles may be partially outside the original image.
    @param scaleFactor Parameter specifying how much the image size is reduced at each image scale.
    @param minNeighbors Parameter specifying how many neighbors each candidate rectangle should have to retain it.
    @param flags Parameter with the same meaning for an old cascade as in the function
    cvHaarDetectObjects. It is not used for a new cascade.
    @param minSize Minimum possible object size. Objects smaller than that are ignored.
    @param maxSize Maximum possible object size. Objects larger than that are ignored. If `maxSize == minSize` model is evaluated on single scale.

    The function is parallelized with the TBB library.

    @note
       -   (Python) A face detection example using cascade classifiers can be found at
            opencv_source_code/samples/python/facedetect.py
    */

opencv基础篇 ——(十八)级联分类器CascadeClassifier-CSDN博客icon-default.png?t=O83Ahttps://blog.csdn.net/fengqiao1999/article/details/139039422

3.4  TCP网络通信(一):与服务器端创建连接

(TCP3次握手中的前两次) 

代码实现: 

step0. .pro配置文件中加入network模块,包含头文件<QTcpSocket> <QTimer>,

// .pro配置文件
QT       += core gui network

// attendance.h文件
#include <QTcpSocket>
#include <QTimer>

step1. 创建网络套接字tcpSocket、定时器对象connectTimer;

private:
    QTcpSocket tcpSocket; // 创建网络套接字
    QTimer connectTimer;      // 创建定时器(定时网络连接

step2. 定时连接服务器:QTimer间隔5s发送timeout信号 --> 槽函数timerConnect调用connectHost()函数执行1次网络连接;

step3. 实现断线重连:QTcpSocket断开连接会发送disconnected信号 --> 槽函数startConnect打开定时器;连接成功会发送QTcpSocket::connected信号--> 槽函数stopConnect关闭定时器

private slots:
    void timerConnect();
    void stopConnect();
    void startConnect();
//定时连接服务器
connect(&connectTimer, &QTimer::timeout, this, &AttendanceClient::timerConnect);
    
//实现断线重连:QTcpSocket断开连接发送disconnected信号(打开connectTimer),连接成功会发送connected(关闭connectTimer)
connect(&tcpSocket, &QTcpSocket::disconnected, this, &AttendanceClient::startConnect);
connect(&tcpSocket, &QTcpSocket::connected, this, &AttendanceClient::stopConnect);
    
//每5s建立一次连接,直到连接成功
connectTimer.start(5000);
void AttendanceClient::timerConnect()
{
    tcpSocket.connectToHost("your server ip", your server port); //连接服务器
    qDebug()<<"正在连接服务器";
}

void AttendanceClient::stopConnect()
{
    connectTimer.stop();
    qDebug()<<"成功连接服务器";
}

void AttendanceClient::startConnect()
{
    connectTimer.start(5000);
    qDebug()<<"断开连接";
}

API:QTimer, QTcpSocket

  • QTimer:

        Qt中定时器的使用有两种方法,一种是使用QObject类提供的定时器(如摄像头数据实时显示中用到的定时器),还有一种就是使用QTimer类。QTime类在超时时会发出QTime::time_out信号,通过绑定自定义的槽函数可实现定时执行某任务的功能。

  • QTcpSocket:

        QTcpSocket 是 QAbstractSocket 的子类,用于建立 TCP 连接并传输数据流。

        对于客户端,创建好 QTcpSocket 对象后,调用 connectToHost() 连接到服务端;连接成功和连接断开会触发 connected() 和 disconnected() 信号;

        连接成功之后,可以调用 QIODevice 继承来的 read,write 等接口,当有新的数据到来,会触发 readyRead() 信号,此时在槽函数中进行读取即可,操作完之后,调用相关接口关闭 TCP 连接。

virtual void connectToHost(const QString &hostName, quint16 port, OpenMode mode = ReadWrite, NetworkLayerProtocol protocol = AnyIPProtocol);
virtual void connectToHost(const QHostAddress &address, quint16 port, OpenMode mode = ReadWrite);
void QAbstractSocket::connected()
void QAbstractSocket::disconnected()
qint64 QIODevice::read(char *data, qint64 maxSize)
QByteArray QIODevice::read(qint64 maxSize)
QByteArray QIODevice::readAll()
qint64 QIODevice::write(const char *data, qint64 maxSize)
qint64 QIODevice::write(const char *data)
qint64 QIODevice::write(const QByteArray &byteArray)
void QIODevice::readyRead()
void QAbstractSocket::disconnectFromHost()
void QAbstractSocket::close()
void QAbstractSocket::abort()

Qt网络编程(1):QTcpSocket和QTcpServer的基本使用_qt qtcpsocket-CSDN博客icon-default.png?t=O83Ahttps://blog.csdn.net/gongjianbo1992/article/details/107743780

3.5 TCP网络通信(二):人脸数据发送

        cv::imencode将图像压缩为Jpeg格式字节流,QByteArry作为缓冲区存储待发送的字节数据,与QDataStream对象绑定,对数据信息串行打包,最后由QTcpSocket套接字串行发送数据(发送顺序:缓冲数据长度,缓冲数据)。

代码实现:

        // JPEG压缩
        vector<uchar> buf;
        vector<int> params = {IMWRITE_JPEG_QUALITY, 90};
        imencode(".jpg", srcImage, buf, params);
        QByteArray byteBuf((char*)buf.data(), buf.size()); //Mat->jpeg格式的QbyteArray
        // QDataStream 对数据串行打包
        quint64 bufSize = byteBuf.size();          
        QByteArray sendData;    // QByteArray 作为缓冲区存储字节数据
        QDataStream sendStream(&sendData, QIODevice::WriteOnly); // 串行化流
        sendStream.setVersion(QDataStream::Qt_5_14);    // 设置Qt串行化版本
        sendStream<<bufSize<<byteBuf;
        // 发送
        tcpSocket.write(sendData);

API:cv::imencode, QByteArray, QDataStream, QTcpSocket

  • 图像编码:cv::imencode
CV_EXPORTS_W bool imencode( const String& ext, InputArray img,
                            CV_OUT std::vector<uchar>& buf,
                            const std::vector<int>& params = std::vector<int>());
  • QByteArray类 (作为缓冲区存储字节数据)

[QT_024]Qt学习之QByteArray详解-CSDN博客icon-default.png?t=O83Ahttps://blog.csdn.net/kongcheng253/article/details/128749134?ops_request_misc=%257B%2522request%255Fid%2522%253A%2522172173736816800184154852%2522%252C%2522scm%2522%253A%252220140713.130102334..%2522%257D&request_id=172173736816800184154852&biz_id=0&utm_medium=distribute.pc_search_result.none-task-blog-2~all~top_click~default-2-128749134-null-null.142%5Ev100%5Epc_search_result_base7&utm_term=QByteArray&spm=1018.2226.3001.4187

  •  QDataStream类 (对数据串行打包)

Qt扫盲-QDataStream 序列化和反序列化理论_qdatastream 结构体序列化-CSDN博客icon-default.png?t=O83Ahttps://blog.csdn.net/qq_43680827/article/details/133848077?ops_request_misc=&request_id=&biz_id=102&utm_term=QDataStream&utm_medium=distribute.pc_search_result.none-task-blog-2~all~sobaiduweb~default-3-133848077.142%5Ev100%5Epc_search_result_base7&spm=1018.2226.3001.4187

3.6 TCP网络通信(三): 接收服务器发送的人脸关联数据

        调用 QIODevice 继承来的write 等接口,当有新的数据到来,触发 readyRead() 信号,在槽函数中读取服务器发送的人脸关联信息。(服务器端是以json格式打包发送的)

    //当有数据到达,tcpSocket发送readyRead信号 -> 槽函数读取数据
    connect(&tcpSocket, &QTcpSocket::readyRead, this, &AttendanceClient::readSocketData);
void AttendanceClient::readSocketData()
{
    QString readData = tcpSocket.readAll();
    qDebug()<<readData;
}

3.7 json数据解析及显示

#include <QJsonDocument>
#include <QJsonParseError>
#include <QJsonObject>
void AttendanceClient::readSocketData()
{
    // {employeeID:%1,name:%2,address:软件,time:%3}
    QByteArray readData = tcpSocket.readAll();
    qDebug()<<readData;

    // json解析
    QJsonParseError jsonErr;
    QJsonDocument josnDoc = QJsonDocument::fromJson(readData, &jsonErr);
    // debugjson解析错误
    if(jsonErr.error != QJsonParseError::NoError){
        qDebug()<<"json数据错误";
        return;
    }
    // 获取json对象
    QJsonObject jsonObj = josnDoc.object();
    QString employeeID = jsonObj.value("employeeID").toString();
    QString name = jsonObj.value("name").toString();
    QString address = jsonObj.value("address").toString();
    QString timestr = jsonObj.value("time").toString();

    ui->IDLabel->setText(employeeID);
    ui->nameLabel->setText(name);
    ui->addreLabel->setText(address);
    ui->attenTimeLabel->setText(timestr);

    // 头像显示
    ui->headLabel->setStyleSheet("border-radius:75px;border-image: url(./tmpFace.jpg);");
    ui->attenSucceWidgt->show();
}

4.系统服务器端搭建

4.1 TCP网络通信(一):监听 响应 客户端连接

 代码实现:

step0. .pro配置文件中加入network模块,包含头文件<QTcpSocket> <QTcpServer>.

step1. 创建tcp服务器对象,tcp套接字对象.

QTcpServer tcpServer;   // TCP服务器对象
QTcpSocket * tcpSocket; // 创建套接字对象指针

step2.TcpServer对象调用 listen() 监听指定的ip地址和端口.

tcpServer.listen(QHostAddress::Any, 22); //服务器监听指定 ip port

step3.  QTcpServer对象受到新的TCP连接请求时,会触发newConnection信号-->槽函数调用nextPendingConnection()响应连接(即将挂起状态的连接 转换 为已连接状态)获取与客户端通信的QTcpSocket套接字对象。

connect(&tcpServer, &QTcpServer::newConnection, this, &AttendanceWindow::acceptClient);

/* 响应客户端连接*/
void AttendanceWindow::acceptClient()
{
    // 获取与客户端通信的套接字:一个指向(与客户端建立号连接的)TcpSocket对象的指针
    tcpSocket = tcpServer.nextPendingConnection();

    //当客户端有数据到达,tcpSocket发送readyRead信号 -> 槽函数读取数据
    connect(tcpSocket, &QTcpSocket::readyRead, this, &AttendanceWindow::readSocketData);
}

step4. 通过已连接的QTcpSocket对象实现客户端与服务端的通信: 当客户端有数据到达,QTcpSocket对象发送readyRead信号 --> 槽函数从网络套接字中读取数据。

/* 读取客户端发送的数据 */
void AttendanceWindow::readSocketData()
{
    QString msg = tcpSocket->readAll();
    qDebug()<<msg;
}

AttendanceWindow.h

#ifndef ATTENDANCEWINDOW_H
#define ATTENDANCEWINDOW_H

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

QT_BEGIN_NAMESPACE
namespace Ui { class AttendanceWindow; }
QT_END_NAMESPACE

class AttendanceWindow : public QMainWindow
{
    Q_OBJECT

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

private:
    Ui::AttendanceWindow *ui;
    QTcpServer tcpServer;   // TCP服务器对象
    QTcpSocket * tcpSocket; // 创建套接字对象指针

protected slots:
    void acceptClient();
    void readSocketData();
};
#endif // ATTENDANCEWINDOW_H

AttendanceWindow.cpp

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

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

    //QTcpServer对象受到新的TCP连接请求时,会触发newConnection信号(->槽函数调用nextPendingConnection()响应连接,并获取与客户端通信的套接字)
    connect(&tcpServer, &QTcpServer::newConnection, this, &AttendanceWindow::acceptClient);
    tcpServer.listen(QHostAddress::Any, 22); //服务器监听指定 ip port

}

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

/* 响应客户端连接*/
void AttendanceWindow::acceptClient()
{
    // 获取与客户端通信的套接字:一个指向(与客户端建立号连接的)TcpSocket对象的指针
    tcpSocket = tcpServer.nextPendingConnection();

    //当客户端有数据到达,tcpSocket发送readyRead信号 -> 槽函数读取数据
    connect(tcpSocket, &QTcpSocket::readyRead, this, &AttendanceWindow::readSocketData);
}

/* 读取客户端发送的数据 */
void AttendanceWindow::readSocketData()
{
    QString msg = tcpSocket->readAll();
    qDebug()<<msg;
}

使用网络调试助手测试,连接成功:

API: QTcpServer QTcpSocket

  • QTcpServer

       QTcpServer 类提供基于 TCP 的服务器。首先,调用 listen() 监听指定的地址和端口,当有新的 TCP 连接,会触发 newConnection() 信号,此时可以调用 nextPendingConnection() 以将挂起的连接接受为已连接的 QTcpSocket,通过该套接字对象实现与客户端通信。

bool QTcpServer::listen(const QHostAddress &address = QHostAddress::Any, quint16 port = 0)
void QTcpServer::disconnected()
QTcpSocket *QTcpServer::nextPendingConnection()
  •  QTcpSocket

        QTcpSocket 是 Qt 库中的一个核心组件,它是一个实现了 TCP 协议的面向对象网络套接字。作为 QAbstractSocket 类的派生类,QTcpSocket 提供了方便的方式来创建、管理TCP连接,并处理数据收发。

        对于TCP服务器,一旦有新的客户端连接请求并成功建立连接,你可以通过QTcpServer 类的 nextPendingConnection() 函数获取到下一个等待连接的客户端对应的QTcpSocket实例。这个函数会返回一个指向新连接的QTcpSocket对象,你可以通过这个对象来监听其状态变化,比如当 readyRead() 信号触发时,说明客户端有数据可以读取。

具体操作流程通常是这样的:

        1.创建一个 QTcpServer 实例,开始监听指定的端口,响应连接。

        2.当 readyRead()信号发出时,通过QTcpSocket对象的waitForReadyRead()函数等待数据准备好。

        3.使用 readAll()或其他相应方法从套接字读取客户端发送的数据。

        4.处理收到的数据,如解析、响应等。

        5.完成数据交互后,可以根据需要关闭连接或者继续接收其他客户端的连接。

4.2 TCP网络通信(二):人脸数据接收

       ( 服务器端的AttendanceQWindow主界面用于接收显示客户端发送的人脸数据)   

        将已建立连接 的QTcoSocket套接字对象与QDataStream流对象绑定,串行读取客户端发送的数据至QByteArray缓冲中(客户端发送的顺序:8字节数据长度->人脸数据),QPixmap将人脸数据显示至QLabel ui组件上。

代码实现:

// AttendanceWindow类内定义私有成员
private:
    quint64 readDataSize;       // socket接收数据的大小

// AttendanceWindow 初始化函数内初始化
readDataSize = 0;
void AttendanceWindow::readSocketData()
{
    QByteArray readData;
    QDataStream stream(tcpSocket);  // 把套接字绑定至数据流
    stream.setVersion(QDataStream::Qt_5_14);

    // read 客户端发送数据长度(小于8字节->等待)
    if(readDataSize == 0){
        if(tcpSocket->bytesAvailable() < (qint64)sizeof(readDataSize))
            return ;
        stream >> readDataSize;
    }

    // read 客户端发送数据 (小于readDataSize->等待)
    if(tcpSocket->bytesAvailable() < readDataSize){
        return ;
    }else{
        stream >> readData;
        readDataSize = 0;
    }

    // 没有读到数据
    if(readData.size() == 0){
        return ;
    }

    // 显示
    QPixmap pixmap;
    if( !pixmap.loadFromData(readData, "jpg")){
        return;
    }
    pixmap = pixmap.scaled(ui->picLabel->size());
    ui->picLabel->setPixmap(pixmap);
}

涉及API:QTcpSocket,  QByteArray, QDataStream, QPixmap

4.3 基于seetaFace2的人脸识别模块 FaceObject

4.3.1  初始化

        .在.pro配置文件中添加seetaFace头文件及库目录,封装一个C++类FaceObject,调用seetaFace::FaceEngine API 实现人脸数据存储、检测、识别.

#ifndef FACEOBJECT_H
#define FACEOBJECT_H

#include <QObject>
#include <seeta/FaceEngine.h>
#include <opencv.hpp>

// 人脸数据存储, 人脸检测,人脸识别
class FaceObject : public QObject
{
    Q_OBJECT
public:
    explicit FaceObject(QObject *parent = nullptr);
    ~FaceObject();

public slots:
    int64_t faceRegister(cv::Mat& faceImage);
    int64_t faceQuery(cv::Mat& faceImage);

private:
    seeta::FaceEngine * faceEnginePtr;

signals:

};

#endif // FACEOBJECT_H
#include "faceobject.h"
#include <QDebug>

FaceObject::FaceObject(QObject *parent) : QObject(parent)
{
    // 初始化 FaceEngine ( 人脸检测FD, 数据库存储PD, 人脸识别FR )
    seeta::ModelSetting FDmodel("E:/project/Environment/SeetaFace2_MinGW64/bin/model/fd_2_00.dat", seeta::ModelSetting::CPU, 0);
    seeta::ModelSetting PDmodel("E:/project/Environment/SeetaFace2_MinGW64/bin/model/pd_2_00_pts5.dat", seeta::ModelSetting::CPU, 0);
    seeta::ModelSetting FRmodel("E:/project/Environment/SeetaFace2_MinGW64/bin/model/fr_2_10.dat", seeta::ModelSetting::CPU, 0);
    this->faceEnginePtr = new seeta::FaceEngine(FDmodel, PDmodel, FRmodel);

    //导入已有的人脸数据库
    this->faceEnginePtr->Load("./face.db");
}

FaceObject::~FaceObject()
{
    delete faceEnginePtr;
}
API:  seetaFace::FaceEngine
namespace seeta
{
    class FaceEngine {
    public:
        FaceEngine( const SeetaModelSetting &FD_model, const SeetaModelSetting &PD_model, const SeetaModelSetting &FR_model )
            : FD( FD_model ), PD( PD_model ), FDB( FR_model ) {
        }

4.3.2 注册与查询

        调用seetaFcae库的RegisterQuery接口,封装qFaceOject的注册、查询槽函数。

public slots:
    int64_t faceRegister(cv::Mat& faceImage);
    int faceQuery(cv::Mat& faceImage);
/* 注册
 * 注册成功: 返回 faceID
 * 注册失败: 返回 -1
*/
int64_t FaceObject::faceRegister(cv::Mat &faceImage)
{
    // 将Mat格式数据->seetaIMageData格式数据
    SeetaImageData seetImage;
    seetImage.data = faceImage.data;
    seetImage.width = faceImage.cols;
    seetImage.height = faceImage.rows;
    seetImage.channels = faceImage.channels();
    // 调用seetaFace::faceEngine提供的Register接口
    int64_t faceID = this->faceEnginePtr->Register(seetImage);
    if(faceID >= 0){
        this->faceEnginePtr->Save("./face.db");
    }
    return faceID;
}

/* 查询
 * 查询成功: 返回 faceID
 * 查询失败: 返回 -1
*/
int64_t FaceObject::faceQuery(cv::Mat &faceImage)
{
    // Mat -> seetaIMageData
    SeetaImageData seetImage;
    seetImage.data = faceImage.data;
    seetImage.width = faceImage.cols;
    seetImage.height = faceImage.rows;
    seetImage.channels = faceImage.channels();
    // 调用seetaFace::faceEngine提供的Query接口
    float similarity = 0;
    int64_t faceID = this->faceEnginePtr->Query(seetImage, &similarity);
    qDebug()<<"查询"<<faceID<<similarity;
    return faceID;
}
API:SeetaImageData, Register, Save, Query
struct SeetaImageData
{
    int width;
    int height;
    int channels;
    unsigned char *data;
};
class FaceEngine {
public:
    int64_t Register( const SeetaImageData &image, const SeetaPointF *points ) {
        return FDB.Register( image, points );
    }
    
    bool Save( StreamWriter &writer ) const {
        return FDB.Save( writer );
    }

    int64_t Query( const SeetaImageData &image, const SeetaPointF *points, float *similarity = nullptr ) const {
        return FDB.Query( image, points, similarity );
    }

    ...
}

4.4 基于SQLite搭建服务器数据库SQL

         seetaFace::FaceEngine 已实现人脸数据与人脸ID的关联,还需实现人脸ID与个人信息的关联,以实现考勤信息记录,故我们需要调用Qt自带的数据库模块SQLite(轻量级sql,适用嵌入式设备),实现员工信息表考勤表的搭建。

表格设计: 

 数据库SQLite: 

API: QSqlDatabase, QSqlQuery

深入探索 Qt 中的 SQLite 支持:QSqlite 详解-CSDN博客

五分钟教会你在Qt中使用SQLite数据库,非常有用,建议收藏!_qt使用sqlite数据库-CSDN博客

 代码实现:

 step0. 在.pro配置文件中添加sql模块,添加sql相关头文件

QT       += core gui network sql
#include <QSqlDatabase>
#include <QSqlError>
#include <QSqlQuery>

step1. 新建一个C++类SQL,封装QSqlDatabase, QSqlQuery, QSqlError数据库接口,配置server数据库。

(具体的:

  • SQL初始化函数中调用QSqlDatabase接口,创建数据库;
  • 在SQL::sql_setting中调用QSqlQuery接口,实现attendanceServer数据库的设置--创建员工信息表,创建考勤表。)

PS:我最初在SQL类的头文件中定义QSqlQuery对象,以期灵活得在SQL成员函数中实现增删查改各功能,但是出现以下报错:

Database opened successfully!
QSqlQuery::exec: database not open
"Driver not loaded Driver not loaded"

解决:  QSqlDatabase数据库建立成功后,再定义QSqlQuery对象,执行数据库操作。

#ifndef SQL_H
#define SQL_H
#include <QSqlDatabase>
#include <QSqlError>
#include <QSqlQuery>
#include <QString>


class SQL
{
public:
    SQL();
    ~SQL();
    bool sql_create();

    QSqlDatabase database;  // 创建数据库对象
};

#endif // SQL_H
#include "sql.h"
#include <QDebug>

SQL::SQL()
{
    /* QSqlDatabase 配置数据库 */
    // 添加数据库驱动 SQLite
    database = QSqlDatabase::addDatabase("QSQLITE");
    // 设置数据库名称
    database.setDatabaseName("AttendenceSever.db");
    // 设置用户名密码
    database.setUserName("capybara");
    database.setPassword("123456");
    // 打开数据库
    if(!database.open()){
        qDebug()<<database.lastError().text();
    }
    else{
        qDebug()<<"Database opened successfully!";
    }
}

SQL::~SQL()
{
    database.close();
    qDebug()<<"Database closed.";
}

bool SQL::sql_create(){
    /* QSqlQuery 创建数据表 */
    QSqlQuery sql_query; // sql操作对象
    // 员工信息表 employ table
    QString infoTable = "create table if not exists employee(employeeID integer primary key autoincrement,name varchar(256), sex varchar(32),"
                        "birthday text, address text, phone text, faceID integer unique, headfile text)";
    if(!sql_query.exec((infoTable))){
        qDebug()<<sql_query.lastError().text();
        return false;
    }

    // 考勤表 attendance table
    QString attenTable = "create table if not exists attendance(attendanceID integer primary key autoincrement, employeeID integer,"
                         "attendanceTime TimeStamp NOT NULL DEFAULT(datetime('now','localtime')))";
    if(!sql_query.exec((attenTable))){
        qDebug()<<sql_query.lastError().text();
        return false;
    }

    return true;
}



main.cpp

#include "attendancewindow.h"
#include "sql.h"
#include <QApplication>
#include <QString>

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

    // 配置QSqlite数据库
    SQL serverSql;
    if( serverSql.sql_setting() < 0)
        return -1;

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

运行结果: 

        debug目录下生成数据库文件。 

 

 SQLiteStudio打开db文件查看数据表:

4.5  注册页面设计

ui设计:

        新建一个Qwidgt页面RegisterWin,用于实现信息注册(后续添加在AttendenceWindow主界面上)。 通过转到槽,实现各按键功能。

 4.5.1 重置按键——清除页面数据

void RegisterWin::on_resetPButton_clicked()
{
    // 清空数据
    ui->nameEdit->clear();
    ui->birthdayEdit->setDate(QDate::currentDate());
    ui->addressEdit->clear();
    ui->phoneEdit->clear();
    ui->picFileEdit->clear();
    ui->picShowLabel->clear();
}

4.5.2 添加头像按键——文本框打开图片显示

void RegisterWin::on_addPicPButton_clicked()
{
    // 通过文本对话框 选择图片路径
    QString filePath = QFileDialog::getOpenFileName(this);
    ui->picFileEdit->setText(filePath);

    // 显示图片
    QPixmap pixMap(filePath);
    pixMap = pixMap.scaledToWidth(ui->picShowLabel->width());
    ui->picShowLabel->setPixmap(pixMap);
}

4.5.3 打开摄像头按键&拍照按键——(API:  cv::VideoCapture, QTimerEvent)

  • 打开摄像头按键: cv::VideoCapture打开摄像头,每隔100ms触发QTimerEvent定时事件,读取摄像头数据;
  • 拍照按键:维护一个RegisterWin的成员变量camImage,存储当前图片数据,关闭定时器事件,释放摄像头资源。
class RegisterWin : public QWidget
{
    Q_OBJECT
...
private:    
    cv::VideoCapture cameraCap;
    int timerFd;            // QTimerEvent定时事件文件符
    cv::Mat camImage;       // 摄像头捕获数据
...
}
void RegisterWin::timerEvent(QTimerEvent *event)
{
     // 定时获取摄像头数据
    if(cameraCap.isOpened()){
        cameraCap >> camImage;
        if( camImage.data == nullptr) return ;
    }
    // Mat -> QImage
    cv::Mat rgbImage;
    cv::cvtColor(camImage, rgbImage, cv::COLOR_BGR2RGB);
    QImage showImage(rgbImage.data, rgbImage.cols, rgbImage.rows, rgbImage.step1(), QImage::Format_RGB888);
    QPixmap pixMap = QPixmap::fromImage(showImage);
    pixMap = pixMap.scaledToWidth(ui->picShowLabel->width());
    ui->picShowLabel->setPixmap(pixMap);
}

void RegisterWin::on_openCamPButton_clicked()
{
    if(ui->openCamPButton->text() == "打开摄像头"){
        // 打开摄像头
        if(cameraCap.open(0)){
            ui->openCamPButton->setText("关闭摄像头");
            timerFd = startTimer(100);  // 打开定时器
        }
    }
    else{
        killTimer(timerFd); // 关闭定时器事件
        ui->openCamPButton->setText("打开摄像头");
        cameraCap.release();    //关闭摄像头
    }
}

void RegisterWin::on_takePhotoPButton_clicked()
{
    //保存图像数据
    QString imageFilePath = QString("./camImage/tmp.jpg");
    ui->picFileEdit->setText(imageFilePath);
    cv::imwrite(imageFilePath.toUtf8().data(), camImage);
    // 关闭定时器事件
    killTimer(timerFd);
    ui->openCamPButton->setText("打开摄像头");
    // 关闭摄像头
    cameraCap.release();
}

 4.5.4 注册按键——(API: QSqlTableModel, QSqlRecord)

        调用前面封装好的人脸识别模块FaceObject,根据图片获取人脸ID;调用QSqlTableModelQSqlRecord操作数据库的API,将register界面上的员工信息以及对应的人脸ID插入employee数据库中,实现注册功能。
 【Qt】数据库实战之QSqlTableModel模型-CSDN博客

class RegisterWin : public QWidget
{
    Q_OBJECT
...
private:    
    ...
    QSqlTableModel employeeTable;   // employee数据表
    FaceObject *faceObject;
}

PS:记得在析构函数中释放faceObject申请的内存空间 

RegisterWin::RegisterWin(QWidget *parent) :
    QWidget(parent),
    ui(new Ui::RegisterWin),
    faceObject(new FaceObject)
{
    ui->setupUi(this);
    employeeTable.setTable("employee");
}

RegisterWin::~RegisterWin()
{
    delete ui;
    if(faceObject){
        delete faceObject;
        faceObject = nullptr;
    }
}
void RegisterWin::on_registerPButton_clicked()
{
    //1.通过照片,结合faceObject模块得到faceID
    cv::Mat image = cv::imread(ui->picFileEdit->text().toUtf8().data());
    if(image.data == nullptr){
        QMessageBox::information(this, "警告","请选择注册图片");
        return ;
    }
    int64_t faceID = faceObject.faceRegister(image);
    qDebug()<<faceID;
    //把头像保存到一个固定路径下
    QString imageFilePath = QString("./userImage/faceID_%1.jpg").arg(QString::number(faceID));
    cv::imwrite(imageFilePath.toUtf8().data(), image);

    //2.把个人信息存储到数据库employee
    QSqlRecord record = employeeTable.record();
    record.setValue("name",ui->nameEdit->text());
    record.setValue("sex",ui->maleRButton->isChecked()?"男":"女");
    record.setValue("birthday", ui->birthdayEdit->text());
    record.setValue("address",ui->addressEdit->text());
    record.setValue("phone",ui->phoneEdit->text());
    record.setValue("faceID", faceID);
    if( record.isEmpty()){
        QMessageBox::information(this, "警告", "请完善信息");
        return ;
    }

    int row = employeeTable.rowCount();
    if( employeeTable.insertRecord(row, record) ){
        employeeTable.submitAll();
        QMessageBox::information(this, "注册提示", "注册成功");
    }
    else QMessageBox::information(this, "注册提示", "注册失败");
}

注册结果: 

4.6 接收客户端数据-识别人脸ID

       继4.2节中接收客户端发送的人脸数据,  本节基于前面封装的人脸识别模块FaceObject,调用其faceQuery查询接口,从FaceEngine数据库中查询已注册的人脸ID:存在->返回faceID; 不存在->返回-1.

/* 读取客户端发送的数据 */
void AttendanceWindow::readSocketData()
{
    QByteArray readData;
    QDataStream stream(tcpSocket);  // 把套接字绑定至数据流
    stream.setVersion(QDataStream::Qt_5_14);

    // 读取 客户端发送数据长度(小于8字节->等待)
    ...
    // 显示
    ...
    // 识别人脸ID
    cv::Mat faceImage;
    std::vector<uchar> decodeData;
    decodeData.resize(readData.size());
    memcpy(decodeData.data(), readData.data(), readData.size());
    faceImage = cv::imdecode(decodeData, cv::IMREAD_COLOR);
    int64_t faceID = faceObject.faceQuery(faceImage);
    qDebug()<<"faceID: "<<faceID;

}

4.7  TCP网络通信(三):发送数据库检索信息

         根据faceObject识别的人脸ID,调用QSqlTableModel在employee数据库中检索关联信息(setFilter()),并打包为jason格式,通过tcpSocket套接字发送给客户端(write())。

/* 读取客户端发送的数据 */
void AttendanceWindow::readSocketData()
{
    // read 客户端发送数据长度(小于8字节->等待)
    ...

    // read 客户端发送数据 (小于readDataSize->等待)
    ...

    // 显示
    QPixmap pixmap;
    ...

    // 识别人脸ID
    ...

    // write 将人脸ID关联数据发送给客户端
    tableModel->setFilter(QString("faceID=%1").arg(faceID));
    tableModel->select();
    if(tableModel->rowCount() >= 1){
        record = tableModel->record(0); // 获取第一条记录
        // 将数据打包为json格式
        QString sendData = QString("{\"employeeID\":\"%1\",\"name\":\"%2\",\"address\":\"%3\",\"time\":\"%4\"}")
                .arg(record.value("employeeID").toString())
                .arg(record.value("name").toString())
                .arg(record.value("address").toString())
                .arg(QDateTime::currentDateTime().toString("yyyy-MM-dd hh:mm:ss"));
        // 发送数据
        tcpSocket->write(sendData.toUtf8());
    }

4.8 考勤信息录入

         调用QSqlTableModel, QSqlRecord接口实现实时考勤数据的录入。


    // 把数据写入考勤表
    tableModel->setTable("attendance");
    record = tableModel->record();
    record.setValue("employeeID", employeeID);
    record.setValue("attendanceTime", QDateTime::currentDateTime().toString("yyyy-MM-dd hh:mm:ss"));

    // 发送数据
    if (tableModel->insertRecord(-1, record)){
        tableModel->submitAll();
        //qDebug()<<sendData;
        tcpSocket->write(sendData.toUtf8());
    }
    else{
        qDebug() << tableModel->lastError().text();
        sendData = QString("{\"employeeID\":\" \",\"name\":\" \",\"address\":\" \",\"time\":\" \"}");
        tcpSocket->write(sendData.toUtf8());
    }
}

4.9 数据库查询界面设计

#ifndef TABLEQUERYWIN_H
#define TABLEQUERYWIN_H

#include <QWidget>
#include <QSqlTableModel>

namespace Ui {
class TableQueryWin;
}

class TableQueryWin : public QWidget
{
    Q_OBJECT

public:
    explicit TableQueryWin(QWidget *parent = nullptr);
    ~TableQueryWin();

private slots:
    void on_queryPButton_clicked();

private:
    Ui::TableQueryWin *ui;
    QSqlTableModel *tableModel;

};

#endif // TABLEQUERYWIN_H
#include "tablequerywin.h"
#include "ui_tablequerywin.h"

TableQueryWin::TableQueryWin(QWidget *parent) :
    QWidget(parent),
    ui(new Ui::TableQueryWin)
{
    ui->setupUi(this);
    tableModel = new QSqlTableModel();
}

TableQueryWin::~TableQueryWin()
{
    delete ui;
    if(tableModel){
        delete tableModel;
        tableModel = nullptr;
    }
    if(tableModel){
        delete tableModel;
        tableModel = nullptr;
    }
}

void TableQueryWin::on_queryPButton_clicked()
{
    if(ui->employRButton->isChecked()){
        tableModel->setTable("employee");
    }
    if(ui->attenRButton->isChecked()){
        tableModel->setTable("attendance");
    }

    // 查询
    tableModel->select();
    ui->tableView->setModel(tableModel);

}

4.10  整合服务器界面

        基于TabWidget将服务器端的ReigisterWin, TableQueryWin界面整合至AttendanceWin界面。

 5. 优化客户端与服务器端的信息传输

5.1 客户端 优化有效图像帧发送速率

        之前的设计中,客户端通过QTimerEvent事件每间隔100ms读取一次摄像头数据,检测是否有人脸存在,存在则向服务器端发送人脸数据。

       存在的问题:客户端向服务器以接近100ms的速率发送大量包含相同人脸数据的图片,以及闪烁误检的无关图片,极大地浪费通信资源。

       分析:我们希望客户端稳定地向服务器端发送包含有效人脸数据的有效图片帧,那么需要客户端发送前需要满足两个条件:1)首次检测到人脸数据 && 2)连续 n 帧均检测到人脸数据,那么第n帧图片则为稳定有效帧。

        改进:在AttendanceClient提供的接口中添加两个常变量:fps(控制摄像头显示的帧率);keepFrame(控制检测稳定性的参数,连续【keepFrame/fps】秒检测到人脸,该帧即为有效帧),供用户根据需要改变传输的灵敏度。此外,再通过设置一个detectFlag标志位,来判断当前图像帧是否满足上述两个条件。

class AttendanceClient : public QMainWindow
{
    Q_OBJECT

public:
    AttendanceClient(QWidget *parent = nullptr, const int fps = 10, const int keepFrame = 10);
    ~AttendanceClient();

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

private:
    Ui::AttendanceClient *ui;

    ...
    const int fps;             // 摄像头显示帧率
    const int keepFrame;       // 连续(keepFrame/fps)s检测到人脸
    int detectFlag;           //  标志是否满足发送条件(发送条件:首次检测到人脸 && 连续(keepFrame/fps)s检测到人脸)
    ...
};
AttendanceClient::AttendanceClient(QWidget *parent, const int fps, const int keepFrame)
    : QMainWindow(parent)
    , ui(new Ui::AttendanceClient)
    , fps(fps)
    , keepFrame(keepFrame)
{
    ui->setupUi(this);

    //启动定时器事件100ms
    startTimer(1/fps*1000);

    ...

    detectFlag = 0;
}
void AttendanceClient::timerEvent(QTimerEvent *event)
{
    // 采集摄像头数据
    ...

    // 人脸检测
     ...

    // 检测到人脸 ----- detectFlag验证发送条件:首次检测到人脸 && 连续(keepFrame/fps)s检测到人脸
    if(faceRects.size()>0){

        // 人脸框显示
        Rect rect = faceRects.at(0); //第一个人脸矩形框
        ui->headPicLb->setGeometry(rect.x, rect.y, rect.width, rect.height);

        /// 发送condition1. 连续(keepFrame/fps)s检测到人脸
        if(detectFlag > keepFrame){
            // JPEG压缩
            ...
            // QDataStream 对数据串行打包
            ...
            // write 发送
            tcpSocket.write(sendData);
            detectFlag = -1;
        }
        /// 发送condition0. 首次检测到人脸
        if(detectFlag >= 0) detectFlag++;
    }
    else{
        detectFlag = 0;
        ui->headPicLb->move(90, 20);
    }
    // 摄像头数据显示: opencv Mat数据格式(BGR)-> QImage数据格式(RGB)
    ...
}

5.2 服务器端 多线程实现人脸识别

问题: FaceObject实现人脸识别时资源消耗较大,实时处理客户端传输的图片数据时,易发生卡顿现象。

int64_t faceID = faceObject->faceQuery(faceImage);  // 耗时

解决:故我们创建一个新线程,把FaceObject模块移至线程中处理。于是在AttendenceWindow类中调用FaceObject对象的faceQuery识别接口时,存在跨线程的对象间通信问题。Qt中通过信号槽的机制实现:

 在AttendenceWindow初始函数中创建开启子线程thread,并完成信号槽的连接。 

  • 主线程AttendenceWindow对象发送query信号--->触发子线程中FaceObject对象的faceQuery槽函数进行人脸识别;
  • 人脸识别完成后,子线程FaceObject对象发送sendFaceID信号--->触发主线程Attendence _Window对象的recvFaceID槽函数,在数据库中过滤faceID关联数据,录入考勤信息,并发送给客户端。
class AttendanceWindow : public QMainWindow
{
...
signals:
    void query(cv::Mat& image);
protected slots:
    void recvFaceID(int64_t faceID);
...
};
class FaceObject : public QObject
{
...
public slots:
    int64_t faceQuery(cv::Mat& faceImage);
signals:
    void sendFaceID(int64_t faceID);
};
// 创建一个线程
QThread *thread = new QThread();
faceObject->moveToThread(thread);   // 把FaceObject对象移动至线程运行
thread->start();
connect(this, &AttendanceWindow::query, faceObject, &FaceObject::faceQuery);
connect(faceObject, &FaceObject::sendFaceID, this, &AttendanceWindow::recvFaceID);
/* 读取客户端发送的数据 */
void AttendanceWindow::readSocketData()
{
    // read 客户端发送数据长度(小于8字节->等待)
    ...
    // read 客户端发送数据 (小于readDataSize->等待)
    ...
    // 显示
    ...

    // 识别人脸ID
    ...
    //int64_t faceID = faceObject->faceQuery(faceImage);  // 耗时
    emit query(faceImage);  // 通过信号触发线程中的faceQuery
}

void AttendanceWindow::recvFaceID(int64_t faceID)
{
    QString sendData = QString("{\"employeeID\":\" \",\"name\":\" \",\"address\":\" \",\"time\":\" \"}");;
    QString employeeID = " ";

     qDebug()<<"faceID: "<<faceID;
     if(faceID < 0){
        tcpSocket->write(sendData.toUtf8());
        return ;
     }

     // write 将人脸ID关联数据发送给客户端
    tableModel->setTable("employee");
    tableModel->setFilter(QString("faceID=%1").arg(faceID));
    tableModel->select();
    qDebug()<<tableModel->rowCount();
    if(tableModel->rowCount() >= 1){
        record = tableModel->record(0);
        // 将数据打包为json格式
        sendData = QString("{\"employeeID\":\"%1\",\"name\":\"%2\",\"address\":\"%3\",\"time\":\"%4\"}")
                .arg(record.value("employeeID").toString())
                .arg(record.value("name").toString())
                .arg(record.value("address").toString())
                .arg(QDateTime::currentDateTime().toString("yyyy-MM-dd hh:mm:ss"));
        employeeID = record.value("employeeID").toString();
    }

    // 把数据写入考勤表
    tableModel->setTable("attendance");
    record = tableModel->record();
    record.setValue("employeeID", employeeID);
    record.setValue("attendanceTime", QDateTime::currentDateTime().toString("yyyy-MM-dd hh:mm:ss"));

    // 发送数据
    if (tableModel->insertRecord(-1, record)){
        tableModel->submitAll();
        //qDebug()<<sendData;
        tcpSocket->write(sendData.toUtf8());
    }
    else{
        qDebug() << tableModel->lastError().text();
        sendData = QString("{\"employeeID\":\" \",\"name\":\" \",\"address\":\" \",\"time\":\" \"}");
        tcpSocket->write(sendData.toUtf8());
    }
}
int64_t FaceObject::faceQuery(cv::Mat &faceImage)
{
    // Mat -> seetaIMageData
    ...
    // 调用seetaFace::faceEngine提供的Query接口
    float similarity = 0;
    int64_t faceID = this->faceEnginePtr->Query(seetImage, &similarity);
    if(similarity > 0.65){
        emit sendFaceID(faceID);
    }
    else{
        emit sendFaceID(-1);
    }
    return faceID;
}

PS:如果要在Qt信号槽中使用自定义类型,需要注意使用qRegisterMetaType对自定义类型进行注册。不跨线程时使用自定义类型signal/slot来传递,可能不会出现什么问题;一旦涉及跨线程就很容易出错。

mainn.c 

    // 在Qt信号槽中使用自定义类型,需要注意使用qRegisterMetaType对自定义类型进行注册(尤其在跨线程通信中)
    qRegisterMetaType<cv::Mat>("cv::Mat&");
    qRegisterMetaType<cv::Mat>("cv::Mat");
    qRegisterMetaType<int64_t>("int64_t");

6. 客户端与服务端测试结果

 注册:

考勤:
 

7.我的改进

7.1 客户端多线程实现人脸检测

         源码中调用opencv多级分类器实现人脸检测时,为了降低检测时延,将RGB图像转换为单通道的灰度图像再送入模型中检测,一定程度降低检测精度。

改进:我希望在客户端中通过多线程实现人脸检测任务,即将人脸检测任务移入子线程中执行。通过信号槽触发机制,一定程度上来实现跨线程通信,控制不同任务间同步关系。具体如下:

-主线程:通过StartTimer的TimerEvent事件,定时读取摄像头数据并显示,然后发送camImage图像信号和detecflag标志位(表示当前检测图像的人脸状态)至子线程任务槽函数,执行人脸检测任务,等待子线程发送信号,执行人脸框显示任务、将含人脸图像detectImage发送至server。(发送成功后,置位detecflag=-1)

-子线程:调用opencv分类模型对接收图像进行人脸检测,并根据检测结果执行不同操作:

检测成功:

  • detecFlag<0, 表示当前图像已发送至server,等待新人员出现,并发送检测到的人脸框faceRects至主线程显示;
  • detectFlag>=0, detectFlag++;
  • detectFlag>5, 连续5帧图像检测到同一人员 ,发送人脸框faceRects和图像deteImage至主线程socketSend槽;

检测失败:detecFlag=0

PS:这里多线程涉及两个问题

1)子线程任务函数与主线程socket数据发送函数中均涉及对主线程栈区变量detectFlag的访问,如何实现线程同步

        通过QMutex互斥锁实现,一个共享资源对应一个互斥锁,在主线程类中定义互斥锁mutex,子线程类引用该互斥锁。分别在子线程主线程代码上下文处上锁lock(),解锁unlock()。当前线程访问共享资源时,另一线程阻塞。

2)还存在一个由于线程异步导致的检测传输不对齐问题:子线程执行当前图像的人脸检测时,主线程可能已经触发下一次TimerEvent事件,主线程的camImage更新;若检测完成,主线程socket传输读取camImage,则会导致检测结果与传输图像不对齐问题(传输图像中可能不含人脸)。

        这里是通过信号槽通信的值传递, 将子线程检测图像拷贝传至主线程socket发送函数,以实现检测与传输对齐。

(是否可以通过消息队列push、pop实现?)

 主线程构造函数中新建子线程、任务类对象:

    /* 子线程检测人脸 */
    //faceDetect = new FaceDetect(mutex, this);   //PS:指定了父对象的线程,无法实现任务的移入
    faceDetect = new FaceDetect(mutex);
    thread = new QThread(this);
    faceDetect->moveToThread(thread);
    thread->start();
    connect(this, &AttendanceClient::sendCamImage, faceDetect, &FaceDetect::detect);
    connect(faceDetect, &FaceDetect::sendFaceRects, this, &AttendanceClient::showFaceRect);
    connect(faceDetect, &FaceDetect::sendDetectImage, this, &AttendanceClient::sendSocketData);

主线程timerEvent事件:

void AttendanceClient::timerEvent(QTimerEvent *event)
{
    // 采集摄像头数据
    //Mat srcImage;
    if(cameraCap.grab())    // 获取摄像头
    {
        cameraCap.read(camImage);   //读取一帧数据
    }
    if(camImage.data == nullptr) return;
    flip(camImage, camImage, 1);

    // 摄像头数据显示: opencv Mat数据格式(BGR)-> QImage数据格式(RGB)
    Mat showImage;
    cvtColor(camImage, showImage, COLOR_BGR2RGB);
    QImage image(showImage.data, showImage.cols, showImage.rows, showImage.step1(), QImage::Format_RGB888);
    QPixmap mmp = QPixmap::fromImage(image);
    ui->videoShowLb->setPixmap(mmp);

    // 发送camImage -> 槽函数detectFace(子线程)
    emit sendCamImage(camImage, &detectFlag);
}

子线程FaceDetect任务类 (实现人脸检测,即检测状态判断)

通过指针传递主线程成员变量detectFlag,更新检测状态。

FaceDetect::FaceDetect(QMutex &mutex, QObject *parent)
    : mutex(mutex), QObject(parent)
{
    //导入opencv级联分类器文件
    detectModel.load("E:/project/Environment/opencv452_MinGW64/etc/haarcascades/haarcascade_frontalface_alt2.xml");
}

void FaceDetect::detect(cv::Mat image, int* detectFlag)
{
    qDebug()<<"detect_thread";
    vector<cv::Rect> faceRects;
    detectModel.detectMultiScale(image, faceRects);

    mutex.lock();
    // 检测成功
    if(faceRects.size()>0){
        qDebug()<<"detectFlag: "<<*detectFlag;
        if(*detectFlag < 0 ) emit sendFaceRects(faceRects);          // 考勤成功客户端完成图像发送 detectFlag=-1 -> 等待下一次检测失败(新人员出现)
        if(*detectFlag >= 0) (*detectFlag)++;                           // 第一次检测失败 detectFlag=0 -> 新人员考勤 detectFlag++
        if(*detectFlag > 5) emit sendDetectImage(faceRects, image);  // 连续5帧图像检测出人脸 sendImage to server -> detectFlag=-1
    }
    // 检测失败
    else{
        qDebug()<<"no people in image";
        *detectFlag = 0;
    }
    mutex.unlock();

}

主线程sendSocketData槽函数(发送人脸检测成功图像至server)

void AttendanceClient::sendSocketData(vector<Rect> faceRects, cv::Mat detecImage)
{
    qDebug()<<"sendSocketData";
    Rect rect = faceRects.at(0); //第一个人脸矩形框
    ui->headPicLb->setGeometry(rect.x, rect.y, rect.width, rect.height);

    // socket send image
    // JPEG压缩
    vector<uchar> buf;
    vector<int> params = {IMWRITE_JPEG_QUALITY, 90};
    imencode(".jpg", detecImage, buf, params);
    QByteArray byteBuf((const char*)buf.data(), buf.size()); //Mat->jpeg格式的QbyteArray

    // QDataStream 对数据串行打包
    quint64 bufSize = byteBuf.size();
    QByteArray sendData;    // QByteArray 作为缓冲区存储字节数据
    QDataStream sendStream(&sendData, QIODevice::WriteOnly); // 串行化流
    sendStream.setVersion(QDataStream::Qt_5_14);    // 设置Qt串行化版本
    sendStream<<bufSize<<byteBuf;   // 串行发送:数据长度->人脸数据

    // write 发送
    mutex.lock();
    tcpSocket.write(sendData);
    detectFlag = -1;
    mutex.unlock();

    // 保存头像用于后续显示
    imwrite("./tmpFace.jpg", detecImage(rect));

    if(detectFlag == 0){
        ui->attenSucceWidgt->hide();
        ui->headPicLb->hide();
    }
}

void AttendanceClient::showFaceRect(vector<Rect> faceRects)
{
    qDebug()<<"showFaceRect";

    Rect rect = faceRects.at(0); //第一个人脸矩形框
    ui->headPicLb->setGeometry(rect.x, rect.y, rect.width, rect.height);

}

7.2 客户端多线程实现ui界面显示(主线程)、人脸检测(检测线程)、socket网络通信(通信线程)三个任务。 

主线程:

  1. TimerEvent实时读取摄像头数据显示,并发送图像数据至detect_thread进行人脸检测
  2. QTimer定时发送信号至socket_thread建立TCP连接,连接成功则关闭定时器,断开连接则打开定时器(信号槽实现);
  3. 显示detect_thread检测的人脸框,显示socket_thread接收的来着server端返回的考勤信息。

检测线程:

  1. 人脸检测;
  2. 检测成功,送人脸框至主线程显示;(if detectFlag>0, detectFlag++) 若连续检测到5帧人脸(detectFlag>5),触发socket_thread向server端发送人脸图像
  3. 检测失败,(detectFlag=0)

网络通信线程:

  1. 基于QTcpSocket与server端创建TCP连接,得到socket通信对象;
  2. write发送人脸图像至sever端(detectFlag=-1);
  3. read接收server返回的考勤信息 。

记:子线程实现QTcpSocket读写的问题-CSDN博客icon-default.png?t=O83Ahttps://blog.csdn.net/qq_43855258/article/details/141873386

8. Arm移植

Qt文件更改:

1. opencv级联检测模型位置(复制至当前工程目录下)
2. 摄像头(设备号 /dev/videox)
3.Qt version(QDataStream)
       Qt_5_14 -> Qt-5.7.1
4.配置文件中库的位置(client 不需要seet2face)

准备工具:
1.交叉编译工具(平台提供)
    需要配置交叉编译工具的环境变量(or找不到的)
2.opencv-arm库
3.Qt-arm库(平台提供)
 

Qt程序移植至Arm开发板_qt移植到arm开发板-CSDN博客icon-default.png?t=O83Ahttps://blog.csdn.net/qq_43855258/article/details/140620918

9.总结

-基于人脸识别考勤系统的系统框图

-客户端软件框图

-服务器端(注册)

-服务器端(考勤)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值