《OpenCV3和Qt5计算机视觉应用开发》学习笔记第8章

本章将介绍在线程中用opencv处理图像,在Qt中创建线程有两种方式,一种是继承QThread类,另一个是通过moveToThread(new QThread(this));

不久之前,计算机程序还被设计和构建成一个接一个地运行的一系列指令。实际上,这种方法非常容易理解且易于实现,即便在今天,我们也使用相同的方法来编写以串行方式处理所需任务的脚本和简单的应用程序。然而,随着时间的推移,尤其是随着更强大处理器的出现,多任务处理问题日益凸显出来。人们希望计算机能够一次执行多个任务,这是因为计算机运行速度快,能够执行多个程序的指令,并且仍有一些空闲时间。当然,随着时间的推移,人们编写了更为复杂的应用程序(如游戏、图形程序等),此时需要处理器公平地管理不同程序所占用的时间片,以便所有程序都能正确运行。程序(或进程,在该环境中使用这个词更合适)被分割成更小的碎片,称为线程。这种方法(或称为多线程)到目前为止已经帮助人们创建了响应性更好、速度更快的进程,这样的进程能够与类似或完全不相关的进程一起运行,从而流畅地完成多任务处理任务。
在单处理器(且单核)计算机上,每个线程都被分配了一个时间片,而且很明显,处理器一次只能处理一个线程,但是通常多个线程之间可以快速地切换,因此,在用户看来,这种方式看起来像是真正的并行运行。然而,现在人们随身携带的大部分智能手机里的处理器都有能力使用处理器中的多个内核来处理多个线程。
为保证我们对线程及其用法有一个清晰的认识,并深入理解为什么必须使用线程才能编写强大的计算机视觉程序,下面请看进程与线程的主要区别:
❑ 进程与单个程序类似,可以由操作系统直接执行。
❑ 线程是一个进程的子集,也就是说,一个进程可以包含多个线程。
❑ 通常情况下,不同的进程彼此是无关的,而不同线程共享内存和资源(注意,进程可以通过操作系统提供的手段实现彼此间的交互)。
取决于设计方式的不同,每个进程可能或不可能创建和执行不同的线程以获得最优性能及响应性。另一方面,每个线程都执行一个进程所需的特定任务。在Qt和GUI编程中,一个这方面的典型例子是进度信息。运行一个复杂且耗时的进程时,通常需要显示一些有关进度的阶段以及状态信息,如剩余工作的百分比、完成所需的剩余时间等。最好将实际任务和GUI更新任务分解到不同的线程中。计算机视觉中的另一个常见例子是视频(或摄像机)处理,你需要确保在需要时能够正确读取、处理和显示视频。这也是本章学习Qt框架中的多线程功能时应该重点关注的内容。
本章将介绍以下主题:
❑ Qt中的多线程方法
❑ 如何使用Qt中的QThread和多线程类
❑ 如何创建响应型GUI
❑ 如何处理多个图像
❑ 如何处理多个摄像机或视频

