• 【基于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);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    updateProgress 信号用于更新识别进度,它接受一个整数参数,表示识别进度的百分比。
    updateResult 信号用于更新识别结果,它接受一个字符串参数,表示识别的结果信息。
    onProgressUpdate 槽函数用于接收进度更新信号,并在主线程中更新用户界面的进度条。
    onResultUpdate 槽函数用于接收结果更新信号,并在主线程中更新用户界面的结果文本。

    2、信号的发射
    在 ImageRecognitionThread 类的 run 函数中,根据图像识别的进度和结果,使用以下方式发射信号:

    // 发射进度更新信号
    emit updateProgress(progress);
    
    // 发射结果更新信号
    emit updateResult("图像 " + imageFile + " 中检测到人脸并已保存。");
    
    • 1
    • 2
    • 3
    • 4
    • 5

    通过 emit 关键字,可以发射定义的信号,并传递相应的参数。

    3、槽函数的连接
    在主线程中,当创建 ImageRecognitionThread 的对象时,需要建立信号和槽的连接,以便接收来自线程的信号并执行槽函数。这通常在主窗口类的构造函数中完成。例如:

    // 创建ImageRecognitionThread对象
    imageThread = new ImageRecognitionThread(this);
    
    // 连接信号和槽
    connect(imageThread, &ImageRecognitionThread::updateProgress, this, &MainWindow::onProgressUpdate);
    connect(imageThread, &ImageRecognitionThread::updateResult, this, &MainWindow::onResultUpdate);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    这些连接操作确保当 ImageRecognitionThread 中的信号被发射时,相关的槽函数会在主线程中执行。

    4、槽函数的执行

    槽函数会在主线程中执行,因此可以直接更新用户界面的进度条和结果文本。例如:

    void MainWindow::onProgressUpdate(int progress)
    {
        ui->progressBar->setValue(progress);
    }
    
    void MainWindow::onResultUpdate(const QString& result)
    {
        ui->resultTextEdit->append(result);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    在这里,onProgressUpdate 槽函数更新了主窗口中的进度条,而 onResultUpdate 槽函数更新了结果文本框。
    通过信号和槽机制,项目中的不同线程能够安全地进行通信,而不会导致竞争条件或数据损坏。这种机制允许图像识别线程实时更新识别进度和结果,同时保持了用户界面的响应性,提供了更好的用户体验。

    图像识别

    图像识别的流程是这个项目的核心部分,它包括了加载图像、使用OpenCV的人脸检测器识别人脸、以及根据结果保存图像等步骤。以下是详细描述的图像识别流程:

    1、加载图像

    首先,从用户选择的识别文件夹中加载图像。这个步骤包括以下操作:
    获取用户选择的识别文件夹路径。
    遍历该文件夹中的所有图像文件。
    逐个加载图像文件。在项目中,可以使用OpenCV库的 cv::imread 函数来加载图像。

    // 从文件夹中加载图像
    cv::Mat image = cv::imread(imageFile.toStdString());
    
    • 1
    • 2

    2、 人脸识别

    一旦图像加载完成,接下来的任务是识别图像中的人脸。这个项目使用OpenCV提供的人脸检测器来完成这个任务,通常使用Haar级联分类器或深度学习模型。在本项目中,我们使用了OpenCV内置的Haar级联分类器。

    创建一个 cv::CascadeClassifier 对象并加载Haar级联分类器的XML文件。

    cv::CascadeClassifier faceCascade;
    faceCascade.load("haarcascade_frontalface_default.xml");
    
    • 1
    • 2

    使用加载的分类器检测图像中的人脸。这将返回一个矩形列表,每个矩形表示一个检测到的人脸的位置。

    std::vector<cv::Rect> faces;
    faceCascade.detectMultiScale(image, faces, scaleFactor, minNeighbors, flags, minSize, maxSize);
    
    • 1
    • 2

    根据检测到的人脸位置,可以在图像上绘制矩形框,以标记人脸的位置。

    for (const cv::Rect& faceRect : faces) {
        cv::rectangle(image, faceRect, cv::Scalar(0, 255, 0), 2); // 在图像上绘制矩形框
    }
    
    • 1
    • 2
    • 3

    3、 结果保存
    最后,根据识别的结果,将图像保存到相应的文件夹。在本项目中,根据是否检测到人脸,有两个不同的保存路径:一个用于保存包含人脸的图像,另一个用于保存不包含人脸的图像。

    如果检测到了人脸,将图像保存到包含人脸的文件夹中。

    if (!faces.empty()) {
        QString savePathWithFace = saveFolderPath + "/with_face/" + QFileInfo(imageFile).fileName();
        cv::imwrite(savePathWithFace.toStdString(), image);
    }
    
    • 1
    • 2
    • 3
    • 4

    如果没有检测到人脸,将图像保存到不包含人脸的文件夹中。

    else {
        QString savePathWithoutFace = saveFolderPath + "/without_face/" + QFileInfo(imageFile).fileName();
        cv::imwrite(savePathWithoutFace.toStdString(), image);
    }
    
    • 1
    • 2
    • 3
    • 4

    以上就是图像识别的主要流程。通过这个流程,项目能够加载、识别和保存图像,根据识别结果将图像分别保存到两个不同的文件夹中,以实现人脸识别功能。这个流程结合了OpenCV的图像处理能力,为图像识别提供了一个基本框架。

    项目代码

    项目结构

    项目分为两个主要部分:
    1、用户界面:使用Qt框架创建,包括选择识别文件夹、选择保存结果文件夹、启动和停止识别等功能。
    2、图像识别线程:使用Qt的QThread类创建,负责加载图像、识别人脸、保存结果,并通过信号和槽机制与用户界面通信。

    各部分代码

    1、imagerecognitionthread.h

    #ifndef IMAGERECOGNITIONTHREAD_H
    #define IMAGERECOGNITIONTHREAD_H
    
    #include 
    #include 
    
    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 
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29

    2、imagerecognitionthread.cpp

    #include "imagerecognitionthread.h"
    #include 
    #include 
    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("识别完成,结果保存在相应文件夹中");
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76

    3、mainwindow.h

    #ifndef MAINWINDOW_H
    #define MAINWINDOW_H
    
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #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
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46

    4、mainwindow.cpp

    #include "mainwindow.h"
    #include "imagerecognitionthread.h"
    #include 
    #include 
    #include 
    
    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);
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99

    项目演示

    在这里插入图片描述在这里插入图片描述在这里插入图片描述

    小结

    特别提醒:在使用OpenCv的时候一定要配置好环境哦,这也是一个相对比较麻烦的事情,可以看看其他博主的教程!
    点赞加关注,从此不迷路!!

  • 相关阅读:
    【总结】我与Python有个缘
    网络请求中如何封装对话框并调用
    Apache Airflow (九) :Airflow Operators及案例之BashOperator及调度Shell命令及脚本
    hyperf 模型批量更新数据
    洛谷P2451 遗传代码
    数据结构——AVL树(详解 + C++模拟实现)
    Redis 除了做缓存,还能做什么?
    PyTorch - Sequential和ModuleList
    Spring框架系列(3) - 深入浅出Spring核心之控制反转(IOC)
    SpringBoot和Vue实现文件上传与下载——基于SpringBoot和Vue的后台管理系统项目系列博客(十五)
  • 原文地址:https://blog.csdn.net/a1379292747/article/details/133341407