OpenCV-5-文件和UI操作

1 前言

OpenCV中用于和操作系統(tǒng)、文件系統(tǒng)及相機(jī)等硬件交互的函數(shù)被包含在模塊HighGUI(high-level graphics user interface)中。HighGUI劃分為硬件、文件系統(tǒng)和GUI三部分。

總的來說該模塊能夠讀取或者寫入如圖像和視頻文件,打開并管理窗口,顯示圖片,處理簡單的鼠標(biāo)和鍵盤事件。也可以用于創(chuàng)建有用的控件,如創(chuàng)建一個滑動條并添加到窗口中。當(dāng)然如果你熟悉當(dāng)前平臺原生的圖形界面系統(tǒng),也可以直接使用原生接口。

硬件部分主要處理和相機(jī)相關(guān)的任務(wù),在大多數(shù)操作系統(tǒng)中,相機(jī)交互都是一個繁瑣和痛苦的任務(wù)。

文件系統(tǒng)部分主要負(fù)責(zé)加載和存儲圖片及視頻,OpenCV一個不錯的設(shè)計(jì)方式是我們可以使用和從相機(jī)中讀取數(shù)據(jù)的方法去讀取一個視頻文件。類似的OpenCV提供了一組相當(dāng)通用的函數(shù)用于讀取和存儲圖片,這些方法通過文件內(nèi)部數(shù)據(jù)決定需要讀取的文件類型,并自動正確的處理圖像編解碼邏輯。另外OpenCV還提供一組基于XML或YML的函數(shù)從而很方便的以一種簡單,可讀的,基于文本的方式去載入和存儲OpenCV原生類型數(shù)據(jù)。

GUI部分提供了一些簡單的函數(shù)用于創(chuàng)建窗口,及將圖片顯示到某個窗口中,同時也負(fù)責(zé)處理這個窗口中的簡單的鼠標(biāo)和鍵盤事件。通過鏈接Qt庫(一個跨平臺的窗口工具包),還可以實(shí)現(xiàn)更豐富的功能。

在OpenCV3.0以后,HighGUI核心功能被拆解為imgcodecs(圖像編解碼)、videoio(視頻及圖像捕獲和視頻編解碼)和highgui(用戶界面)三個模塊,其XML和YML文件操作部分被劃分到Core模塊中。

2 文件操作

OpenCV提供了一系列用于圖像加載和保存的函數(shù),這些函數(shù)在很多方面都和通用的基于XML或者YAML數(shù)據(jù)的函數(shù)存在差異。主要區(qū)別是前者依賴于圖像編碼和解碼算法。其中一些有損算法會丟失部分圖像信息,這對圖像而言是可以接受的,但是對于非圖像數(shù)據(jù)如參數(shù)數(shù)據(jù)是不能夠接受的。另外需要注意的是有損圖像算法造成的肉眼難以觀測的瑕疵可能會給視覺算法造成影響。

2.1 圖像

函數(shù)cv::imread()cv::imwrite()用于從磁盤中加載圖片資源,或者將圖片寫入到磁盤中,這兩個方法內(nèi)部包含圖片的編解碼邏輯,也包含和文件系統(tǒng)的交互。

2.1.1 載入圖片

載入圖片的函數(shù)原型如下。如果圖像加載失敗將會返回一個空數(shù)組,可以通過矩陣的成員函數(shù)cv::Mat::empty() == true判斷。

// filename:需要載人文件的絕對路徑
// flags:圖片文件的識別標(biāo)記,具體取值見下表
cv::Mat cv::imread(const string& filename, int flags = cv::IMREAD_COLOR);

該函數(shù)會不關(guān)心文件路徑中包含的文件擴(kuò)展名,而是分析文件中前幾個字節(jié)的內(nèi)容(被稱為簽名Signature或者魔法序列Magic Sequence)來決定文件的格式,從而使用正確的解碼算法。參數(shù)flags的所有取值及其含義如下表。

參數(shù)flags取值 含義
cv::IMREAD_COLOR 加載為三通道矩陣,如果原圖為灰度,則三個通道值都等于灰度值
cv::IMREAD_GRAYSCALE 加載為單通道灰度矩陣,即使原圖為彩色圖像,都會計(jì)算出灰度值加載
cv::IMREAD_ANYCOLOR 加載為文件實(shí)際通道數(shù)的矩陣,但是不超過3個通道
cv::IMREAD_ANYDEPTH 允許加載位深度為8的圖像,默認(rèn)按8位深度加載圖像
cv::IMREAD_UNCHANGED 加載原始圖像,可以大于3個通道,可以大于8位深度
2.1.2 寫入圖片

寫入圖片的函數(shù)原型如下。

// 返回值,圖片是否保存成功
// fileName:圖片文件寫入的位置,需要通過后綴確定文件格式,詳細(xì)信息見下表
// image:圖片數(shù)據(jù)
// params:文件編碼參數(shù),詳細(xì)信息見下表
bool cv::imwrite(const string& filename, cv::InputArray image,
                 const vector<int>& params = vector<int>());

參數(shù)fileName中常用的后綴及其對應(yīng)的圖片編碼標(biāo)準(zhǔn)見下表。該函數(shù)支持存儲整型或者浮點(diǎn)型,單通道、三通道或者四通道的圖片。

fileName可用后綴 編碼標(biāo)準(zhǔn) 備注
.jpg或.jpeg JPEG baseline 8位位深度,單或者三通道輸入
.jp2 JPEG 2000 8或者16位位深度,單或者三通道輸入
.tif或.tiff TIFF 8或者16位位深度,單、三或者四通道輸入
.png PNG 8或者16位位深度,單、三或者四通道輸入
.bmp BMP 8位位深度,單、三或者四通道輸入
.ppm或.pgm NetPBM 8位位深度,單通道(PGM)或者三通道(PPM)輸入

第三個參數(shù)params指定了圖像編碼時的一些參數(shù),對于不同的圖片格式,可以接受的參數(shù)是不同的。該參數(shù)接收包含整型元素的標(biāo)準(zhǔn)向量,該向量結(jié)構(gòu)為一個標(biāo)識后緊跟其值,再接下一個標(biāo)識和值。所有可選的表示以及對應(yīng)的取值范圍和使用條件見下表。

params可用標(biāo)識 含義 取值區(qū)間 范圍
cv::IMWRITE_JPG_QUALITY JPEG標(biāo)準(zhǔn)使用的壓縮質(zhì)量 [0, 100] 95
cv::IMWRITE_PNG_COMPRESSION PNG標(biāo)準(zhǔn)的壓縮比,值越高表示壓縮率更高 [0, 9] 3
cv::IMWRITE_PXM_BINARY 在處理PPM、PGM和PBM標(biāo)準(zhǔn)時是否使用二進(jìn)制格式存儲 0或者1 1
2.1.3 圖像編解碼

OpenCV涉及到編解碼的函數(shù)需要底層庫的支持,通常情況下大多數(shù)平臺的操作系統(tǒng)中對于不同的圖片格式都至少包含1個可以用的編解碼庫。另外OpenCV自身也包含一些常用圖片格式的編解碼庫,如JPEG、PNG、TIFF等。對于每一種編碼標(biāo)準(zhǔn)等庫,你有三種處理方式。第一種是不包含這個庫,這意味著你將不能正確的顯示和存儲這種格式的圖片。第二種是使用OpenCV提供的編解碼庫,那么你需要在編譯OpenCV時同時編譯這些庫。第三種是使用OpenCV外部(如操作系統(tǒng))提供的庫。

在編譯Windows環(huán)境中的OpenCV時,默認(rèn)的時第二種選項(xiàng)。在編譯OS X或者Linux版本的CV庫時,默認(rèn)使用第三種方案,但是如果CMake未檢測到相應(yīng)的編解碼庫,則會使用第二種方案。當(dāng)然你也可以更改CMake的編譯配置。需要注意如果在Linux上編譯CV庫選擇了第三種方案,那么需要在編譯CV庫之前安裝對應(yīng)的編解碼庫,如libjpeg和libjpeg-dev。

前文講到的讀取和寫入圖片操作都是一系列操作的集合,有時我們也需要單獨(dú)使用圖像的編解碼函數(shù)完成一些任務(wù),如處理內(nèi)存中的數(shù)據(jù)。圖像編碼函數(shù)cv::imencode可以直接處理矩陣數(shù)據(jù),其原型如下。它輸出了字節(jié)流數(shù)據(jù),這并不奇怪,因此經(jīng)過編碼器處理后的數(shù)據(jù)已經(jīng)是壓縮數(shù)據(jù),其大小和格式都和原圖不相同。此外參數(shù)ext不僅用于推斷圖片的格式,在很多操作系統(tǒng)中它也用于索引相應(yīng)的編解碼庫。

// ext:文件擴(kuò)展名,用于推斷出應(yīng)該使用的編碼器
// img:需要編碼的圖像數(shù)據(jù)
// buf:編碼后的圖像數(shù)據(jù),無符號字符數(shù)組,該函數(shù)會自動分配合適的內(nèi)存空間
// params:特定編碼器所需要的參數(shù)字典,取值請參照函數(shù)cv::imwrite
void cv::imencode(const string& ext, cv::InputArray img,
                  vector<uchar>& buf, const vector<int>& params = vector<int>());

圖像解碼函數(shù)cv::imdecode()可以從字節(jié)流數(shù)據(jù)解碼得到圖像數(shù)據(jù),其原型如下。

// buf:壓縮的圖像數(shù)據(jù)緩存,類型通常為std::vector<uchar>
// flag:解碼后得到的圖像數(shù)據(jù)處理策略,見函數(shù)cv::imread()說明
cv::Mat cv::imdecode(cv::InputArray buf, int flags = cv::IMREAD_COLOR);

