相关源码
相关的代码更新请参考提交记录,我做了详细的备注,方便观察代码变化
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
使用MinGW64构建项目,稍等一会,等qt吧资源都加载完
点击项目,搜索ms取消这两个安装项目,将第三方库的人脸识别模块加入,==注意debug为release版本,所有操作都是在release版本上执行的,图片还没更改==
修改安装路径
修改下边构建步骤里边 为install,取消勾选all,最后执行Cmake
1.3 执行Cmake一直卡着data: Download: face_landmark_model.dat
发现一直卡着,这个face_landmark_model.dat下载不下来
采取手动下载的方式
放到opencv的路径下,并且把没下载完的文件的前缀也加上,替换,再次执行Cmake成功
完毕后回到编辑点击启动,更改构建模式为release,运行
2 编译SeetaFace2代码
2.1 遇到报错By not providing "FindOpenCV.cmake" in CMAKE_MODULE_PATH this project has
下载代码:https://github.com/seetafaceengine/SeetaFace2
依旧是打开CmakeList文件,release模式,遇到报错
打开CmakeList文件添加上这一句,加上你刚刚安装的opcv的路径,注意斜杠的位置
set(OpenCV_DIR E:/Environment/opencv452)
修改安装路径,opencv路径等等,更上边类似,执行cmake,执行
2.2遇到报错Model missing
执行后遇到报错模型丢失
下载模型,把四个模型都下载了,放入生成release的文件夹
https://github.com/seetafaceengine/SeetaFace2?tab=readme-ov-file
再次报错,没有图片,找张人脸图片放入
3 测试两个环境能否使用
3.1 配置环境变量
找到系统环境变量PATH,添加两个库的bin文件夹
新建一个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*
#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 考勤界面设计
按照以下界面搭建窗口,后边是样式表代码,实现功能就行,美化后边再说
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,覆盖住左边那一块的区域,右击放到后面,不会挡住认证成功的显示
在.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);
移动检测的圆形代码也很简单,调用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,方便等会展示图片
导入头文件,定义两个槽函数,一个连接成功,一个读取客户端发送的数据
#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;
}
最后打开测试工具,客户端模式连接,发送数据,服务器端已经接收到了
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 也无法存储足够大的数据大小,导致无法正确读取图像数据。
7 人脸识别模块封装–注册和登陆
新建一个c++class,继承自QObject,因为封装的功能不需要界面引入头文件FaceEngine.h,报错显示没有。从seeta2的源代码里搜索,复制放到我们的编译目录。
定义一个人脸注册槽函数,一个人脸识别槽函数,注册和识别成功都返回人脸ID。一开始初始化人脸引擎对象,需要传入三个模型。首先将传入的opencv mat图像转换为SeetaImageData。然后就是直接调用引擎的register和query,注册后把人脸数据保存到数据库。
SeetaImageData是一个结构体,直接一个个赋值就行
#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和人的相关信息进行绑定
我们需要搭建这样的表格
8.2 代码编写
在服务器的.pro文件中添加上sql模块,使用代码来创建表格,也可以先用navicat生成表格来使用
代码原理。首先连接上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来查看,两个表里都有标题了,表格就创建完毕
9 注册页面设计
新建一个qt界面设计师类,widget(方面我们嵌入到主界面),按照下边简易搭建一个界面
开始右击按钮转到槽,编写逻辑
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;
识别效果,坤坤识别出来了,丁真也识别出来了
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);
但是此时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); // 时间标签
}
显示认证成功和头像,认证成功的话很简单就是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();
13 考勤信息
13.1 界面搭建
按照下图搭建一个界面
新建一个指针,查询时判断按钮选中的哪个表,把表格里的内容进行展示
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());
}
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);
大佬的论坛老牛逼了
都是抄的,我太菜了
太强了太强了
大佬 你能帮我看看为什么我打不开摄像头吗
你发下报错啥的看看
连接摄像头那一步直接异常退出了,也不报错,怎么办啊
你一步步注释一下,看看是哪里出问题了
博主太赞了 !刚好最近在学这个小项目 对我太有帮助了 配合你这个一起学事半功倍 逻辑也很清晰!!! 博主还有其他推荐的嵌入式linuxqt项目吗?
都是b站找的,自己搭着玩
博主,新开始弄可以跳过编译,直接用文章开头的环境安装文件吗
应该来说,你把环境变量加上就能用,但最好还是自己编译
写的太好了
写的太好了
佬,从github下载的模型放入realse/model文件下,继续运行还是显示modle missing咋办
你看看路径啥的对不对,把所有模型都放进去
怎么能联系上你啊 我有一些需求想联系你一下 opencv 显卡的问题
我大概率解决不了,多百度,opencv和显卡的各种版本都要对应上,你可以csdn搜一下
大佬,服务端程序异常退出,不知道什么问题
一步步注释回到你没崩的代码,查找问题
大佬,服务端程序异常退出,不知道什么问题呀
大佬为什么我在github下载的SeetaFace2文件中找不到include这个文件呀
你把代码编译完才会生成include吧,有点忘了
.
请问下,下载源码的三个文件好像是用不了了吗
能用啊
请问下,下载源码的三个文件好像是用不了了吗
大佬为什么我运行之后会报错没有gobject-2.0-0.dll等乱七八糟的dll文件啊
你百度看看是啥问题
大佬为什么我运行之后会报错没有gobject-2.0-0.dll等乱七八糟的dll文件啊
大佬,为什么用QT打开CMakeList.txt的时候会没有合适的工具包啊
我没遇到过这个问题
大佬,为什么用QT打开CMakeList.txt的时候会没有合适的工具包啊
大佬,为什么用QT打开CMakeList.txt的时候会没有合适的工具包啊