【OpenCV學習】之基本圖像容器 - 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 對象

你可以通過 Mat 的運算符 << 來實現,但要記住這隻對二維矩陣有效。

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
  • Create() function: 函數
  •  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

打印其它常用項目

OpenCV支持使用運算符<<來打印其它常用OpenCV數據結構。

  • 2維點
Point2f P(5, 1);
cout << "Point (2D) = " << P << endl << endl;

                                            Default Output

  • 3維點
Point3f P3f(2, 6, 7);
cout << "Point (3D) = " << P3f << endl << endl;

                                           Default Output

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

                                    Default Output

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

                         Default Output

這裏的例子大多數出現在一個短小的控制檯應用程序中,你可以在 here 下載到,或者在c++示例部分中找到。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章