第2章 OpenCV基礎知識導論
在第1章介紹了在不同作業系統上安裝OpenCV之後,我們将在本章介紹OpenCV開發的基礎知識。首先介紹如何使用CMake建立項目。我們将介紹基本的圖像資料結構和矩陣,以及在項目中工作所需的其他結構。我們還會介紹如何通過OpenCV的XML/YAML存儲函數将變量和資料儲存到檔案中。
本章介紹以下主題:
- 使用CMake配置項目
- 從/向磁盤讀取/寫入圖像
- 讀取視訊和通路相機裝置
- 主要圖像結構(例如,矩陣)
- 其他重要和基本的結構(例如,向量和标量)
- 基本矩陣運算簡介
- 使用XML/YAML存儲OpenCV API進行檔案存儲操作
2.1 技術要求
本章需要讀者熟悉基本的C++程式設計語言,所使用的所有代碼都可以從以下GitHub連結下載下傳:
https://github.com/PacktPublishing/Learn-OpenCV-4-By-Building-Projects-Second-Edition/tree/master/Chapter_02。代碼可以在任何作業系統上執行,盡管隻在Ubuntu上測試過。
2.2 基本CMake配置檔案
為配置和檢查項目的所有必要依賴項,我們會用到CMake,但這不是唯一可以完成此操作的方法。我們可以在任何其他工具或IDE中配置我們的項目,例如Makefiles或Visual Studio,但CMake是一種用于配置多平台C++項目的更便攜的方式。
CMake使用名為CMakeLists.txt的配置檔案,可以在其中定義編譯和依賴關系過程。對于從單個源代碼檔案建構可執行檔案的基本項目,隻需要一個包含三行代碼的CMakeLists.txt檔案。
該檔案的内容類似于:

第一行定義所需的CMake最低版本,該行在CMakeLists.txt檔案中是必需的,它使我們能夠使用在特定版本中定義的CMake功能。在我們的例子中,要求最低版本為CMake 3.0。第二行定義項目的名稱。這個名稱儲存在名為PROJECT_NAME的變量中。
最後一行從main.cpp檔案建立一個可執行指令(add_executable()),并将其命名為與項目(${PROJECT_NAME})相同的名稱,然後将源代碼編譯成一個名為CMakeTest的可執行檔案,這是我們設定的項目名稱。${}表達式能夠通路環境中定義的任何變量。之後,我們就可以用${PROJECT_NAME}變量作為輸出的可執行檔案的名稱。
2.3 建立一個庫
CMake可用于建立由OpenCV建構系統使用的庫。在多個應用程式之間分解共享代碼是軟體開發中常見且有用的做法。在大型應用程式中,或者在多個應用程式共享的公共代碼中,這種做法非常有用。在這種情況下,我們不建立二進制可執行檔案,而是建立一個包含所有函數、類等的編譯檔案。這樣就可以和其他應用程式分享此庫檔案,而無須共享我們的源代碼。
CMake為此提供了add_library函數:
以#開頭的行是添加的注釋,會被CMake忽略。add_library(Hello hello.cpp hello.h)指令定義庫的源檔案及其名稱,其中Hello是庫名,hello.cpp和hello.h是源檔案。我們還添加了頭檔案,使得諸如Visual Studio這樣的IDE能夠連結到頭檔案。該行将會生成一個共享(.so适用于Mac OS X和Unix,.dll适用于Windows)或靜态庫(.a适用于Mac OS X和Unix,.lib适用于Windows)檔案,具體取決于我們是否在庫名和源檔案之間添加SHARED或STATIC字。target_link_libraries(executable Hello)是将可執行檔案連結到所需庫的函數,在我們的例子中,需要的庫是Hello庫。
2.4 管理依賴項
CMake具備搜尋依賴項和外部庫的能力,這使我們能夠根據項目中的外部元件建構複雜的項目,并添加一些要求。
在本書中,最重要的依賴項自然是OpenCV,我們将把它添加到我們的所有項目中:
現在,我們通過以下代碼了解腳本的工作原理:
第一行定義CMake的最低版本,第二行告訴CMake使用CMake的新行為,以便識别正确的數字和布爾常量,而無須使用這些名稱間接引用變量。該政策是在CMake 2.8.0中引入的,當3.0.2版本中未設定此政策時,CMake會發出警告。最後一行定義項目的标題。定義項目名稱後,我們必須定義需求、庫和依賴項:
這段代碼搜尋OpenCV依賴項。FIND_PACKAGE能夠查找依賴項、所需的最低版本以及該依賴是必需的還是可選的。在這個示例腳本中,我們查找4.0.0或更高版本的OpenCV,并聲明它是必需包。
如果CMake沒有找到它,就會傳回錯誤,并且不會阻止我們編譯應用程式。MESSAGE函數在終端或CMake GUI中顯示一條消息。在這個例子中,我們将這樣顯示OpenCV版本:
${OpenCV_VERSION}是CMake用來存儲OpenCV包版本的變量。include_directories()和link_directories()向環境中添加指定庫的頭檔案和路徑。OpenCV CMake的子產品将這些資料儲存在${OpenCV_INCLUDE_DIRS}和${OpenCV_LIB_DIR}變量中。并非所有平台(例如Linux)都需要這些指令行,因為這些路徑通常位于環境中,但是建議使用多個OpenCV版本來選擇正确的連結并包含路徑。現在包含我們開發的源檔案:
最後一行建立可執行檔案,并将可執行檔案與OpenCV庫連結,如上一節中所述。這段代碼中有一個新的函數SET,該函數建立一個新變量,并向其添加我們需要的任何值。在這個例子中,我們将main.cpp值合并到SRC變量中。我們還可以在同一個變量中添加更多的值,如下面的腳本所示:
2.5 讓腳本更複雜
在本節中,我們将要展示一個更複雜的腳本,它包括子檔案夾、庫和可執行檔案。但實際上,該腳本隻有兩個檔案和幾行代碼,如下例所示。沒有必要建立多個CMakeLists.txt檔案,因為我們可以在主CMakeLists.txt檔案中指定所有内容。但是,為每個項目子檔案夾使用不同的CMakeLists.txt檔案更為常見,可以使其更加靈活和便攜。
這個例子有一個代碼結構檔案夾,其中包含一個utils庫檔案夾和一個根檔案夾,後者包含主可執行檔案:
然後,我們必須定義兩個CMakeLists.txt檔案,一個在根檔案夾中,另一個在utils檔案夾中。CMakeLists.txt根檔案夾檔案具有以下内容:
除了我們将要解釋的一些函數之外,幾乎所有的代碼行都在前面中有過描述。add_subdirectory()告訴CMake分析所需子檔案夾的CMakeLists.txt。在繼續說明主CMakeLists.txt檔案之前,我們先解釋utils中的CMakeLists.txt檔案。
在utils檔案夾的CMakeLists.txt檔案中,我們将編寫一個将包含在主項目檔案夾中的新庫:
此CMake腳本檔案定義一個變量UTILS_LIB_SRC,我們在其中添加庫中包含的所有源檔案,并使用add_library函數生成庫,并且使用target_include_directories函數以便允許主項目檢測所有頭檔案。離開utils子檔案夾,繼續準備根CMake腳本,其中,Option函數建立一個新的變量,在這個例子中為WITH_LOG,并附帶一小段描述。可以通過ccmake指令行或顯示描述内容的CMake GUI界面更改這個變量,使用者還可以通過一個複選框啟用或禁用此選項。這個函數非常有用,它使使用者能夠決定編譯時功能,例如,我們是否要啟用或禁用日志,是否像OpenCV一樣使用Java或Python進行編譯,等等。
在這個例子中,我們使用此選項在應用程式中啟用記錄器。為啟用記錄器,我們在代碼中使用了一個預編譯器定義,如下所示:
可以通過調用add_definitions函數(-DLOG)在CMakeLists.txt中定義這個LOG宏,該函數本身可以使用簡單條件根據CMake變量WITH_LOG運作或隐藏:
至此,我們就完成了建立CMake腳本檔案的準備工作,可以在任何作業系統中編譯我們的計算機視覺項目。然後,在開始示例工程之前,我們會繼續介紹OpenCV的基礎知識。
2.6 圖像和矩陣
毫無疑問,計算機視覺中最重要的結構是圖像。計算機視覺中的圖像是用數字裝置捕獲的實體世界的表示。這種圖檔隻是以矩陣格式存儲的一系列數字(參見圖2-1)。每個數字是所考慮的波長(例如,彩色圖像中的紅色、綠色或藍色)或波長範圍(對于全色裝置)的光強度的測量結果。圖像中的每個點都稱為像素(對于圖像元素),并且每個像素可以存儲一個或多個值,這取決于它是否是僅存儲一個值的黑白圖像(也稱為二進制圖像,比如隻存儲0或1),還是存儲兩個值的灰階圖像,或者是存儲三個值的彩色圖像。這些值通常在整數0~255,但也可以使用其他範圍,比如在高動态範圍成像(high dynamic range imaging,簡稱HDRI)或熱圖像領域中的浮點數0~1。
圖像是以矩陣格式存儲的,其中的每個像素都有一個位置,并且可以通過列和行的編号來引用。OpenCV用Mat類來達到這個目的。在灰階圖像中,使用單個矩陣,如圖2-2所示。
在如圖2-3所示的彩色圖像中,使用了一個寬度×高度×顔色通道數的矩陣。
但Mat類不僅僅用于存儲圖像,它還能存儲任何類型和不同大小的矩陣。你可以将其用作代數矩陣并用它執行運算。在接下來的内容中,我們将描述最重要的矩陣運算,例如加法、乘法、對角化。但是,在此之前,了解矩陣如何存儲在計算機記憶體中是非常重要的,因為直接通路記憶體,總比用OpenCV函數通路每個像素更加高效。
在記憶體中,矩陣被儲存為按列和行排序的數組或值序列。表2-1顯示BGR圖像格式的像素序列。
按照這個順序,我們可以通過以下公式來通路任何像素:
2.7 讀/寫圖像
在介紹矩陣之後,我們将首先讨論OpenCV代碼的基礎知識。我們要學習的第一件事是如何讀/寫圖像:
現在我們來了解代碼。
首先,必須包括例子中需要的函數的聲明。這些函數來自core(基本圖像資料處理)和highgui(OpenCV提供的跨平台I/O函數是core和highgui;第一個包括基本類,比如矩陣,而第二個包括讀函數、寫函數,以及用圖形界面顯示圖像的函數)。現在讀取圖像:
imread是讀取圖像的主函數。該函數打開圖像,并以矩陣格式存儲它。imread接受兩個參數,第一個參數是圖像路徑字元串,第二個參數是可選的,用于指定要加載的圖像類型,預設情況下為彩色圖像。第二個參數可以使用以下選項:
- cv::IMREAD_UNCHANGED:如果設定,當輸入具有相應的深度時,傳回16位/ 32位圖像,否則将其轉換為8位
- cv::IMREAD_COLOR:如果設定,它總是将圖像轉換為彩色圖像(BGR,8位無符号)
- cv::IMREAD_GRAYSCALE:如果設定,它總是将圖像轉換為灰階圖像(8位無符号)
要儲存圖像,可以使用imwrite函數,它将矩陣圖像存儲在計算機中:
第一個參數是儲存圖像的路徑,以及想要的擴充名格式,第二個參數是要儲存的矩陣圖像。在這個代碼例子中,我們建立并存儲圖像的灰階版本,然後将其另存為.jpg檔案。加載的灰階圖像将存儲在gray變量中:
通過使用矩陣的.cols和.rows屬性,可以通路圖像的列數和行數,換句話說,可以通路其寬度和高度:
要通路圖像的一個像素,可以用Mat OpenCV類中的模闆函數cv::Mat::at (row,col),模闆參數是所需的傳回類型。8位彩色圖像中的類型名稱是Vec3b類,它存儲三個無符号字元資料(Vec =向量,3 =元件數,b = 一個位元組)。在灰階圖像中,可以直接使用無符号字元,或圖像中使用的任何其他數字格式,例如uchar pixel = color.at (myRow,myCol)。最後,為了展示圖像,可以使用imshow函數,它建立一個視窗,其标題作為第一個參數,圖像矩陣作為第二個參數:
前面代碼的結果如圖2-4所示,左邊的圖像是彩色圖像,右邊的圖像是灰階圖像。
最後,我們按以下示例建立CMakeLists.txt檔案,并使用該檔案編譯代碼。
以下代碼描述了CMakeLists.txt檔案:
要使用此CMakeLists.txt檔案編譯代碼,必須執行以下步驟:
- 建立一個build檔案夾。
- 在build檔案夾内,(在Windows中)執行CMake或打開CMake GUI應用程式,選擇source檔案夾和build檔案夾,然後按下“Configure”(配置)和“Generate”(生成)按鈕。
- 如果正在使用Linux或MacOSX,請照常生成Makefile,然後用make指令編譯項目。如果正在使用Windows,請用在步驟2中選擇的編輯器打開項目,然後進行編譯。
在編譯應用程式之後,将會在build檔案夾中生成一個名為app的可執行檔案。
2.8 讀取視訊和攝像頭
本節将用這個簡單示例向你介紹視訊和攝像頭的讀取。在解釋如何讀取視訊或攝像頭的輸入之前,我們想介紹一個非常有用的新類,它可以幫助我們管理輸入指令行參數。這個新類是在OpenCV 3.0版中引入的,它就是CommandLineParser類:
我們必須為CommandLineParser做的第一件事是在常量char向量中定義我們需要或允許的參數,每一行都采用以下模式:
name_param可以以@開頭,這會将此參數定義為預設輸入。我們可以使用多個name_param:
構造函數将擷取main函數的輸入和先前定義的key常量:
.has類方法檢查參數是否存在。在示例中,我們檢查使用者是否添加參數help或?,然後使用類函數printMessage顯示所有描述參數:
使用.get(parameterName)函數可以通路和讀取任何輸入參數:
擷取所有必需的參數以後,即可檢查這些參數是否被正确解析,并在其中一個參數未被解析時顯示錯誤消息,例如,添加的是一個字元串而不是一個數字:
用于視訊讀取和攝像頭讀取的類是相同的VideoCapture類,與之前版本的OpenCV中一樣,它屬于videoio子子產品而不是highgui子子產品。建立對象後,我們檢查輸入指令行參數videoFile是否有路徑檔案名。如果它是空的,那麼嘗試打開網絡攝像頭;如果它有檔案名,則打開視訊檔案。為此,可以使用open函數,将視訊檔案名或我們要打開的索引攝像頭作為參數。如果我們有一個攝像頭,可以用0作為參數。
要檢查是否可以讀取視訊檔案名或攝像頭,可以使用isOpened函數:
最後,建立一個視窗,使用namedWindow函數和無限循環來顯示幀,用>>操作抓取每個幀,如果正确地檢索到幀,則使用imshow函數顯示該幀。在這種情況下,我們不想讓應用程式停止,但是會調用waitKey(30)等待30毫秒,以此檢查使用者是否使用任何鍵停止應用程式的執行。
當使用者想結束應用程式時,他們所要做的就是按下任意鍵,然後我們必須使用釋放函數釋放所有的視訊資源。
前面代碼的結果是用一個新視窗顯示BGR格式的視訊或網絡攝像頭。
2.9 其他基本對象類型
我們已經了解了Mat和Vec3b類,但還有很多類需要學習。
在本節中,我們将學習大多數項目中所需的最基本的對象類型:
- Vec
- Scalar
- Point
- Size
- Rect
- RotatedRect
2.9.1 Vec對象類型
Vec是一個主要用于數值向量的模闆類。我們可以定義向量的類型群組件的數量:
我們還可以使用任何的預定義類型:
2.9.2 Scalar對象類型
Scalar對象類型是從Vec派生的模闆類,有四個元素。Scalar類型在OpenCV中廣泛用于傳遞和讀取像素值。
要通路Vec和Scalar值,可以使用[]運算符,其初始化可以用傳值的方式通過設定另一個标量、向量或值來完成,如下例所示:
2.9.3 Point對象類型
另一個非常常見的類模闆是Point。該類定義一個由其坐标x和y指定的2D點。
與Vec類一樣,OpenCV為友善起見定義了以下Point别名:
OpenCV為Point定義了以下運算符:
2.9.4 Size對象類型
Size是另一個非常重要并且在OpenCV中廣泛使用的模闆類,用于指定圖像或矩形大小。這個類添加了兩個成員width和height,以及有用的area()函數。在下面的示例中,我們可以看到許多使用Size的方法:
2.9.5 Rect對象類型
Rect是另一個重要的模闆類,用于定義由以下參數定義的2D矩形:
- 左上角的坐标
- 矩形的寬度和高度
Rect模闆類可用于定義圖像的感興趣區域(Region of Interest,簡稱ROI),如下所示:
2.9.6 RotatedRect對象類型
最後一個有用的類是名為RotatedRect的特定矩形。該類表示一個旋轉矩形,該矩形由中心點、矩形的寬度和高度以及機關為度的旋轉角度指定:
這個類的一個有趣的函數是boundingBox,該函數傳回一個包含旋轉矩形的Rect,如圖2-5所示。
2.10 基本矩陣運算
在本節中,我們将學習一些基本和重要的矩陣運算,這些運算可以應用于圖像或任何矩陣資料。我們已經知道如何加載圖像并将其存儲在變量Mat中,此外還可以手動建立Mat。最常見的構造函數是為矩陣提供大小和類型,如下所示:
受支援的類型取決于要存儲的數字類型和通道數,最常見的類型如下:
初始化不會設定資料的值,是以可能獲得不需要的值。為了避免不需要的值,可以使用0或1值及其各自的函數來初始化矩陣:
前面矩陣的結果如圖2-6所示。
一個特殊矩陣初始化是eye函數,它可以建立具有指定類型和大小的機關矩陣:
其輸出如圖2-7所示。
OpenCV的Mat類能夠執行所有的矩陣運算。我們可以用+和-運算符來加上或減去兩個相同大小的矩陣,如以下代碼塊所示:
上述操作的結果如圖2-8所示。
我們可以用運算符乘以一個标量,或者用mul函數乘以矩陣的每個元素,也可以用運算符執行矩陣乘法:
上述操作的結果如圖2-9所示。
其他常見的數學矩陣運算是轉置(transposition)和矩陣求逆(matrix inversion),分别由t()和inv()函數定義。OpenCV提供的其他有趣的函數是矩陣中的數組運算,例如,計算非零元素。這對于計算對象的像素或區域很有用:
OpenCV提供了一些統計功能,可以使用meanStdDev函數計算通道的平均值和标準差:
另一個有用的統計函數是minMaxLoc,該函數可以查找矩陣或數組的最小值和最大值,并傳回位置和值:
這裡的src是輸入矩陣,minVal和maxVal是檢測到的最小值和最大值,minLoc和maxLoc是檢測到的Point值。
2.11 基本資料存儲
在結束本章之前,我們将探讨OpenCV用來存儲和讀取資料的函數。在許多應用程式中(例如校準或機器學習),當我們完成大量計算時,需要儲存這些結果,以便在後續操作中檢索它們。OpenCV為此提供了XML / YAML持久層。
寫入FileStorage
要把一些OpenCV或其他數值資料寫入檔案,可以用FileStorage類,同時要使用流運算符<<操作STL流:
要建立儲存資料的檔案,隻需調用構造函數,并提供包含所需擴充名格式的路徑檔案名(XML或YAML),以及第二個要寫入的參數集:
如果要儲存資料,隻需在第一步給出一個辨別符,然後提供想要儲存的矩陣或值,通過這種方式來使用流操作符。例如,要儲存int變量,隻需要編寫以下代碼行:
否則,可以按如下所示寫入/儲存mat:
上述代碼的結果是YAML格式:
從檔案中讀取先前儲存的檔案與save函數非常相似:
第一個階段是通過調用FileStorage構造函數并使用适當的參數、路徑和FileStorage::READ來打開一個儲存的檔案:
要讀取任何存儲的變量,隻需使用公共的流運算符>>并使用FileStorage對象和帶[]運算符的辨別符:
2.12 總結
在本章中,我們學習了OpenCV的基礎知識和最重要的類型和操作(通路圖像和視訊),以及它們如何存儲在矩陣中。我們還學習了基本的矩陣運算和用于存儲像素的其他基本OpenCV類、向量等。最後,我們學習了如何将資料儲存在檔案中,以便在其他應用程式或其他操作中讀取它們。
在下一章中,我們将學習如何建立第一個應用程式,進而學習OpenCV提供的圖形使用者界面的基礎知識。我們将建立按鈕和滑塊,并介紹一些圖像處理的基礎知識。