在上一篇博客中,我们介绍了如何使用OpenCV在主线程中实现实时画面显示以及视频的存储与回放,本文主要介绍如何将摄像头的画面获取放到子线程中
关于线程的创建本文采用继承于QObject
+MoveToThread
的方法,具体创建方法可以移步Qt多线程的创建详解,本文不做赘述
一、项目创建
首先还是创建一个主窗口项目,命名为multiThreadCamera,完成后在项目上右击–>添加新文件–>C++类,类名为CamThread
,继承于QObject
,源文件和头文件默认为camthread.cpp
和camthread.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
界面如图所示
控件说明:
控件名 | 作用 |
| 画面显示 |
| 显示摄像设备 |
| 查找摄像设备 |
| 打开摄像头 |
| 关闭摄像头 |
| 保存视频 |
| 结束保存 |
| 视频回放 |
二、功能实现
在编写代码前 ,脑中还是要有一个思路,那就是主线程是干嘛的,子线程是干嘛的
本项目中显而易见——主线程负责画面显示及指令响应,子线程负责调用视频设备与获取画面,线程之间通信主要采用信号与槽机制。搞清楚这些,接下来一步步实现——
这里代码虽然零散,但体现了我编写代码时的一个思路历程,初学者可以尝试阅读 找找思路 文章最后附有完整代码
- 线程的创建
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);
- 各按钮槽函数(直接右击–>转到槽)
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();
}