和函數(shù)cv::imread()一樣,該函數(shù)并不需要文件擴(kuò)展名來推斷編碼標(biāo)準(zhǔn),因?yàn)閴嚎s的字節(jié)流數(shù)據(jù)的前幾個字節(jié)包含了使用的編碼標(biāo)準(zhǔn)信息。如果傳入的參數(shù)buf為空,或者包含無效的、錯誤的數(shù)據(jù)以及其他異常情況,該函數(shù)返回值為空矩陣。

2.2 視頻

2.2.1 讀取視頻數(shù)據(jù)

cv::VideoCapture用于讀取視頻數(shù)據(jù),它是本系列前面文章中講到的函數(shù)對象概念的一種。它可以從文件中讀取或者通過攝像頭捕獲視頻數(shù)據(jù),其構(gòu)造函數(shù)如下。

// fileName:視頻文件的絕對路徑
cv::VideoCapture::VideoCapture(const string& filename);

// device:攝像頭硬件的標(biāo)識
cv::VideoCapture::VideoCapture(int device);

cv::VideoCapture::VideoCapture();

對于第一個構(gòu)造方法,可以通過成員函數(shù)cv::VideoCapture::isOpened()來判斷文件是否成功打開。導(dǎo)致文件無法打開的原因除了文件本身不存在外,還可能是無法找到對應(yīng)的編解碼庫。由于編解碼器涉及到很多計(jì)算、專利及法律問題,和編碼相關(guān)函數(shù)無法工作的情況并不像我們希望的那樣很少發(fā)生。

使用攝像頭標(biāo)識的構(gòu)造函數(shù)不會受到解碼器限制,但是需要可用的硬件資源。該參數(shù)表示我們希望使用哪個硬件,以及操作系統(tǒng)使用何種方式與相機(jī)硬件交互。使用哪個硬件由相機(jī)的編號(Identifier)決定,從0遞增。而交互的方式由相機(jī)域(Domain)表示,可以用的域值見下表。而參數(shù)device是相機(jī)標(biāo)識和域的和。

相機(jī)域的取值 真實(shí)值
cv::CAP_ANY 0
cv::CAP_MIL 100
cv::CAP_VFW 200
cv::CAP_V4L 200
cv::CAP_V4L2 200
cv::CAP_FIREWIRE 300
cv::CAP_IEEE1394 300
cv::CAP_DC1394 300
cv::CAP_CMU1394 300
cv::CAP_QT 500
cv::CAP_DSHOW 700
cv::CAP_PVAPI 800
cv::CAP_OPENNI 900
cv::CAP_ANDROID 1000
... ...

下面的代碼片段創(chuàng)建了一個cv::VideoCapture實(shí)例,并用其打開第一個FireWire類型的相機(jī)。在大多數(shù)情況下如果只存在一個相機(jī),則指定默認(rèn)的域cv::CAP_ANY即可。在某些平臺上,該參數(shù)可設(shè)置為-1,這樣會彈出一個窗口用于選擇相機(jī)。

cv::VideoCapture capture( cv::CAP_FIREWIRE );

使用不帶參數(shù)的構(gòu)造函數(shù)創(chuàng)建出的cv::VideoCapture實(shí)例是不可用的,需要指定相機(jī)的資源后才可以使用,指定方式如下。

cv::VideoCapture cap;
// 可以指定文件
cap.open("my_video.avi");
// 可以指定相機(jī)
cap.open(cv::CAP_FIREWIRE);
2.2.2 讀取視頻幀

函數(shù)cv::VideoCapture::read可以讀取當(dāng)前的視頻幀,并將“標(biāo)記”向后移動,使得下次再調(diào)用該函數(shù)時得到的是新的視頻幀。其原型如下。

// 返回值:讀取是否成功,如讀到視頻文件末尾后會返回false
// image:讀取到的視頻幀數(shù)據(jù),如果讀取失敗則為空矩陣
bool cv::VideoCapture::read(cv::OutputArray image);

重載后的運(yùn)算符>>也可以讀取下一幀數(shù)據(jù),其原型如下。

// image:讀取到的視頻幀數(shù)據(jù),如果讀取失敗則為空矩陣
cv::VideoCapture& cv::VideoCapture::operator >> (cv::Mat& image);

前面的兩種方式都是一次從文件或者相機(jī)中取出一幀解碼后的圖片,而使用函數(shù)cv::VideoCapture::grabcv::VideoCapture::retrieve可以將這個過程分解為抓取(Grab)和解碼(Retrieve)兩個階段,其函數(shù)原型如下。

// 返回值:是否成功獲取到原始數(shù)據(jù)
bool cv::VideoCapture::grab();

// image:讀取到的圖像數(shù)據(jù)
// channel:硬件設(shè)備的傳感器的索引,對于立體成像攝像機(jī)很常見,目前僅支持Kinect和Videre相機(jī)
// 此時該參數(shù)表示處理哪個傳感器拍攝到的圖片
// 對于支持多傳感器的情況,可以調(diào)用一次cv::VideoCapture::grab()函數(shù)
// 多次bool cv::VideoCapture::retrieve函數(shù)
bool cv::VideoCapture::retrieve(cv::OutputArray image, int channel = 0);

函數(shù)cv::VideoCapture::grab會將當(dāng)前未處理的數(shù)據(jù)拷貝至一個中間緩存,這部分緩存是外部不可見的。但是這樣做是有原因的,將讀取視頻幀分為提取原始數(shù)據(jù)和解碼數(shù)據(jù)兩步也是很有必要的。最常見的情況就是需要處理多個相機(jī)數(shù)據(jù)時,如需要合成立體圖。在這種情況下,縮短各個相機(jī)獲得圖像的時間差就變得很重要,最理想的情況下所有相機(jī)采集的都是同一時刻的圖像。因此將原始數(shù)據(jù)采集和編解碼操作分開就很有意義,可以使得各個相機(jī)盡可能的同步。

當(dāng)獲取到原始數(shù)據(jù)后,函數(shù)bool cv::VideoCapture::retrieve就可以處理這些原始數(shù)據(jù),并且分配內(nèi)存空間,做必要的數(shù)據(jù)拷貝,最終將處理好的圖像數(shù)據(jù)用cv::Mat實(shí)例的方式返回。

2.2.3 操作元數(shù)據(jù)

視頻文件不僅包含視頻數(shù)據(jù),還包含重要的元數(shù)據(jù),這部分?jǐn)?shù)據(jù)對于正確的處理視頻文件是必不可少的。當(dāng)視頻文件被打開后,這部分信息被拷貝到cv::VideoCapture實(shí)例內(nèi)部數(shù)據(jù)區(qū)。負(fù)責(zé)讀取和寫入這部分元數(shù)據(jù)以及其他屬性的函數(shù)原型如下。

// propid:屬性標(biāo)識,詳細(xì)信息見下表
double cv::VideoCapture::get(int propid);

// 返回值:是否設(shè)置成功,該方法只能設(shè)置部分屬性,更多的細(xì)節(jié)再后面詳細(xì)介紹視頻編解碼時再展開
// value:需要設(shè)置的值
bool cv::VideoCapture::set(int propid, double value);

可以操作的屬性見下表,需要注意的是并不是所有被OpenCV識別的屬性都能夠被處理,這取決于具體的場景是否包含了這部分信息。同樣的也不是所有存在的屬性都被OpenCV所支持,因?yàn)榧夹g(shù)和標(biāo)準(zhǔn)總是在不斷更新的。

視頻捕獲屬性 是否只支持相機(jī)模式 含義
cv::CAP_PROP_POS_MSEC 視頻文件當(dāng)前播放的時刻(單位為毫秒)或者視頻捕捉的當(dāng)前幀時間戳
cv::CAP_PROP_POS_FRAMES 當(dāng)前幀的索引值,從0開始計(jì)數(shù)
cv::CAP_PROP_POS_AVI_RATIO 當(dāng)前視頻播放的相對位置,取值范圍為[0.0, 1.0]
cv::CAP_PROP_FRAME_WIDTH 每一幀圖像的寬度
cv::CAP_PROP_FRAME_HEIGHT 每一幀圖像的高度
cv::CAP_PROP_FPS 僅限于視頻文件 視頻的幀率,即每秒的幀數(shù)
cv::CAP_PROP_FOURCC 編解碼標(biāo)準(zhǔn)的四字符代碼
cv::CAP_PROP_FRAME_COUNT 視頻文件的總幀數(shù),并不總是可靠的
cv::CAP_PROP_FORMAT 視頻數(shù)據(jù)的格式,如CV_8UC3
cv::CAP_PROP_MODE 視頻捕獲模式,其值和捕獲視頻使用的特定硬件(如DC1394)相關(guān)
cv::CAP_PROP_BRIGHTNESS ? 相機(jī)的亮度設(shè)置,僅在支持時才有效
cv::CAP_PROP_CONTRAST ? 相機(jī)的對比度設(shè)置,僅在支持時才有效
cv::CAP_PROP_SATURATION ? 相機(jī)的飽和度設(shè)置,僅在支持時才有效
cv::CAP_PROP_HUE ? 相機(jī)的色調(diào)設(shè)置,僅在支持時才有效
cv::CAP_PROP_GAIN ? 相機(jī)的增益設(shè)置,僅在支持時才有效
cv::CAP_PROP_EXPOSURE ? 相機(jī)的曝光度設(shè)置,僅在支持時才有效
cv::CAP_PROP_CONVERT_RGB ? 如果為非0值,捕獲到的圖像將會被轉(zhuǎn)換為3個通道
cv::CAP_PROP_WHITE_BALANCE ? 相機(jī)的白平衡設(shè)置,僅在支持時才有效
cv::CAP_PROP_RECTIFICATION ? 立體相機(jī)整流標(biāo)志,僅支持DC1394-2.x

上表的所有值都是以雙精度型讀取或者設(shè)置的,這對于大多數(shù)場景都是有效或者可以接受的,但是對于屬性cv::CAP_PROP_FOURCC而言,必須經(jīng)過轉(zhuǎn)換才有意義,轉(zhuǎn)換的示例代碼如下。

