Mat - 圖像的容器
在對圖像進行處理時,首先需要將圖像載入到內存中,而Mat就是圖像在內存中的容器,管理着圖像在內存中的數據。Mat是C++ 的一個類,由於OpenCV2中引入了內存自動管理機制,所以不必手動的為Mat開辟內存空間以及手動的釋放內存。Mat中包含的數據主要由兩個部分構成:矩陣頭(矩陣尺寸、存儲方法、存儲地址等信息)和一個指向存儲圖像所有像素值的矩陣(根據所選的存儲方法不同的矩陣可以是不同的維數)的指針。
在圖像處理中,對圖像的處理不可能是在一個函數中完成的,這就需要在不同的函數間傳遞Mat。同時,圖像處理的計算量是很大,除非萬不得已就不要去傳遞比較大的Mat。這就要求使用某種機制來實現Mat的快速傳遞。Mat中主要有矩陣頭和一個指向矩陣的指針,矩陣頭是一個常數值,但是矩陣保存了圖像所有的像素值,通常會比矩陣頭大幾個數量級,因此傳遞Mat是主要的消耗是在矩陣復制上。為了解決這個問題,OpenCV中引入了計數機制。每個Mat都有自己的信息頭,但是共享同一個矩陣,也就是在傳遞Mat時,只復制矩陣頭和指向矩陣的指針。
1: Mat a,c ;
2: a = imread("d:\\test.jpg",1) ;
3: Mat b(a) ; //拷貝構造函數
4: a = c ; //復制運算符
上面代碼中3個Mat對象a,b,c指向同一個矩陣,由於都指向了同一個矩陣,某一個對象對矩陣進行操作時也會影響到其他對象讀取到的矩陣。
多個對象同時使用一個矩陣,那么當不需要該矩陣時,誰來負責清理?簡單的回答是,最后一個使用它的對象。通過引用計數機制,無論什么時候Mat對象的信息頭被復制了,都會增加矩陣的引用次數加1;反之,當一個Mat的信息頭被釋放后,引用計數就會被減1;當計數被減到0時,矩陣就會被釋放。
當然,有些時候還是需要拷貝矩陣本身的,這時候可以使用clone和 copyTo。通過clone和copyTo創建的Mat,都有自己的矩陣,修改其中一個的矩陣不會對其他的造成影響。
訪問像素的三種方法
對圖像像素值的訪問是圖像處理最基本的要求,在OpenCV中提供了三種方式來訪問圖像的像素值。
矩陣在內存中的存儲
首先來看一下圖像像素值在內存中的保存方式。前面提到,像素值是以矩陣的方式保存的,矩陣的大小取決於圖像采用的顏色模型,確切的說是圖像的通道數。如果是灰度圖像,矩陣是這樣的:
矩陣的每一個元素代表一個像素 值。而對多通道圖像來說,一個像素值需要多個矩陣元素來存儲,矩陣中的列會包含多個子列,其子列數和通道數目相等。以常見的RGB模型來說:
而且,如果內存比較大,圖像中的各行各列就可以一行一行的連接起來,形成一個長行。連續存儲有助於提升圖像的掃描速度,使用iscontinuous來判斷矩陣是否是連續存儲的。
顏色空間縮減
如果矩陣元素存儲的是單通道像素,使用8位無符號來保存每個元素,那么像素可能有256個不同的值。如果是三通道的話,就會用一千六百多種顏色。如此多的顏色在有些時候不是必須的,而且會對算法的性能造成嚴重的影響。在這種情況下,最常用的做法就是顏色空間的縮減,也就是將現有的顏色空間進行映射,以獲得較少的顏色數。例如:顏色值0到9映射為0,10到19映射為10,以此類推。
以簡單顏色空間縮減為例,使用OpenCV提供的三種方式來遍歷圖像像素。將各個顏色值映射關系存儲到表中,在對格像素的顏色值進行處理時,直接進行查表。下面是對映射表的初始化:
1:
2: uchar table[256] ;
3: int divideWith = 10;
4: for(int i = 0 ; i < 256 ; i ++)
5: table[i] = (uchar) ( divideWith * (i / divideWith));
這里將各個像素的顏色值整除以10,然后再乘以10,這樣會像上面所說的將0到9的顏色值映射為0,10到19的顏色值映射為10,以此類推。
指針遍歷圖像 Efficient Way
1: Mat& scanImageWithPointer(Mat &img , const uchar * const table)
2: {
3: CV_Assert(img.depth () == sizeof(uchar));
4:
5: int channels = img.channels() ;
6:
7: int rows = img.rows * channels;
8: int cols = img.cols ;
9:
10: if(img.isContinuous()) {
11: cols *= rows ;
12: rows = 1 ;
13: }
14:
15: uchar * p ;
16: for(int i = 0 ; i < rows ; i ++){
17: p = img.ptr<uchar>(i);
18: for(int j = 0 ; j < cols ; j ++){
19: p[j] = table[p[j]] ;
20: }
21: }
22: return img ;
23: }
首先使用斷言,只處理使用8位無符號數保存元素值的矩陣。然后在取出舉證的行數和列數,如果是多通道的話矩陣是有子列的,用通道數乘以矩陣的行數作為最終遍歷時行數。另外,調用isContinuous來判斷矩陣在內存中是不是連續存儲的。p = img.ptr<uchar>(i); 來獲取每一行開始處的指針,然后遍歷至改行的末尾。如果是連續存儲的,就只需要獲取一次每行的開始指針,一路遍歷下去即可。
Mat中的data字段會返回指向矩陣第一行第一列的指針,通過可以使用該字段來檢查圖像是否被載入成功了。當矩陣是連續存儲時,也可以通過data來遍歷整個圖像。
1: uchar * p = img.data ;
2: for(unsigned int i = 0 ; i < img.rows * img.cols * img.channels() ; i ++)
3: *p ++ = table[*p] ;
但是這種代碼可讀性差,並且進一步操作困難,其在性能上的表現並不明顯的優於上面的方法。
迭代遍歷圖像 Safe Method
1: Mat& scanImageWithIterator(Mat &img,const uchar * const table)
2: {
3: CV_Assert(img.depth () == sizeof(uchar));
4:
5: const int channels = img.channels() ;
6:
7: switch (channels){
8: case 1:
9: {
10: MatIterator_<uchar> it,end ;
11: end = img.end<uchar>() ;
12: for(it = img.begin<uchar>(); it != end ; it ++) {
13: *it = table[*it] ;
14: }
15: break ;
16: }
17: case 2:
18: {
19: MatIterator_<Vec3b> it,end ;
20: end = img.end<Vec3b>() ;
21: for(it = img.begin<Vec3b>(); it != end ; it ++) {
22: (*it)[0] = table[(*it)[0]] ;
23: (*it)[1] = table[(*it)[1]] ;
24: (*it)[2] = table[(*it)[2]] ;
25: }
26: break ;
27: }
28: }
29: return img ;
30: }
1: cv::MatIterator_<cv::Vec3b> it = image.begin<cv::Vec3b>();
2: cv::Mat_<cv::Vec3b>::iterator it = image.begin<cv::Vec3b>();
同樣的獲取圖像的常量迭代器也有兩種方式
1: cv::MatConstIterator_<cv::Vec3b> it = image.begin<cv::Vec3b>();
2: cv::Mat_<cv::Vec3b>::const_iterator end = image.end<cv::Vec3b>();
at<>遍歷圖像
這種方法不推薦用來遍歷圖像,它主要用來獲取或更改圖像的中隨機元素的。基本用途是用來訪問特定的矩陣元素(知道行數和列數)
1: Mat& scanImageWithAt(Mat& img,const uchar * const table)
2: {
3: CV_Assert(img.depth () == sizeof(uchar));
4: const int channels = img.channels() ;
5:
6: switch (channels){
7: case 1:
8: {
9: for (int i = 0 ; i < img.rows ; i ++)
10: for(int j = 0 ; j < img.cols ; j ++)
11: img.at<uchar>(i,j) = table[img.at<uchar>(i,j)] ;
12: break ;
13: }
14: case 2:
15: {
16: Mat_<Vec3b> I = img ;
17: for(int i = 0 ; i < I.rows ; i ++){
18: for(int j = 0 ; j < I.cols ; j ++){
19: I(i,j)[0] = table[I(i,j)[0]] ;
20: I(i,j)[1] = table[I(i,j)[1]] ;
21: I(i,j)[2] = table[I(i,j)[2]] ;
22: }
23: }
24: img = I ;
25: break ;
26: }
27: }
28: return img ;
29: }
1: //n添加椒鹽噪聲的個數
2: void salt(Mat& img,int n)
3: {
4: for(int k = 0 ; k < n ; k ++) {
5: int i = rand() % img.rows ;
6: int j = rand() % img.cols ;
7:
8: if(img.channels() == 1){
9: img.at<uchar>(i,j) = 255 ;
10: }else if(img.channels() == 3){
11: img.at<Vec3b>(i,j)[0] = 255 ;
12: img.at<Vec3b>(i,j)[1] = 255 ;
13: img.at<Vec3b>(i,j)[2] = 255 ;
14: }
15: }
16: }
lenna圖片添加椒鹽噪聲后
這里要提下,在使用imshow將Mat顯示到命名窗口時,需要調用waitkey()這個函數下,不然的話在命名窗口顯示不出來。
LUT
在圖像處理中,對圖像的所有像素重新映射是很常見的,在OpenCV中提供一個函數來實現該該操作,不需要去掃描整個圖像,operation on array :LUT。
1: Mat lookupTable(1,256,CV_8S); 2: uchar *p = lookupTable.data ;3: for(int i = 0 ; i < 256 ; i ++)
4: p[i] = table[i] ; 5: 6: Mat result ; 7: LUT(img,lookupTable,result) ;LUT的函數原型
void LUT(InputArray src, InputArray lut, OutputArray dst)src 輸入的8位矩陣
lut 256個元素的查找表,為了應對多通道輸入矩陣,查找表要么是單通道(此時,輸入矩陣的多個通道使用相同的查找表),要么和輸入矩陣有相同的通道數
dst 輸出矩陣。和輸入矩陣有相同的尺寸和通道



