目的¶
從真實世界中獲取數字圖像有很多方法,比如數碼相機、掃描儀、CT或者磁共振成像。無論哪種方法,我們(人類)看到的是圖像,而讓數字設備來“看“的時候,則是在記錄圖像中的每一個點的數值。

比如上面的圖像,在標出的鏡子區域中你見到的只是一個矩陣,該矩陣包含了所有像素點的強度值。如何獲取並存儲這些像素值由我們的需求而定,最終在計算機世界里所有圖像都可以簡化為數值矩以及矩陣信息。作為一個計算機視覺庫, OpenCV 其主要目的就是通過處理和操作這些信息,來獲取更高級的信息。因此,OpenCV如何存儲並操作圖像是你首先要學習的。
Mat¶
在2001年剛剛出現的時候,OpenCV基於 C 語言接口而建。為了在內存(memory)中存放圖像,當時采用名為 IplImage 的C語言結構體,時至今日這仍出現在大多數的舊版教程和教學材料。但這種方法必須接受C語言所有的不足,這其中最大的不足要數手動內存管理,其依據是用戶要為開辟和銷毀內存負責。雖然對於小型的程序來說手動管理內存不是問題,但一旦代碼開始變得越來越龐大,你需要越來越多地糾纏於這個問題,而不是着力解決你的開發目標。
幸運的是,C++出現了,並且帶來類的概念,這給用戶帶來另外一個選擇:自動的內存管理(不嚴謹地說)。這是一個好消息,如果C++完全兼容C的話,這個變化不會帶來兼容性問題。為此,OpenCV在2.0版本中引入了一個新的C++接口,利用自動內存管理給出了解決問題的新方法。使用這個方法,你不需要糾結在管理內存上,而且你的代碼會變得簡潔(少寫多得)。但C++接口唯一的不足是當前許多嵌入式開發系統只支持C語言。所以,當目標不是這種開發平台時,沒有必要使用 舊 方法(除非你是自找麻煩的受虐狂碼農)。
關於 Mat ,首先要知道的是你不必再手動地(1)為其開辟空間(2)在不需要時立即將空間釋放。但手動地做還是可以的:大多數OpenCV函數仍會手動地為輸出數據開辟空間。當傳遞一個已經存在的 Mat 對象時,開辟好的矩陣空間會被重用。也就是說,我們每次都使用大小正好的內存來完成任務。
基本上講 Mat 是一個類,由兩個數據部分組成:矩陣頭(包含矩陣尺寸,存儲方法,存儲地址等信息)和一個指向存儲所有像素值的矩陣(根據所選存儲方法的不同矩陣可以是不同的維數)的指針。矩陣頭的尺寸是常數值,但矩陣本身的尺寸會依圖像的不同而不同,通常比矩陣頭的尺寸大數個數量級。因此,當在程序中傳遞圖像並創建拷貝時,大的開銷是由矩陣造成的,而不是信息頭。OpenCV是一個圖像處理庫,囊括了大量的圖像處理函數,為了解決問題通常要使用庫中的多個函數,因此在函數中傳遞圖像是家常便飯。同時不要忘了我們正在討論的是計算量很大的圖像處理算法,因此,除非萬不得已,我們不應該拷貝 大 的圖像,因為這會降低程序速度。
為了搞定這個問題,OpenCV使用引用計數機制。其思路是讓每個 Mat 對象有自己的信息頭,但共享同一個矩陣。這通過讓矩陣指針指向同一地址而實現。而拷貝構造函數則 只拷貝信息頭和矩陣指針 ,而不拷貝矩陣。
1 2 3 4 5 6 |
Mat A, C; // 只創建信息頭部分
A = imread(argv[1], CV_LOAD_IMAGE_COLOR); // 這里為矩陣開辟內存
Mat B(A); // 使用拷貝構造函數
C = A; // 賦值運算符
|
以上代碼中的所有Mat對象最終都指向同一個也是唯一一個數據矩陣。雖然它們的信息頭不同,但通過任何一個對象所做的改變也會影響其它對象。實際上,不同的對象只是訪問相同數據的不同途徑而已。這里還要提及一個比較棒的功能:你可以創建只引用部分數據的信息頭。比如想要創建一個感興趣區域( ROI ),你只需要創建包含邊界信息的信息頭:
1 2 |
Mat D (A, Rect(10, 10, 100, 100) ); // using a rectangle
Mat E = A(Range:all(), Range(1,3)); // using row and column boundaries
|
現在你也許會問,如果矩陣屬於多個 Mat 對象,那么當不再需要它時誰來負責清理?簡單的回答是:最后一個使用它的對象。通過引用計數機制來實現。無論什么時候有人拷貝了一個 Mat 對象的信息頭,都會增加矩陣的引用次數;反之當一個頭被釋放之后,這個計數被減一;當計數值為零,矩陣會被清理。但某些時候你仍會想拷貝矩陣本身(不只是信息頭和矩陣指針),這時可以使用函數 clone() 或者 copyTo() 。
1 2 3 |
Mat F = A.clone();
Mat G;
A.copyTo(G);
|
現在改變 F 或者 G 就不會影響 Mat 信息頭所指向的矩陣。總結一下,你需要記住的是
存儲 方法¶
這里講述如何存儲像素值。需要指定顏色空間和數據類型。顏色空間是指對一個給定的顏色,如何組合顏色元素以對其編碼。最簡單的顏色空間要屬灰度級空間,只處理黑色和白色,對它們進行組合可以產生不同程度的灰色。
對於 彩色 方式則有更多種類的顏色空間,但不論哪種方式都是把顏色分成三個或者四個基元素,通過組合基元素可以產生所有的顏色。RGB顏色空間是最常用的一種顏色空間,這歸功於它也是人眼內部構成顏色的方式。它的基色是紅色、綠色和藍色,有時為了表示透明顏色也會加入第四個元素 alpha (A)。
有很多的顏色系統,各有自身優勢:
- RGB是最常見的,這是因為人眼采用相似的工作機制,它也被顯示設備所采用。
- HSV和HLS把顏色分解成色調、飽和度和亮度/明度。這是描述顏色更自然的方式,比如可以通過拋棄最后一個元素,使算法對輸入圖像的光照條件不敏感。
- YCrCb在JPEG圖像格式中廣泛使用。
- CIE L*a*b*是一種在感知上均勻的顏色空間,它適合用來度量兩個顏色之間的 距離 。
每個組成元素都有其自己的定義域,取決於其數據類型。如何存儲一個元素決定了我們在其定義域上能夠控制的精度。最小的數據類型是 char ,占一個字節或者8位,可以是有符號型(0到255之間)或無符號型(-127到+127之間)。盡管使用三個 char 型元素已經可以表示1600萬種可能的顏色(使用RGB顏色空間),但若使用float(4字節,32位)或double(8字節,64位)則能給出更加精細的顏色分辨能力。但同時也要切記增加元素的尺寸也會增加了圖像所占的內存空間。
顯式地創建一個 Mat 對象¶
教程 讀取、修改、保存圖像 已經講解了如何使用函數 imwrite() 將一個矩陣寫入圖像文件中。但是為了debug,更加方便的方式是看實際值。為此,你可以通過 Mat 的運算符 << 來實現,但要記住這只對二維矩陣有效。
Mat 不但是一個很贊的圖像容器類,它同時也是一個通用的矩陣類,所以可以用來創建和操作多維矩陣。創建一個Mat對象有多種方法:
-
Mat() 構造函數
Mat M(2,2, CV_8UC3, Scalar(0,0,255)); cout << "M = " << endl << " " << M << endl << endl;
![]()
對於二維多通道圖像,首先要定義其尺寸,即行數和列數。
然后,需要指定存儲元素的數據類型以及每個矩陣點的通道數。為此,依據下面的規則有多種定義
CV_[The number of bits per item][Signed or Unsigned][Type Prefix]C[The channel number]比如 CV_8UC3 表示使用8位的 unsigned char 型,每個像素由三個元素組成三通道。預先定義的通道數可以多達四個。 Scalar 是個short型vector。指定這個能夠使用指定的定制化值來初始化矩陣。當然,如果你需要更多通道數,你可以使用大寫的宏並把通道數放在小括號中,如下所示
-
在 C\C++ 中通過構造函數進行初始化
int sz[3] = {2,2,2}; Mat L(3,sz, CV_8UC(1), Scalar::all(0));
上面的例子演示了如何創建一個超過兩維的矩陣:指定維數,然后傳遞一個指向一個數組的指針,這個數組包含每個維度的尺寸;其余的相同
-
為已存在IplImage指針創建信息頭:
IplImage* img = cvLoadImage("greatwave.png", 1); Mat mtx(img); // convert IplImage* -> Mat
-
Create() function: 函數
M.create(4,4, CV_8UC(2)); cout << "M = "<< endl << " " << M << endl << endl;
![]()
這個創建方法不能為矩陣設初值,它只是在改變尺寸時重新為矩陣數據開辟內存。
-
MATLAB形式的初始化方式: zeros(), ones(), :eyes() 。使用以下方式指定尺寸和數據類型:
Mat E = Mat::eye(4, 4, CV_64F); cout << "E = " << endl << " " << E << endl << endl; Mat O = Mat::ones(2, 2, CV_32F); cout << "O = " << endl << " " << O << endl << endl; Mat Z = Mat::zeros(3,3, CV_8UC1); cout << "Z = " << endl << " " << Z << endl << endl;
-
對於小矩陣你可以用逗號分隔的初始化函數:
Mat C = (Mat_<double>(3,3) << 0, -1, 0, -1, 5, -1, 0, -1, 0); cout << "C = " << endl << " " << C << endl << endl;
格式化打印¶
Note
調用函數 randu() 來對一個矩陣使用隨機數填充,需要指定隨機數的上界和下界:
Mat R = Mat(3, 2, CV_8UC3);
randu(R, Scalar::all(0), Scalar::all(255));
從上面的例子中可以看到默認格式,除此之外,OpenCV還支持以下的輸出習慣
-
默認方式
cout << "R (default) = " << endl << R << endl << endl;
-
Python
cout << "R (python) = " << endl << format(R,"python") << endl << endl;
-
以逗號分隔的數值 (CSV)
cout << "R (csv) = " << endl << format(R,"csv" ) << endl << endl;
-
Numpy
cout << "R (numpy) = " << endl << format(R,"numpy" ) << endl << endl;
-
C語言
cout << "R (c) = " << endl << format(R,"C" ) << endl << endl;
打印其它常用項目¶
OpenCV支持使用運算符<<來打印其它常用OpenCV數據結構。
-
2維點
Point2f P(5, 1); cout << "Point (2D) = " << P << endl << endl;
-
3維點
Point3f P3f(2, 6, 7); cout << "Point (3D) = " << P3f << endl << endl;
-
基於cv::Mat的std::vector
vector<float> v; v.push_back( (float)CV_PI); v.push_back(2); v.push_back(3.01f); cout << "Vector of floats via Mat = " << Mat(v) << endl << endl;
-
std::vector點
vector<Point2f> vPoints(20); for (size_t E = 0; E < vPoints.size(); ++E) vPoints[E] = Point2f((float)(E * 5), (float)(E % 7)); cout << "A vector of 2D Points = " << vPoints << endl << endl;
這里的例子大多數出現在一個短小的控制台應用程序中,你可以在 here 下載到,或者在c++示例部分中找到。