cv::VideoCapture cap("my_video.avi");

unsigned f = (unsigned)cap.get(cv::CAP_PROP_FOURCC);
char fourcc[] = {(char) f, //第一個字符取最低8位
                 (char)(f >> 8),  // 第二個字符為8~15位
                 (char)(f >> 16), // 第三個字符為16~23位
                 (char)(f >> 24), // 第四個字符為24~31位
                 '\0'};
2.2.4 寫入視頻文件

cv::VideoWriter可以處理視頻文件寫入的任務(wù),它包含兩個構(gòu)造函數(shù),其中默認(rèn)構(gòu)造函數(shù)不包含任何參數(shù),但是需要在使用時設(shè)置。包含寫入器能正常工作的所有必要參數(shù)的構(gòu)造函數(shù)如下。

// filename:輸出文件的絕對路徑
// fourcc:使用的編碼標(biāo)準(zhǔn),使用宏CV_FOURCC()生成
// fps:生成的視頻文件的幀率
// frame_size:每幀畫面都尺寸
// is_color:是否是彩色視頻,設(shè)置為false時可以處理灰度幀
cv::VideoWriter::VideoWriter(const string& filename, int fourcc,
                             double fps, cv::Size frame_size,
                             bool is_color = true);

在實(shí)際應(yīng)用的時候也可以使用默認(rèn)構(gòu)造函數(shù)對象,然后再調(diào)用函數(shù)cv::VideoWriter::open來設(shè)置必要參數(shù),其代碼如下。

cv::VideoWriter out;

out.open("my_video.mpg", CV_FOURCC('D','I','V','X'), 30.0,
         cv::Size( 640, 480 ), true);

在完成視頻寫入器后不要忘記調(diào)用函數(shù)cv::VideoWriter::isOpened來判斷環(huán)境是否初始化成功,如果初始化失敗,可能是由于程序沒有指定的路徑的文件寫入權(quán)限,或者大多數(shù)情況下都是你指定的編碼器不可用。能夠使用的編碼器取決于你的操作系統(tǒng)自帶的或者額外安裝的庫,對于跨平臺的程序,一定要處理好在特定平臺特定的編碼器不可用的情況。

當(dāng)環(huán)境成功初始化后,函數(shù)cv::VideoWriter::write可以寫入指定的視頻幀,其原型如下。需要注意寫入的視頻幀必須和初始化寫入器環(huán)境時設(shè)置的幀大小即灰度模式相同,即如果設(shè)置了isColorfalse,則必須是三通道的數(shù)據(jù)。

// image:待寫入的下一幀數(shù)據(jù)
cv::VideoWriter::write(const Mat& image);

重載后的運(yùn)算符<<也可以寫入視頻幀,示例代碼如下。

my_video_writer << my_frame;

2.3 YAML和XML文件

除了標(biāo)準(zhǔn)的視頻文件輸出外,OpenCV還提供了一個機(jī)制用于序列化和反序列化其提供的各種數(shù)據(jù)結(jié)構(gòu),并以YAML或者XML文件格式從磁盤讀取或者存儲到磁盤中,此外這套機(jī)制也支持OpenCV到配置和日志文件。類cv::FileStorage是這個功能的核心類,它可以上表示磁盤上的一個文件,其構(gòu)造函數(shù)如下。

FileStorage::FileStorage();

// fileName:YAML或者XML文件的絕對路徑
// flag:數(shù)據(jù)寫入策略,可選值為cv::FileStorage::WRITE或cv::FileStorage::APPEND
FileStorage::FileStorage(string fileName, int flag);

如果使用了默認(rèn)的構(gòu)造函數(shù),則需要調(diào)用函數(shù)FileStorage::open打開一個文件,其原型如下。

FileStorage::open(string fileName, int flag);
2.3.1 寫入數(shù)據(jù)

當(dāng)文件被成功打開后,就可以使用重載的運(yùn)算符<<寫入數(shù)據(jù)。cv::FileStorage內(nèi)部以兩種方式存儲數(shù)據(jù),分別是鍵值對的映射模式(Mapping)和直接存儲的序列(Sequence)模式。使用任意模式存儲時,需要先為其提供一個String類型的名字,然后再存儲條目的內(nèi)容,存儲方式如下。

// 存儲一個整形數(shù)據(jù)
myFileStorage << "someInteger" << 27;
// 存儲一個矩陣
myFileStorage << "anArray" << cv::Mat::eye(3,3,CV_32F);

如果創(chuàng)建一個映射關(guān)系可以在映射的名后使用特殊符號{}開啟或結(jié)束一個映射。如果創(chuàng)建的序列關(guān)系存在多個值時可以使用特殊符號[]開啟或結(jié)束一個序列。使用方式如下。

myFileStorage << "theCat" << "{";
myFileStorage << "fur" << "gray" << "eyes" << "green" << "weightLbs" << 16;
myFileStorage << "}";

myFileStorage << "theTeam" << "[";
myFileStorage << "eddie" << "tom" << "Scott";
myFileStorage << "]";

在完成數(shù)據(jù)寫入后,調(diào)用函數(shù)cv::FileStorage::release()關(guān)閉文件,實(shí)例程序WriteYML創(chuàng)建了一個yml格式的數(shù)據(jù)文件,其核心代碼如下。

int main(int, char** argv) {
    cv::FileStorage fs("test.yml", cv::FileStorage::WRITE);
    fs << "frameCount" << 5;

    time_t rawtime;
    time(&rawtime);
    fs << "calibrationDate" << asctime(localtime(&rawtime));

    cv::Mat cameraMatrix = 
        (cv::Mat_<double>(3,3) << 1000, 0, 320, 0, 1000, 240, 0, 0, 1);
    cv::Mat distCoeffs = (cv::Mat_<double>(5,1) << 0.1, 0.01, -0.001, 0, 0);
    fs << "cameraMatrix" << cameraMatrix << "distCoeffs" << distCoeffs;

    fs << "features" << "[";
    for( int i = 0; i < 3; i++ ) {
        int x = rand() % 640;
        int y = rand() % 480;
        uchar lbp = rand() % 256;

        fs << "{:" << "x" << x << "y" << y << "lbp" << "[:";
        for( int j = 0; j < 8; j++ ) {
            fs << ((lbp >> j) & 1);
        }
        fs << "]" << "}";
    }
    fs << "]";
    fs.release();

    return 0;
}

該程序運(yùn)行后得到的YAML文件內(nèi)容如下。

%YAML:1.0
frameCount: 5
calibrationDate: "Fri Jun 17 14:09:29 2011\n"
cameraMatrix: !!opencv-matrix
   rows: 3
   cols: 3
   dt: d
   data: [ 1000., 0., 320., 0., 1000., 240., 0., 0., 1. ]
distCoeffs: !!opencv-matrix
   rows: 5
   cols: 1
   dt: d
   data: [ 1.0000000000000001e-01, 1.0000000000000000e-02,
       -1.0000000000000000e-03, 0., 0. ]
features:
   - { x:167, y:49,  lbp:[ 1, 0, 0, 1, 1, 0, 1, 1 ] }
   - { x:298, y:130, lbp:[ 0, 0, 0, 1, 0, 0, 1, 1 ] }
   - { x:344, y:158, lbp:[ 1, 1, 0, 0, 0, 0, 1, 0 ] }

