zoukankan      html  css  js  c++  java
  • GOQTTemplate3的多线程化改造

        GOQTTemplate3作为一个QT+OpenCV的平台,希望能够为使用者提供基础的跨平台的图像处理框架。图像处理算法和GUI两个线程的隔离,是必然需要的。在之前的版本中,都采用了OnTimer的方法:
        
        并且在选择并打开摄像头的时候,开启这个timer

        看上去没有问题,但是实际上这种“线程”处理的方法低效却又粗暴;最为严重的是,它可能会降低整个程序的效率。这篇博客,就是从现有问题出发、引入相关资料提出自己的思考,并且最终尝试得到“优雅”的解决方法。
    一、使用QTimer存在的问题;
        图像处理问题面临的棘手问题之一,就是“效率”:一个实时的图像处理算法,其单幅处理时间需要降低到50ms以下,这个困难不言而喻;反过来说,比较耗时的视频算法,经常是存在的。在使用QTimer的这种情况下,这种比较耗时的图像处理算法,很可能会拉低整个程序的运行速度。
        我们可以通过一个简单的例子来观察:
        通过将现有的图像处理函数,修改为一个比较耗时的操作:
    这种情况下,不仅整个界面不响应输入,而且会出现假死亡的情况:
    那们这里模拟的就是比较极端的耗时线程,我们会将这个耗时操作在后面反复使用。
    二、引入moveToThread函数;
        要说moveToThread是什么,最好的资料是QT文档。它是一个QObject的函数,也就是基本上所有QT对象都会继承这个函数。
    void QObject::moveToThread(QThread *targetThread)
    myObject->moveToThread(QApplication::instance()->thread());
        而我们肯定是要将处理视频的这个工作线程插入到主线程中去。工作线程它的特点,就是重复进行从摄像头中获取图片->处理这张图片->显示处理结果这个过程。
        使用Timer方法,是没有办法使用moveToThread函数的,必须要将工作线程独立出来。
    三、引入Process线程的解决方法;
        《ComputerVision with opencv3 and qt5》书中为我们提供了非常好的例子,首先来学习:
        通过引入videoprocessor这个QT对OpenCV videocapture的再封装来解决问题,其中使用的信号/槽机制非常精彩,代码可以说是非常精简的。
    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;
    }; 
      
    其实现方式:
    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;
    }
    在其实现中,只有一个信号量“ stopped 主要是StartVideo函数,而图像处理算法以“三明治”方式加载StartVideo函数中。比较一下,这里直接将inFrameoutFrame 这两个Mat以信号的方式emit出来,而后在主线程中
    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(stopVideo()));

        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;
    }
    直接以这种方式启动Started和finished(这两者都是Thread自己的函数),并且将VideoProcessor 传出的两个信号直接显示在界面上。
    这种情况下,将那句
    写入主要线程,产生的结果是虽然有2、3秒的延时,但是还是最终能够退掉的。证明这种效果下线程的时效性更好,应该被采用。
    其中,值得注意的是moveToThread的运用,是在主线程中生成工作线程,而后通过moveToThread方法插入主线程中去。
    四、采用videoprocess方法升级GOQttemplate
        细节不赘述,你可以直接看结果。这里需要说明的是改成这种线程方法后可能存在的问题。在原来的方法中,由于函数都在主线程中,这样包括videocapture的号,是否使用算法等问题,都直接可以传递变量。但是在目前这种情况下,则必须采用一些方法才能够传递。
    比如原代码中的StartVideo,默认打开的是camera(0),这个肯定是需要修改的。
    这里就涉及到一些传值的问题,我们还是力图用最简单、直接的方法解决问题,这里还是采用传递信号变量的方式。

    值得注意的是,由于这里采用了多线程,所以在打开新摄像头的时候要尤其注意。

    //打开摄像头
    void MainWindow::on_pushButton_OpenCam_clicked()
    {
        //stop camera first
        processor->stopVideo();
        processor->thread()->quit();
        processor->thread()->wait();
        //打开摄像头,从摄像头中获取视频
        processor->n_cameraindex =  ui->comboCamera->currentIndex();
        processor->thread()->start();
    }

    下面我将其它功能进行完善。

    五、将camera线程和图像处理线程分开
        在前面提到了“从摄像头中获取图片->处理这张图片->显示处理结果”,实际上这不是一个原子操作,图片的获取、显示和图片的处理应该是可以分开了的。
         但是这里肯定就涉及到了较为复杂的线程间通信问题。那么这个操作对于用户体验是否会有提高了?答案应该是要和这个项目本身有关。对于视频处理程序来说,只关心的是最终处理的结果,那么将处理操作放在工作线程中(而不是开两个工作线程)就是最省时间的方法;但是也可能存在这种情况,一方面用户需要看到全图,另一方面又需要将处理的结果叠加到原图上去,那么分两个工作现场就是必要的了。
         在这个思想的指导下,我完成了这方面工作设计:
         
         为了创造2个线程,所以就必须创建2个类。其中1个是这样
    1个是这样
    实现这块,主要是看这个消息/槽的机制
    这个连接的定义,是写在主程序中的。为了让Mat能够被QT识别,还需要写一句
    感谢阅读至此,希望有所帮助!






  • 相关阅读:
    drf3
    字典的操作方法
    列表的操作方法
    字符串的操作方法
    while循环和基本运算符
    初识数据类型
    USDT相关
    带团队
    CentOS7更改时区及同步网络时间
    mac胡刷新dns
  • 原文地址:https://www.cnblogs.com/jsxyhelu/p/10713349.html
Copyright © 2011-2022 走看看