之前我们已经完成了人脸录入,人脸识别,活体检测的demo,接下来对项目进行整合,并且设计美观的UI界面。
一.客户端功能完善
1. 客户端UI界面
(1)其中活体检测提示以及识别成功提示,网络连接状态提示均使用的是widget(live_widget,ok_widget,isconnect_widget,connected_widget)装载,widget设计如下:
background-color: rgba(49, 50, 54,63);
border-radius:10px;
background-color: rgba(49, 50, 54,0);
border-radius:10px;
(2)人员信息使用的是Label+LineEdit(readOnly)组合,上方头像使用的也是Label
(3)另外,由于我们服务器端需要条件查询迟到早退的人员,因此在打卡时,需要设置上班还是下班打卡的type(使用QComBox实现checktype_cbox)旁边是用于显示上下班图标的check_lb,两者也是用widget装载(checktype_widget)。
连接QComboBox的currentIndexChanged信号到一个槽函数,在这个槽函数中根据当前选中的项来更改QLabel(check_lb)的样式表,设置对应的border-image属性。QComboBox的currentIndexChanged信号只在选中的项发生变化时触发一次。这个信号会传递当前选中项的索引或者是当前选中项的文本
//连接combox信号(根据index来设置图片)
connect(this->ui->checktype_cbox, QOverload<int>::of(&QComboBox::currentIndexChanged), this, &FcaeRecognition::updateLabelBorderImage);
//combox选择不同的内容更新对应的图标
void FcaeRecognition::updateLabelBorderImage(int index)
{
switch (index) {
case 0: // 第一个选项
this->ui->checktype_lb->setStyleSheet("border-image: url(:/icon/start_check.png);");
break;
case 1: // 第二个选项
this->ui->checktype_lb->setStyleSheet("border-image: url(:/icon/end_check.png);");
break;
}
}
2.人脸识别模块
(1)GUI画面更新:在程序启动的时候,同时启动了人脸识别线程和主GUI线程。程序利用一个定时器(frameTimer)来周期性地更新视频帧。更新的流程大致如下:
①帧处理:首先,从视频源捕获当前的帧,接着对此帧进行克隆,以保留原始图像。然后,将这个克隆的帧转换为灰度图,并对其进行直方图均衡化处理。这一系列预处理操作是为了提升后续的人脸检测和识别的准确率。
②人脸检测与活体检测:处理后的帧被用于人脸检测。在确认存在人脸情况下,若用户尚未完成活体检测,则live_widget会显示出来。一旦用户完成活体检测,live_widget会被隐藏,并且活体检测标志live_detection被设置为true,表明活体检测已经完成。
③人脸截取与识别:完成活体检测后,程序会根据检测到的人脸特征点截取人脸部分并调整大小,以实现人脸对齐,更新face_chip变量的值。这个变量保存了经过处理和对齐的人脸截图,供后续的人脸识别函数使用。人脸识别过程通过异步线程进行,以避免影响主GUI的流畅性。识别成功后,会返回识别到的工号recognizedId。
④数据库查询与UI更新:在成功识别出人脸信息后,程序通过识别到的工号recognizedId在数据库中查询相应的用户信息。一旦查询到匹配的用户信息,就会将这些信息(包括用户头像等)显示在UI界面上,让用户得以看到识别结果与个人信息的反馈。
通过上述流程,程序实现了一个从捕获视频帧、处理图像、检测和识别人脸,到查询数据库并更新UI的完整人脸识别过程。这个过程不仅包含了图像处理和人脸识别技术,还涉及到异步编程和数据库操作,以确保程序运行效率和用户体验。
更新画面的函数如下:
//更新画面
void FcaeRecognition::updateFrame()
{
// 读取帧
cap >> frame;
flip(frame, frame, 1);
cv::Mat processedFrame = frame.clone(); //克隆一个图像用于人脸识别,保留原本的彩色图像
cvtColor(processedFrame,processedFrame,CV_BGR2GRAY);//将彩色图转换从灰度图
equalizeHist(processedFrame,processedFrame);//进行均衡化处理
// 将OpenCV灰度图转换为dlib图像
dlib::cv_image<unsigned char> dlibImage(processedFrame);
// 使用人脸检测器检测人脸
faces = detector(dlibImage);
// 对检测到的第一个人脸进行处理,有人脸才处理
if (!faces.empty()) {
// 获取面部关键点
shape = sp(dlibImage, faces[0]);
// 在图像上绘制人脸框(在frame上绘制,防止存入人脸库的照片上有标记)
cv::rectangle(frame, cv::Point(faces[0].left(), faces[0].top()),
cv::Point(faces[0].right(), faces[0].bottom()), cv::Scalar(0, 255, 0), 2);
if(!live_detection)
{
this->ui->live_widget->show();//显示要求进行活体识别的提示
//活体检测
LiveDetection(shape);
}
else
{ //通过了活体检测
this->ui->live_widget->hide();//隐藏要求进行活体识别的提示
// 根据68位关键点截取人脸部分并调整大小,人脸对齐
extract_image_chip(dlibImage, get_face_chip_details(shape, 150, 0.25), face_chip);
// 输出识别结果
if (!recognizedId.isEmpty()) {
this->ui->ok_widget->show();//显示识别成功的提示
//显示头像
QString imagePath = QString(FACES_PATH) + recognizedId + ".jpg";
QPixmap pixmap(imagePath);
if (!pixmap.isNull()) {
// 根据需要调整大小,保证图片符合label的尺寸
pixmap = pixmap.scaled(this->ui->head_lb->size(), Qt::KeepAspectRatio, Qt::SmoothTransformation);
// 显示图片到label
this->ui->head_lb->setPixmap(pixmap);
} else {
qDebug() << "无法加载图片: " << imagePath;
}
//通过id查找name和section
query.prepare("SELECT name, section FROM staff WHERE id = :id");
query.bindValue(":id", recognizedId);
if(query.exec()) {
// 查询成功
if(query.next()) {
// 有结果
recognizedName = query.value(0).toString(); // 获取name
recognizedSection = query.value(1).toString(); // 获取section
// 处理查询结果
this->ui->name_edit->setText(recognizedName);
this->ui->section_edit->setText(recognizedSection);
this->ui->id_edit->setText(recognizedId);
}
}
else {
qDebug() << "Error in query: " << query.lastError().text();
}
// 在人脸矩形框上显示识别结果
PutTextChinese(frame, recognizedName, cv::Point(faces[0].left(), faces[0].top() - 5),
20, cv::Scalar(255, 0, 0));
}
else {
// 未识别到人脸
this->ui->ok_widget->hide();//隐藏识别成功的提示
// 在人脸矩形框上显示"unknown"
cv::putText(frame, "unknown", cv::Point(faces[0].left(), faces[0].top() - 5),
cv::FONT_HERSHEY_COMPLEX, 0.5, cv::Scalar(255, 0, 0), 1);
this->ui->head_lb->clear();
this->ui->name_edit->setText("");
this->ui->section_edit->setText("");
this->ui->id_edit->setText("");
}
}
}//有人脸才检测
//没有检测到人脸
else{
this->ui->ok_widget->hide();
this->ui->head_lb->clear();
this->ui->name_edit->setText("");
this->ui->section_edit->setText("");
this->ui->id_edit->setText("");
}
// 转换到QImage
QImage img(frame.data, frame.cols, frame.rows, frame.step, QImage::Format_RGB888);
this->ui->vedioLb->setPixmap(QPixmap::fromImage(img.rgbSwapped()));
this->ui->vedioLb->setFixedSize(frame.cols, frame.rows);
}
(2)人脸识别线程:我们通过异步线程(QtConcurrent实现)来进行人脸识别函数的处理,在构造函数中启动人脸识别线程,人脸识别到的信息会实时更新到recognizedId 上。
// 异步人脸识别
start_face = true;
QtConcurrent::run([this] { this->recognizeFace(); });
人脸识别函数如下:
//人脸识别处理函数
void FcaeRecognition::recognizeFace()
{
//只要没点击停止,就一直识别
while(start_face) {
try {
if(face_chip.size()){//有人脸
// cout << "人脸识别中" << endl;
// 从人脸形状中提取特征
matrix<float, 0, 1> face_descriptor = net(face_chip);
double min_distance = std::numeric_limits<double>::infinity();
// 遍历已保存的人脸特征矩阵,计算特征之间的欧氏距离
QString id; // 默认值为空
for (size_t i = 0; i < stored_descriptors.size(); ++i)
{
double distance = length(face_descriptor - stored_descriptors[i]);
// 如果距离小于阈值,认为是同一个人
if (distance < 0.4 && distance < min_distance)
{
// cout << distance <<endl;
id = files[i].left(files[i].size() - 4);
min_distance = distance;
}
}
// 如果没有找到匹配的人脸id,返回"null"
recognizedId = id;
// 等待一段时间再进行下一次识别
QThread::msleep(100);
}
//如果没有人脸就空循环
else{
recognizedId = "";
continue;
}
} catch (const std::exception& ex) {
qDebug() << "Caught exception in recognizeFace: " << ex.what();
// 根据需要处理异常
}
}
if(!start_face){
return; // 结束线程
}
}
该人脸识别函数的核心目的是在一个循环中持续识别人脸,直到start_face置为false,表示结束人脸识别线程。函数的主要流程与特点概述如下:
①持续识别循环:通过一个while循环,该函数在start_face(一个标志变量,用于控制识别过程是否继续)为真时不断执行人脸识别操作。
②识别逻辑:
- 检查是否有人脸存在(face_chip.size()不为零)。
- 若存在人脸,将其传入深度神经网络(net),从人脸图像(face_chip)中提取128D特征向量(face_descriptor)。
- 然后,通过遍历已存储的人脸特征(stored_descriptors),计算当前人脸特征与每个已存储特征之间的欧氏距离。
- 若发现一个与当前人脸特征的欧式距离小于预设阈值(0.4)的已存储特征,且这个距离是目前为止所有比对中的最小值,则认为当前人脸与该已存储特征代表同一人,此时记录下相应的ID。
③识别结果处理:
- 如果在所有已存储特征中找到了一个与当前人脸匹配的特征(即欧式距离小于阈值),则相应的id被存入recognizedId,更新画面的主GUI线程就可以检测到该内容,并将其ID对应的用户信息反馈到UI界面上。
- 如果没有找到匹配的ID,默认recognizedId为"null",表示未识别到已知人脸。
④在每次识别操作之后,函数会暂停一段时间(100毫秒),以降低处理频率。
⑤异常处理:函数使用try-catch结构捕获并处理可能出现的异常,确保程序的稳定性。若捕获异常,会将异常信息输出到调试控制台。
⑥结束识别:当循环条件不再满足(即start_face变为假)时,退出循环,并结束函数执行,线程也就结束了。
3.打卡逻辑实现
由于recognizedId的更新是在另一个异步线程当中进行,因此,只要识别函数中识别到recognizedId不为空,就显示到UI界面上,并且同步更新到用户信息栏中,这是我们点击打卡按钮,就是选中的UI上的用户进行打卡。
//打卡按钮
void FcaeRecognition::on_check_bt_clicked()
{
if(frameTimer->isActive())
frameTimer->stop();//先停止画面避免gui卡顿以及确保当前用户信息固定
if(!recognizedId.isEmpty()&&live_detection)
{
currenttime = QDateTime::currentDateTime(); // 获取当前时间
QString checktime = currenttime.toString("yyyy-MM-dd hh:mm:ss"); // 将时间转换为字符串
QString check_type = this->ui->checktype_cbox->currentText();//获取当前的打卡类型
query.prepare("INSERT INTO check_record (id,name,check_time,check_type) VALUES (:id,:name,:check_time,:check_type)");
query.bindValue(":id",recognizedId);
query.bindValue(":name", recognizedName);
query.bindValue(":check_time", checktime);
query.bindValue(":check_type",check_type);
// qDebug()<<recognizedId<<recognizedName<<checktime<<endl;
if (query.exec()) {
qDebug() << "Insertion successful";
//同步将打卡信息传输至服务器端
Mat check_pic = frame.clone();//获取打卡时候的照片
//发送数据
sendAttendanceData(&mSocket,check_pic,recognizedId);
//活体检测复位
live_reset();
QMessageBox::information(this,"欢迎!",recognizedName+"打卡成功"+checktime);
} else {
qDebug() << "Error in query: " << query.lastError().text();
QMessageBox::information(this,"欢迎!","打卡失败,请重试");
}
this->ui->ok_widget->hide();
}
else if(faces.empty()){
QMessageBox::information(this,"欢迎!","未检测到人脸!");
}
else if(recognizedId.isEmpty()){
//活体检测复位
live_reset();
QMessageBox::information(this,"欢迎!","人员未注册,请联系管理员!");
}
else if(!live_detection){
QMessageBox::information(this,"欢迎!","请先眨眼或张嘴!");
}
else if(!cap.isOpened()){
QMessageBox::information(this,"欢迎!","摄像头未打卡!");
}
if(cap.isOpened()&&!frameTimer->isActive()){
frameTimer->start(30);
}
}
该点击事件槽函数负责处理打卡系统中“打卡”按钮的点击事件。其逻辑流程大致如下:
①停止frameTimer:如果frameTimer计时器正在运行(通常用于实时显示视频流),则停止它,以避免主GUI线程卡顿以及确保当前用户信息固定。
②检查并执行打卡操作:
- 如果recognizedId(识别到的用户ID)不为空且通过了活体检测(live_detection为真),则进行打卡操作处理。
- 获取当前时间(currenttime)并转换为字符串格式(checktime)。
- 从下拉菜单(checktype_cbox)获取选择的打卡类型(check_type)。
- 准备并执行一个SQL插入语句,将用户ID、姓名、打卡时间和打卡类型插入到check_record表中。
- 如果插入成功,通过QMessageBox弹出打卡成功信息,并复位活体检测(隐藏识别成功提示和用户信息栏,显示活体检测提示,并将眨眼张嘴次数置0),并且准备将相关信息发送到服务器(打卡时的照片)。
- 如果插入失败,通过QMessageBox提示打卡失败信息。
③人脸未检测到和用户未注册的情况处理:
- 如果未检测到人脸(faces为空),提示"未检测到人脸!"。
- 如果recognizedId为空,即未识别出用户,提示"人员未注册,请联系管理员!"。并且活体检测复位。
- 如果没有通过活体检测(live_detection为假),提示"请先眨眼或张嘴!"。
④如果摄像头没有打开(cap.isOpened()为假),提示"摄像头未打开!"。
⑤最后如果摄像头已经打开且frameTimer不在运行状态,启动frameTimer,以重新更新画面。
通过这个函数,系统能够响应用户的打卡操作,通过人脸识别技术与活体检测确认用户身份,将打卡数据记录入数据库,并给予用户相应的反馈。同时,该函数还处理了若干可能的异常情况,例如摄像头未打开或用户信息未注册等,并对用户提供适当的提示信息。
4.TCP客户端
我们要实现当用户打卡时,将用户的打卡信息(包括打卡时的照片)传输至服务器端并显示出来,我们要实现启动客户端后就自动连接服务器,使用QTcpSocket,QTimer以及信号与槽机制来实现自动连接:当客户端启动时,计时器开始计时,并且每5s触发连接的槽函数,当连接成功时,QTcpSocket发送connected信号,这时我们关闭计时器,停止自动连接,当断开连接时,QTcpSocket发送disconnected信号,这时我们重启计时器,开始自动连接,这样就能够保证客户端断线重连服务器。
(1)客户端掉线自动重连服务器
QT += core gui sql network
#include <QTcpSocket>
//TCP网络
QTcpSocket mSocket;//创建套接字
QTimer *netTimer;//创建用于自动连接服务器的定时器
void sendAttendanceData(QTcpSocket *socket, const Mat &image, const QString &userId);//将打卡信息发送给服务器端
void time_connect();//定时自动连接服务器
void stop_connect();//连接成功后就停止连接
void start_connect();//断开连接后开始连接
connect(netTimer,&QTimer::timeout,this,&FcaeRecognition::time_connect);//关联自动连接服务器槽函数
//QTcpSocket连接成功发送connected信号,断开发送disconnected信号
connect(&mSocket,&QTcpSocket::disconnected,this,&FcaeRecognition::start_connect);//断开后
connect(&mSocket,&QTcpSocket::connected,this,&FcaeRecognition::stop_connect);//连接后
//定时自动连接服务器
void FcaeRecognition::time_connect()
{
mSocket.connectToHost("",9999);//
}
//连接成功后就停止连接
void FcaeRecognition::stop_connect()
{
netTimer->stop();
//连接成功后显示连接成功的widget,隐藏正在连接的widget
this->ui->connected_widget->show();
this->ui->isconnect_widget->hide();
cout<<"成功连接服务器"<<endl;
}
//断开连接后开始连接
void FcaeRecognition::start_connect()
{
netTimer->start(5000);//断开连接后又启动自动连接
//断开连接后显示正在连接,隐藏连接成功
this->ui->connected_widget->hide();
this->ui->isconnect_widget->show();
cout<<"服务器断开"<<endl;
}
已经实现断开自动重连服务器
(2)打卡信息发送
我们要实现当用户成功打卡时将打卡的照片和打卡用户的信息实时发送给服务器,具体的发送任务就在之前的打卡成功时的函数里完成。发送数据的函数之前已经定义:
void sendAttendanceData(QTcpSocket *socket, const Mat &image, const QString &userId);//将打卡信息发送给服务器端
具体实现
//将打卡信息发送给服务器端
void FcaeRecognition::sendAttendanceData(QTcpSocket *socket, const Mat &image, const QString &userId)
{
//没有连接上服务器
if (!socket) return;
// 将OpenCV图像转换为QByteArray,使用jpg格式编码图像
std::vector<uchar> buf;
imencode(".jpg", image, buf);
QByteArray imageBytes = QByteArray(reinterpret_cast<const char*>(buf.data()), int(buf.size()));
// 创建一个QByteArray用于存储要发送的所有数据
QByteArray senddata;
//将QByteArray转换成发送数据流QDataStream,只读
QDataStream out(&senddata, QIODevice::WriteOnly);
out.setVersion(QDataStream::Qt_5_12); // 设置QDataStream的版本
// 首先写入用户ID
out << userId;
// 然后写入图像数据的大小
out << (quint64)(imageBytes.size());
// 最后写入图像数据本身
senddata.append(imageBytes);
// 发送数据
socket->write(senddata);
socket->flush(); // 确保所有数据都被发送
}
这段代码的功能是将考勤数据,包括用户ID和用户的面部图像,通过TCP网络发送到服务器端。具体步骤如下:
①检查连接:首先检查是否已经成功建立了与服务器的连接。如果 socket 指针为空,则表示没有连接到服务器,函数返回。
②图像转换:使用 OpenCV 的函数 imencode 将面部图像(image),这是 Mat 类型,转换成 jpg格式的图像数据,并存储在一个 uchar 类型的向量(buf)中。
③构建发送数据:接着,程序通过 reinterpret_cast 将图像数据的向量转换为 QByteArray 类型(imageBytes),这是 Qt 中用于存储字节序列的类。
④数据打包:创建一个新的 QByteArray(senddata)来存储要发送的所有数据,包括用户ID和图像数据。将 senddata 与 QDataStream 关联,并设置数据流的版本为 Qt 5.12。然后依次往数据流中写入用户ID、图像数据的大小( quint64 类型)和图像数据本身。(QDataStream 在写入 QString 类型时,会在内部包括字符串的长度信息,所以无需写入ID的长度)
⑤发送数据:最后,通过已连接的 socket 对象发送包含所有考勤数据的 senddata,并调用 flush 方法确保所有数据都被发送出去。
二.服务器端功能完善
1.服务器功能概述
服务器主要实现的功能就包括:实时接收打卡信息(个人信息+打卡时间+打卡头像),查询考勤信息(迟到,早退),人脸录入,员工信息删除等。
2.人脸录入模块
摄像头拍摄的比例为640X480
我们可以从左侧摄像头出拍摄头像并显示在右上方的label中,或者从电脑中添加头像。当用户点击保存时,我们需要做的就是:获取label上的图片,将其转化为OpenCV格式,并进行灰度化,均衡化,人脸对齐,然后传入深度神经网络获取128D面部特征,并保存到人脸库文件夹中去。
Mat head_pic始终存放的是准备存入人脸库的头像图片文件(无论是从照片中选择还是从摄像头拍照),因此给head_pic赋值的过程应当与显示头像到head_lb的过程一起。
(1)从文件夹中添加头像
//从文件夹中添加头像
void Register::on_addpic_bt_clicked()
{
frameTimer->stop();//先要停止更新画面,避免gui卡顿
QString filepath = QFileDialog::getOpenFileName(this);
QPixmap pixmap(filepath);
if (!pixmap.isNull()) {
// 使用 OpenCV 的 imread() 函数将图片文件转换为 cv::Mat 格式
//将选择的头像赋值给head_pic,以便于录入人脸库
head_pic = cv::imread(filepath.toStdString(), cv::IMREAD_COLOR);
// 根据需要调整大小,保证图片符合label的尺寸
pixmap = pixmap.scaled(this->ui->head_lb->size(), Qt::KeepAspectRatio, Qt::SmoothTransformation);
// 显示图片到label
this->ui->head_lb->setPixmap(pixmap);
} else {
qDebug() << "无法加载图片: " << filepath;
}
frameTimer->start(30);//再次启动视频
// cv::imshow("001",head_pic);
}
其主要作用是让用户通过文件对话框从文件系统中选择一张图片作为头像,并将其显示在应用程序的界面中。这个过程通常是在用户点击添加图片按钮时触发的。
步骤如下:
①首先,如果用于更新摄像头画面的定时器frameTimer当前正在运行,则停止它。这是为了防止在选择文件时主GUI线程出现卡顿,导致程序崩溃。
②QFileDialog::getOpenFileName函数调用会显示一个文件选择对话框,允许用户选择一个图片文件(拓展名为jpg, png, JPG, PNG)。
③如果用户选择了一个文件(即filepath不为空),将尝试加载这个文件到QPixmap对象中。
④如果图片加载成功(pixmap不为空且有效),则执行以下操作:
- 使用OpenCV的cv::imread()函数把图片文件读取到head_pic变量中,格式为cv::Mat,以便后续使用(录入人脸库文件夹)。
- pixmap通过调用scaled函数来调整其大小,以适应用户界面中用于展示头像的head_lb标签的尺寸,同时保持图片的长宽比和平滑缩放。
- 然后,调用setPixmap函数,在head_lb标签中显示调整大小后的图片。
⑤如果图片加载失败(pixmap.isNull()),则打印错误信息并提示用户图片加载失败。
⑥如果用户在文件选择对话框中取消操作(文件路径为空,filepath.isEmpty()),则不执行任何操作。
⑦最后,如果cap对象(代表摄像头)是打开的且frameTimer定时器当前不活跃,将会重新启动定时器,以恢复从摄像头实时获取画面的功能。
总的来说,这段代码允许用户在不打开摄像头的情况下(也包括摄像头打开的情况,这时候会先暂停画面,保存成功或者取消操作后再继续画面)从本地文件系统中选择图片,提供给用户一个交互式的方式来设置头像,同时确保了添加图片过程中不会影响应用程序的画面更新和性能。
在将QImage转换为cv::Mat时,颜色格式转换非常重要,因为QImage和OpenCV中的cv::Mat可能使用不同的内部排列方式来存储颜色信息。以下是几个原因说明为什么颜色格式转换是必要的:
不同的颜色通道顺序: QImage和OpenCV在彩色图像中存储颜色通道的顺序通常不相同。QImage在其Format_RGB888格式中使用RGB顺序,而OpenCV使用BGR顺序来储存彩色图像数据。如果不转换,那么从QImage转换到cv::Mat后图像的颜色会出现混乱,例如原本蓝色的部分会显示为红色,反之亦然。
透明度的处理: 一些QImage的格式,比如Format_ARGB32支持透明度通道(Alpha通道),而OpenCV的某些cv::Mat格式也支持它。不过,在转换过程中需要确保透明度通道被正确处理,否则可能会损失此信息或者导致错误的图像表示。
颜色深度和类型转换: 不仅颜色通道的顺序,颜色的表示(例如8位颜色,16位颜色)也需要匹配。如果QImage的格式采用了不同的颜色深度,则可能需要将其转换为OpenCV理解的形式。
效率考虑: 在某些情况下,转换颜色格式可能具有性能上的考虑,因为OpenCV针对BGR格式进行了许多内部优化。
由于上述原因,确保在QImage和cv::Mat之间转换时,颜色格式正确无误是很重要的。颜色格式不正确可能会导致图像颜色显示异常,这在进行图像处理时尤为关键,比如颜色检测、对象跟踪等应用场景中。
(2)从摄像头拍照获取头像
if(cap.isOpened()&&(!faces.empty())){//确保摄像头打开以及有人脸才进行采集
head_pic = frame.clone();//将选择的这一帧图片赋值给head_pic,以便于录入人脸库
Mat headFrame = frame.clone();//将当前的帧克隆一份用于显示人脸
// 转换到label上显示
QImage img(headFrame.data, headFrame.cols, headFrame.rows, headFrame.step, QImage::Format_RGB888);
QPixmap pixmap = QPixmap::fromImage(img.rgbSwapped());
// 将Pixmap缩放至head_lb的尺寸并显示到head_lb上
QPixmap scaledPixmap = pixmap.scaled(ui->head_lb->size(), Qt::KeepAspectRatio, Qt::SmoothTransformation);
this->ui->head_lb->setPixmap(scaledPixmap);
}
//没有打开摄像头
else if(!cap.isOpened()){
QMessageBox::information(this,"提示","请打开摄像头");
}
//没有人脸
else if(faces.empty()){
QMessageBox::information(this,"提示","没有检测到人脸!");
}
函数的开始部分,检查了摄像头(cap)是否已打开且已检测到人脸。如果满足这两个条件,函数会执行以下操作:
①从帧缓存(frame)复制当前帧的图像至head_pic。这个动作的目的是为了将当前帧的图像保存下来,稍后可能被用作录入人脸库的头像照片。
②创建一个名为headFrame的新的Mat对象,复制当前帧的内容,这个对象将被用于显示给用户看的人脸图像。
③接着代码将headFrame转换为QImage,然后将其转换为QPixmap,以实现Qt图形库与OpenCV库中表示图像的格式间的转换。
④使用QPixmap::scaled()函数,将pixmap对象缩放到用户界面上head_lb标签的大小,缩放时保持了图片的长宽比,并且使用平滑变换来提供更好的图像质量。
最后,将缩放后的QPixmap对象显示到用户界面的head_lb标签上。
⑤如果摄像头没有打开,或者虽然摄像头打开了,但是没有检测到人脸,函数会向用户显示一个相应的提示信息,告诉用户相应的解决办法。
总的来说,这个函数实现了在用户点击拍照按钮时,如果摄像头已打开并且已检测到人脸,就将当前帧的照片添加到head_pic,准备录入人脸库文件夹,并在界面上显示该照片,供用户选择是否使用该照片为头像。如果未打开摄像头或者没有检测到人脸,就显示相应提示信息。
(3)保存选定的人脸
由上面我们知道:head_pic中始终保存的是我们要存入人脸库的头像,因此保存函数的实现就是识别head_pic是否存在人脸(有可能用户选择的图片不是包含人脸的图片),然后检查填写的用户信息是否完整,随后进行灰度化,均衡化,人脸对齐,人脸裁剪,传入深度神经网络提取128D特征值,并以ID为文件名存储头像以及对应的特征值文件(dat文件)。
//保存人脸到人脸库文件夹
void Register::on_save_bt_clicked()
{
//确保head_pic不是空的
if(!head_pic.empty()){
if(frameTimer->isActive())//保存时如果计时器是活跃的就暂停
{
frameTimer->stop();
}
Mat processedhead = head_pic.clone();//processedhead用于提取特征,而head_pic用于保存头像
cvtColor(processedhead,processedhead,CV_BGR2GRAY);//将彩色图转换从灰度图
equalizeHist(processedhead,processedhead);//进行均衡化处理
// 将OpenCV灰度图转换为dlib图像
dlib::cv_image<unsigned char> dlibImage(processedhead);
// 使用人脸检测器检测人脸
faces = detector(dlibImage);
//有人脸才处理
if (faces.empty()) {
QMessageBox::warning(this, "提示", "头像中未检测到人脸!请重新上传!");
return;
}
// //测试传入神经网络的头像
// cv::Mat faceMat(face_chip.nr(), face_chip.nc(), CV_8UC1); // 灰度图,单通道
// for (int r = 0; r < face_chip.nr(); ++r) {
// for (int c = 0; c < face_chip.nc(); ++c) {
// // 因为是灰度图,只取一个通道的值
// faceMat.at<uchar>(r, c) = static_cast<uchar>(face_chip(r, c).blue);
// }
// }
// imshow("net",faceMat);
// cv::waitKey(0);
QString name = this->ui->name_edit->text(); //获取人名字
QString section = this->ui->section_edit->text();//获取部门
QString id = this->ui->id_edit->text();//获取id
if (name.isEmpty() || section.isEmpty() || id.isEmpty()) {
QMessageBox::warning(this, "提示", "信息不完整,请检查!");
return;
}
//插入数据库
query.prepare("INSERT INTO staff (id,name,section) VALUES (:id,:name,:section)");
query.bindValue(":id",id);
query.bindValue(":name", name);
query.bindValue(":section", section);
if (query.exec()) {
qDebug() << "Insertion successful";
// 获取人脸关键点
full_object_detection shape = sp(dlibImage, faces[0]);
// 截取人脸部分并调整大小对齐
matrix<rgb_pixel> face_chip;
extract_image_chip(dlibImage, get_face_chip_details(shape, 150, 0.25), face_chip);
// 提取人脸特征值
matrix<float, 0, 1> face_descriptor = net(face_chip);
//保存人脸图片到文件夹
string photo_name = FACES_PATH + id.toStdString()+ + ".jpg";
cv::imwrite(photo_name, head_pic);
// 保存人脸特征值到文件
string file_name = FACES_PATH + id.toStdString() + ".dat";
ofstream outfile(file_name, ios::binary);
serialize(face_descriptor, outfile);
outfile.close();
QMessageBox::information(this, "提示", name + "保存成功!");
this->ui->head_lb->clear();
this->ui->name_edit->setText("");
this->ui->section_edit->setText("");
this->ui->id_edit->setText("");
}
else {
// 这里我们捕获具体的数据库错误
QSqlError err = query.lastError();
QString errCode = err.nativeErrorCode();
// 检查错误类型,如果是因为主键冲突导致的错误
// 对于MySQL,主键冲突的错误代码是1062,不同的数据库可能有不同的错误代码
if(errCode == "1062") {
QMessageBox::information(this, "错误", "用户已存在!");
} else {
// 其他类型的错误
QMessageBox::information(this, "提示", "错误信息:"+err.text() + ",插入数据库失败,请检查后重试!");
}
}
//保存结束,如果摄像头是打开的并且计时器关闭了,再继续视频
if(cap.isOpened()&&!frameTimer->isActive()){
frameTimer->start(30);//继续视频
}
}//headpic不空
else//head_pic为空
{
QMessageBox::information(this, "提示", "没有头像!");
}
}
里面涉及到多处错误处理:
①先要检查head_pic是否为空,为空就不需要进行下一步,直接提示用户头像为空,检查后重试即可。
②在head_pic不为空的情况下,先检查更新画面的计时器是否处于活跃状态,如果处于活跃状态,就先暂停(因为读取人脸特征值涉及到深度神经网络,会比较慢,暂停画面更新以避免阻塞主GUI线程)。
③随后克隆一份processedhead(processedhead用于提取特征,而head_pic用于保存头像,因为要对头像进行灰度化,均衡化等处理,但是保存的头像要求是彩色的原图,所以克隆一份进行特征提取等处理),在进行灰度化,均衡化后,利用人脸检测器对processedhead进行人脸检测,确保存在人脸(这时我们使用更新画面的faces容器即可,因为此时如果有画面,也是暂停的,不会对faces进行修改,这也就保证了所检测的人脸数量信息就是processedhead的人脸数量信息)。
④在确定存在人脸后,我们就提取管理员输入的用户信息(姓名,部门,ID),并且确保每个信息都是非空的,否则提示用户确保输入信息完整。
⑤在获取到头像和对应信息后,我们先尝试插入到数据库中(可能存在ID冲突的问题),如果主键ID冲突了(MySQL代码1062),就提示用户用户已存在!
⑥如果信息无误,就先把信息插入到数据库,然后就可以对头像进行人脸对齐,裁剪,传入神经网络提取128D特征值。
⑦然后使用imwrite保存头像到人脸库文件夹,使用serialize(序列化)保存特征值到人脸库文件夹,均以ID命名。保存成功后提示用户,保存失败也提示用户重新上传并附上错误信息。
⑧最后,保存结束,如果摄像头是打开的并且计时器关闭了,再继续视频。(第二步和最后一步检查的原因:如果管理员并没有开启摄像头,直接选择文件,就不需要关闭计时器,并且最后如果摄像头没打卡并且计时器没关闭,就不需要开启计时器,否则,如果摄像头没打开,但是我们启动计时器,就会导致更新画面捕捉不到视频帧而出错)
4.考勤信息查询模块
1. UI界面
2. 考勤信息和用户信息查询
//查询按钮
void Query::on_select_bt_clicked()
{
//先判断查询什么内容
//选中的是用户信息
if(this->ui->userinfo_bt->isChecked()){
model->setTable("staff");
model->setHeaderData(0, Qt::Horizontal, QObject::tr("用户ID"));
model->setHeaderData(1, Qt::Horizontal, QObject::tr("用户名"));
model->setHeaderData(2, Qt::Horizontal, QObject::tr("部门"));
ui->tableView->setEditTriggers(QAbstractItemView::NoEditTriggers);//用户信息只可读
}
//选中的是考勤信息
else if(this->ui->checkinfo_bt->isChecked()){
model->setTable("check_record");
model->setHeaderData(0, Qt::Horizontal, QObject::tr("用户ID"));
model->setHeaderData(1, Qt::Horizontal, QObject::tr("用户名"));
model->setHeaderData(2, Qt::Horizontal, QObject::tr("打卡时间"));
model->setHeaderData(3, Qt::Horizontal, QObject::tr("打卡类型"));
}
model->select();
this->ui->tableView->setModel(model);
}
这里我们使用QSqlTableModel进行数据库信息查询,使用QTableView显示查询的内容。并且设置表格填充整个QTableView的宽度,以获得最佳的视觉效果。
查询按钮的功能是从数据库中检索特定的数据信息并将其展示在用户界面的表格控件上。并且将用户信息和考勤信息两个按钮(QRadioButton)加入QButtonGroup,以确保同时只选中一个信息查询。
具体来说,当用户点击查询按钮时,程序会根据用户之前通过界面选择的选项(如用户信息或考勤信息)来决定需要查询哪张数据表。查询按钮的代码会根据这个选择设置QSqlTableModel对象model的数据表,配置数据表的表头,并选择表格的显示模式。
如果用户想要查询用户信息,那么程序将会设置到"staff"数据表并配置对应的表头名称(用户ID、用户名、部门),界面上的tableView将展示这个表的内容而且用户无法编辑这些内容。
如果用户想要查询考勤信息,那么程序将会设置到"check_record"数据表并配置对应的表头名称(用户ID、用户名、打卡时间,打卡类型),tableView则显示考勤记录的内容。
3. 条件查询(迟到,早退)
程序使用两个QTimeEdit来分别设置上班时间和下班时间(start_timeEdit和end_timeEdit),
//查询迟到的人员信息
void Query::on_select_startBt_clicked()
{
model->setTable("check_record");
model->setHeaderData(0, Qt::Horizontal, QObject::tr("迟到用户ID"));
model->setHeaderData(1, Qt::Horizontal, QObject::tr("迟到用户名"));
model->setHeaderData(2, Qt::Horizontal, QObject::tr("上班打卡时间"));
model->setHeaderData(3, Qt::Horizontal, QObject::tr("打卡类型"));
QTime startTime = ui->start_timeEdit->time();//获取上班时间
QString startTimeStr = startTime.toString("HH:mm:ss");
// 构建一个筛选条件,使用TIME()来格式化时间(只获取HH:mm:ss),并且只看上班打卡
QString filter = QString("TIME(check_time) > '%1' AND check_type = '上班打卡'").arg(startTimeStr);
model->setFilter(filter);
model->select();
if(model->lastError().isValid()) {
qDebug() << model->lastError();
}
this->ui->tableView->setModel(model);
}
//查询早退的人员信息
void Query::on_select_endBt_clicked()
{
model->setTable("check_record");
model->setHeaderData(0, Qt::Horizontal, QObject::tr("早退用户ID"));
model->setHeaderData(1, Qt::Horizontal, QObject::tr("早退用户名"));
model->setHeaderData(2, Qt::Horizontal, QObject::tr("下班打卡时间"));
model->setHeaderData(3, Qt::Horizontal, QObject::tr("打卡类型"));
QTime endTime = ui->end_timeEdit->time();//获取下班时间
QString endTimeStr = endTime.toString("HH:mm:ss");
// 构建一个筛选条件,使用TIME()来格式化时间(只获取HH:mm:ss),并且只看下班打卡
QString filter = QString("TIME(check_time) < '%1' AND check_type = '下班打卡'").arg(endTimeStr);
model->setFilter(filter);
model->select();
if(model->lastError().isValid()) {
qDebug() << model->lastError().text();
}
ui->tableView->setModel(model);
}
程序中查询员工的迟到和早退信息,通过以下步骤实现:
①设定数据模型和表格:首先,选择check_record数据库表格。接下来,使用QSqlTableModel数据模型与这个表格关联。
②配置表头:为了使表格中的数据更加易于理解,需要设置适当的表头。对于迟到查询,表头为“迟到用户ID”、“迟到用户名”、“上班打卡时间”和“打卡类型”;对于早退查询,则相应地设置为“早退用户ID”、“早退用户名”、“下班打卡时间”和“打卡类型”。
③获取时间条件:通过界面上的时间选择组件(QTimeEdit),让用户设定用于查询的上班或下班的时间点。这个设置的时间以及打卡类型将作为过滤条件。
④建立筛选条件并执行查询:根据用户设定的时间点,构建过滤条件(setFilter)来进行查询。对于查询迟到的情况,筛选条件为打卡类型为“上班打卡”且打卡时间晚于设定的上班时间;而对于查询早退的情况,筛选条件为打卡类型为“下班打卡”且打卡时间早于设定的下班时间。
⑤处理查询结果:执行查询后,将查询到的数据展示在界面上的表格中(tableView)。用户可以清晰地看到哪些员工迟到或早退。
5.删除用户模块
(1)UI界面
(2)搜索符合条件用户并删除
主要代码:
//根据姓名或者id查询用户
void DeleteUser::on_select_bt_clicked()
{
//获取准备删除用户的姓名和id
QString id = this->ui->deleteid_edit->text();
QString name = this->ui->deletename_edit->text();
qDebug() << id << name;
if(id.isEmpty() && name.isEmpty()){
QMessageBox::information(this, "提示", "请至少输入一个条件!");
}
else{
// 构建 SQL 查询字符串
if (!id.isEmpty() && !name.isEmpty()) {
// 如果两个条件都有输入,则使用 AND 连接
query.prepare("SELECT * FROM staff WHERE id = :id OR name = :name");
query.bindValue(":id",id);
query.bindValue(":name",name);
} else if (!id.isEmpty()) {
// 只有 id 有输入
query.prepare("SELECT * FROM staff WHERE id = :id");
query.bindValue(":id",id);
} else {
// 只有 name 有输入
query.prepare("SELECT * FROM staff WHERE name = :name");
query.bindValue(":name",name);
}
if(query.exec()){
//点击查询时先显示第一个符合条件的用户
if(query.next()) {
showUser();
} else {
QMessageBox::information(this, "未找到", "没有更多符合条件的用户!");
}
}
}
}
//查找下一个符合要求的用户
void DeleteUser::on_nextselect_bt_clicked()
{
if(query.next()) {
showUser();
} else {
QMessageBox::information(this, "未找到", "没有更多符合条件的用户!");
}
}
//显示查询结果到ui上面
void DeleteUser::showUser()
{
QString foundId = query.value("id").toString();
QString foundName = query.value("name").toString();
QString foundSection = query.value("section").toString();
this->ui->id_edit->setText(foundId);
this->ui->name_edit->setText(foundName);
this->ui->section_edit->setText(foundSection);
//显示头像
QString imagePath = QString(FACES_PATH) + foundId + ".jpg";
QPixmap pixmap(imagePath);
if (!pixmap.isNull()) {
// 根据需要调整大小,保证图片符合label的尺寸
pixmap = pixmap.scaled(this->ui->head_lb->size(), Qt::KeepAspectRatio, Qt::SmoothTransformation);
// 显示图片到label
this->ui->head_lb->setPixmap(pixmap);
} else {
qDebug() << "无法加载图片: " << imagePath;
}
}
//删除用户
void DeleteUser::on_delete_bt_clicked()
{
QString deletedId = this->ui->id_edit->text(); // 确定删除的用户id
if(!deletedId.isEmpty()){
// 确认删除操作
QMessageBox::StandardButton reply;
reply = QMessageBox::question(this, "确认删除", "你确定要删除ID为"+deletedId+"的用户吗?",
QMessageBox::Yes|QMessageBox::No);
if (reply == QMessageBox::Yes) {
// 从数据库中删除用户
QSqlQuery query(QSqlDatabase::database());
QString deleteQuery = QString("DELETE FROM staff WHERE id = '%1'").arg(deletedId);
if(query.exec(deleteQuery)) {
// 删除对应的照片和特征值文件
QDir dir(FACES_PATH);
QStringList filters;
filters << deletedId + "*.jpg" << deletedId + "*.png" << deletedId + "*.JPG" << deletedId + "*.PNG";
QList<QFileInfo> files = dir.entryInfoList(filters, QDir::Files | QDir::NoDotAndDotDot);
foreach(QFileInfo file, files) {
if(QFile::remove(file.absoluteFilePath())) {
qDebug() << "文件删除成功:" << file.fileName();
} else {
qDebug() << "文件删除失败:" << file.fileName();
}
}
// 删除.dat文件
QString dataPath = FACES_PATH + deletedId + ".dat";
if(QFile::remove(dataPath)) {
qDebug() << "数据文件删除成功:" << dataPath;
} else {
qDebug() << "数据文件删除失败:" << dataPath;
}
QMessageBox::information(this, "删除成功", "用户数据及相关文件已从数据库和系统中删除。");
} else {
QMessageBox::critical(this, "错误", "删除用户数据时出错:" + query.lastError().text());
}
}
else {
QMessageBox::information(this, "删除取消", "用户数据没有被删除。");
}
}
//没有信息
else{
QMessageBox::information(this, "提示", "没有要删除的用户!");
}
}
该程序实现对员工信息的删除和查询。通过用户界面,提供了对员工信息按照id或name查询以及删除的功能。它主要包括以下几个关键部分的功能:
①查询用户信息(on_select_bt_clicked):
- 用户通过在提供的文本框内输入用户的id或name,来查询用户信息。
- 如果两个条件(即id和name)都未输入,程序会提示至少输入一个条件。
- 如果输入了id或者name,则两者会被用作查询条件。
- 程序执行查询后,会首先显示第一个与条件匹配的用户信息。
②查找并显示下一个符合要求的用户(on_nextselect_bt_clicked):
- 在已有查询结果的基础上,点击“查找下一个”按钮会查找下一个符合查询条件的用户。
- 如果没有更多符合条件的用户,程序将提示没有更多用户。
③显示查询结果(showUser):
- 将查询结果显示在界面的相应部分上,包括用户的id、name和section。
- 并且程序还会根据用户的id查找并显示对应的头像图片到head_lb上。
④删除用户(on_delete_bt_clicked):
- 当准备删除一个用户时,首先会再次确认是否确实要删除指定的用户。这是通过显示一个消息框来完成的,询问用户是否确定要删除,以确保安全删除。
- 如果确认删除,则程序将从数据库中删除用户信息,并且也会从文件系统中删除与该用户相关的所有.jpg、.png、.JPG、.PNG格式的图片(头像)以及.dat文件(特征值文件)。
- 如果在执行删除操作期间出现任何问题,例如数据库操作错误,程序将显示相应的错误消息。
- 如果用户选择取消删除操作,程序将显示一条消息说明用户数据未被删除。
这个程序综合运用了Qt的数据库操作、文件系统管理和用户界面交互设计,以实现用户信息的有效管理。通过提供直观的界面和简单的操作步骤,使得用户能够轻松地查询和管理员工或用户数据。
6.TCP服务器
我们的客户端与服务器主要通过数据库以及QTcpServer来进行连接。在这里,我的TCP主要的功能设计想法是:如果客户端连接上了服务器,那么我们将服务器作为一个“监控程序”,来实时地监控我们用户的打卡照片,也就是在用户打卡时,实时地将打卡照片和用户信息传输至我们的服务器端。
(1)创建UI(Monitor)
(2)接收解析数据
QT += core gui sql network
#include <QTcpServer>
#include <QTcpSocket>
QTcpServer mserver;//服务器
QTcpSocket *msocket;//套接字(收发数据)
quint64 expectedImageSize = 0;//期望的图片大小
QString userId; //读取发送过来的用户ID
//数据库
QSqlQuery query;//sql语句执行对象
msocket = new QTcpSocket(this);
//有新连接时触发newConnection信号
connect(&mserver,&QTcpServer::newConnection,this,&Monitor::acceptData);
mserver.listen(QHostAddress::Any,9999);//启动服务器监听端口9999
//接收客户端的数据
void Monitor::acceptData()
{
qDebug()<<"lianjie";
msocket = mserver.nextPendingConnection();//获取连接
connect(msocket,&QTcpSocket::readyRead,this,&Monitor::readData);//有数据传来时触发readData槽函数
}
//读取客户端数据
void Monitor::readData()
{
qDebug()<<"正在接受客户端数据!";
// 创建一个QDataStream,绑定socket接收的数据
QDataStream in(msocket);
in.setVersion(QDataStream::Qt_5_12); // 设置版本需要与发送端一致
// 读取ID
in >> userId;
if (expectedImageSize == 0) {
if (msocket->bytesAvailable() < (qint64)sizeof(expectedImageSize))
return;
// 读取发送过来的图像数据的大小
in >> expectedImageSize;
}
//数据还没传输完
if ((quint64)msocket->bytesAvailable() < expectedImageSize)
return;
QByteArray imageBytes;
in>>imageBytes;
if(imageBytes.size() == 0) return;//没有读取到数据
//打卡照片
QPixmap checkpic;
if (checkpic.loadFromData(imageBytes, "jpg")) {
// 在此处理 QPixmap 对象,设置监控图片(打卡照片)
// 根据需要调整大小,保证图片符合label的尺寸
checkpic = checkpic.scaled(this->ui->checkpic_lb->size(), Qt::KeepAspectRatio, Qt::SmoothTransformation);
this->ui->checkpic_lb->setPixmap(checkpic);
} else {
// 图像加载失败
qDebug()<<"打卡图片加载失败!";
}
query.prepare("SELECT * FROM staff WHERE id = :id");
query.bindValue(":id",userId);
if(query.exec()){
if(query.next()) {
showUser();
} else {
//查询失败
qDebug()<<query.lastError().text();
}
}
// 数据处理完毕,清除临时变量
userId.clear();
expectedImageSize = 0;
imageBytes.clear();
}
//显示用户信息到ui
void Monitor::showUser()
{
QString foundId = query.value("id").toString();
QString foundName = query.value("name").toString();
QString foundSection = query.value("section").toString();
QDateTime currenttime = QDateTime::currentDateTime(); // 获取当前时间
QString currentTimeStr = currenttime.toString("yyyy-MM-dd hh:mm:ss");
this->ui->id_edit->setText(foundId);
this->ui->name_edit->setText(foundName);
this->ui->section_edit->setText(foundSection);
this->ui->checktime_edit->setText(currentTimeStr);
//显示头像
QString imagePath = QString(FACES_PATH) + foundId + ".jpg";
QPixmap pixmap(imagePath);
if (!pixmap.isNull()) {
// 根据需要调整大小,保证图片符合label的尺寸
pixmap = pixmap.scaled(this->ui->head_lb->size(), Qt::KeepAspectRatio, Qt::SmoothTransformation);
// 显示图片到label
this->ui->head_lb->setPixmap(pixmap);
} else {
qDebug() << "无法加载头像: " << imagePath;
}
}
readData(): 这个函数负责从与客户端相连的socket接收数据。它使用QDataStream来读取数据,并处理按顺序接收的协议数据字段。步骤如下:
①打印消息到控制台,表示开始接收数据。
②创建一个QDataStream对象并关联到msocket。
③先接收传来的用户ID(userId),再检查是否接收到了图像的大小信息,如果大小为0,则意味着需要先接收图像大小。
④检查socket的可用字节是否满足预期的图像大小(expectedImageSize,是客户端传来的,表示这个图片有多大),如果不够,则返回等待更多数据到来。
⑤读取图像数据到QByteArray对象中。
⑥使用QPixmap::loadFromData从字节数组中加载图像,并在UI中的QLabel显示打卡照片。
⑦准备和执行一个数据库查询,根据客户端传来的userId检索职员信息。
⑧调用showUser()函数在UI上显示当前打卡用户的信息。
7.登录注册功能
(1)UI界面
登录:
注册:
(2)注册和登录实现
先介绍Hash加密:
当我们创建在线账号并设定密码时,为了保护账户安全,这些密码应当通过某种方式进行加密存储,而非直接明文存储,以防止信息泄漏导致账户被非法访问。这就涉及到了加密技术中的哈希函数。
流程上,
注册:用户设置用户名和密码,密码在存储到数据库前通过哈希函数加密,生成哈希值存储到数据库中。这个过程中,我们需要确保选用的哈希函数安全,如SHA-256等。
登录:用户输入用户名和密码,输入的密码同样通过哈希函数处理生成哈希值,与数据库中存储的哈希值对比,如果一致,则登录成功,否则登录失败。
意义上,
安全性:哈希函数是不可逆的,尤其是对于安全的哈希函数如SHA-256,它几乎不可能从输出的哈希值中恢复出原始的输入(密码),大大增强了密码的安全性。
隐私保护:因为密码哈希化后存储,所以即使数据库被未授权的访问或者数据泄露,攻击者获得的也只是密码的哈希值,不能直接得到用户的明文密码。
防止彩虹表攻击:如果采用盐值(salt)一同参与哈希,即使攻击者使用彩虹表(预先计算好的一组明文和哈希值的对应关系)也无法直接破解,因为彩虹表通常不包括盐值处理过的哈希,这提高了破解的计算复杂度。
使用这种加密方式,当你的数据库信息被盗或者服务被侵入时,你的用户密码情报还是安全的,侵入者只能获知这些无法直接破解的哈希值,而用户的原始密码并没有在数据库中直接暴露。这就是哈希密码存储的主要意义和目的。
注册:
//确认注册
void SignUp::on_sure_bt_clicked()
{
QString username = this->ui->lineEdit_username->text();
QString password = this->ui->lineEdit_passwd->text();
QString surepass = this->ui->lineEdit_surepasswd->text();
QString secret_key = this->ui->secretkey_edit->text();
// 检查输入是否完整
if (username.isEmpty() || password.isEmpty() || surepass.isEmpty() || secret_key.isEmpty()) {
QMessageBox::warning(this, "警告", "输入信息不完整!");
return;
}
// 检查注册秘钥
if (secret_key != SECRET_KEY) {
QMessageBox::warning(this, "警告", "注册秘钥错误!");
return;
}
// 检查两次密码输入是否一致
if (password != surepass) {
QMessageBox::warning(this, "警告", "两次输入的密码不一致!");
return;
}
//将密码加密存储
QByteArray passwordData = password.toUtf8(); // 转换为QByteArray进行哈希处理
QByteArray hash = QCryptographicHash::hash(passwordData, QCryptographicHash::Sha256); // 使用SHA-256算法进行哈希处理,得到结果为QByteArray
QString passwordHash = hash.toHex(); // 转换哈希结果为16进制字符串,以方便存储和比对
query.prepare("INSERT INTO administrator (username, passwordHash) VALUES (:username, :passwordHash)");
query.bindValue(":username", username);
query.bindValue(":passwordHash", passwordHash);
if(query.exec()) {
QMessageBox::information(this, "成功", "注册成功!");
} else {
QMessageBox::critical(this, "错误", "注册失败: " + query.lastError().text());
}
}
登录:
//登陆
void Login::on_loginbt_clicked()
{
QString username = this->ui->lineEdit_username->text();//获取账号
QString password = this->ui->lineEdit_password->text();//获取密码
if(username.isEmpty() && password.isEmpty()){
QMessageBox::information(this, "提示", "信息不完整");
return;
}
QByteArray inputPasswordData = password.toUtf8();
// 使用相同的哈希算法来处理用户输入的密码
QByteArray hash = QCryptographicHash::hash(inputPasswordData, QCryptographicHash::Sha256);
// 把哈希结果转换为16进制字符串,用于与数据库中存储的哈希比对
QString inputPasswordHash = hash.toHex();
query.prepare("SELECT * FROM administrator WHERE username = :username AND passwordHash = :inputPasswordHash");
query.bindValue(":username", username);
query.bindValue(":inputPasswordHash", inputPasswordHash);
if(query.exec()){
if(query.next()){
// 登录成功
Home *homepage = new Home;
homepage->setWindowFlags(Qt::FramelessWindowHint);
homepage->setGeometry(this->geometry()); //获取当前串口的x和y
this->close();
homepage->show();
} else {
// 登录失败
QMessageBox::warning(this, "登录失败", "账号或密码错误");
}
}
else {
qDebug()<<query.last();
}
}
注册过程:
①收集用户信息:获取用户输入的用户名、密码、确认密码和注册密钥。
②输入验证:检查用户提供的所有字段是否完整,确保注册密钥正确,以及两次输入的密码是否一致。这些步骤保证了提交到后端的数据是有效和期望的。
③密码哈希处理:将用户密码转换为QByteArray,然后使用QCryptographicHash类的Sha256算法计算密码的哈希值。这确保了存储在数据库中的数据不是明文密码,即使在数据库被非法访问的情况下,密码信息也不容易被泄露。
④存储用户信息:将用户名和对应的密码哈希值存入数据库,这里使用prepare和bindValue是为了防止SQL注入攻击。
⑤反馈结果:根据数据库操作的结果,显示相应的信息给用户,如果注册成功,通知用户成功信息;如果失败,提供错误信息。
登录过程:
①获取用户凭据:接收用户输入的用户名和密码。
②输入验证:检查用户名和密码是否都已输入,未输入时给出提示。
③密码哈希比对:使用与注册相同的哈希算法处理用户输入的密码,计算得到密码的哈希值,并将其与数据库中存储的哈希值进行比对。
④身份验证:通过查询数据库中存储的用户名和密码哈希值的组合来验证用户。如果查询成功并返回记录,表示用户名和密码匹配,登录成功;如果没有返回记录,表示凭据无效。
⑤登录反馈:根据验证结果,向用户显示登录成功或登录失败的信息。成功时进入系统,失败时给予错误提示。
保证数据安全的措施:
哈希算法:使用SHA-256哈希算法确保密码不以明文形式存储。
防止SQL注入:使用预处理语句和参数绑定,有效防止SQL注入攻击。
输入验证:检查用户输入的完整性和一致性,避免不合法数据的提交。
注册秘钥(SECRET_KEY):管理员需要获取注册秘钥才能进行注册的操作,因为人脸识别考勤管理系统所有用户信息都会在服务器端呈现,因此,保护用户隐私安全极其重要,我们需要有注册账户权利的管理员才能进行注册操作。
8.界面整合
(1)创建一个QMainWindow(Home)用于主页面(注意设置可显示的大小为960 X 480,我们前面所有的页面尺寸均为960X480)。并且增加一个QTableWidget来装载所有的页面。
(2)创建好后直接提升为我们前面创建好的Widget即可
提升后如图所示:
现在已经可以在Home中顺利切换各个页面了,但是还有个小问题就是,在注册界面时,我们打开摄像头,但是切换到其他页面时,如果不关闭摄像头,就会导致摄像头一直开启,导致资源的浪费,所以我们关联QTabWidget 的 currentChanged信号,来实现当页面切换出注册界面时,自动关闭摄像头。
首先在Register中实现释放摄像头的函数
//释放摄像头资源
void Register::closeCamera()
{
if(cap.isOpened()){
frameTimer->stop();
cap.release();
}
}
接下来在Home中连接信号与槽函数即可
// 使用 lambda 表达式连接 currentChanged 信号
connect(ui->tabWidget, &QTabWidget::currentChanged, [this](int index) {
// 当从 Register 界面切换出去时关闭摄像头
if (index != 4) { // 已知的 registerW 的索引号4
this->ui->registerW->closeCamera(); // 关闭摄像头
}
});
三.效果预览
最后的最后,我们终于完成了这个项目,经测试,功能完善,异常处理得当,效果良好!完结撒花!