前言: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);
QImage 類參考
Mat類參考
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