在上面的處理結(jié)果中,可以看到有時一個映射或者一個序列中的所有數(shù)據(jù)都被存儲到了同一行,有時它們被存儲在了不同行。這是因?yàn)閅AML可以識別特殊符號{:[:,它們表示不需要換行,如果輸出文件格式為XML,則 :會被忽略。

2.3.2 讀取數(shù)據(jù)

如果需要從某個文件中讀取數(shù)據(jù),則在構(gòu)造FileStorage對象時參數(shù)flag需要設(shè)置為cv::FileStorage::READ。另外,和寫入數(shù)據(jù)一樣,也可以使用默認(rèn)構(gòu)造方法先創(chuàng)建一個實(shí)例,再調(diào)用FileStorage::open方法打開一個指定的文件。

當(dāng)文件被成功打開后就可以使用重載的數(shù)組運(yùn)算符[]或者使用迭代器cv::FileNodeIterator查詢數(shù)據(jù),需要注意的是不再需要讀取數(shù)據(jù)時,記得調(diào)用函數(shù)cv::FileStorage::release()關(guān)閉文件。

使用重載的數(shù)組運(yùn)算符[]方式獲取數(shù)據(jù)時,如果讀取的是映射關(guān)系數(shù)據(jù)時,需要使用和目標(biāo)對象關(guān)聯(lián)的字符串類型的鍵值查詢數(shù)據(jù),如果讀取的是序列數(shù)據(jù)時,則使用整型的下標(biāo)即可。返回值是cv::FileNode的實(shí)例,它是查詢結(jié)果的封裝。

cv::FileNode的實(shí)例可以表示一個對象、數(shù)字或者字符串,通過重載的運(yùn)算符>>可以提取其中的數(shù)據(jù),使用示例如下。

cv::Mat anArray;
myFileStorage["calibrationMatrix"] >> anArray;

int aNumber;
myFileStorage["someInteger"] >> aNumber;

// 甚至可以使用如下方式獲取數(shù)據(jù)
int aNumber;
aNumber = (int)myFileStorage["someInteger"];

迭代器對象cv::FileNodeIterator可以遍歷一個cv::FileNode對象,其成員函數(shù)cv::FileNode::begin()cv::FileNode::end()分別獲取映射或者序列的首個元素以及末尾元素后一個標(biāo)記。通過重載的求值運(yùn)算符*可以得到當(dāng)前位置的cv::FileNode對象。在讀取數(shù)據(jù)時,OpenCV將每個數(shù)據(jù)都視為cv::FileNode對象,即其自身內(nèi)部的映射或者序列都被視為由多個cv::FileNode對象組成。該迭代器對象也支持遞增和遞減運(yùn)算符。如果遍歷的是映射,則每個對象都包含一個名字,可以通過cv::FileNode::Name獲取。

cv::FileNode的成員函數(shù)見下表,其中函數(shù)cv::FileNode::type()返回的值是一個枚舉類型,可能的取值在下文介紹。

成員函數(shù) 描述
cv::FileNode fn() 默認(rèn)構(gòu)造函數(shù)
cv::FileNode fn1(fn0) 復(fù)制構(gòu)造函數(shù),從節(jié)點(diǎn)fn0復(fù)制到節(jié)點(diǎn)f1
cv::FileNode fn1(fs, node) 構(gòu)造函數(shù),從C風(fēng)格的CvFileStorage指針和C風(fēng)格的CvFileNode指針中構(gòu)造C++風(fēng)格的文件節(jié)點(diǎn)cv::FileNode對象
fn[(string)key]
fn[(const char*)key]
使用映射節(jié)點(diǎn)的名字獲取對應(yīng)的節(jié)點(diǎn),可以是STL和C風(fēng)格的字符串
fn[(int)id] 通過下標(biāo)獲取序列中的子節(jié)點(diǎn)
fn.type() 返回節(jié)點(diǎn)類型
fn.empty() 判斷節(jié)點(diǎn)是否為空
fn.isNone() 判斷節(jié)點(diǎn)是否含None值
fn.isSeq() 判斷節(jié)點(diǎn)是否為一個序列
fn.isMap() 判斷節(jié)點(diǎn)是否為一個映射
fn.isInt()
fn.isReal()
fn.isString()
判斷節(jié)點(diǎn)是否為一個整型值,浮點(diǎn)值或者是字符串
fn.name() 如果該節(jié)點(diǎn)是一個映射,則返回其名字
size_t sz = fn.size() 如果該節(jié)點(diǎn)為映射或者為序列,則返回元素個數(shù)
(int)fn
(float)fn
(double)fn
(string)fn
如果節(jié)點(diǎn)類型為int、浮點(diǎn)型、雙精度型或者字符串型,則分別返回節(jié)點(diǎn)的值

函數(shù)cv::FileNode::type()返回的值和其對應(yīng)的含義如下表。需要注意的是浮點(diǎn)型數(shù)據(jù)和雙精度數(shù)據(jù)并沒有區(qū)分,這是因?yàn)閄ML和YAML文件都是ASCII格式的文本文件,在這種格式下浮點(diǎn)型數(shù)據(jù)在被轉(zhuǎn)換成內(nèi)部機(jī)器變量類型之前是不帶有精度信息的,因此這里沒有對它們進(jìn)行區(qū)分。

枚舉值 描述
cv::FileNode::NONE = 0 節(jié)點(diǎn)不包含任何數(shù)據(jù),類型為None
cv::FileNode::INT = 1 整型數(shù)據(jù)
cv::FileNode::REAL = 2
cv::FileNode::FLOAT = 2
浮點(diǎn)型數(shù)據(jù)
cv::FileNode::STR = 3
cv::FileNode::STRING = 3
字符串型數(shù)據(jù)
cv::FileNode::REF = 4 引用類型,如一個復(fù)合對象
cv::FileNode::SEQ = 5 包含子節(jié)點(diǎn)的序列
cv::FileNode::MAP = 6 包含子節(jié)點(diǎn)的映射
cv::FileNode::FLOW = 8 節(jié)點(diǎn)是序列或者映射的緊湊表示
cv::FileNode::USER = 16 節(jié)點(diǎn)是注冊對象,如矩陣
cv::FileNode::EMPTY = 32 空節(jié)點(diǎn),未賦值
cv::FileNode::NAMED = 64 不包含子節(jié)點(diǎn)的映射,包含名字

cv::FileNode::FLOW開始,所有枚舉值的真實(shí)值都是2的冪,這是因?yàn)楹?種的任意一種或者全部都能與前面的任意一種組合。示例ReadYML使用前面介紹的方法從文件test.yml中讀取數(shù)據(jù),其核心代碼如下。

cv::FileStorage fs2("test.yml", cv::FileStorage::READ);

// 第一種方式,使用重載的類型轉(zhuǎn)換運(yùn)算符
int frameCount = (int)fs2["frameCount"];

// 第二種方式,使用重載的輸入運(yùn)算符>>
std::string date;
fs2["calibrationDate"] >> date;

cv::Mat cameraMatrix2, distCoeffs2;
fs2["cameraMatrix"] >> cameraMatrix2;
fs2["distCoeffs"] >> distCoeffs2;

cout << "frameCount: " << frameCount << endl
     << "calibration date: " << date << endl
     << "camera matrix: " << cameraMatrix2 << endl
     << "distortion coeffs: " << distCoeffs2 << endl;

cv::FileNode features = fs2["features"];
cv::FileNodeIterator it = features.begin(), it_end = features.end();
int idx = 0;
std::vector<uchar> lbpval;

// 使用迭代器遍歷序列
for (; it != it_end; ++it, idx++) {
    cout << "feature #" << idx << ": ";
    cout << "x=" << (int)(*it)["x"] << ", y=" << (int)(*it)["y"] << ", lbp: (";

    // 使用重載的輸入運(yùn)算符>>讀取數(shù)字組成的數(shù)組
    (*it)["lbp"] >> lbpval;
    for (int i = 0; i < (int)lbpval.size(); i++) {
        cout << " " << (int)lbpval[i];
        cout << ")" << endl;
    }
}
fs.release();

3 UI操作

HighGUI模塊處理能夠處理文件和設(shè)備相關(guān)任務(wù)外,還提供了一些基礎(chǔ)的UI功能,如創(chuàng)建窗口,在窗口中顯示圖片,處理這些窗口中的用戶交互事件。這些功能是部分跨平臺的,因?yàn)橥瑯拥慕涌趯τ诓煌牟僮飨到y(tǒng)實(shí)現(xiàn)并不一樣,如在Linux系統(tǒng)上使用了X11,在Mac OS上使用了Cocoa,在Windows系統(tǒng)中使用了Win32 API。另外對于部分系統(tǒng)而言這些功能是不可用的,如在安卓系統(tǒng)中就沒有實(shí)現(xiàn)。Qt是一個跨平臺的UI工具庫,它比HighGUI提供的接口更強(qiáng)大,可以預(yù)見在未來它能夠取代HighGUI的地位。

3.1 不包含Qt庫的原生接口

需要注意如果在編譯OpenCV時使用了Qt工具庫,某些函數(shù)的表現(xiàn)將會和原生函數(shù)有一定差異,這里先介紹原生函數(shù),在下小節(jié)中再介紹Qt庫。HighGUI的輸入工具函數(shù)支持鍵盤輸入,圖像區(qū)域的鼠標(biāo)點(diǎn)擊,和鼠標(biāo)滑輪滾動事件。

3.1.1 窗口

創(chuàng)建窗口的函數(shù)原型如下。OpenCV引用窗口的方式是名字,而一些操作系統(tǒng)是通過句柄,OpenCV內(nèi)部提供在兩種模式間轉(zhuǎn)換的功能。目前參數(shù)flags可以選的值只能是0或者cv::WINDOW_AUTOSIZE,在介紹完Qt庫后,它會有更多選項(xiàng)。另外當(dāng)設(shè)置為cv::WINDOW_AUTOSIZE時,用戶不能調(diào)整窗口的大小。

// name:窗口的名字和標(biāo)識
//      作為名字將會顯示在窗口上,作為標(biāo)識可以作為其他HighGUI函數(shù)的參數(shù)從而操作同一個窗口對象
// flags:當(dāng)該窗口用于顯示圖片時,是否需要自動調(diào)整其大小和圖片尺寸相同
int cv::namedWindow(const string& name, int flags = 0);

銷毀窗口的函數(shù)原型如下。

// name:需要銷毀的窗口名字
int cv::destroyWindow(const string& name);
3.1.2 顯示圖片

在某個窗口上顯示圖片的函數(shù)原型如下。需要注意的是窗口將會拷貝圖片里的數(shù)據(jù)用于展示,因此調(diào)用該函數(shù)后更改圖片的內(nèi)容,窗口并不會刷新。

// name:需要顯示圖片的窗口標(biāo)識
// image:需要顯示的圖片
void cv::imshow(const string& name, cv::InputArray image);

函數(shù)cv::waitKey包含事件監(jiān)聽和更新窗口兩個功能。事件監(jiān)聽功能指它在指定時間內(nèi)等待鍵盤輸入事件,當(dāng)然也可以一直等待。他可以接收來自任意窗口的鍵盤事件,當(dāng)然在沒有窗口存在時這個功能將不會生效。更新窗口功能指的是可以創(chuàng)造一個任意窗口更新的機(jī)會,這意味著如果不調(diào)用該函數(shù),可能窗口永遠(yuǎn)不會顯示圖片,或者在窗口移動、尺寸更新、或者從被遮擋狀態(tài)下顯示的時候可能會發(fā)生十分奇怪的現(xiàn)象。該函數(shù)原型如下。

// return:如果在指定時間內(nèi)沒有任何鍵盤時間,則返回-1,否則返回鍵位對應(yīng)的ASCII值
// delay:等待的時間,設(shè)置為0時表示一直等待,單位為毫秒
int cv::waitKey(int delay = 0);

示例DisplayImage在桌面上展示了一張圖片,其核心代碼如下。

int main( int argc, char** argv ) {
    // 使用圖片路徑位名字創(chuàng)建窗口
    cv::namedWindow( argv[1], 1 );
    // 加載圖片
    cv::Mat = cv::imread( argv[1] );
    // 在窗口中顯示圖片
    cv::imshow( argv[1], img );

    // 掛起線程等待用戶輸入ESC鍵
    while( true ) {
        if( cv::waitKey( 100 /* milliseconds */ ) == 27 ) {
            break;
        }
    }

    // 銷毀窗口
    cv::destroyWindow( argv[1] );
    exit(0);
}

