概述
这一部分我们将学习使用QThread及其附属类来创建多线程应用,我们将创建一个示例程序,这个程序在使用单独的线程在界面显示相机的输入、输出图像。这使得GUI线程(主线程)空闲并保持响应,密集的计算通过第二个进程进行。正如之前所述,我们将使用计算机视觉的GUI开发的示例,不过这个方法具有普适性,可用于其它多线程场合。
我们将使用QThread中的两种方式来完成这一任务。第一,子类化并重写run方法。第二,使用Qt objects中都有的moveToThread函数完成多线程。
子类化 QThread
首先创建一个Qt Widgets应用,命名为MultithreadedCV,将openCV库加载到项目中。
在mainwindow.ui文件中,移除工具栏、菜单栏和状态栏,然后添加两个label, 一个用于显示原始图像,另一个用于显示处理后的图像。
确保objectName属性为inVideo和outVideo。将label的alignment/Horizontal设为AlignHCenter。
现在,创建一个名为VideoProcessorThread的新类,右击工程目录在弹出的菜单中选择Add New,然后选择C++ Class,如下图,
创建完成后,videoprocessorthread.h和videoprocessthread.cpp添加到项目中去了,我们将在这个类中实现将GUI与图像显示线程分离。首先,确保此类继承于QThread
#include <QThread>
#include "opencv2/opencv.hpp"
class VidwoProcessorThread : public QThread
在cpp文件中修构造函数代码如下
VidwoProcessorThread::VidwoProcessorThread(QObject *parent)
: QThread(parent)
现在在头文件中添加必要的声明,在私有成员中添加如下代码
void run() override;
然后在signals部分添加:
void inDisplay(QPixmap pixmap);
void outDisplay(QPixmap pixmap);
最后,在cpp文件中添加run函数的实现:
void VidwoProcessorThread::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函数被重写,在run函数的实现中添加了视频处理的任务。假如你在mainwindow.cpp中执行上述代码,你会发现你的程序不会响应,最终强制退出。然而通过这种方式,同样的代码段就得以正确的执行,你只需调用start函数(不是run!),注意run是用来内部调用的,所以你只需实现它的代码即可,要控制这个线程的执行行为,你要使用下面这个函数:
- start: 此函数启动一个线程,调用run函数,可以向该函数传递一个参数以控制线程的优先级
- QThread::LowestPriority
- QThread::LowPriority
- QThread::NormalPriority
- QThread::HighPriority
- QThread::HighesetPriority
- QThread::TimeCriticalPriority (尽可能的多安排处理该线程)
- QThread::InheritPriority(默认值,简单的继承于父对象的优先级)
- terminate: 强制终止线程,仅用于极端情况,尽量不要用
- setTerminationEnabled: 允许或禁止terminate函数
- wait: 强制等待线程结束,或等待时间溢出(time out)
- requestInterruption 和 isRequestInterrupted: 分别为设置和获取中断请求状态,这是中止线程的一个安全的方式。
- isRunning 和 isFinished:用于获取线程的执行状态
除了这些函数外,QThread还有其它有用的函数,例如quit exit idealThreadCount等等。可以自己尝试一下使用这些函数。QThread可以让你的应用程序达到最佳性能。
让我们继续我们的例子,在run函数中,我们使用了VideoCaputre 类来读取视频帧(死循环),然后对原始图像进行了按位取反bitwise_not操作,当然了,你可以对其进行任意的处理,然后发向射了两个信号。注意我们的循环是死的,不断检查摄像机是否开启及线程是否有中断请求。
现在我们在mainwindow.h中添加这个线程子类的头文件
#include "videoprocessorthread.h"
然后在private成员中添加
VideoPorcessorThread processor;
现在我们去MainWindow的构造函数中添加
connect(&processor,
SIGNAL(inDisplay(QPixmap)),
ui->inVidwo,
SLOT(setPixmap(QPixmap));
connect(&processor,
SIGNAL(outDisplay(QPixmap)),
ui->outVideo,
SLOT(setPixmap(QPixmap));
processor.start();
然后在析构函数delete ui;之前添加
processor.requestInterruption();
processor.wait();
我们把VideoProcessorThread类中的两个信号与MainWindow GUI中的label连接起来,然后启动了这个线程。在GUI删除ui之前,我们请求中止线程,wait函数确保了程序在退出之前线程被安全中止并清除。现在你可以运行这个程序了,界面大概是这样的。
程序开始,从默认摄像头中读取的图像,只要程序关闭,这一过程就会中止。你可以开启多个线程,只要connect线程中的信号与界面的槽函数即可,这样你就可以同时动态的显示多个摄像机的内容了。
使用moveToThread函数
之前讲到,你可以在任何QObject中使用moveToThread函数确保其运行在单独线程中。我们用此方法重复上面的示例。首先创建相同的GUI,然后添加一个新C++类,但是这次命名为VideoProcessor,创建完成后,你不需要继承于QThread, 让它保持QObjcet就行,将以下成员加到videoprocessor.h文件中。
signals:
void inDisplay(QPixmap pixmap);
void outDisplay(QPixmap pixmap);
public slots:
void startVideo();
void stopVideo();
private:
bool stopped;
signals与之前的相同,stopped标志位用于中止视频,防止它永久运行。startVideo和stopVideo用于开始和终止从网络摄像头中处理视频。现在我们到videoprocessor.cpp中,添加如下代码。与之前的代码类似,因为这个类不是继承于QThread,所以我们不需要实现run函数了。
void VideoProcessor::startVideo()
{
using namespace cv;
VideoCapture camera(0);
Mat inFrame;
Mat 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()
{
stopped = true;
}
现在我们可以在MainWindow类中引用这个类了,在mainwindow头文件中include videoprocessor,然后在私有成员中添加 一个VideoProcessor成员。
VideoProcessor *processor;
在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函数时,这样做是很关键的。一个有父对象的对象是不可以被移动到一个新线程中去。第二个重要的经验是我们不应该直接调用startVideo函数,它应该connect一个适当的信号,我们这里使用了线程的一个开启信号,你可以使用任何有同样签名的信号来connect。
在MainWindow的析构函数中,添加以下几行
processor->stopVideo();
processor->thread()->quit();
processor->thread()->wait();
像这样开启一个新线程之后,它就必须调用quit函数来终止运行,并且此时线程中不应该有任何的循环及未执行的指令(我们用stopVideo()终止了线程中的所有任务),如果不这样做,将面临严重的线程处理问题。