在上一篇博客中,我们介绍了如何使用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
界面如图所示
控件说明:
控件名 | 作用 |
---|---|
label_videoviewer |
画面显示 |
camera_name |
显示摄像设备 |
pushbutton_searchcamera |
查找摄像设备 |
pushbutton_opencamera |
打开摄像头 |
pushbutton_closecamera |
关闭摄像头 |
pushbutton_savevideo |
保存视频 |
pushbutton_savecomplete |
结束保存 |
pushbutton_videoreview |
视频回放 |
二、功能实现
在编写代码前 ,脑中还是要有一个思路,那就是主线程是干嘛的,子线程是干嘛的
本项目中显而易见——主线程负责画面显示及指令响应,子线程负责调用视频设备与获取画面,线程之间通信主要采用信号与槽机制。搞清楚这些,接下来一步步实现——
这里代码虽然零散,但体现了我编写代码时的一个思路历程,初学者可以尝试阅读 找找思路 文章最后附有完整代码
-
线程的创建
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_OBJECTpublic: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();}elseQMessageBox::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();
}
项目连接:https://gitee.com/Mr-Yslf/BlogResources/tree/master/MultiThreadCamera