該示例程序的運(yùn)行結(jié)果如下圖。

在繼續(xù)介紹后續(xù)內(nèi)容之前,先熟悉一些操作窗口的函數(shù),它們的原型如下。其中函數(shù)cv::startWindowThread適用于Linux和Mac OS X系統(tǒng),它會開啟一個線程用于自動更新窗口,負(fù)責(zé)處理窗口尺寸更新等任務(wù)。如果未調(diào)用該函數(shù),只有在顯示處理窗口更新事件時才會更新窗口,即調(diào)用函數(shù)cv::waitKey時。

// 移動窗口至左上角頂點(diǎn)的位置為P(x,y),單位為像素
void cv::moveWindow(const char* name, int x, int y);

// 銷毀所有由OpenCV創(chuàng)建的窗口,釋放內(nèi)存資源
void cv::destroyAllWindows(void);

// 開啟自動更新窗口的線程
// 返回值為0時表示開啟失敗,這可能是由于當(dāng)前版本的OpenCV不支持這個功能
int cv::startWindowThread(void);
3.1.3 鼠標(biāo)事件

響應(yīng)鼠標(biāo)事件是通過向OpenCV注冊回調(diào)函數(shù)完成的,該回調(diào)函數(shù)類型為cv::MouseCallback,其原型如下。

// event:鼠標(biāo)事件的類型,詳情見下文,如左鍵單擊事件
// x,y:相對于圖片,鼠標(biāo)事件發(fā)生的坐標(biāo),單位為像素
// flags:鼠標(biāo)事件發(fā)生的標(biāo)記,詳情見下文,如鼠標(biāo)事件發(fā)生的時候同時按住了某個鍵
// param:OpenCV透傳的信息
void your_mouse_callback(int event, int x, int y,
                         int flags, void* param);

參數(shù)event的可能取值及其含義如下表。

參數(shù)event枚舉值 真實(shí)值
cv::EVENT_MOUSEMOVE 0
cv::EVENT_LBUTTONDOWN 1
cv::EVENT_RBUTTONDOWN 2
cv::EVENT_MBUTTONDOWN 3
cv::EVENT_LBUTTONUP 4
cv::EVENT_RBUTTONUP 5
cv::EVENT_MBUTTONUP 6
cv::EVENT_LBUTTONDBLCLK 7
cv::EVENT_RBUTTONDBLCLK 8
cv::EVENT_MBUTTONDBLCLK 9

參數(shù)flags表示鼠標(biāo)事件發(fā)生時,同時鍵盤上被按住的鍵。其取值如下表。

參數(shù)flags枚舉值 真實(shí)值
cv::EVENT_FLAG_LBUTTON 1
cv::EVENT_FLAG_RBUTTON 2
cv::EVENT_FLAG_MBUTTON 4
cv::EVENT_FLAG_CTRLKEY 8
cv::EVENT_FLAG_SHIFTKEY 16
cv::EVENT_FLAG_ALTKEY 32

參數(shù)param是用于OpenCV向外部傳遞額外信息的,他可以是任意對象。一個常見的場景就是在調(diào)用接下來將要講到的注冊回調(diào)函數(shù)接口時,在注冊接口中將對應(yīng)的參數(shù)傳為this,即表示注釋邏輯發(fā)生的類。在這個回調(diào)函數(shù)被調(diào)用時,this會被作為參數(shù)拋出,則我們就可以知道它是在哪個類中注冊的了。

注冊注冊回調(diào)函數(shù)的接口原型如下。

// windowName:接收事件的窗口標(biāo)識,只有該窗口發(fā)生的事件才會觸發(fā)回調(diào)
// on_mouse:回調(diào)函數(shù)的地址
// param:回調(diào)執(zhí)行時攜帶的額外信息
void cv::setMouseCallback(const string& windowName, 
                          cv::MouseCallback on_mouse,
                          void* param = NULL);

示例DrawBoxes支持使用鼠標(biāo)畫框,其核心代碼如下。

Rect box;
bool drawing_box = false;

// 繪圖函數(shù)
void draw_box(cv::Mat& img, cv::Rect box) {
    // 繪制紅色矩形
    cv::rectangle(img, box.tl(), box.br(), cv::Scalar(0x00,0x00,0xff));
}

int main(int argc, char** argv) {
    box = cv::Rect(-1,-1,0,0);
    // 使用temp和image兩張圖片,image作為原圖,每次拷貝至temp后繪制矩形再顯示
    cv::Mat image(200, 200, CV_8UC3), temp;
    image.copyTo(temp);
    // 重載=運(yùn)算符,以該值賦值每一個元素
    image = cv::Scalar::all(0);

    cv::namedWindow( "Box Example" );
    // 注冊回調(diào)函數(shù),透傳圖像image
    // 傳image是因?yàn)槊恳淮卫L制的結(jié)果都需要疊加
    cv::setMouseCallback("Box Example", my_mouse_callback, (void*)&image);

    // 程序主循環(huán),以15毫秒(66.6幀率)不斷更新圖片,并且如果此時正在繪制,則繪制最新的矩形
    // 如果輸入了ESC鍵,則退出循環(huán)
    for (;;) {
        image.copyTo(temp);
        if (drawing_box) {
            draw_box(temp, box);
        }
        cv::imshow( "Box Example", temp );

        if (cv::waitKey( 15 ) == 27) {
            break;
        }
    }
    return 0;
}

// 鼠標(biāo)事件監(jiān)聽函數(shù),如果按下左鍵,則記錄開始繪制矩形,在移動過程中更新矩形尺寸,
// 抬起左鍵時繪制矩形,繪圖過程結(jié)束
void my_mouse_callback(int event, int x, int y, int flags, void* param) {
    cv::Mat& image = *(cv::Mat*) param;
    switch (event) {
        case cv::EVENT_LBUTTONDOWN: {
            drawing_box = true;
            box = cv::Rect( x, y, 0, 0 );
        }
            break;
        case cv::EVENT_MOUSEMOVE: {
            if (drawing_box) {
                box.width = x - box.x;
                box.height = y - box.y;
            }
        }
            break;
        case cv::EVENT_LBUTTONUP: {
            drawing_box = false;
            if (box.width < 0) {
                box.x += box.width;
                box.width *= -1;
            }
            if (box.height < 0) {
                box.y += box.height;
                box.height *= -1;
            }
            draw_box(image, box);
        }
            break;
    }
}
3.1.4 滑動條

HighGUI模塊提供了滾動條(Trackbar)UI組件,和窗口一樣它也是通過名字來標(biāo)識的,其構(gòu)建函數(shù)如下?;瑒訔l會被添加到指定窗口的頂部或者底部,這和具體的操作系統(tǒng)相關(guān)?;瑒訔l不會擋住窗口內(nèi)已經(jīng)顯示的圖像,但是通常會放大窗口。另外滾動條的名字也會被顯示在控件旁邊,同樣其顯示的位置取決于具體的操作系統(tǒng),但是通常位于滾動條左側(cè)。

// trackbarName:滾動條的名字和標(biāo)識
// windowName:滾動條將被添加至的窗口標(biāo)識
// value:滾動條當(dāng)前的數(shù)值,使用指針的原因是在回調(diào)函數(shù)里可以訪問這個值
// count:滾動條的最大值
// onChange:滾動條發(fā)生改變時的回調(diào)函數(shù),詳細(xì)信息下文介紹
// param:給滾動條回調(diào)函數(shù)使用的透傳參數(shù),可以是任何對象
int cv::createTrackbar(const string& trackbarName, const string& windowName,
                       int* value, int count,
                       cv::TrackbarCallback onChange = NULL, void* param = NULL);

滾動條顯示的效果如下。

參數(shù)onChange是滾動條發(fā)生改變的回調(diào)函數(shù),它和鼠標(biāo)事件的回調(diào)函數(shù)類似,其函數(shù)類型原型如下。該參數(shù)不是必須的,當(dāng)其設(shè)置為NULL時,需要通過監(jiān)聽參數(shù)value指向的變量的改變從而響應(yīng)滾動條發(fā)生的事件。

// pos:當(dāng)前滾動條的值
// param:透傳出來的參數(shù),在調(diào)用cv::setTrackbarCallback()時設(shè)置
void your_trackbar_callback(int pos, void* param = NULL);

此外,還可以通過如下兩個函數(shù)來獲取和設(shè)置滾動條的游標(biāo)位置。

int cv::getTrackbarPos(const string& trackbarName,
                       const string& windowName);

void cv::setTrackbarPos(const string& trackbarName,
                        const string& windowName, int pos);
3.1.5 另類按鈕

HighGUI模塊不支持按鈕控件,如果嫌使用其他庫麻煩,有一個小技巧可以另類的實(shí)現(xiàn)按鈕功能??梢詣?chuàng)建一個只有兩個位置的滾動條,或者使用鍵盤事件來替代按鈕。甚至可以使用更復(fù)雜的方案,即創(chuàng)建一個假的控制面板,通過監(jiān)聽鼠標(biāo)的點(diǎn)擊區(qū)域來模擬按鈕事件,但是與其使用這么復(fù)雜的方式,還不如直接使用Qt庫內(nèi)的UI控件。

示例Trackbar使用了只包含兩個值的滾動條來模擬了一個開關(guān)按鈕,用于控制視頻的播放和暫停邏輯。其核心代碼如下。

// 設(shè)置滾動條游標(biāo)值
int g_switch_value = 1;

// 定義滾動條的回調(diào)函數(shù)
void switch_callback(int position, void * param) {
    if (position == 0) {
        cout << "Pause\n"; 
    } else {
        cout << "Run\n";
    }
}

