在上一篇博客中,我们介绍了如何使用OpenCV在主线程中实现实时画面显示以及视频的存储与回放,本文主要介绍如何将摄像头的画面获取放到子线程中


关于线程的创建本文采用继承于QObject+MoveToThread的方法,具体创建方法可以移步Qt多线程的创建详解,本文不做赘述


一、项目创建

首先还是创建一个主窗口项目,命名为multiThreadCamera,完成后在项目上右击–>添加新文件–>C++类,类名为CamThread,继承于QObject,源文件和头文件默认为camthread.cppcamthread.h,并在头文件中加入OpenCV的头文件

#include <opencv2/opencv.hpp>
#include <opencv2/core/core.hpp>
#include <opencv2/videoio.hpp>
#include <opencv2/highgui/highgui.hpp>

声明相关的私有变量

private:
    cv::VideoCapture capture;
    cv::VideoWriter writer;
    cv::Mat src_image;
    bool stopFlag=false;
    int camera_num = 0;

然后在mainwindow.h中添加

#include <QThread>
#include <QTimer>
#include <QDebug>
#include <QThread>
#include <QCameraInfo> 
#include <QList>
#include "camthread.h"

UI界面如图所示

opencv录制出视频时长短 opencv录屏功能实现_qt


控件说明:

控件名

作用

label_videoviewer

画面显示

camera_name

显示摄像设备

pushbutton_searchcamera

查找摄像设备

pushbutton_opencamera

打开摄像头

pushbutton_closecamera

关闭摄像头

pushbutton_savevideo

保存视频

pushbutton_savecomplete

结束保存

pushbutton_videoreview

视频回放

二、功能实现

在编写代码前 ,脑中还是要有一个思路,那就是主线程是干嘛的,子线程是干嘛的
本项目中显而易见——主线程负责画面显示及指令响应,子线程负责调用视频设备与获取画面,线程之间通信主要采用信号与槽机制。搞清楚这些,接下来一步步实现——


这里代码虽然零散,但体现了我编写代码时的一个思路历程,初学者可以尝试阅读 找找思路 文章最后附有完整代码


  1. 线程的创建
    mainwindow.h:
private:
    Ui::MainWindow *ui;
    QThread *firstThread;
    CamThread *MyCamThread;
    QList<QCameraInfo> camera_list;
    QTimer fps_timer;

mainwindow.cpp:

ui->setupUi(this);
 firstThread = new QThread;
 MyCamThread = new CamThread;
 MyCamThread->moveToThread(firstThread);
  1. 各按钮槽函数(直接右击–>转到槽)
    2.1 查找摄像头时先清空下拉框,然后将查到的摄像头信息依次加入到下拉框中:
void MainWindow::on_pushButton_searchcamera_clicked()
{
	ui->camera_name->clear();
	camera_list = QCameraInfo::availableCameras();
	for(auto i =0;i<camera_list.size();i++)
	{
    	ui->camera_name->addItem(camera_list.at(i).description());
	}
}

2.2 打开摄像头时应先获取摄像头标号,在子线程中打开:

//获取摄像头标号
void CamThread::camNumber(const int &n)
{
    camera_num = n; //camera_num 是全局变量,在camthread.h中
}
//打开摄像头
void CamThread::openCamera()
{
    capture.open(camera_num);
    if(!capture.isOpened())
    {
        return;
    }
}

主线程中开启子线程、发送标号、启动定时器、打开摄像头:

void MainWindow::on_pushButton_opencamera_clicked()
{
    if(ui->camera_name->currentIndex() >= 0)
    {
        firstThread->start();
        MyCamThread->camNumber(ui->camera_name->currentIndex());
        fps_timer.start();
        MyCamThread->openCamera();
    }
    else //没有找到视频设备
        QMessageBox::information(this,tr("Error"),tr("Have No Camera Device!"),QMessageBox::Ok);
}

2.3 视频在label上显示
这里主要是通过让子线程每隔50ms向主线程发送一帧画面,然后主线程接受并显示。
主线程:

connect(&fps_timer,    SIGNAL(timeout()), MyCamThread, SLOT(mainwindowDisplay()));
 connect(MyCamThread,SIGNAL(sendPicture(QImage)),this,SLOT(recivePicture(QImage)));
 fps_timer.setInterval(50);


void MainWindow::recivePicture(QImage img)
{
    ui->label_videoViewer->setPixmap(QPixmap::fromImage(img));
}

子线程:

void CamThread::mainwindowDisplay()
{
    capture >> src_image;
    QImage img1 = QImage((const unsigned char*)src_image.data,
                         src_image.cols, src_image.rows, QImage::Format_RGB888).rgbSwapped();
    emit sendPicture(img1);
}

2.4 关闭摄像头:

void CamThread::closeCamera()
{
    capture.release();
    writer.release();
}

主线程中:

void MainWindow::on_pushButton_closecamera_clicked()
{
    fps_timer.stop();
    ui->label_videoViewer->clear();
    MyCamThread->closeCamera();
    firstThread->quit();
    firstThread->wait();
}

2.5保存视频:

void CamThread::startsave()
{
    QString path = QCoreApplication::applicationDirPath().append("/Video/")
            .append(QDateTime::currentDateTime().toString("yyyyMMddhhmmss")).append(".avi");
    cv::String file_path = path.toStdString() ;
    writer.open(file_path,cv::VideoWriter::fourcc('X', 'V', 'I', 'D'), 20.0, cv::Size(640, 480));
    while(!stopFlag)
    {
        capture >> src_image;
        writer.write(src_image);
        cv::namedWindow("video", cv::WINDOW_NORMAL);
        cv::imshow("video", src_image);
        cv::waitKey(50);
    }
}

主线程中:

void MainWindow::on_pushButton_savevideo_clicked()
	{
	    MyCamThread->setFlag(false);
	    MyCamThread->startsave();
	}

2.6 保存完成

void CamThread::closeImshow()
{
    cv::destroyWindow("video");
}
void MainWindow::on_pushButton_savecomplete_clicked()
{
    MyCamThread->setFlag(true);
    MyCamThread->closeImshow();
}

2.7 视频回放
和在主线程的操作一样,只是最后回访结束之后发送结束信号,主线程再进行相应操作。

void CamThread::reviewVideo()
{
    cv::VideoCapture video;
    cv::Mat video_src;
    QString path = QFileDialog::getOpenFileName(0,"打开","../","");
    cv::String openpath = path.toStdString();
    video.open(openpath);
    while(video.isOpened())
    {
        video>>video_src;
        if(video_src.empty())
            break;
        cv::imshow("video_review",video_src);
        if(cv::waitKey(50)==27)
        {
            cv::destroyWindow("video_review");
            break;
        }
    }
    emit reviewComplete();
}

主线程:
因为回放是在摄像头关闭的情况下进行的,所以回放时先开启线程,回访结束后关闭线程。

void MainWindow::on_pushButton_videoreview_clicked()
{
    firstThread->start();
    MyCamThread->reviewVideo();
}


connect(MyCamThread,SIGNAL(reviewComplete()),this,SLOT(reviewVideo_complete()));


void MainWindow::reviewVideo_complete()
{
    firstThread->quit();
    firstThread->wait();
}

三、总结

从上面的代码看下来或许可以加深对于主线程、子线程的理解——主线程主要负责UI显示、信号与槽函数的映射、函数的调用,子线程才是真正的实现这些功能的地方。
另外,为了防止窗口关闭时视频设备资源并未被完全释放,所以修改析构函数:

MainWindow::~MainWindow()
{
    delete ui;
    delete MyCamThread;
}

使直接关闭窗口时资源也能被回收


附上完整代码:
camthread.h:

#ifndef CAMTHREAD_H
#define CAMTHREAD_H

#include <QObject>
#include <opencv2/opencv.hpp>
#include <opencv2/core/core.hpp>
#include <opencv2/videoio.hpp>
#include <opencv2/highgui/highgui.hpp>


class CamThread :public QObject
{
    Q_OBJECT
public:
    explicit CamThread(QObject *parent = 0);
signals:
    void reviewComplete();
    void sendPicture(const QImage &img);
public slots:
    void setFlag(bool flag = false);
    void openCamera();
    void closeCamera();
    void startsave();
    void camNumber(const int &n);
    void reviewVideo();
    void closeImshow();
    void mainwindowDisplay();
private slots:

private:
    cv::VideoCapture capture;
    cv::VideoWriter writer;
    cv::Mat src_image;
    bool stopFlag=false;
    int camera_num = 0;
};

#endif // CAMTHREAD_H

mainwindow.h:

#ifndef MAINWINDOW_H
#define MAINWINDOW_H

#include <QMainWindow>
#include <QTimer>
#include <QDebug>
#include <QThread>
#include <QCameraInfo>
#include <QList>
#include "camthread.h"
namespace Ui {
class MainWindow;
}

class MainWindow : public QMainWindow
{
    Q_OBJECT

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

private slots:
    void on_pushButton_opencamera_clicked();

    void on_pushButton_closecamera_clicked();

    void on_pushButton_savevideo_clicked();

    void on_pushButton_savecomplete_clicked();

    void on_pushButton_videoreview_clicked();

    void display_frame();

    void on_pushButton_searchcamera_clicked();

    void recivePicture(QImage img);

    void reviewVideo_complete();
private:
    Ui::MainWindow *ui;
    QThread *firstThread;
    CamThread *MyCamThread;
    QList<QCameraInfo> camera_list;
    QTimer fps_timer;
};

#endif // MAINWINDOW_H

camthread.cpp:

#include "camthread.h"
#include <QMessageBox>
#include <iostream>
#include <QDebug>
#include <QFileDialog>
#include <QDateTime>
#include <QCoreApplication>
CamThread::CamThread(QObject *parent) : QObject(parent)
{
    stopFlag = false;
}
void CamThread::startsave()
{
    QString path = QCoreApplication::applicationDirPath().append("/Video/")
            .append(QDateTime::currentDateTime().toString("yyyyMMddhhmmss")).append(".avi");
    cv::String file_path = path.toStdString() ;
    writer.open(file_path,cv::VideoWriter::fourcc('X', 'V', 'I', 'D'), 20.0, cv::Size(640, 480));
    while(!stopFlag)
    {
        capture >> src_image;
        writer.write(src_image);
        cv::namedWindow("video", cv::WINDOW_NORMAL);
        cv::imshow("video", src_image);
        cv::waitKey(50);
    }
}
void CamThread::mainwindowDisplay()
{
    capture >> src_image;
    QImage img1 = QImage((const unsigned char*)src_image.data,
                         src_image.cols, src_image.rows, QImage::Format_RGB888).rgbSwapped();
    emit sendPicture(img1);
}
void CamThread::camNumber(const int &n)
{
    camera_num = n;
}
void CamThread::openCamera()
{
    capture.open(camera_num);
    if(!capture.isOpened())
    {
        return;
    }
}
void CamThread::closeCamera()
{
    if(!stopFlag)   // 如果还在保存视频 则关闭cv窗口
    {
        cv::destroyWindow("video");
    }
    capture.release();
    writer.release();
}
void CamThread::setFlag(bool flag)
{
    stopFlag = flag;
}

void CamThread::closeImshow()
{
    cv::destroyWindow("video");
}
void CamThread::reviewVideo()
{
    cv::VideoCapture video;
    cv::Mat video_src;
    QString path = QFileDialog::getOpenFileName(0,"打开","../","");
    cv::String openpath = path.toStdString();
    video.open(openpath);
    while(video.isOpened())
    {
        video>>video_src;
        if(video_src.empty())
            break;
        cv::imshow("video_review",video_src);
        if(cv::waitKey(50)==27)
        {
            cv::destroyWindow("video_review");
            break;
        }
    }
    emit reviewComplete();
}

mainwindow.cpp:

#include "mainwindow.h"
#include "ui_mainwindow.h"
#include <QMessageBox>
#include <Qdir>
MainWindow::MainWindow(QWidget *parent) :
    QMainWindow(parent),
    ui(new Ui::MainWindow)
{
    ui->setupUi(this);
    firstThread = new QThread;
    MyCamThread = new CamThread;
    MyCamThread->moveToThread(firstThread);
    connect(&fps_timer,    SIGNAL(timeout()), MyCamThread, SLOT(mainwindowDisplay()));
    connect(MyCamThread,SIGNAL(sendPicture(QImage)),this,SLOT(recivePicture(QImage)));
    fps_timer.setInterval(50);
    connect(MyCamThread,SIGNAL(reviewComplete()),this,SLOT(reviewVideo_complete()));

    QString save_picture = QCoreApplication::applicationDirPath();
    QDir dir;
    dir.cd(save_picture);
    if(!dir.exists("video"))
    {
        dir.mkdir("video");
    }
}

MainWindow::~MainWindow()
{
    delete ui;
    delete MyCamThread;
}

void MainWindow::on_pushButton_opencamera_clicked()
{
    if(ui->camera_name->currentIndex() >= 0)
    {
        firstThread->start();
        MyCamThread->camNumber(ui->camera_name->currentIndex());
        fps_timer.start();
        MyCamThread->openCamera();
    }
    else
        QMessageBox::information(this,tr("Error"),tr("Have No Camera Device!"),QMessageBox::Ok);
}

void MainWindow::on_pushButton_closecamera_clicked()
{
    fps_timer.stop();
    ui->label_videoViewer->clear();
    MyCamThread->closeCamera();
    firstThread->quit();
    firstThread->wait();
}

void MainWindow::on_pushButton_savevideo_clicked()
{
    MyCamThread->setFlag(false);
    MyCamThread->startsave();
}

void MainWindow::on_pushButton_savecomplete_clicked()
{
    MyCamThread->setFlag(true);
    MyCamThread->closeImshow();
}

void MainWindow::on_pushButton_videoreview_clicked()
{
    firstThread->start();
    MyCamThread->reviewVideo();
}

void MainWindow::display_frame()
{

}

void MainWindow::on_pushButton_searchcamera_clicked()
{
    ui->camera_name->clear();
    camera_list = QCameraInfo::availableCameras();
    for(auto i =0;i<camera_list.size();i++)
    {
        ui->camera_name->addItem(camera_list.at(i).description());
    }
}
void MainWindow::recivePicture(QImage img)
{
    ui->label_videoViewer->setPixmap(QPixmap::fromImage(img));
}
void MainWindow::reviewVideo_complete()
{
    firstThread->quit();
    firstThread->wait();
}