8.1 Qt中的多线程
Qt框架为处理应用程序中的多线程问题提供了各种不同的技术。正如将在本章中看到的,QThread类用于处理各种多线程功能,QThread类也是Qt框架中用于处理线程的最为强大且灵活的方式。除了QThread之外,Qt框架还提供了很多其他的命名空间、类和函数,它们可以帮助处理各种多线程需求。在学习它们的用法之前,先来看看它们的列表:
❑QThread:该类是Qt框架中所有线程的基类,可以从QThread派生子类以创建新的线程。此时,需要重写run方法,或者创建QThread的新实例,并通过调用moveToThread函数将任何Qt对象(QObject子类)移动到新线程中。
❑QThreadPool:可用于管理线程,并且允许重用已有线程以实现新的功能,从而降低线程创建的成本。每个Qt应用程序都包含一个全局QThreadPool实例,可通过使用QThreadPool::globalInstance()静态函数来访问该实例。QThreadPool类可与QRunnable类的实例一同使用,以控制、管理并回收Qt应用程序中的runnable对象。
❑QRunnable:可提供创建线程的另一种方法,这是Qt中所有runnable对象的基类。与QThread不同,QRunnable不是QObject的子类,它可用作一段需要执行的代码的接口。你需要编写一个派生自QRunnable的类,并重写其中的纯虚函数run(),以便能够使用QRunnable。正如前面介绍的那样,QRunnable的实例是由QThreadPool类管理的。
❑QMutex、QMutexLocker、QSemaphore、QWaitCondition、QReadLocker、QWrite-Locker和QWriteLocke:这些类主要用于处理线程间的同步任务。根据情况的不同,这些类可用来避免各种问题,比如线程相互覆盖计算结果、线程试图读取或写入一次只能处理一个线程的设备以及其他一些类似的问题。在创建多线程应用程序时,经常需要手动处理此类问题。
❑QtConcurrent:QtConcurrent是一个命名空间,可用于使用高级API创建多线程应用程序。能够使多线程应用程序的编写更容易,无须处理互斥锁、信号量以及线程间同步问题。
❑Qfuture、QfutureWatcher、QFututeIterator和QFutureSynchronizer:这些类与QtConcurrent命名空间共同使用,可以处理多线程及异步操作结果。
一般来说,Qt中有两种不同的多线程处理方法。第一种是基于QThread的低级方法,该方法提供了很强的灵活性,并有效地实现了对线程的控制,但是需要更多的代码及耐心才能使线程准确无误地工作。此外,还有很多其他方法能够以更少的工作量,使用QThread创建多线程应用程序,这部分内容将在本章中介绍。第二种方法基于QtConcurrent命名空间(或Qt并发框架),这是在一个应用程序中创建并运行多个任务的高级方法。
8.2 利用QThread实现低级多线程
这一节将介绍如何使用QThread及其附属类来创建多线程应用程序。我们将通过创建一个示例项目来说明这一过程,该示例项目使用一个单独的线程处理和显示来自一个视频源的输入帧和输出帧。这有助于使GUI线程(主线程)保持空闲及可响应性,而用第二线程处理更密集的进程。正如前面介绍的那样,我们将主要关注计算机视觉及GUI开发中的常见用例,但是,相同或类似的方法可以应用于任何多线程问题。
通过利用Qt中可用的两种不同方法来使用QThread类,我们将使用该示例项目实现多线程。首先子类化并重写run方法,然后使用所有Qt对象(换句话说,也就是Qobject子类)中可用的moveToThread函数。
8.2.1 子类化QThread
首先,在QtCreator中创建名一个为MultithreadedCV的示例Qt控件应用程序,与在本书开始几章学习到的方式一样,将OpenCV框架添加到本项目中:在MultithreadedCV.pro文件中包含下面的

win32: {
    include("./opencv.pri")
}

unix: !macx{
    CONFIG += link_pkgconfig
    PKGCONFIG += opencv
}

unix: macx{
INCLUDEPATH += /usr/local/include
LIBS += -L"/usr/local/lib" \
    -lopencv_world
}

opencv.pri文件的内容就是包含头文件目录和动态库依赖路径

创建一个类继承QThread
class VideoProcessorThread : public QThread
重写里面的run函数。

void VideoProcessorThread::run()
{
    using namespace cv;
    VideoCapture camera(0);
    Mat inFrame, outFrame;
    while(camera.isOpened() && !isInterruptionRequested())
    {
        camera >> inFrame;
        if(inFrame.empty())
            continue;

        bitwise_not(inFrame, outFrame);

        emit inDisplay(QPixmap::fromImage(QImage(inFrame.data, inFrame.cols, inFrame.rows, inFrame.step, QImage::Format_RGB888).rgbSwapped()));

        emit outDisplay(QPixmap::fromImage(QImage(outFrame.data,outFrame.cols,outFrame.rows,outFrame.step,QImage::Format_RGB888).rgbSwapped()));
    }
}

run函数只能在内部调用,因此只需重新实现它,如本例所示。但是,为了控制线程及其执行行为,需要用到以下函数:
❑start:可用于启动一个尚未启动的线程,该函数通过调用所实现的run函数来启动执行。可以将下面的某个值传递给start函数以实现对线程优先级的控制
○ QThread::IdlePriority(没有其他线程运行时调度它)
○ QThread::LowestPriority
○ QThread::LowPriority
○ QThread::NormalPriority
○ QThread::HighPriority
○ QThread::HighestPriority
○ QThread::TimeCriticalPriority(尽可能频繁地调度)
○ QThread::InheritPriority(默认值,只继承父进程的优先级)

