Mat - 基本影象容器
目的
從真實世界中獲取數字影象有很多方法,比如數碼相機、掃描器、CT或者磁共振成像。無論哪種方法,我們(人類)看到的是影象,而讓數字裝置來“看“的時候,則是在記錄影象中的每一個點的數值。
比如上面的影象,在標出的鏡子區域中你見到的只是一個矩陣,該矩陣包含了所有畫素點的強度值。如何獲取並存儲這些畫素值由我們的需求而定,最終在計算機世界裡所有影象都可以簡化為數值矩以及矩陣資訊。作為一個計算機視覺庫, OpenCV 其主要目的就是通過處理和操作這些資訊,來獲取更高階的資訊。因此,OpenCV如何儲存並操作影象是你首先要學習的。
Mat
在2001年剛剛出現的時候,OpenCV基於 C
幸運的是,C++出現了,並且帶來類的概念,這給使用者帶來另外一個選擇:自動的記憶體管理(不嚴謹地說)。這是一個好訊息,如果C++完全相容C的話,這個變化不會帶來相容性問題。為此,OpenCV在2.0版本中引入了一個新的C++介面,利用自動記憶體管理給出瞭解決問題的新方法。使用這個方法,你不需要糾結在管理記憶體上,而且你的程式碼會變得簡潔(少寫多得)。但C++介面唯一的不足是當前許多嵌入式開發系統只支援C語言。所以,當目標不是這種開發平臺時,沒有必要使用 舊
關於 Mat ,首先要知道的是你不必再手動地(1)為其開闢空間(2)在不需要時立即將空間釋放。但手動地做還是可以的:大多數OpenCV函式仍會手動地為輸出資料開闢空間。當傳遞一個已經存在的 Mat 物件時,開闢好的矩陣空間會被重用。也就是說,我們每次都使用大小正好的記憶體來完成任務。
基本上講 Mat 是一個類,由兩個資料部分組成:矩陣頭(包含矩陣尺寸,儲存方法,儲存地址等資訊)和一個指向儲存所有畫素值的矩陣(根據所選儲存方法的不同矩陣可以是不同的維數)的指標。矩陣頭的尺寸是常數值,但矩陣本身的尺寸會依影象的不同而不同,通常比矩陣頭的尺寸大數個數量級。因此,當在程式中傳遞影象並建立拷貝時,大的開銷是由矩陣造成的,而不是資訊頭。OpenCV是一個影象處理庫,囊括了大量的影象處理函式,為了解決問題通常要使用庫中的多個函式,因此在函式中傳遞影象是家常便飯。同時不要忘了我們正在討論的是計算量很大的影象處理演算法,因此,除非萬不得已,我們不應該拷貝 大
為了搞定這個問題,OpenCV使用引用計數機制。其思路是讓每個 Mat 物件有自己的資訊頭,但共享同一個矩陣。這通過讓矩陣指標指向同一地址而實現。而拷貝建構函式則 只拷貝資訊頭和矩陣指標 ,而不拷貝矩陣。
Mat A, C; // 只建立資訊頭部分
A = imread(argv[1], CV_LOAD_IMAGE_COLOR); // 這裡為矩陣開闢記憶體
Mat B(A); // 使用拷貝建構函式
C = A; // 賦值運算子
以上程式碼中的所有Mat物件最終都指向同一個也是唯一一個數據矩陣。雖然它們的資訊頭不同,但通過任何一個物件所做的改變也會影響其它物件。實際上,不同的物件只是訪問相同資料的不同途徑而已。這裡還要提及一個比較棒的功能:你可以建立只引用部分資料的資訊頭。比如想要建立一個感興趣區域( ROI ),你只需要建立包含邊界資訊的資訊頭:
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() 。
Mat F = A.clone();
Mat G;
A.copyTo(G);
現在改變 F 或者 G 就不會影響 Mat 資訊頭所指向的矩陣。總結一下,你需要記住的是
- OpenCV函式中輸出影象的記憶體分配是自動完成的(如果不特別指定的話)。
- 使用OpenCV的C++介面時不需要考慮記憶體釋放問題。
- 賦值運算子和拷貝建構函式( ctor )只拷貝資訊頭。
- 使用函式 clone() 或者 copyTo() 來拷貝一副影象的矩陣。
儲存方法
對於 彩色 方式則有更多種類的顏色空間,但不論哪種方式都是把顏色分成三個或者四個基元素,通過組合基元素可以產生所有的顏色。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;
這個建立方法不能為矩陣設初值,它只是在改變尺寸時重新為矩陣資料開闢記憶體。
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;
Mat RowClone = C.row(1).clone();
cout << "RowClone = " << endl << " " << RowClone << 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;