int main(int argc, char** argv) {
    // 當(dāng)前視頻幀
    cv::Mat frame;

    cv::VideoCapture g_capture;
    cv::namedWindow("Example", 1);
    // 創(chuàng)建滾動條
    cv::createTrackbar("Switch", "Example", &g_switch_value, 1, switch_callback);

    // 掛起程序,直至按下退出鍵,掛起程序時根據(jù)開關(guān)狀態(tài)播放或暫停視頻播放
    for (;;) {
        if (g_switch_value) {
            g_capture >> frame;
            if (frame.empty()) {
                break;
            }
            cv::imshow("Example", frame);
        }
        if (cv::waitKey(10) == 27) {
            break;
        }
    }
    return 0;
}

3.2 Qt庫

盡管我們看到的GUI庫里面提供的基礎(chǔ)功的接口都是統(tǒng)一的,但是實(shí)際上它們在不同平臺上都是基于特定原生庫封裝的。OpenCV還支持在編譯時導(dǎo)入Qt庫,從而提供更多跨平臺的強(qiáng)大UI功能。需要注意的是使用依賴Qt庫的OpenCV函數(shù)和直接使用Qt庫是有差異的,在本小節(jié)末尾將會詳細(xì)講到這點(diǎn)。其實(shí)OpenCV即使使用了Qt庫支持的UI接口,這部分功能更多的仍然是面向?qū)嶒?yàn)室的開發(fā)和調(diào)試需求。如果想要創(chuàng)建商業(yè)的應(yīng)用,還是需要使用平臺原生庫或者更強(qiáng)大的UI庫。

在編譯時使用的cmake配置中-D WITH_QT設(shè)置為ON就可以將Qt庫編譯到OpenCV中。

3.2.1 工具條和狀態(tài)條

在包含Qt庫時打開窗口會看到兩個額外的UI控件,分別是工具條(Toolbar)和狀態(tài)條(Status bar),它們顯示的效果如下圖。

工具條前四個按鈕用于移動圖像,接下來四個用于縮放圖像,再接下來是保存圖像,最后是打開屬性窗口的按鈕。狀態(tài)條會顯示當(dāng)前鼠標(biāo)的位置,以及當(dāng)前位置的像素顏色RGB值。在包含Qt庫時,也可以在調(diào)用函數(shù)cv::namedWindow()時使用標(biāo)記cv::GUI_NORMAL隱藏工具條和狀態(tài)條。與之對應(yīng)的是cv::GUI_EXTENDED選項(xiàng),但是它的值為0,所以這是默認(rèn)選項(xiàng)。

默認(rèn)情況下,狀態(tài)條顯示的是鼠標(biāo)位置和像素顏色,可以調(diào)用如下函數(shù)自定義文本內(nèi)容。需要注意該函數(shù)只有對使用了標(biāo)記cv::GUI_EXTENDED創(chuàng)建的窗口才有效。當(dāng)達(dá)到設(shè)置的超時時間后,狀態(tài)條會展示默認(rèn)的文案。

// name:需要展示文本蓋層的窗口
// text:展示的文本內(nèi)容
// delay:文本展示的時長,0表示一直展示
int cv::displayStatusBar(const string& name, const string& text, int delay);
3.2.2 操作菜單

在創(chuàng)建窗口時啟用cv::GUI_EXTENDED選項(xiàng)后,在圖片上點(diǎn)擊右鍵可以彈出操作按鈕,其效果如下圖。其選項(xiàng)和工具條的按鈕相同。

3.2.3 文本蓋層

使用Qt庫的OpenCV還提供了函數(shù)用于在圖片上展示一個透明的文本蓋層,函數(shù)原型如下。需要注意文字有固定的尺寸,如果內(nèi)容過長會溢出,但是可以使用換行符\n輸入多行文字。默認(rèn)情況下,文本居中顯示。另外文本蓋層是唯一的,即當(dāng)前展示的文本蓋層會替換掉之前的蓋層,并且重新開始計(jì)時。

// name:需要展示文本蓋層的窗口
// text:展示的文本內(nèi)容
// delay:文本展示的時長,0表示一直展示
int cv::displayOverlay(const string& name, const string& text, int delay);
3.2.4 屬性窗口

在工具條和以及使用鼠標(biāo)右鍵點(diǎn)擊窗口區(qū)域彈出的菜單中,都提供了屬性按鈕,點(diǎn)擊該按鈕將會彈出一個屬性窗口,另外也可以使用快捷鍵Ctrl+P。需要注意的是每個程序只有一個屬性窗口,我們只需要配置這個窗口,在其中加入需要的按鈕以及滑動條等控件。需要注意的是當(dāng)該窗口未配置時是不可用的。屬性窗口的示例如下圖。

3.2.5 滑動條

從上圖中可以看出添加Qt庫的依賴后,使用同一個函數(shù)cv::createTrackbar創(chuàng)建的滑動條細(xì)節(jié)更豐富。另外通過將窗口名參數(shù)設(shè)置為空字符串可以直接將滾動條添加到屬性窗口中,示例代碼如下。

int contrast = 128;
cv::createTrackbar("Contrast:", "", &contrast, 255, on_change_contrast);
3.2.6 按鈕

Qt庫支持在屬性窗口中創(chuàng)建普通按鈕、單選按鈕和復(fù)選框。創(chuàng)建按鈕的函數(shù)原型如下。

// buttonName:按鈕的標(biāo)識,會顯示在按鈕旁邊,傳入“”時,會自動依次編號生成,如button 0
// onChange:按鈕事件的回調(diào)函數(shù)
// params:按鈕事件回調(diào)函數(shù)的透傳參數(shù)
// buttonType:按鈕類型,取值為cv::PUSH_BUTTON等
// initialState:按鈕的初始狀態(tài)
int cv::createButton(const string& buttonName,
                     cv::ButtonCallback onChange = NULL, void* params,
                     int buttonType = cv::PUSH_BUTTON, int initialState = 0);

參數(shù)buttonType的取值為cv::PUSH_BUTTON、cv::RADIOBOXcv::CHECKBOXcv::PUSH_BUTTON為普通按鈕,響應(yīng)點(diǎn)擊事件。cv::RADIOBOX為單選按鈕,同為一組cv::RADIOBOX類型的按鈕在按下時所有按鈕都會收到回調(diào)事件,稍后將會介紹此類按鈕的分組方法。cv::CHECKBOX為多選按鈕,選中和為選中狀態(tài)時值分別為1或者0。

參數(shù)onChange是處理按鈕事件的回調(diào)函數(shù),其原型如下。

// state:按鈕事件類型
// params:創(chuàng)建按鈕時設(shè)置的透傳參數(shù)
void your_button_callback(int state, void* params);

按鈕創(chuàng)建后會被自動組織為按鈕欄(Button Bars),一個按鈕欄就是一組按鈕,它們在屬性窗口中會被排列為一行。上圖中配置屬性窗口的代碼如下。

cv::createButton("", NULL, NULL, cv::PUSH_BUTTON);
cv::createButton("", NULL, NULL, cv::PUSH_BUTTON);
cv::createButton("", NULL, NULL, cv::PUSH_BUTTON);
cv::createTrackbar("Trackbar2", "", &mybar1, 255);
cv::createButton("Button3", NULL, NULL, cv::RADIOBOX, 1);
cv::createButton("Button4", NULL, NULL, cv::RADIOBOX, 0);
cv::createButton("Button5", NULL, NULL, cv::CHECKBOX, 0);

在上面的代碼中前三個按鈕和后三個按鈕中間插入了一個滑動條,因此它們被分別組織成為兩組即兩行控件。遺憾的是不存在手動為按鈕分組或者分行的接口,只能通過插入滾動條來完成。

3.2.7 字體和字號

Qt庫還支持一些更美觀和靈活的文字樣式,它被封裝為類CvFont,其構(gòu)造方式如下。

// fontName:系統(tǒng)字體名字,如Times,如果系統(tǒng)中不存在指定的字體,則加載默認(rèn)字體
// pointSize:文字的大小,單位為點(diǎn),設(shè)置為0時使用默認(rèn)字號,通常為12point
// color:文字的顏色,BGR顏色模型,默認(rèn)值為黑色
// weight:文字的線條寬度,取值空間為1-100,或者可以使用定義好的值,詳細(xì)信息見下表
// spacing:每個字符之間的間距,可以取負(fù)值
CvFont fontQt(const string& fontName, int pointSize,
              cv::Scalar color = cv::Scalar::all(0), 
              int weight = cv::FONT_NORMAL, int spacing = 0);

參數(shù)weight預(yù)定義的常量和真實(shí)值如下表。

參數(shù)weight取值 描述
cv::FONT_LIGHT 25
cv::FONT_NORMAL 50
cv::FONT_DEMIBOLD 63
cv::FONT_BOLD 75
cv::FONT_BLACK 87

成功創(chuàng)建文字樣式實(shí)例CvFont后,就可以使用如下函數(shù)在某個圖片上顯示文字。

// image:需要添加文字的圖像
// text:需要添加的文字
// location:文字展示的坐標(biāo),第一個字符的基線左下角坐標(biāo)
// font:文字樣式
void cv::addText(cv::Mat& image,
                 const string& text, cv::Point location, CvFont *font);
3.2.8 窗口

使用Qt庫創(chuàng)建的窗口大部分狀態(tài)屬性都是可以讀可寫的,相應(yīng)的函數(shù)原型如下。

// name:窗口的標(biāo)識
// prop_id:屬性的標(biāo)識,取值見下表
// prop_value:屬性的值
void cv::setWindowProperty(const string& name, int prop_id, double prop_value);

double cv::getWindowProperty(const string& name, int prop_id);

參數(shù)prop_id的取值信息見下表。

