1. 程式人生 > >Mat - 基本影象容器

Mat - 基本影象容器

目的


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

A matrix of the mirror of a car

 比如上面的影象,在標出的鏡子區域中你見到的只是一個矩陣,該矩陣包含了所有畫素點的強度值。如何獲取並存儲這些畫素值由我們的需求而定,最終在計算機世界裡所有影象都可以簡化為數值矩以及矩陣資訊。作為一個計算機視覺庫, 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 物件有自己的資訊頭,但共享同一個矩陣。這通過讓矩陣指標指向同一地址而實現。而拷貝建構函式則 只拷貝資訊頭和矩陣指標 ,而不拷貝矩陣。

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 M(2,2, CV_8UC3, Scalar(0,0,255)); 
cout << "M = " << endl << " " << M << endl << endl; 

Demo image of the matrix output

對於二維多通道影象,首先要定義其尺寸,即行數和列數。然後,需要指定儲存元素的資料型別以及每個矩陣點的通道數。為此,依據下面的規則有多種定義:

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
M.create(4,4, CV_8UC(2));
cout << "M = "<< endl << " "  << M << endl << endl;

Demo image of the matrix output

這個建立方法不能為矩陣設初值,它只是在改變尺寸時重新為矩陣資料開闢記憶體。

 

  • 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;

Demo image of the matrix output

  • 對於小矩陣你可以用逗號分隔的初始化函式: 
Mat C = (Mat_<double>(3,3) << 0, -1, 0, -1, 5, -1, 0, -1, 0); 
cout << "C = " << endl << " " << C << endl << endl;

Demo image of the matrix output

  • 使用 clone() 或者 copyTo() 為一個存在的 Mat 物件建立一個新的資訊頭。 
Mat RowClone = C.row(1).clone();
cout << "RowClone = " << endl << " " << RowClone << endl << endl;

Demo image of the matrix output

格式化列印


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;

Default Output

  • Python 
cout << "R (python)  = " << endl << format(R,"python") << endl << endl;

Default Output

  • 以逗號分隔的數值 (CSV)
cout << "R (csv)     = " << endl << format(R,"csv"   ) << endl << endl;

Default Output

  • Numpy 
cout << "R (numpy)   = " << endl << format(R,"numpy" ) << endl << endl;

Default Output

  • C語言 
cout << "R (c)       = " << endl << format(R,"C"     ) << endl << endl;

Default Output