前言:OpenCV对图像及视频的处理方便且很专业,对于摄像头的支持也很好,但有个不足就是它虽然具有GUI模块(即highgui),但是实在是很简陋,就连一个按键都无法直接实现(需要借助滚动条实现),这一点难以满足可视化的图像处理的想法;另一方面,Qt作为一个优秀的图形库,在GUI上表现出色,且界面设计可以可视化的借助Designer来完成,所以笔者就想何不充分发挥两者的优势交互使用呢?基于此,我实现了一个简单的视频播放器,使用openCV来读取视频文件或摄像头文件,并实现简单的图像处理过程,然后将openCV的Mat数据转换为Qt的QImage数据并用Qt显示出来。
平台环境:Qt5.5,QtCreator( ubuntu),openCV3.1.0
1.新建一个Qt的GUI工程
首先双击工程自动创建的UI文件尽情的设计你的界面吧,在布局上放置一个label和5个button,并拖动到你喜欢的位置!定义好外观之后关键的是要对各个控件修改好相应的对象名(objectName),这个关系到控件和代码的紧密衔接。然后需要给每个按键设置一个行为,即槽函数,比如对pushButton_open按键,可以直接右击该按键,然后选择转到槽,选择clicked()信号,开发环境会直接在代码里生成一个槽函数 on_pushButton_open_clicked(),目前是空白的,我们稍后根据需要再写实现代码。如图我设计了5个按键分别为:打开视频文件、打开默认摄像头、处理图像、开始录制、结束录制。如图所示。
2.openCV打开视频文件
实际上只需创建一个cv::VideoCapture类的实例,然后在循环中按照一定的时间间隔读取每一帧图像即可实现视频文件的读取。cv::VideoCapture类有一个方法是open(),用它可以打开一个视频文件或者打开摄像头来获取视频流,具体实现可见官方参考手册,我们为了实现点击相应的按键打开文件或摄像头,可以在槽函数中实现该过程,代码如下。
//open file void MainWindow::on_pushButton_open_clicked() { if (capture.isOpened()) capture.release(); //decide if capture is already opened; if so,close it QString filename =QFileDialog::getOpenFileName(this,tr("Open Video File"),".",tr("Video Files(*.avi *.mp4 *.flv *.mkv)")); capture.open(filename.toLocal8Bit().data()); if (capture.isOpened()) { rate= capture.get(CV_CAP_PROP_FPS); capture >> frame; if (!frame.empty()) { image = Mat2QImage(frame); ui->label->setPixmap(QPixmap::fromImage(image)); timer = new QTimer(this); timer->setInterval(1000/rate); //set timer match with FPS connect(timer, SIGNAL(timeout()), this, SLOT(nextFrame())); timer->start(); } } } //open camera void MainWindow::on_pushButton_camera_clicked() { if (capture.isOpened()) capture.release(); //decide if capture is already opened; if so,close it capture.open(0); //open the default camera if (capture.isOpened()) { rate= capture.get(CV_CAP_PROP_FPS); capture >> frame; if (!frame.empty()) { image = Mat2QImage(frame); ui->label->setPixmap(QPixmap::fromImage(image)); timer = new QTimer(this); timer->setInterval(1000/rate); //set timer match with FPS connect(timer, SIGNAL(timeout()), this, SLOT(nextFrame())); timer->start(); } } }
3.Mat类型转换为QImage类型
在上述代码中,我们看到有个Mat2QImage函数,这个是我封装好的数据格式转换函数,将openCV的Mat类型转换为QImage类型,以便在Qt下显示视频图像。其中需要将cv::Mat的BGR通道顺序变换为QImage的RGB顺序,可以调用cv::cvtColor函数实现,以上是对两种图像类型的data部分的格式进行调整,下一步只需要明确Mat的头结构里的变量与QImage的头结构里的变量的对应关系即可实现转换,有如下对应关系
QImage Mat
数据指针 uchar* bits() uchar* data
宽度 int width() int cols
高度 int height() int rows
步长 int bytesPerLine() cols * channels()
格式 Format_Indexed8 8UC1, GRAY,灰度图
Format_RGB888 8UC3, BGR,3通道真彩色 (需要使用cvtColor调换顺序)
Format_ARGB32 8UC4, BGRA,4通道真彩色(需要使用cvtColor调换顺序)
Mat2QImage的参考代码如下:
QImage Mat2QImage(cv::Mat cvImg) { QImage qImg; if(cvImg.channels()==3) //3 channels color image { cv::cvtColor(cvImg,cvImg,CV_BGR2RGB); qImg =QImage((const unsigned char*)(cvImg.data), cvImg.cols, cvImg.rows, cvImg.cols*cvImg.channels(), QImage::Format_RGB888); } else if(cvImg.channels()==1) //grayscale image { qImg =QImage((const unsigned char*)(cvImg.data), cvImg.cols,cvImg.rows, cvImg.cols*cvImg.channels(), QImage::Format_Indexed8); } else { qImg =QImage((const unsigned char*)(cvImg.data), cvImg.cols,cvImg.rows, cvImg.cols*cvImg.channels(), QImage::Format_RGB888); } return qImg; }
另外需要注意的是由于openCV版本的更新,之前的IplImage图像类型已被Mat取代,虽然最新版本仍然兼容旧的结构,但建议废弃这类数据类型。openCV2以上版本相对于1.x版本引入了全新的C++接口,之前的图像的数据类型为IplImage,对应的C风格的图像操作函数有cvLoadImage(), cvShowImage(), cvReleaseImage()等,最新的2.x及3.x版本,引入了C++风格的cv::Mat类来彻底取代了IplImage结构,相应的图像操作函数为imread(), imshow(), C++风格的Mat对内存的管理上更为方便,(之前的C数据类型需要手动释放分配的IplImage内存),当cv::Mat 对象离开作用域后,相应的内存会由析构函数自动释放,这避免了内存泄露的困扰,另外,Mat实现了引用计数及浅拷贝,当图像对象赋值时,图像数据并没有复制,其数据指针指向同一个内存区块,引用计数的作用是只有当所有引用内存数据的对象都析构以后才会释放该内存。所以当使用最新的openCV版本时建议彻底废弃掉以前的数据类型,改为C++的风格,同时对于旧的IplImage类型,也可以很方便通过以下语句转换为Mat,
IplImage* image =cvLoadImage("C:\img.jpg");
cv::Mat image1(image,false);
4.处理图像
简单使用canny函数进行边缘检测,在按键的槽函数中实现,代码如下
void MainWindow::on_pushButton_process_clicked() { cv::Mat cannyImg ; cv::Canny(frame, cannyImg, 0, 30, 3); cv::namedWindow("Canny"); cv::imshow("Canny", cannyImg); }
canny处理图像后的效果图
5.视频的录制
使用openCV进行视频写入文件,只需要创建一个cv::VideoWriter对象即可,然后执行其方法write(frame),其中frame为将要写入文件的帧,写入完毕执行其方法release()即可;相应代码如下,需要注意的是创建的写入文件必须为avi后缀,因为openCV貌似不支持其他格式,另外对应的编码格式可参考如下(本例中只实现了简单捕获100帧视频还不能实现连续捕获,后续需要改进)
CV_FOURCC(’P’,’I’,’M’,’1’) = MPEG-1 codec
CV_FOURCC(’M’,’J’,’P’,’G’) = motion-jpeg codec (does not work well)
CV_FOURCC(’M’, ’P’, ’4’, ’2’) = MPEG-4.2 codec
CV_FOURCC(’D’, ’I’, ’V’, ’3’) = MPEG-4.3 codec
CV_FOURCC(’D’, ’I’, ’V’, ’X’) = MPEG-4 codec
CV_FOURCC(’U’, ’2’, ’6’, ’3’) = H263 codec
CV_FOURCC(’I’, ’2’, ’6’, ’3’) = H263I codec
CV_FOURCC(’F’, ’L’, ’V’, ’1’) = FLV1 codec
将上面的改成 -1 将会打开一个编码器的选择窗口.
void MainWindow::on_pushButton_start_clicked() { writer.open("./myrec.avi",cv::VideoWriter::fourcc('P','I','M','1'), /*capture.get(CV_CAP_PROP_FPS)*/30, cv::Size(frame.cols, frame.rows),true); int t=100; while(t--) {writer.write(frame);} //record 100 frames ui->pushButton_start->setDisabled(true); //if successfully start videoWriter, disable the button } void MainWindow::on_pushButton_end_clicked() { writer.release(); }
6.Qt下显示图像的问题
本例中创建了一个label来显示图像,并设置了QTimer定时器按照视频的帧速率来进行定时刷新帧,并重绘图像,实现动态视频的展示,其中重绘图像可以在定时刷新的槽函数中使用重新设置像素的方法来实现: ui->label->setPixmap(QPixmap::fromImage(image))(本例采用这种方法),也可重新实现paintEvent方法来完成,在Qt中,paintEvent方法是进行重绘的,只要出现以下几种情况,系统就会自动调用paintEvent方法。
a)当窗口部件第一次显示时,系统会自动产生一个绘图事件
b)重新调整窗口部件大小
c)当窗口部件被其他部件遮挡,然后又再次显示出来时,就会对隐藏的区域产生一个重绘事件
也可以通过调用QWidget::update()和QWidget::repaint()来产生一个绘图事件,其中repaint会强制产生一个即时的重绘事件,update会在Qt下一次处理事件时才会调用绘制事件,如果窗口部件在屏幕上是不可见的,则update和repaint什么都不会做。如果连续多次调用update方法,Qt会自动的将其压缩为一个单一的绘制事件
7.其他部分的代码如下
mainwindow的构造函数及自动更新帧函数
MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent), ui(new Ui::MainWindow) { ui->setupUi(this); ui->label->setScaledContents(true); //fit video to label area } ///auto get next frame void MainWindow::nextFrame() { capture >> frame; if (!frame.empty()) { image = Mat2QImage(frame); ui->label->setPixmap(QPixmap::fromImage(image)); //this->update(); } } ///re-implement paintEvent /* void MainWindow::paintEvent(QPaintEvent * e) { // update image QPainter painter(this); //display an image on a label area painter.drawImage(QRect(ui->label->x(), ui->label->y(), ui->label->width(), ui->label->height()), image); } */
类接口定义
class MainWindow : public QMainWindow { Q_OBJECT public: explicit MainWindow(QWidget *parent = 0); ~MainWindow(); private: Ui::MainWindow *ui; cv::Mat frame; cv::VideoCapture capture; QImage image; QTimer *timer; double rate; //FPS cv::VideoWriter writer; //make a video record protected: // void paintEvent(QPaintEvent * e); private slots: void nextFrame(); void on_pushButton_open_clicked(); void on_pushButton_camera_clicked(); void on_pushButton_process_clicked(); void on_pushButton_start_clicked(); void on_pushButton_end_clicked(); };
展示一张播放器的效果图吧!
总结:通过将Qt和openCV结合起来,实现了一个简单的视频播放器,虽然它还很简陋(没有滚动条甚至没有音频),但是充分利用了各自的优势,即Qt的GUI及openCV的图像数据处理能力,这一点将会对图像处理过程的可视化大有帮助,后续还将继续探讨。
参考资料: 1.OpenCV2 计算机视觉编程手册,Robert著,张静 译
2.Qt Reference Pages
3.OpenCV 3.1.0 Class Index