參數(shù)prop_id取值 描述
cv::WIND_PROP_FULL_SIZE 設(shè)置為cv::WINDOW_FULLSCREEN表示全屏窗口,設(shè)置為cv::WINDOW_NORMAL表示常規(guī)窗口
cv::WIND_PROP_AUTOSIZE 設(shè)置為cv::WINDOW_AUTOSIZE表示窗口會根據(jù)顯示的圖片自動調(diào)整大小,設(shè)置為cv::WINDOW_NORMAL表示將圖像大小調(diào)整為窗口大小
cv::WIND_PROP_ASPECTRATIO 設(shè)置為cv::WINDOW_FREERATIO表示允許窗口有任意長寬比,即用戶可以調(diào)整窗口長寬比,設(shè)置為cv::WINDOW_KEEPRATIO表示用戶不能調(diào)整長寬比,但是可以改變大小

窗口狀態(tài)是可以被存儲和復(fù)原的,它不僅包含窗口的位置和大小,還包括所有的滾動條和按鈕狀態(tài)。存儲窗口狀態(tài)的函數(shù)原型如下。

// name:需要保存狀態(tài)的窗口標(biāo)識
void cv::saveWindowParameters(const string& name);

復(fù)原窗口狀態(tài)的函數(shù)原型如下。

// name:需要復(fù)原狀態(tài)的窗口標(biāo)識
void cv::loadWindowParameters(const string& name);

復(fù)原窗口狀態(tài)函數(shù)在即使程序被殺死或者重啟后仍然能夠恢復(fù)到上一次保存到狀態(tài),它是如何實(shí)現(xiàn)的并不需要我們關(guān)心,只需要知道狀態(tài)保存和可執(zhí)行文件的名字相關(guān),也就是說如果改變了可執(zhí)行程序的名字 ,則不能恢復(fù)到上一次保存到狀態(tài)。

3.2.9 和OpenGL交互

在編譯OpenCV庫時如果同時打開了Qt庫,并啟用了OpenGL相關(guān)功能,就能夠調(diào)用相應(yīng)的接口生成圖像,或者在圖像上面添加蒙板。打開Qt庫的CMake選項(xiàng)在前文已經(jīng)講過,打開OpenGL功能需要在CMake中將選項(xiàng)-D WITH_QT_OPENGL設(shè)置為ON。該功能可以高效的可視化和調(diào)試機(jī)器人或者增強(qiáng)現(xiàn)實(shí)應(yīng)用,另外它對想要在原始圖像上生成三維模型并觀測效果的場景也很有幫助。下圖是類似場景一個簡單例子的示意效果。

使用OpenGL繪圖的方式很簡單,首先需要定義一個符合規(guī)范的OpenGL回調(diào)函數(shù),在其內(nèi)部使用OpenGL接口完成需要的繪制任務(wù)。每次窗口重繪時都會調(diào)用這個函數(shù),包含調(diào)用函數(shù)cv::imshow()。該函數(shù)原型如下。

// params:在注冊O(shè)penGL回調(diào)函數(shù)時設(shè)置的透傳參數(shù)
void your_opengl_callback(void* params);

聲明好自定義的回調(diào)函數(shù)后,調(diào)用還需要如下接口注冊該回調(diào)函數(shù)。

// windowName:注冊的窗口標(biāo)識,繪制的結(jié)果將被顯示在這個窗口上
// callback:注冊的回調(diào)函數(shù)
// params:透傳參數(shù)
void cv::createOpenGLCallback(const string& windowName, 
                              cv::OpenGLCallback callback, void* params = NULL);

另外如果如果需要獲得不同的顯示效果,需要在回調(diào)函數(shù)開始處使用OpenGL接口gluPerspective()設(shè)置投影矩陣,當(dāng)然這是OpenGL老版本的接口,這些接口僅用于調(diào)試。如果想要了解現(xiàn)代OpenGL的更多信息,可以移步至文集OpenGL

OpenCV文檔中有一段繪制立方體的代碼,對其稍為修改,使用變量rotxroty替換固定的旋轉(zhuǎn)角度,通過滑動條來設(shè)置這兩個參數(shù)的值,從而改變立方體現(xiàn)實(shí)效果。最終的示例程序Cube核心代碼如下。

void on_opengl( void* param ) {
    // 這里使用到的都是OpenGL老版本的接口,這些接口僅用于調(diào)試
    // 如果想要了解現(xiàn)代OpenGL的更多信息,可以移步至文集OpenGL
    glMatrixModel( GL_MODELVIEW );
    glLoadIdentity();
    glTranslated( 0.0, 0.0, -1.0 );

    glRotatef( rotx, 1, 0, 0 );
    glRotatef( roty, 0, 1, 0 );
    glRotatef( 0, 0, 0, 1 );

    static const int coords[6][4][3] = {
        { { +1, -1, -1 }, { -1, -1, -1 }, { -1, +1, -1 }, { +1, +1, -1 } },
        { { +1, +1, -1 }, { -1, +1, -1 }, { -1, +1, +1 }, { +1, +1, +1 } },
        { { +1, -1, +1 }, { +1, -1, -1 }, { +1, +1, -1 }, { +1, +1, +1 } },
        { { -1, -1, -1 }, { -1, -1, +1 }, { -1, +1, +1 }, { -1, +1, -1 } },
        { { +1, -1, +1 }, { -1, -1, +1 }, { -1, -1, -1 }, { +1, -1, -1 } },
        { { -1, -1, +1 }, { +1, -1, +1 }, { +1, +1, +1 }, { -1, +1, +1 } }
    };

    for (int i = 0; i < 6; ++i) {
        glColor3ub( i*20, 100+i*10, i*42 );
        glBegin(GL_QUADS);
        for (int j = 0; j < 4; ++j) {
            glVertex3d(0.2 * coords[i][j][0],
                       0.2 * coords[i][j][1], 0.2 * coords[i][j][2]);
        }
        glEnd();
    }
}

3.3 外置的跨平臺GUI庫

在開發(fā)正式工程時,即使使用內(nèi)置的Qt庫仍然難以滿足需求,因此我們需要使用外置的GUI工具包。本節(jié)會簡單介紹Qt庫,wxWidgets庫和微軟模版庫(Windows Template Library, WTL)。使用外置工具包的首要任務(wù)就是如何將OpenCV的圖像轉(zhuǎn)換成工具庫所期望的圖片格式,以及弄清工具包內(nèi)的哪個組件是用于圖像顯示的。

3.3.1 Qt庫

示例程序Qt使用Qt庫讀取并在屏幕展示視頻文件,其核心代碼如下。該示例創(chuàng)建了一個Qt程序?qū)ο?,使用自定義的QMoviePlayer對象來讀取和顯示視頻文件。

int main(int argc, char* argv[]) {
    // 創(chuàng)建Qt的應(yīng)用程序
    QApplication app(argc, argv);
    QMoviePlayer mp;
    mp.open(argv[1]);
    mp.show();

    return app.exec();
}

QMoviePlayer的頭文件定義如下。

class QMoviePlayer : public QWidget {
    Q_OBJECT;

    public:
    QMoviePlayer(QWidget *parent = NULL);
    virtual ~QMoviePlayer() {};
    bool open( string file );

    private:
    // Qt庫的播放器UI控制類
    Ui::QMoviePlayer ui;
    cv::VideoCapture m_cap;

    // Qt庫內(nèi)部的圖像抽象類
    QImage  m_qt_img;
    // CV庫內(nèi)部的圖像抽象類
    cv::Mat m_cv_img;
    // 控制視頻播放速率的時鐘
    QTimer* m_timer;

    // 重載QWidget的繪制函數(shù)
    void paintEvent( QPaintEvent* q );
    // 將CV的圖像數(shù)據(jù)拷貝至Qt的圖像數(shù)據(jù)中
    void _copyImage( void );

    public slots:
    // 讀取并顯示下一幀圖像
    void nextFrame();
};

QMoviePlayer的構(gòu)造函數(shù)實(shí)現(xiàn)如下。其中僅包含Ui::QMoviePlayer的配置邏輯。

QMoviePlayer::QMoviePlayer(QWidget *parent)
: QWidget(parent) {
    ui.setupUi(this);
}

打開視頻文件的函數(shù)QMoviePlayer::open(string file)實(shí)現(xiàn)如下。

bool QMoviePlayer::open(string file) {
    // 使用OpenCV接口打開視頻文件
    if (!m_cap.open(file)) {
        return false;
    }
    
    // 讀取第一幀到OpenCV的圖片模型中
    m_cap.read(m_cv_img);
    // 創(chuàng)建Qt庫圖像模型
    m_qt_img = QImage(QSize(m_cv_img.cols, m_cv_img.rows), QImage::Format_RGB888);
    // 設(shè)置Qt庫視頻播放器UI的邏輯分辨率
    ui.frame->setMinimumSize(m_qt_img.width(), m_qt_img.height());
    ui.frame->setMaximumSize(m_qt_img.width(), m_qt_img.height());
    // 將圖像數(shù)據(jù)從CV模型拷貝到Qt模型
    _copyImage();

    // 創(chuàng)建時鐘并綁定事件,每個設(shè)定的間隔結(jié)束后都會調(diào)用函數(shù)nextFrame()
    m_timer = new QTimer(this);
    connect(m_timer, SIGNAL(timeout()), this, SLOT(nextFrame()));
    // 運(yùn)行時鐘,時間間隔為1000/視頻幀率,即每幀畫面的顯示時間,單位為毫秒
    m_timer->start(1000. / m_cap.get(cv::CAP_PROP_FPS));

    return true;
}

將圖像數(shù)據(jù)從CV模型拷貝到Qt模型的函數(shù)QMoviePlayer::_copyImage(void)實(shí)現(xiàn)如下。

void QMoviePlayer::_copyImage(void) {
    // 首先使用m_qt_img的數(shù)據(jù)段指針創(chuàng)建一個CV的圖片模型
    cv::Mat cv_header_to_qt_image(cv::Size(m_qt_img.width(), m_qt_img.height()),
                                  CV_8UC3, m_qt_img.bits());
    // 使用CV的函數(shù)將m_cv_img數(shù)據(jù)轉(zhuǎn)換后拷貝至cv_header_to_qt_image中,
    //從而將其拷貝到m_qt_img中
    cv::cvtColor(m_cv_img, cv_header_to_qt_image, cv::BGR2RGB);
}