❑ terminate:这个函数只在极端情况下使用(不鼓励使用该函数),它强制终止线程。
❑ setTerminationEnabled:可用于允许或禁止terminate函数。
❑ wait:该函数可用于阻塞线程(强制等待),直至线程完成或者达到超时值(以毫秒为单位)。
❑requestInterruption和isRequestInterrupted:可用于设置和获取中断请求状态,通过恰当地使用这些函数,可确保在一个可永久运行的进程的中间能够安全地停止线程。
❑ isRunning和isFinished:可用于请求线程的执行状态。
除了上述函数之外,QThread还包含其他一些用于处理多线程的函数,例如quit、exit、idealThreadCount等。最好的办法是自己了解并学习每一个用例。QThread是一个强大的类,可以帮助你最大限度地提高应用程序的效率。

让我们继续我们的例子。在run函数中,使用OpenCV的VideoCapture类来读取视频帧,并将一个简单的bitwise_not运算符应用于Mat帧(此时,也可以进行任何其他图像处理操作,因为bitwise_not只是用于解释相关观点的一个较为简单的例子),然后通过QImage将它转换为QPixmap,然后使用两个信号来发送原始及修改后的帧。请注意,在循环中,这个过程将永远持续下去,同时将始终检查摄像头是否仍处于打开状态,并检查该线程是否存在中断请求。现在,在MainWindow中使用线程,首先将其头文件包含在mainwindow.h文件中:
#include "videoprocessorthread.h"
然后,在mainwindow.h文件中,将下面的代码行添加到MainWindow的私有成员部分:
VideoProcessorThread processor;
现在,将下面的代码添加至MainWindow构造函数中,放在setupUi代码行之后:
connect(&processor, SIGNAL(inDisplay(QPixmap)), ui->inVideo, SLOT(setPixmap(QPixmap)));
connect(&processor, SIGNAL(outDisplay(QPixmap)), ui->outVideo, SLOT(setPixmap(QPixmap)));
然后将下列代码段添加至MainWindow析构函数中,放在delete ui;代码行之前:
processor.requestInterruption();
processor.wait();
将VideoProcessorThread类的两个信号连接至已经添加到MainWindowGUI的两个标签,然后,在一旦程序启动时启动线程。还在一旦MainWindow关闭并且在删除GUI之前请求线程停止。通过调用wait函数,能够确保等待线程完成清理并安全地执行完毕之后再继续执行delete指令。

一旦程序启动,计算机上默认摄像头的视频应该随之启动。一旦程序关闭,视频也将停止。通过向VideoProcessorThread类传递摄像头索引号或视频文件路径,可扩展VideoProcessorThread类。你可以实例化任意数量的VideoProcessorThread类。只需确保其信号连接到GUI上正确的控件,通过这种方式,可以在运行时动态地处理并显示多个视频或摄像头。

8.2.2 使用moveToThread函数
如前所述,还可以使用任意QObject子类的moveToThread函数,以确保它在一个单独的线程中运行。为了介绍它的工作原理,将重复前文中的示例,创建完全相同的GUI,并创建新的C++类(和之前的一样),但是,这次将其命名为VideoProcessor。该类创建之后,不需从QThread而是保留从QObject继承它。请将以下成员添加到videoprocessor.h文件中:
signals:
    void inDisplay(QPixmap pixmap);
    void outDisplay(QPixmap pixmap);

public slots:
    void startVideo();
    void stopVideo();

private:
    bool stopped;
这些信号与之前的完全一样。stopped是标志符,用于帮助终止视频,使之不会一直持续下去。startVideo和stopVideo函数用来启动和终止对来自默认摄像头的视频的处理。现在,可以切换到videoprocessor.cpp文件,添加下列代码块。与之前的情况非常类似,明显的区别是不需要实现run函数,因为这不是QThread的子类,此外,还另外命名了函数。

void VideoProcessor::startVideo()
{
    using namespace cv;
    VideoCapture camera(0);
    Mat inFrame, outFrame;
    stopped = false;
    while(camera.isOpened() && !stopped)
    {
        camera >> inFrame;
        if(inFrame.empty())
            continue;

        bitwise_not(inFrame, outFrame);

        emit inDisplay(QPixmap::fromImage(QImage(inFrame.data, inFrame.cols, inFrame.rows, inFrame.step, QImage::Format_RGB888).rgbSwapped()));

        emit outDisplay(QPixmap::fromImage(QImage(outFrame.data, outFrame.cols, outFrame.rows, outFrame.step, QImage::Format_RGB888).rgbSwapped()));
    }
}

