基于Qt和OpenCV的多线程图像识别应用
- 前言
- 多线程编程
- 为什么需要多线程
- Qt如何实现多线程
- 线程间通信
- 图像识别
- 项目代码
- 项目结构
- 各部分代码
- 项目演示
- 小结
前言
这是一个简单的小项目,使用Qt和OpenCV构建的多线程图像识别应用程序,旨在识别图像中的人脸并将结果保存到不同的文件夹中。这个项目结合了图像处理、多线程编程和用户界面设计。 用户可以通过界面选择要识别的文件夹和保存结果的文件夹。然后,启动识别进程。图像识别线程并行处理选定文件夹中的图像,检测图像中的人脸并将其保存到一个文件夹,同时将不包含人脸的图像保存到另一个文件夹。进度和结果将实时显示在用户界面上。
多线程编程
为什么需要多线程
1、并行处理:在处理大量图像时,使用单线程可能会导致应用程序变得非常慢,因为它必须依次处理每个图像(这里我没有去实现,感兴趣的小伙伴可以自己尝试一下)。多线程允许应用程序同时处理多个图像,从而提高了处理速度。
2、防止阻塞:如果在主线程中执行耗时的操作,比如图像识别,会导致用户界面在操作执行期间被冻结,用户无法与应用程序互动。多线程可以将这些耗时操作移到后台线程,以避免界面阻塞。
3、利用多核处理器:现代计算机通常具有多核处理器,多线程可以充分利用这些多核来加速任务的执行。
Qt如何实现多线程
在项目中,多线程编程主要使用了Qt的 QThread 类来实现。以下是在项目中使用多线程的关键步骤:
1、继承QThread类: 首先,创建一个自定义的线程类,继承自 QThread 类。这个类将负责执行多线程任务。在项目中,这个自定义线程类是 ImageRecognitionThread。
2、重写run函数: 在自定义线程类中,重写 run 函数。run 函数定义了线程的执行体,也就是线程启动后会执行的代码。在本项目中,run 函数包含了图像识别的逻辑。
3、创建线程对象: 在应用程序中,创建自定义线程类的对象,例如 ImageRecognitionThread 的对象。然后,通过调用 start 函数来启动线程。
4、信号和槽机制: 使用Qt的信号和槽机制来实现线程间的通信。在项目中,使用信号来更新识别进度和结果,以便主线程可以实时显示这些信息。
5、线程安全性: 要确保多个线程安全地访问共享资源,例如文件系统或图像数据,通常需要使用互斥锁(Mutex)等机制来防止竞争条件和数据损坏。
线程间通信
在线程间进行通信是多线程编程中的关键概念,特别是在项目中,其中一个线程负责图像识别任务,另一个线程用于用户界面更新。在这个项目中,使用了Qt的信号和槽机制来实现线程间的通信,以便更新识别进度和结果。具体步骤如下:
1、信号和槽的定义:
首先定义了信号和槽函数,分别用于更新进度和结果:
signals:
void updateProgress(int progress);
void updateResult(const QString& result);
private slots:
void onProgressUpdate(int progress);
void onResultUpdate(const QString& result);
updateProgress 信号用于更新识别进度,它接受一个整数参数,表示识别进度的百分比。
updateResult 信号用于更新识别结果,它接受一个字符串参数,表示识别的结果信息。
onProgressUpdate 槽函数用于接收进度更新信号,并在主线程中更新用户界面的进度条。
onResultUpdate 槽函数用于接收结果更新信号,并在主线程中更新用户界面的结果文本。
2、信号的发射:
在 ImageRecognitionThread 类的 run 函数中,根据图像识别的进度和结果,使用以下方式发射信号:
// 发射进度更新信号
emit updateProgress(progress);
// 发射结果更新信号
emit updateResult("图像 " + imageFile + " 中检测到人脸并已保存。");
通过 emit 关键字,可以发射定义的信号,并传递相应的参数。
3、槽函数的连接:
在主线程中,当创建 ImageRecognitionThread 的对象时,需要建立信号和槽的连接,以便接收来自线程的信号并执行槽函数。这通常在主窗口类的构造函数中完成。例如:
// 创建ImageRecognitionThread对象
imageThread = new ImageRecognitionThread(this);
// 连接信号和槽
connect(imageThread, &ImageRecognitionThread::updateProgress, this, &MainWindow::onProgressUpdate);
connect(imageThread, &ImageRecognitionThread::updateResult, this, &MainWindow::onResultUpdate);
这些连接操作确保当 ImageRecognitionThread 中的信号被发射时,相关的槽函数会在主线程中执行。
4、槽函数的执行:
槽函数会在主线程中执行,因此可以直接更新用户界面的进度条和结果文本。例如:
void MainWindow::onProgressUpdate(int progress)
{
ui->progressBar->setValue(progress);
}
void MainWindow::onResultUpdate(const QString& result)
{
ui->resultTextEdit->append(result);
}
在这里,onProgressUpdate 槽函数更新了主窗口中的进度条,而 onResultUpdate 槽函数更新了结果文本框。
通过信号和槽机制,项目中的不同线程能够安全地进行通信,而不会导致竞争条件或数据损坏。这种机制允许图像识别线程实时更新识别进度和结果,同时保持了用户界面的响应性,提供了更好的用户体验。
图像识别
图像识别的流程是这个项目的核心部分,它包括了加载图像、使用OpenCV的人脸检测器识别人脸、以及根据结果保存图像等步骤。以下是详细描述的图像识别流程:
1、加载图像:
首先,从用户选择的识别文件夹中加载图像。这个步骤包括以下操作:
获取用户选择的识别文件夹路径。
遍历该文件夹中的所有图像文件。
逐个加载图像文件。在项目中,可以使用OpenCV库的 cv::imread 函数来加载图像。
// 从文件夹中加载图像
cv::Mat image = cv::imread(imageFile.toStdString());
2、 人脸识别:
一旦图像加载完成,接下来的任务是识别图像中的人脸。这个项目使用OpenCV提供的人脸检测器来完成这个任务,通常使用Haar级联分类器或深度学习模型。在本项目中,我们使用了OpenCV内置的Haar级联分类器。
创建一个 cv::CascadeClassifier 对象并加载Haar级联分类器的XML文件。
cv::CascadeClassifier faceCascade;
faceCascade.load("haarcascade_frontalface_default.xml");
使用加载的分类器检测图像中的人脸。这将返回一个矩形列表,每个矩形表示一个检测到的人脸的位置。
std::vector<cv::Rect> faces;
faceCascade.detectMultiScale(image, faces, scaleFactor, minNeighbors, flags, minSize, maxSize);
根据检测到的人脸位置,可以在图像上绘制矩形框,以标记人脸的位置。
for (const cv::Rect& faceRect : faces) {
cv::rectangle(image, faceRect, cv::Scalar(0, 255, 0), 2); // 在图像上绘制矩形框
}
3、 结果保存:
最后,根据识别的结果,将图像保存到相应的文件夹。在本项目中,根据是否检测到人脸,有两个不同的保存路径:一个用于保存包含人脸的图像,另一个用于保存不包含人脸的图像。
如果检测到了人脸,将图像保存到包含人脸的文件夹中。
if (!faces.empty()) {
QString savePathWithFace = saveFolderPath + "/with_face/" + QFileInfo(imageFile).fileName();
cv::imwrite(savePathWithFace.toStdString(), image);
}
如果没有检测到人脸,将图像保存到不包含人脸的文件夹中。
else {
QString savePathWithoutFace = saveFolderPath + "/without_face/" + QFileInfo(imageFile).fileName();
cv::imwrite(savePathWithoutFace.toStdString(), image);
}
以上就是图像识别的主要流程。通过这个流程,项目能够加载、识别和保存图像,根据识别结果将图像分别保存到两个不同的文件夹中,以实现人脸识别功能。这个流程结合了OpenCV的图像处理能力,为图像识别提供了一个基本框架。
项目代码
项目结构
项目分为两个主要部分:
1、用户界面:使用Qt框架创建,包括选择识别文件夹、选择保存结果文件夹、启动和停止识别等功能。
2、图像识别线程:使用Qt的QThread类创建,负责加载图像、识别人脸、保存结果,并通过信号和槽机制与用户界面通信。
各部分代码
1、imagerecognitionthread.h
#ifndef IMAGERECOGNITIONTHREAD_H
#define IMAGERECOGNITIONTHREAD_H
#include <QThread>
#include <QString>
class ImageRecognitionThread : public QThread
{
Q_OBJECT
public:
explicit ImageRecognitionThread(QObject* parent = nullptr);
void setFolderPath(const QString& folderPath);
void setSaveFolderPath(const QString& saveFolderPath);
protected:
void run() override;
signals:
void updateProgress(int progress);
void updateResult(const QString& result);
private:
QString folderPath;
QString saveFolderPath;
};
#endif
2、imagerecognitionthread.cpp
#include "imagerecognitionthread.h"
#include <opencv2/opencv.hpp>
#include <QDir>
ImageRecognitionThread::ImageRecognitionThread(QObject* parent)
: QThread(parent), folderPath(""), saveFolderPath("")
{
}
void ImageRecognitionThread::setFolderPath(const QString& folderPath)
{
this->folderPath = folderPath;
}
void ImageRecognitionThread::setSaveFolderPath(const QString& saveFolderPath)
{
this->saveFolderPath = saveFolderPath;
}
void ImageRecognitionThread::run()
{
QString faceCascadePath = "D:\\DownLoad\\opencv\\sources\\data\\haarcascades\\haarcascade_frontalface_default.xml";
cv::CascadeClassifier faceCascade;
if (!faceCascade.load(faceCascadePath.toStdString()))
{
emit updateResult("无法加载人脸检测器");
return;
}
QDir imageDir(folderPath);
QStringList imageFilters;
imageFilters << "*.jpg" << "*.png";
QStringList imageFiles = imageDir.entryList(imageFilters, QDir::Files);
int totalImages = imageFiles.size();
int processedImages = 0;
QString faceSaveFolderPath = saveFolderPath + "/faces"; // 用于保存包含人脸的图像的文件夹
QString noFaceSaveFolderPath = saveFolderPath + "/no_faces"; // 用于保存不包含人脸的图像的文件夹
// 创建保存结果的文件夹
QDir().mkpath(faceSaveFolderPath);
QDir().mkpath(noFaceSaveFolderPath);
for (const QString& imageFile : imageFiles)
{
processedImages++;
int progress = (processedImages * 100) / totalImages;
emit updateProgress(progress);
QString imagePath = folderPath + "/" + imageFile;
cv::Mat image = cv::imread(imagePath.toStdString());
if (!image.empty())
{
std::vector<cv::Rect> faces;
faceCascade.detectMultiScale(image, faces, 1.1, 4, 0 | cv::CASCADE_SCALE_IMAGE, cv::Size(30, 30));
if (!faces.empty())
{
QString targetPath = faceSaveFolderPath + "/" + imageFile;
cv::imwrite(targetPath.toStdString(), image);
emit updateResult("图像 " + imageFile + " 中检测到人脸并已保存到人脸文件夹。");
}
else
{
QString targetPath = noFaceSaveFolderPath + "/" + imageFile;
cv::imwrite(targetPath.toStdString(), image);
emit updateResult("图像 " + imageFile + " 中未检测到人脸并已保存到非人脸文件夹。");
}
}
}
emit updateResult("识别完成,结果保存在相应文件夹中");
}
3、mainwindow.h
#ifndef MAINWINDOW_H
#define MAINWINDOW_H
#include <QMainWindow>
#include <QLineEdit>
#include <QPushButton>
#include <QLabel>
#include <QProgressBar>
#include <QListWidget>
#include "imagerecognitionthread.h"
class MainWindow : public QMainWindow
{
Q_OBJECT
public:
MainWindow(QWidget* parent = nullptr);
private slots:
void startRecognition();
void stopRecognition();
void updateProgress(int progress);
void updateResult(const QString& result);
void selectRecognitionFolder();
void selectSaveFolder();
private:
void setupUi();
void connectSignalsAndSlots();
QLineEdit* folderPathLineEdit;
QLineEdit* saveFolderPathLineEdit;
QPushButton* startButton;
QPushButton* stopButton;
QPushButton* selectRecognitionFolderButton;
QPushButton* selectSaveFolderButton;
QLabel* progressLabel;
QProgressBar* progressBar;
QLabel* resultsLabel;
QListWidget* resultsList;
ImageRecognitionThread* recognitionThread;
};
#endif // MAINWINDOW_H
4、mainwindow.cpp
#include "mainwindow.h"
#include "imagerecognitionthread.h"
#include <QVBoxLayout>
#include <QFileDialog>
#include <QDebug>
MainWindow::MainWindow(QWidget* parent)
: QMainWindow(parent), recognitionThread(nullptr)
{
setupUi();
connectSignalsAndSlots();
}
void MainWindow::startRecognition()
{
// 获取文件夹路径
QString folderPath = folderPathLineEdit->text();
QString saveFolderPath = saveFolderPathLineEdit->text(); // 获取保存结果的文件夹路径
// 创建并启动识别线程
recognitionThread = new ImageRecognitionThread(this);
recognitionThread->setFolderPath(folderPath);
recognitionThread->setSaveFolderPath(saveFolderPath); // 设置保存结果的文件夹路径
connect(recognitionThread, &ImageRecognitionThread::updateProgress, this, &MainWindow::updateProgress);
connect(recognitionThread, &ImageRecognitionThread::updateResult, this, &MainWindow::updateResult);
recognitionThread->start();
}
void MainWindow::stopRecognition()
{
// 如果识别线程正在运行,终止它
if (recognitionThread && recognitionThread->isRunning())
{
recognitionThread->terminate();
recognitionThread->wait();
}
}
void MainWindow::updateProgress(int progress)
{
progressBar->setValue(progress);
}
void MainWindow::updateResult(const QString& result)
{
resultsList->addItem(result);
}
void MainWindow::setupUi()
{
// 创建和布局UI组件
folderPathLineEdit = new QLineEdit(this);
saveFolderPathLineEdit = new QLineEdit(this); // 用于保存结果的文件夹路径
startButton = new QPushButton("开始识别", this);
stopButton = new QPushButton("停止识别", this);
selectRecognitionFolderButton = new QPushButton("选择识别文件夹", this); // 选择识别文件夹按钮
selectSaveFolderButton = new QPushButton("选择保存文件夹", this); // 选择保存文件夹按钮
progressLabel = new QLabel("进度:", this);
progressBar = new QProgressBar(this);
resultsLabel = new QLabel("结果:", this);
resultsList = new QListWidget(this);
QVBoxLayout* layout = new QVBoxLayout();
layout->addWidget(folderPathLineEdit);
layout->addWidget(selectRecognitionFolderButton); // 添加选择识别文件夹按钮
layout->addWidget(saveFolderPathLineEdit); // 添加用于保存结果的文件夹路径输入框
layout->addWidget(selectSaveFolderButton); // 添加选择保存文件夹按钮
layout->addWidget(startButton);
layout->addWidget(stopButton);
layout->addWidget(progressLabel);
layout->addWidget(progressBar);
layout->addWidget(resultsLabel);
layout->addWidget(resultsList);
QWidget* centralWidget = new QWidget(this);
centralWidget->setLayout(layout);
setCentralWidget(centralWidget);
}
void MainWindow::connectSignalsAndSlots()
{
connect(startButton, &QPushButton::clicked, this, &MainWindow::startRecognition);
connect(stopButton, &QPushButton::clicked, this, &MainWindow::stopRecognition);
connect(selectRecognitionFolderButton, &QPushButton::clicked, this, &MainWindow::selectRecognitionFolder); // 连接选择识别文件夹按钮的槽函数
connect(selectSaveFolderButton, &QPushButton::clicked, this, &MainWindow::selectSaveFolder); // 连接选择保存文件夹按钮的槽函数
}
void MainWindow::selectRecognitionFolder()
{
QString folderPath = QFileDialog::getExistingDirectory(this, "选择识别文件夹", "", QFileDialog::ShowDirsOnly | QFileDialog::DontResolveSymlinks);
folderPathLineEdit->setText(folderPath);
}
void MainWindow::selectSaveFolder()
{
QString saveFolderPath = QFileDialog::getExistingDirectory(this, "选择保存结果的文件夹", "", QFileDialog::ShowDirsOnly | QFileDialog::DontResolveSymlinks);
saveFolderPathLineEdit->setText(saveFolderPath);
}
项目演示
小结
特别提醒:在使用OpenCv的时候一定要配置好环境哦,这也是一个相对比较麻烦的事情,可以看看其他博主的教程!
点赞加关注,从此不迷路!!