讀取并顯示下一幀圖像的函數(shù)QMoviePlayer::nextFrame()實(shí)現(xiàn)如下。

void QMoviePlayer::nextFrame() {
    if (!m_cap.isOpened()) {
        return;
    }
    // 讀取當(dāng)前幀圖像到CV模型內(nèi)
    m_cap.read(m_cv_img);
    // 將圖像數(shù)據(jù)從CV模型拷貝到Qt模型
    _copyImage();
    // 通知Qt庫需要刷新頁面
    this->update();
}

Qt庫中QWidget實(shí)例在需要重繪時會自動調(diào)用繪制函數(shù)QMoviePlayer::paintEvent(QPaintEvent* e),該函數(shù)的實(shí)現(xiàn)如下。一個可能需要重繪的時機(jī)是在調(diào)用函數(shù)如在調(diào)用函數(shù)update()后。

void QMoviePlayer::paintEvent(QPaintEvent* e) {
    // 創(chuàng)建繪制器
    QPainter painter(this);
    // 繪制圖像
    painter.drawImage(QPoint(ui.frame->x(), ui.frame->y()), m_qt_img);
}
3.3.2 wxWidgets庫

示例程序WXWidgets使用wxWidgets庫播放視頻文件的例子,和上一個例子類似,本例也將播放視頻任務(wù)的邏輯封裝到一個單獨(dú)的類WxMoviePlayer中。程序執(zhí)行的最頂層代碼如下。

// 繼承wxWidgets中的頂層類wxApp定義類MyApp
class MyApp : public wxApp {
    public:
    virtual bool OnInit();
};

// 創(chuàng)建main函數(shù),創(chuàng)建MyApp實(shí)例作為應(yīng)用程序,并將其和main函數(shù)綁定
DECLARE_APP(MyApp);
IMPLEMENT_APP(MyApp);

// 重寫應(yīng)用程序運(yùn)行后調(diào)用的函數(shù)
bool MyApp::OnInit() {
    // 創(chuàng)建幀(窗口管理對象)
    wxFrame* frame = new wxFrame(NULL, wxID_ANY, wxT("ch4_wx"));
    frame->Show(true);
    
    // 創(chuàng)建我們自己編寫的視頻播放器,并和該窗口關(guān)聯(lián)
    WxMoviePlayer* mp = new WxMoviePlayer(frame, wxPoint(-1, -1), 
                                          wxSize(640, 480));
    // 打開某個視頻文件
    mp->open(wxString(argv[1]));
    mp->Show(true);

    return true;
}

視頻播放器WxMoviePlayer頭文件聲明如下。

// 需要顯示在窗口上的視圖類都應(yīng)該繼承于wxWindow
class WxMoviePlayer : public wxWindow {
    public:
    WxMoviePlayer(wxWindow* parent, const wxPoint& pos, const wxSize& size);
    virtual ~WxMoviePlayer() {};
    bool open(wxString file);

    private:
    cv::VideoCapture m_cap;
    cv::Mat m_cv_img;
    // 和設(shè)備無關(guān)的圖片
    wxImage m_wx_img;
    // 和設(shè)備相關(guān)的圖片
    wxBitmap m_wx_bmp;
    wxTimer *m_timer;
    wxWindow *m_parent;

    void _copyImage(void);

    // 繪制事件,用于繪制視頻幀
    void OnPaint(wxPaintEvent& e);
    // 時鐘事件,用于刷新視頻幀
    void OnTimer(wxTimerEvent& e);
    // 鍵盤事件,用于監(jiān)聽ESC輸入退出程序
    void OnKey(wxKeyEvent& e);

    protected:
    DECLARE_EVENT_TABLE();
};

視頻播放器WxMoviePlayer的實(shí)現(xiàn)文件中注冊事件部分代碼如下。

BEGIN_EVENT_TABLE(WxMoviePlayer, wxWindow)
    EVT_PAINT(WxMoviePlayer::OnPaint)
    EVT_TIMER(TIMER_ID, WxMoviePlayer::OnTimer)
    EVT_CHAR(WxMoviePlayer::OnKey)
END_EVENT_TABLE()

視頻播放器WxMoviePlayer的構(gòu)造函數(shù)實(shí)現(xiàn)如下。

WxMoviePlayer::WxMoviePlayer(wxWindow* parent, const wxPoint& pos, 
                             const wxSize& size)
: wxWindow(parent, -1, pos, size, wxSIMPLE_BORDER) {
    // 打開視頻文件時再設(shè)置時鐘
    m_timer = NULL;
    // 記錄父框架
    m_parent = parent;
}

當(dāng)窗口重繪時會調(diào)用繪制函數(shù)OnPaint(wxPaintEvent& event),其實(shí)現(xiàn)如下。

void WxMoviePlayer::OnPaint(wxPaintEvent& event) {
    wxPaintDC dc(this);
    if (!dc.Ok()) {
        return;
    }

    int x,y,w,h;
    dc.BeginDrawing();
    dc.GetClippingBox(&x, &y, &w, &h);
    // m_wx_bmp是wxBitmap實(shí)例,它和設(shè)備相關(guān),可以直接展示
    dc.DrawBitmap(m_wx_bmp, x, y);
    dc.EndDrawing();

    return;
}

拷貝圖像數(shù)據(jù)方法實(shí)現(xiàn)如下。

void WxMoviePlayer::_copyImage(void) {
    m_wx_bmp = wxBitmap(m_wx_img);
    // 標(biāo)記窗口需要重繪
    Refresh(FALSE);
    // 強(qiáng)制執(zhí)行窗口刷新檢查,如果檢查到窗口需要重會,則調(diào)用重繪函數(shù)
    Update();
}

打開視頻文件的方法實(shí)現(xiàn)如下。

bool WxMoviePlayer::open(wxString file) {
    // 使用CV接口打開一個視頻文件
    if (!m_cap.open(std::string(file.mb_str()))) {
        return false;
    }
    
    // 使用CV接口讀取當(dāng)前視頻幀
    m_cap.read(m_cv_img);
    // 使用CV的圖片數(shù)據(jù)創(chuàng)建wxWidgets的設(shè)備無關(guān)圖片實(shí)例,需要注意這里不存在數(shù)據(jù)拷貝,
    // 指向的都是同一份圖像數(shù)據(jù)
    m_wx_img = wxImage(m_cv_img.cols, m_cv_img.rows, m_cv_img.data, TRUE);

    // 將設(shè)備無關(guān)圖像轉(zhuǎn)換為設(shè)備相關(guān)位圖
    _copyImage();

    // 創(chuàng)建時鐘
    m_timer = new wxTimer(this, TIMER_ID);
    // 按指定間隔運(yùn)行時鐘,每個時間間隔結(jié)束后都會產(chǎn)生一個wxTimerEvent事件,最后會調(diào)用回調(diào)函數(shù)
    m_timer->Start(1000. / m_cap.get(cv::CAP_PROP_FPS));

    return true;
}

時鐘的回調(diào)函數(shù)實(shí)現(xiàn)如下。

void WxMoviePlayer::OnTimer(wxTimerEvent& event) {
    if (!m_cap.isOpened()) {
        return;
    }

    // 讀取當(dāng)前視頻幀
    m_cap.read(m_cv_img);
    // 轉(zhuǎn)換顏色格式
    cv::cvtColor(m_cv_img, m_cv_img, cv::BGR2RGB);
    // 從設(shè)備無關(guān)圖像轉(zhuǎn)換為設(shè)備相關(guān)位圖
    _copyImage();
}

鍵盤事件的回調(diào)函數(shù)實(shí)現(xiàn)如下。

void WxMoviePlayer::OnKey(wxKeyEvent& e) {
    if (e.GetKeyCode() == WXK_ESCAPE) {
        // 關(guān)閉框架,結(jié)束程序
        m_parent->Close();
    }
}
3.3.3 窗口模版庫(Windows Template Library, WTL)

WTL是Win32 API的簡單C++封裝,軟件Visual Studio可以創(chuàng)建一個基于視圖窗口的WTL應(yīng)用程序,這應(yīng)該是默認(rèn)選項(xiàng)。默認(rèn)生成的文件和工程名相關(guān)。示例程序WTL使用這種方式創(chuàng)建了名為OpenCVTest的工程,使用WTL的接口和OpenCV接口完成了一個簡單的視頻播放程序。

該示例程序比直接使用DirectShow來處理視頻流效率更低,但是它更簡單。另外如果使用的是.NET運(yùn)行時,或者C#、VB.NET或者托管的C++(Managed C++),可能需要了解OpenCV的完整封裝Emgu。

4 小結(jié)

本章前半部分主要介紹了如何與磁盤文件以及物理設(shè)備進(jìn)行交互,并介紹了HighGUI模塊中用于讀寫圖像文件,這些接口內(nèi)部會自動處理對應(yīng)格式文件的編解碼任務(wù)。同樣也介紹了和圖像相似的視頻文件的讀寫方法,以及如何使用攝像頭拍攝視頻文件。另外也介紹了OpenCV提供接口使用XML和YML文件讀取以及存儲原生類型數(shù)據(jù),這兩種文件允許數(shù)據(jù)被讀到內(nèi)存中后映射為鍵值對結(jié)構(gòu)以便于檢索。

后半部分主要是和UI操作以及事件監(jiān)聽相關(guān)的內(nèi)容,首先熟悉了原始的HighGUI接口 ,然后介紹了如何在編譯OpenCV時選擇Qt庫優(yōu)化原生HighGUI接口的實(shí)現(xiàn)從而得到更強(qiáng)大的功能。最后簡單介紹了如何使用完全獨(dú)立的外部UI庫,并給出了相應(yīng)示例程序。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

友情鏈接更多精彩內(nèi)容