void VideoProcessor::stopVideo()
{
    qDebug() << Q_FUNC_INFO;
    stopped = true;
}

现在,可以在MainWindow类中使用它。请确保为VideoProcessor类添加include文件,然后将以下内容添加到MainWindow的私有成员部分:

VideoProcessor *processor;

现在,将下列代码段添加至mainwindow.cpp文件内的MainWindow构造函数中:

    processor = new VideoProcessor();
    processor->moveToThread(new QThread(this));
    connect(processor->thread(), SIGNAL(started()), processor, SLOT(startVideo()));
    connect(processor->thread(), SIGNAL(finished()), processor, SLOT(deleteLater()));
    connect(processor, SIGNAL(inDisplay(QPixmap)), ui->inVideo, SLOT(setPixmap(QPixmap)));
    connect(processor, SIGNAL(outDisplay(QPixmap)), ui->outVideo, SLOT(setPixmap(QPixmap)));
    processor->thread()->start();

在前面的代码段中,首先创建了VideoProcessor的实例。请注意,在构造函数中没有分配任何父函数,并且还确保将其定义为指针。这在我们打算使用moveToThread函数时是至关重要的。有父对象的对象不能移动到新线程中。上述代码段中第二个非常重要的注意点是不应该直接调用VideoProcessor的startVideo函数,而应通过将一个适当的信号连接到它进行调用。在这里,使用了它自己的线程的启动信号;但是,也可以使用有相同签名的任何其他信号。其余部分都是有关连接的。

在MainWindow析构函数中,添加以下代码行:

processor->stopVideo();
processor->thread()->quit();
processor->thread()->wait();

上述代码段的功能很明显,但为清楚起见,让我们再做一个解释,即以这种方式启动线程之后,必须通过调用quit函数来终止该线程,并且在其对象中不应含有任何正在运行的循环或挂起的指令。如果不满足这两个条件中的任意一个,则在处理线程时,会遇到严重的问题。

下面是完整的项目代码

MultithreadedCV.pro

QT       += core gui

greaterThan(QT_MAJOR_VERSION, 4): QT += widgets

TARGET = MultithreadedCV
TEMPLATE = app

# The following define makes your compiler emit warnings if you use
# any feature of Qt which as been marked as deprecated (the exact warnings
# depend on your compiler). Please consult the documentation of the
# deprecated API in order to know how to port your code away from it.
DEFINES += QT_DEPRECATED_WARNINGS

# You can also make your code fail to compile if you use deprecated APIs.
# In order to do so, uncomment the following line.
# You can also select to disable deprecated APIs only up to a certain version of Qt.
#DEFINES += QT_DISABLE_DEPRECATED_BEFORE=0x060000    # disables all the APIs deprecated before Qt 6.0.0


SOURCES += \
        main.cpp \
        mainwindow.cpp \
    videoprocessorthread.cpp \
    videoprocessor.cpp

HEADERS += \
        mainwindow.h \
    videoprocessorthread.h \
    videoprocessor.h

FORMS += \
        mainwindow.ui

win32: {
    include("./opencv.pri")
}

unix: !macx{
    CONFIG += link_pkgconfig
    PKGCONFIG += opencv
}

unix: macx{
INCLUDEPATH += /usr/local/include
LIBS += -L"/usr/local/lib" \
    -lopencv_world
}
videoprocessor.h文件
#ifndef VIDEOPROCESSOR_H
#define VIDEOPROCESSOR_H

#include <QObject>
#include <QPixmap>
#include <QDebug>
#include <QMutex>
#include <QReadWriteLock>
#include <QSemaphore>
#include <QWaitCondition>
#include "opencv2/opencv.hpp"

class VideoProcessor : public QObject
{
    Q_OBJECT
public:
    explicit VideoProcessor(QObject *parent = nullptr);

signals:
    void inDisplay(QPixmap pixmap);
    void outDisplay(QPixmap pixmap);

public slots:
    void startVideo();
    void stopVideo();

private:
    bool stopped;

};

