项目源码地址https://github.com/fufufu11/QT5-FacialDetection
项目概述
本项目是一款基于Qt5框架构建的人脸检测应用程序,支持多摄像头选择和用户友好的图形界面。系统集成了百度的人脸检测API,能够通过HTTPS协议POST方法安全地发送请求,并解析返回的JSON数据来获取人脸特征信息,如年龄、性别、情绪状态及是否佩戴口罩等。为确保高性能和稳定性,项目还采用了多线程技术优化图像处理流程。
主要功能
- 实时人脸检测:系统能够实时检测并识别来自多个摄像头输入的视频流中的人脸。
- 人脸属性分析:分析并展示检测到的人脸的详细属性信息。
- 高效图像处理:通过多线程技术优化了图像处理逻辑,确保在处理大量数据时系统的响应速度和稳定性。
技术细节
- 开发环境:Windows 11, Qt Creator 12.0.2, Qt 5.15.2 MinGW 64-bit
- 编程语言:C++
- 框架与库:Qt5
- 网络通信:HTTPS
- 数据处理:JSON解析与Base64编码转换
- 并发处理:多线程技术
项目亮点
- 跨摄像头兼容性:支持多种摄像头设备,增强了系统的灵活性和实用性。
- 安全通信:实现了HTTPS请求的SSL配置,保障了数据传输的安全性。
- 性能优化:通过多线程技术显著提高了图像处理效率,解决了高负载下的系统卡顿问题
项目实际效果展示
部分源码分析
调用摄像头功能与绘制人脸框
1.获取所有的摄像头信息
camerasInfo = QCameraInfo::availableCameras();
2.遍历摄像头信息添加到下拉框中
for(const QCameraInfo &cameraInfo : camerasInfo)
{
// qDebug() << cameraInfo.description();
ui->comboBox->addItem(cameraInfo.description());
}
遍历camerasInfo列表中的每一个摄像头信息,并将其描述信息(通常是摄像头的名称)添加到UI组件comboBox中。这样用户就可以从下拉列表中选择他们想要使用的摄像头。
3.设置摄像头选择事件处理
connect(ui->comboBox,QOverload<int>::of(&QComboBox::currentIndexChanged),this,&ImageRecognition::pickCamera);
当用户更改comboBox的选择时,会触发pickCamera槽函数。这里使用了QOverload来指定信号的类型。
void ImageRecognition::pickCamera(int idx)
{
qDebug() << camerasInfo.at(idx).description();
refreshTimer->stop();
camera->stop();
camera = new QCamera(camerasInfo.at(idx));
imageCapture = new QCameraImageCapture(camera);
connect(imageCapture,&QCameraImageCapture::imageCaptured,this,&ImageRecognition::showCamera);
imageCapture->setCaptureDestination(QCameraImageCapture::CaptureToBuffer);
camera->start();
refreshTimer->start();
}
在Qt中选择并初始化一个摄像头设备,并设置图像捕获。这个函数pickCamera接收一个索引参数idx,用于从可用的摄像头设备列表中选择一个具体的摄像头,并启动图像捕获。
void ImageRecognition::showCamera(int id,QImage preview)
{
Q_UNUSED(id);
img = preview;
//绘制人脸框
QPen pen(Qt::white);
pen.setWidth(5);
QPainter p(&preview);
p.setPen(pen);
p.drawRect(faceLeft,faceTop,faceWidth,faceHeight);
QFont font;
font.setPixelSize(50);
p.setFont(font);
p.drawText(faceLeft+faceWidth+5,faceTop,QString("年龄:").append(QString::number(age)));
p.drawText(faceLeft+faceWidth+5,faceTop+50,QString("性别:").append(gender=="male"?"男":"女"));
p.drawText(faceLeft+faceWidth+5,faceTop+100,QString("情绪:").append(emotion));
p.drawText(faceLeft+faceWidth+5,faceTop+150,QString("颜值:").append(QString::number(beauty)));
p.drawText(faceLeft+faceWidth+5,faceTop+200,QString("口罩:").append(mask==1?"佩戴":"未佩戴"));
p.drawText(faceLeft+faceWidth+5,faceTop+250,QString("眼镜:").append(glasses=="none"?"未佩戴":glasses=="common"?"普通眼镜":"墨镜"));
p.drawText(faceLeft+faceWidth+5,faceTop+300,QString("真人可能性:").append(QString::number(liveness)));
ui->label->setPixmap(QPixmap::fromImage(preview));
}
代码段展示了一个名为showCamera的方法,该方法接收一个图像预览(QImage preview)和一个标识符(int id),并在图像上绘制人脸框以及一些与人脸识别相关的文本信息,这些信息是通过解析百度人脸检测平台返回的JSON数据得来的。
4.初始化和摄像头相关的部件
camera = new QCamera();
finder = new QCameraViewfinder();
imageCapture = new QCameraImageCapture(camera);
创建一个QCamera实例、一个QCameraViewfinder实例以及一个QCameraImageCapture实例。
5.设置摄像头预览窗口
camera->setViewfinder(finder);
6.设置摄像头捕捉模式
camera->setCaptureMode(QCamera::CaptureStillImage);
设置摄像头捕捉模式为静止图像模式。
7.设置摄像头捕获目的地
imageCapture->setCaptureDestination(QCameraImageCapture::CaptureToBuffer);
设置图像捕获的目标为内存缓冲区,即捕获的图像是存储在内存中而不是磁盘上。
8.连接图像捕获信号到槽函数
connect(imageCapture,&QCameraImageCapture::imageCaptured,this,&ImageRecognition::showCamera);
当图像被捕获后,imageCaptured信号会被发射,并且连接到了showCamera槽函数。这意呀着每次捕获图像后都会调用showCamera来处理捕获的图像。
9.连接按钮点击事件到槽函数
connect(ui->pushButton,&QPushButton::clicked,this,&ImageRecognition::controlWorker);
当用户点击按钮ui->pushButton时,controlWorker槽函数会被调用。这个函数用了单独的一个线程来执行图像处理有关的任务,也就是单独处理图片转化为Base64编码的操作。
10.启动摄像头
camera->start();
总结来说,这段代码主要实现了从多个摄像头中选择一个,并设置好相应的组件来捕获静止图像的功能。用户可以通过界面选择摄像头,并通过点击按钮来触发图像捕获。捕获后的图像数据将会被传递给showCamera函数处理。
定时器操作(刷新拍照页面和进行人脸识别请求)
1.创建定时器
refreshTimer = new QTimer();
netTimer = new QTimer();
创建两个QTimer对象,分别用于刷新拍照界面和进行人脸识别请求。
2.连接定时器的timeout
信号到槽函数
- 刷新拍照页面
connect(refreshTimer, &QTimer::timeout, this, &ImageRecognition::takePicture);
当refreshTimer的timeout信号被触发时,将调用ImageRecognition类中的takePicture槽函数。
- 进行人脸识别请求
connect(netTimer, &QTimer::timeout, this, &ImageRecognition::controlWorker);
当netTimer的timeout信号被触发时,将调用ImageRecognition类中的controlWorker槽函数。
- 启动定时器
- 刷新拍照界面定时器
refreshTimer->start(20);
启动refreshTimer定时器,每隔20毫秒触发一次timeout信号。
- 人脸识别请求定时器
netTimer->start(20);
启动netTimer定时器,每隔20毫秒触发一次timeout信号。
界面布局
//进行界面布局
this->resize(1200,700);
finder->setMinimumSize(600,400);
QVBoxLayout* vbox = new QVBoxLayout();
QVBoxLayout* vbox_2 = new QVBoxLayout();
vbox->addWidget(ui->label);
vbox->addWidget(ui->pushButton);
vbox_2->addWidget(ui->comboBox);
vbox_2->addWidget(finder);
vbox_2->addWidget(ui->textBrowser);
QHBoxLayout* hbox = new QHBoxLayout(this);
hbox->addLayout(vbox);
hbox->addLayout(vbox_2);
this->setLayout(hbox);
配置URL以及发送GET请求获取token
1.配置URL
// URL参数设置
QUrl url("https://aip.baidubce.com/oauth/2.0/token");
QUrlQuery query;
query.addQueryItem("grant_type", "client_credentials");
query.addQueryItem("client_id", "oQXBR1aW4BrlvqrvMpCAkyCd");
query.addQueryItem("client_secret", "BxjQcEpqiKDuUR19e1vv3F4CFTBQgPpj");
url.setQuery(query);
这段代码设置了用于向百度AI平台请求访问令牌(access token)的URL及其查询参数。具体来说,这段代码构建了一个带有特定查询参数的URL,用于通过客户端凭证(client credentials)授权方式获取访问令牌。
代码功能概述
- 创建 URL:创建一个 QUrl 对象,指向百度AI平台的 OAuth 2.0 访问令牌端点。
- 设置查询参数:创建一个 QUrlQuery 对象,并添加必要的查询参数。设置 grant_type 为 client_credentials,表示使用客户端凭证授权方式。设置 client_id 和 client_secret,这两个参数是百度AI平台为你的应用分配的唯一标识符和密钥。
- 合并查询参数到 URL:将 QUrlQuery 对象中的查询参数附加到 QUrl 对象中。
2.SSL配置
因为处理的是 HTTPS 请求,所以需要配置一下SSL,设置TLS,需要加入必要的库,可以参考这篇文章https://blog.csdn.net/gm2418/article/details/129865506
//SSL配置
sslConfig = QSslConfiguration::defaultConfiguration();
sslConfig.setPeerVerifyMode(QSslSocket::QueryPeer);
sslConfig.setProtocol(QSsl::TlsV1_2);
//组装请求
QNetworkRequest req;
req.setUrl(url);
req.setSslConfiguration(sslConfig);
//发送get请求
tokenManager->get(req);
- 代码解释
SSL配置:
获取默认的SSL配置。
设置对等验证模式为QueryPeer。
设置TLS协议版本为TlsV1_2。 - 组装请求:
创建一个QNetworkRequest对象。
设置请求的目标URL。
设置请求的SSL配置。 - 发送GET请求:
使用QNetworkAccessManager发送GET请求
访问 token 请求及相应处理
1.创建网络访问管理器
tokenManager = new QNetworkAccessManager(this);
connect(tokenManager, &QNetworkAccessManager::finished, this, &ImageRecognition::tokenReply);
创建一个QNetworkAccessManager对象,并将其与槽函数tokenReply连接起来。这样,当网络请求完成时,QNetworkAccessManager会触发finished信号,从而调用tokenReply函数来处理回复。
2.定义槽函数tokenReply
void ImageRecognition::tokenReply(QNetworkReply *reply)
{
// 错误处理
if (reply->error() != QNetworkReply::NoError)
{
qDebug() << reply->errorString();
return;
}
// 正常应答
const QByteArray reply_data = reply->readAll();
// JSON解析
QJsonParseError jsonErr;
QJsonDocument doc = QJsonDocument::fromJson(reply_data, &jsonErr);
// 解析成功
if (jsonErr.error == QJsonParseError::NoError)
{
QJsonObject obj = doc.object();
if (obj.contains("access_token"))
{
accessToken = obj.take("access_token").toString();
}
ui->textBrowser->setText(accessToken);
}
// 解析失败
else
{
qDebug() << "JSON ERR:" << jsonErr.errorString();
}
netTimer->start(1000);
reply->deleteLater();
}
- 错误处理
if (reply->error() != QNetworkReply::NoError)
{
qDebug() << reply->errorString();
return;
}
检查网络请求是否出现错误。如果请求失败,打印错误信息并提前返回。
2.读取回复数据
const QByteArray reply_data = reply->readAll();
读取网络请求的回复数据,并将其存储在一个QByteArray对象中。
3.JSON解析
QJsonParseError jsonErr;
QJsonDocument doc = QJsonDocument::fromJson(reply_data, &jsonErr);
尝试将回复数据解析为一个QJsonDocument对象。QJsonParseError对象用于记录解析过程中可能发生的错误。
4.检测解析结果
if (jsonErr.error == QJsonParseError::NoError)
{
QJsonObject obj = doc.object();
if (obj.contains("access_token"))
{
accessToken = obj.take("access_token").toString();
}
ui->textBrowser->setText(accessToken);
}
else
{
qDebug() << "JSON ERR:" << jsonErr.errorString();
}
如果解析成功,从JSON对象中提取access_token字段,并将其存储在accessToken变量中。
将提取到的access_token显示在textBrowser中。
如果解析失败,打印解析错误信息。
- 重新启动定时器
netTimer->start(1000);
重新启动定时器netTimer,使其每隔1000毫秒(1秒)再次触发请求。
6.释放资源
reply->deleteLater();
释放QNetworkReply对象,避免内存泄漏。
这段代码主要用于处理网络请求的回复,并从回复中提取JSON格式的数据。具体来说,它实现了以下功能:
- 检查网络请求是否成功。
- 读取并解析网络请求的回复数据。
- 提取JSON数据中的access_token字段。
- 显示提取到的access_token。
- 重新启动定时器以继续发起新的请求。
- 释放网络请求对象以避免内存泄漏。
多线程处理BASE64编码并POST发送
1.定时器设置
// 利用定时器不断进行人脸识别请求
netTimer = new QTimer();
connect(netTimer, &QTimer::timeout, this, &ImageRecognition::controlWorker); // 因为转换成 base64 编码会造成卡顿,把这部分放到线程里执行
- 创建定时器:
- netTimer = new QTimer(); 创建一个新的QTimer对象。
- 连接信号和槽:
- connect(netTimer, &QTimer::timeout, this, &ImageRecognition::controlWorker); 将定时器的timeout信号连接到ImageRecognition类的controlWorker槽函数。每当定时器超时时,就会触发controlWorker方法,从而开始新的一轮人脸识别处理。
2.controlWorker方法
void ImageRecognition::controlWorker()
{
childThread = new QThread(this);
Worker* worker = new Worker;
worker->moveToThread(childThread);
connect(this, &ImageRecognition::operate, worker, &Worker::doWork);
connect(worker, &Worker::resultReady, this, &ImageRecognition::beginFaceDetect);
connect(childThread, &QThread::finished, worker, &QObject::deleteLater);
childThread->start();
emit operate(img, childThread);
}
- 创建新的线程:
- childThread = new QThread(this); 创建一个新的QThread对象。
- 创建Worker对象:
- Worker* worker = new Worker; 创建一个新的Worker对象。
- 将Worker对象移动到新线程:
- worker->moveToThread(childThread); 将Worker对象移动到新创建的线程childThread中。
- 连接信号和槽:
- connect(this, &ImageRecognition::operate, worker, &Worker::doWork); 连接ImageRecognition类的operate信号到Worker类的doWork槽函数。
- connect(worker, &Worker::resultReady, this, &ImageRecognition::beginFaceDetect); 连接Worker类的resultReady信号到ImageRecognition类的beginFaceDetect槽函数。
- connect(childThread, &QThread::finished, worker, &QObject::deleteLater); 连接QThread的finished信号到Worker对象的deleteLater槽函数,确保当线程结束后,Worker对象会被自动删除。
- 启动线程并开始工作:
- childThread->start(); 启动新线程。
- emit operate(img, childThread); 发射operate信号,传递当前图像img和新线程childThread作为参数,触发Worker对象中的doWork方法开始执行。
3.Worker::doWork方法
void Worker::doWork(QImage img, QThread* childThread)
{
// 转换成 base64 编码
QByteArray ba;
QBuffer buff(&ba);
img.save(&buff, "png");
QString b64str = ba.toBase64();
// 请求体 body 参数设置
QJsonObject postJson;
QJsonDocument doc;
postJson.insert("image", b64str);
postJson.insert("image_type", "BASE64");
postJson.insert("face_field", "age,expression,face_shape,gender,glasses,quality,eye_status,emotion,face_type,mask,beauty");
postJson.insert("liveness_control", "NORMAL");
doc.setObject(postJson);
QByteArray postData = doc.toJson(QJsonDocument::Compact);
emit resultReady(postData, childThread);
}
- 转换图像为Base64编码:
- QByteArray ba; 创建一个字节数组。
- QBuffer buff(&ba); 创建一个缓冲区QBuffer,并将其关联到字节数组ba。
- img.save(&buff, “png”); 将QImage对象保存为PNG格式到QBuffer中。
QString b64str = ba.toBase64(); 将字节数组转换为Base64编码的字符串。
- 构建请求体:
- 创建一个QJsonObject对象,并插入必要的键值对,包括图像的Base64编码、图像类型以及其他人脸识别所需的信息。
- 创建一个QJsonDocument对象,并设置其内容为QJsonObject。
- 将QJsonDocument转换为QByteArray格式。
- 发射resultReady信号,传递处理好的postData和当前线程childThread作为参数。
4.beginFaceDetect方法
void ImageRecognition::beginFaceDetect(QByteArray postData, QThread* overThread)
{
overThread->exit();
overThread->wait();
// 组装图像识别请求
QUrl url("https://aip.baidubce.com/rest/2.0/face/v3/detect");
QUrlQuery query;
query.addQueryItem("access_token", accessToken);
url.setQuery(query);
// 组装请求
QNetworkRequest req;
req.setHeader(QNetworkRequest::ContentTypeHeader, QVariant("application/json"));
req.setUrl(url);
req.setSslConfiguration(sslConfig);
imgManager->post(req, postData);
}
- 退出并等待线程:
- overThread->exit(); 使正在运行的子线程退出。
- overThread->wait(); 等待子线程完全退出,确保所有在该线程中运行的任务都已完成。
- 组装请求URL:
- 创建一个QUrl对象,指定百度AI平台的API端点地址。
- 创建一个QUrlQuery对象,并向其中添加access_token查询参数。
- 将查询参数应用到URL对象上,形成最终的请求URL。
- 创建网络请求:
- 创建一个QNetworkRequest对象。
- 设置请求的内容类型为application/json。
- 设置请求的目标URL。
- 设置请求的SSL配置,确保HTTPS请求的安全性。
- 发送POST请求:
- 使用QNetworkAccessManager对象imgManager发送一个POST请求,将处理好的postData作为请求体发送给百度AI平台进行人脸识别。
JSON解析(人脸信息提取与显示)
imgManager = new QNetworkAccessManager(this);
connect(imgManager,&QNetworkAccessManager::finished,this,&ImageRecognition::imgReply);
void ImageRecognition::imgReply(QNetworkReply *reply)
{
if(reply->error() != QNetworkReply::NoError)
{
qDebug() << reply->errorString();
return;
}
const QByteArray replyData = reply->readAll();
qDebug() << replyData;
QString faceInfo = "";
QJsonParseError jsonErr;
QJsonDocument doc = QJsonDocument::fromJson(replyData,&jsonErr);
if(jsonErr.error == QJsonParseError::NoError)
{
QJsonObject obj = doc.object();
if(obj.contains("timestamp"))
{
int tmpTime = obj.take("timestamp").toInt();
if(tmpTime < latestTime)
{
return;
}
else
{
latestTime = tmpTime;
}
}
if(obj.contains("result"))
{
QJsonObject resultObj = obj.take("result").toObject();
//取出人脸列表
if(resultObj.contains("face_list"))
{
QJsonArray faceList = resultObj.take("face_list").toArray();
//取出第一张人脸信息
QJsonObject faceObject = faceList.at(0).toObject();
//取出人脸定位信息
if(faceObject.contains("location"))
{
QJsonObject locObj = faceObject.take("location").toObject();
if(locObj.contains("left"))
{
faceLeft = locObj.take("left").toDouble();
}
if(locObj.contains("top"))
{
faceTop = locObj.take("top").toDouble();
}
if(locObj.contains("width"))
{
faceWidth = locObj.take("width").toDouble();
}
if(locObj.contains("height"))
{
faceHeight = locObj.take("height").toDouble();
}
}
//取出年龄
if(faceObject.contains("age"))
{
age = faceObject.take("age").toDouble();
faceInfo.append("年龄:").append(QString::number(age)).append("\r\n");
}
//取出性别
if(faceObject.contains("gender"))
{
QJsonObject genderObj = faceObject.take("gender").toObject();
if(genderObj.contains("type"))
{
gender = genderObj.take("type").toString();
faceInfo.append("性别:").append(gender=="male"?"男":"女").append("\r\n");
}
}
//取出情绪
if(faceObject.contains("emotion"))
{
QJsonObject emotionObj = faceObject.take("emotion").toObject();
if(emotionObj.contains("type"))
{
emotion = emotionObj.take("type").toString();
faceInfo.append("情绪:").append(emotion).append("\r\n");
}
}
//取出颜值
if(faceObject.contains("beauty"))
{
beauty = faceObject.take("beauty").toDouble();
faceInfo.append("颜值:").append(QString::number(beauty)).append("\r\n");
}
//检测是否戴口罩
if(faceObject.contains("mask"))
{
QJsonObject maskObj = faceObject.take("mask").toObject();
if(maskObj.contains("type"))
{
mask = maskObj.take("type").toInt();
faceInfo.append("是否佩戴口罩:").append(mask?"戴口罩":"未佩戴口罩").append("\r\n");
}
}
//检测是否戴眼镜
if(faceObject.contains("glasses"))
{
QJsonObject glassesObj = faceObject.take("glasses").toObject();
if(glassesObj.contains("type"))
{
glasses = glassesObj.take("type").toString();
faceInfo.append("是否戴眼镜:");
if(glasses == "none")
{
faceInfo.append("未佩戴眼镜").append("\r\n");
}
else if(glasses == "common")
{
faceInfo.append("佩戴普通眼镜").append("\r\n");
}
else if(glasses == "sun")
{
faceInfo.append("佩戴墨镜").append("\r\n");
}
}
}
//取出活体检测值
if(faceObject.contains("liveness"))
{
QJsonObject livenessObj = faceObject.take("liveness").toObject();
if(livenessObj.contains("livemapscore"))
{
liveness = livenessObj.take("livemapscore").toDouble();
faceInfo.append("是真人的可能性:").append(QString::number(liveness));
}
}
ui->textBrowser->setText(faceInfo);
}
}
}
else
{
qDebug() << "JSON ERR:" << jsonErr.errorString();
}
reply->deleteLater();
}
这段代码定义了一个槽函数 imgReply,用于处理通过QNetworkAccessManager 发起的网络请求的回复。具体来说,它解析从服务器接收到的 JSON 数据,并从中提取有关人脸的各种信息,如年龄、性别、情绪、颜值、是否戴口罩、是否戴眼镜以及活体检测值等。
遇到的问题及其解决方案
1.在性能测试过程中,发现了由于大量图片处理导致的系统卡顿问题。通过详细的代码审查(白盒测试),定位到了问题根源在于Base64编码转换过程中消耗了过多的计算资源。为了解决这个问题,引入了多线程机制来分散处理任务,从而有效地减轻了单个线程的压力,并通过回归测试验证了方案的有效性。
什么是BASE64编码?
BASE64编码是一种用于将二进制数据转换为文本格式的编码方案。
主要目的是确保数据能够在只支持文本传输的环境中安全传输,同时保持数据的完整性和可读性。
BASE64编码的核心思想是将二进制数据转换为一组可打印ASCII字符,从而确保数据能够在各种系统之间可靠地传输。
为什么BASE64编码要占用大量CPU和内存资源?
- 数据膨胀。BASE64编码过程中,每3个字节的二进制数据会被编码成4个字节的文本数据。这意味着原始数据量会增加大约33%。这种数据膨胀会导致更大的内存和CPU负担,尤其是在处理大量数据时。
- 内存操作。在编码的过程中,需要将原始二进制数据读取到内存中,并进行一系列的操作。如:1.读取和缓存原始数据。需要将原始二进制数据加载到内存中。2.位运算。3.查找和替换,通过查表替换字符。4.将编码后的字符拼接成最终的字符串
2.在向HTTPS的网页发出请求的时候,需要在QT5中配置一下SSL/TLS,需要安装OpenSSL相应的版本库。
https://blog.csdn.net/gm2418/article/details/129865506?spm=1001.2014.3001.5506
这篇文章里面介绍了如何安装