【基于Qt和OpenCV的多线程图像识别应用】

前言

这是一个简单的小项目,使用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的时候一定要配置好环境哦,这也是一个相对比较麻烦的事情,可以看看其他博主的教程!
点赞加关注,从此不迷路!!

猜你喜欢

转载自blog.csdn.net/a1379292747/article/details/133341407