#endif // VIDEOPROCESSOR_H

videoprocessor.cpp文件

#include "videoprocessor.h"

VideoProcessor::VideoProcessor(QObject *parent)
    : QObject(parent)
{

}

void VideoProcessor::startVideo()
{
    using namespace cv;
    VideoCapture camera(0);
    Mat inFrame, outFrame;
    stopped = false;
    while(camera.isOpened() && !stopped)
    {
        camera >> inFrame;
        if(inFrame.empty())
            continue;

        bitwise_not(inFrame, outFrame);

        emit inDisplay(QPixmap::fromImage(QImage(inFrame.data, inFrame.cols, inFrame.rows, inFrame.step, QImage::Format_RGB888).rgbSwapped()));

        emit outDisplay(QPixmap::fromImage(QImage(outFrame.data, outFrame.cols, outFrame.rows, outFrame.step, QImage::Format_RGB888).rgbSwapped()));
    }
}

void VideoProcessor::stopVideo()
{
    qDebug() << Q_FUNC_INFO;
    stopped = true;
}
videoprocessorthread.h文件
#ifndef VIDEOPROCESSORTHREAD_H
#define VIDEOPROCESSORTHREAD_H

#include <QObject>
#include <QThread>
#include <QPixmap>
#include "opencv2/opencv.hpp"

class VideoProcessorThread : public QThread
{
    Q_OBJECT
public:
    explicit VideoProcessorThread(QObject *parent = nullptr);

signals:
    void inDisplay(QPixmap pixmap);
    void outDisplay(QPixmap pixmap);

public slots:

private:
    void run() override;

};

#endif // VIDEOPROCESSORTHREAD_H

videoprocessorthread.cpp文件

#include "videoprocessorthread.h"

VideoProcessorThread::VideoProcessorThread(QObject *parent)
    : QThread(parent)
{

}

void VideoProcessorThread::run()
{
    using namespace cv;
    VideoCapture camera(0);
    Mat inFrame, outFrame;
    while(camera.isOpened() && !isInterruptionRequested())
    {
        camera >> inFrame;
        if(inFrame.empty())
            continue;

        bitwise_not(inFrame, outFrame);

        emit inDisplay(QPixmap::fromImage(QImage(inFrame.data, inFrame.cols, inFrame.rows, inFrame.step, QImage::Format_RGB888).rgbSwapped()));

        emit outDisplay(QPixmap::fromImage(QImage(outFrame.data,outFrame.cols,outFrame.rows,outFrame.step,QImage::Format_RGB888).rgbSwapped()));
    }
}
mainwindow.h文件
#ifndef MAINWINDOW_H
#define MAINWINDOW_H

#include <QMainWindow>

#include "videoprocessor.h"
#include <QThread>

namespace Ui {
class MainWindow;
}

class MainWindow : public QMainWindow
{
    Q_OBJECT

public:
    explicit MainWindow(QWidget *parent = 0);
    ~MainWindow();

private:
    Ui::MainWindow *ui;

    VideoProcessor *processor;
};

#endif // MAINWINDOW_H

mainwindow.cpp文件

#include "mainwindow.h"
#include "ui_mainwindow.h"

MainWindow::MainWindow(QWidget *parent) :
    QMainWindow(parent),
    ui(new Ui::MainWindow)
{
    ui->setupUi(this);

    processor = new VideoProcessor();

    processor->moveToThread(new QThread(this));

    connect(processor->thread(), SIGNAL(started()), processor, SLOT(startVideo()));

    connect(processor->thread(), SIGNAL(finished()), processor, SLOT(deleteLater()));

    connect(processor, SIGNAL(inDisplay(QPixmap)), ui->inVideo, SLOT(setPixmap(QPixmap)));

    connect(processor, SIGNAL(outDisplay(QPixmap)), ui->outVideo, SLOT(setPixmap(QPixmap)));

    processor->thread()->start();
}

MainWindow::~MainWindow()
{
    processor->stopVideo();
    processor->thread()->quit();
    processor->thread()->wait();

    delete ui;
}

main.cpp文件

#include "mainwindow.h"
#include <QApplication>

int main(int argc, char *argv[])
{
    QApplication a(argc, argv);
    MainWindow w;
    w.show();

    return a.exec();
}

运行结果如下(要接一个摄像头)

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值