天天看點

openCv學習筆記(二)—cv::Mat學習

由于在寫上一篇圖像的資料結構時,發現自己隻知道CvMat,竟然還有Mat資料結構,真是無知了,看了這麼多程式,貌似沒有看到這個結構。有可能那些程式都是些老版本的例子,這是在2.0以後加上的,是以我也得緊跟呀!以下是自己的學習心得。。。。

一、Mat簡介

    在2001年剛剛出現的時候,OpenCV基于 C 語言接口而建。為了在記憶體(memory)中存放圖像,當時采用名為 IplImage 的C語言結構體,時至今日這仍出現在大多數的舊版教程和教學材料。但這種方法必須接受C語言所有的不足,這其中最大的不足要數手動記憶體管理,其依據是使用者要為開辟和銷毀記憶體負責。雖然對于小型的程式來說手動管理内

一、Mat簡介

    在2001年剛剛出現的時候,OpenCV基于 C 語言接口而建。為了在記憶體(memory)中存放圖像,當時采用名為 IplImage 的C語言結構體,時至今日這仍出現在大多數的舊版教程和教學材料。但這種方法必須接受C語言所有的不足,這其中最大的不足要數手動記憶體管理,其依據是使用者要為開辟和銷毀記憶體負責。雖然對于小型的程式來說手動管理記憶體不是問題,但一旦代碼開始變得越來越龐大,你需要越來越多地糾纏于這個問題,而不是着力解決你的開發目标。

   幸運的是,C++出現了,并且帶來類的概念,這給使用者帶來另外一個選擇:自動的記憶體管理(不嚴謹地說)。這是一個好消息,如果C++完全相容C的話,這個變化不會帶來相容性問題。為此,OpenCV在2.0版本中引入了一個新的C++接口,利用自動記憶體管理給出了解決問題的新方法。使用這個方法,你不需要糾結在管理記憶體上,而且你的代碼會變得簡潔(少寫多得)。但C++接口唯一的不足是目前許多嵌入式開發系統隻支援C語言。是以,當目标不是這種開發平台時,沒有必要使用舊 方法(除非你是自找麻煩的受虐狂碼農)。

   關于 Mat ,首先要知道的是你不必再手動地(1)為其開辟空間(2)在不需要時立即将空間釋放。但手動地做還是可以的:大多數OpenCV函數仍會手動地為輸出資料開辟空間。當傳遞一個已經存在的Mat 對象時,開辟好的矩陣空間會被重用。也就是說,我們每次都使用大小正好的記憶體來完成任務。

   基本上講 Mat 是一個類,由兩個資料部分組成:矩陣頭(包含矩陣尺寸,存儲方法,存儲位址等資訊)和一個指向存儲所有像素值的矩陣(根據所選存儲方法的不同矩陣可以是不同的維數)的指針。矩陣頭的尺寸是常數值,但矩陣本身的尺寸會依圖像的不同而不同,通常比矩陣頭的尺寸大數個數量級。是以,當在程式中傳遞圖像并建立拷貝時,大的開銷是由矩陣造成的,而不是資訊頭。OpenCV是一個圖像處理庫,囊括了大量的圖像處理函數,為了解決問題通常要使用庫中的多個函數,是以在函數中傳遞圖像是家常便飯。同時不要忘了我們正在讨論的是計算量很大的圖像處理算法,是以,除非萬不得已,我們不應該拷貝大 的圖像,因為這會降低程式速度。

二、Mat的基本操作

   這裡展示一個例子解釋一下Mat的基本操作

#include<cv.h>
#include<highgui.h>
#include<iostream>
using namespace cv;
using namespace std;
int main()
{
	/*********************************Mat基本操作-矩陣*******************************************/
    //二維三通道矩陣建立
    Mat M(2,2, CV_8UC3, Scalar(0,0,255)); //使用構造函數建立矩陣
	/*
	CV_8UC3 表示使用8位的 unsigned char 型,每個像素由三個元素組成三通道,初始化為(0,0,255)
	*/
    cout << "M = " << endl << " " << M << endl << endl; //格式化輸出
    //三維
	int sz[3] = {3,3,3}; 
    Mat L(3,sz, CV_8UC(1), Scalar::all(0));
	/*
	超過兩維的矩陣:指定維數,然後傳遞一個指向一個數組的指針,這個數組包含每個次元的尺寸;其餘的相同
	*/
    cout << "L = " << endl << " " << M << endl << endl; //格式化輸出

	/********************************************Mat基本操作-圖像*******************************/
	Mat A, C;      // 隻建立資訊頭部分
    A=imread("D:\\openCV\\openCVProject\\openCv筆記\\openCv筆記\\test.jpg", CV_LOAD_IMAGE_COLOR); // 這裡為矩陣開辟記憶體
    Mat B(A);                // 使用拷貝構造函數
    C = A;                  // 指派運算符
    /*
	拷貝構造函數和指派函數 隻拷貝資訊頭和矩陣指針
	*/

    Mat D (A, Rect(10, 10, 100, 100) ); //選取A中一個矩形區域,即隻通路其矩形區域的資訊頭,隻是建立資訊頭
	Mat E = A(cv::Range::all(), Range(1,3)); // 建立通路邊界的資訊頭。
    /*
    要建立一個感興趣區域( ROI ),你隻需要建立包含邊界資訊的資訊頭
	*/

    Mat F = A.clone();//複制圖像,包括資料
    Mat G;
    A.copyTo(G);
    /*
	拷貝矩陣本身(不隻是資訊頭和矩陣指針),
	*/

	//測試
	namedWindow( "a", CV_WINDOW_AUTOSIZE );
	namedWindow( "c", CV_WINDOW_AUTOSIZE );
	
	imshow( "a", D);
	imshow( "c", E );

	
	/****************************************圖像的讀取、處理和儲存**************************************/
	  Mat image;
	 image = imread( "D:\\openCV\\openCVProject\\openCv筆記\\openCv筆記\\test.jpg", CV_LOAD_IMAGE_COLOR);//導入圖像

	 if( !image.data )
	 {
	    cout<< " No image data \n " ;
	    return -1;
	 }

	 Mat gray_image;
	 cvtColor( image, gray_image, CV_BGR2GRAY );//轉化為灰階圖

	 imwrite( "../../images/Gray_Image.jpg", gray_image );//寫入圖像

	 namedWindow( "source", CV_WINDOW_AUTOSIZE );
	 namedWindow( "Gray image", CV_WINDOW_AUTOSIZE );

	 imshow( "source", image );
	 imshow( "Gray image", gray_image );
     /*******************************************************************************************/
	 waitKey(0);
	 return 0;
}
           

  對于Mat資料結構,在對圖像進行處理時要注意:

  • OpenCV函數中輸出圖像的記憶體配置設定是自動完成的(如果不特别指定的話)。
  • 使用OpenCV的C++接口時不需要考慮記憶體釋放問題。
  • 指派運算符和拷貝構造函數( ctor )隻拷貝資訊頭。
  • 使用函數 clone() 或者copyTo() 來拷貝一副圖像的矩陣
  • 三、掃描圖像的方法
    #include<cv.h>
    #include<highgui.h>
    #include<time.h>
    #include<iostream>
    using namespace cv;
    using namespace std;
    int main()
    {
        //Mat img(10,10,CV_8UC3,Scalar(0,0,255));
    	Mat img,img_gray,img_gray2;
    	img=imread("D:\\openCV\\openCVProject\\openCv筆記\\openCv筆記\\test.jpg", CV_LOAD_IMAGE_COLOR);
        cvtColor( img, img_gray, CV_BGR2GRAY );//轉化為灰階圖
    	img_gray.copyTo(img_gray2);
        //方式一
    	for( int i=0;i<img_gray.rows;i++)
    	{
    		uchar* data = img_gray.ptr<uchar>(i);
    		for(int j=0;j<img_gray.cols;j++)
    		{
    			data[j] = 255; 
    		}
    	}
    	//img.create(10,10,CV_8UC3,Scalar(0,0,255));
    	//cout << "img = " << endl << " " << img_gray << endl << endl; //格式化輸出
        //方式二 W*H的一幅圖像看成是一個1*(w*h)的一個一維數組
    	int nc;
    	if(img_gray.isContinuous())//判斷是否被所有的像素填滿
    	{
    		nc = img_gray.rows*img_gray.cols*img_gray.channels();	
    	}
    	else
    	{
    		cout<<"像素未填滿,不可用第二種方式"<<endl;
    		return -1;
    	}
    	uchar* data_2 = img_gray.ptr<uchar>(0);//提取第一個像素點指針
    	for(int i=0;i<nc;i++)//周遊所有的元素
    	{
    		data_2[i] = 255;
    	}
        //方式三指針掃描
    	uchar* data_3 = img.data;//單個元素
    	img.at<uchar>(0,0)=0;
    	for(int i=0;i<img.rows;i++)//周遊所有的元素
    	{
    		for(int j=0;j<img.cols;j++)
    		{
                 data_3 = img.data + i*img.step + j * img.elemSize(); 
    			 //對各個通道指派
    			 data_3[0]=100;
                 data_3[1]=100;
    			 data_3[2]=100;
    		}
    	}
    	/*時間函數
        double start = getTickCount();
    	finish = clock(); 
        duration = (double)(finish - start) / CLOCKS_PER_SEC; 
        */
    	//方式四 疊代器iterator掃描圖像
    	Mat_<Vec3b>::iterator it = img.begin<Vec3b>();  
    	Mat_<Vec3b>::iterator itend = img.end<Vec3b>();  
    	for (; it!=itend; it++)  
    	{  
    		     //對各個通道指派
    			 (*it)[0] = 200;  
    			 (*it)[1] = 200; 
    			 (*it)[2] = 200; 
    	}  
        
    	//測試,根據自己的選擇檢視結果
    	namedWindow("sorce",WINDOW_AUTOSIZE);
    	namedWindow("result",WINDOW_AUTOSIZE);
    
    	cv::imshow("sorce",img);
    	cv::imshow("result",img_gray);
    
    	waitKey(0);
    	return 0;
    
    }
               
    以上是對http://blog.csdn.net/yang_xian521/article/details/7182185#的綜合,以下是其博文,正如部落客所說的, data_3 = img.data + i*img.step + j * img.elemSize();,int i=0;i<img_gray.rows;i++。。。這種在循環中出現的語句識别比較耗時的,注意避免。以下是其博文
  • 1.存取單個像素值

    最通常的方法就是

    [cpp] view plain copy print ?

    1. img.at<uchar>(i,j) = 255; 
    2. img.at<Vec3b>(i,j)[0] = 255; 
    img.at<uchar>(i,j) = 255;
    img.at<Vec3b>(i,j)[0] = 255;
               

    如果你覺得at操作顯得太笨重了,不想用Mat這個類,也可以考慮使用輕量級的Mat_類,使用重載操作符()實作取元素的操作。

    [cpp] view plain copy print ?

    1. cv::Mat_<uchar> im2= img; // im2 refers to image 
    2.    im2(50,100)= 0; // access to row 50 and column 100 
    cv::Mat_<uchar> im2= img; // im2 refers to image
       im2(50,100)= 0; // access to row 50 and column 100
               

    2.用指針掃描一幅圖像

    對于一幅圖像的掃描,用at就顯得不太好了,還是是用指針的操作方法更加推薦。先介紹一種上一講提到過的

    [cpp] view plain copy print ?

    1. for (int j=0; j<nl; j++) 
    2.         uchar* data= image.ptr<uchar>(j); 
    3.         for (int i=0; i<nc; i++) 
    4.        {                  
    5.                   data[i] = 255; 
    6.         } 
    for (int j=0; j<nl; j++)
    {
            uchar* data= image.ptr<uchar>(j);
            for (int i=0; i<nc; i++)
           {                 
                      data[i] = 255;
            }
    }
               

    更高效的掃描連續圖像的做法可能是把W*H的衣服圖像看成是一個1*(w*h)的一個一維數組,這個想法是不是有點奇葩,這裡要利用isContinuous這個函數判斷圖像内的像素是否填充滿,使用方法如下:

    [cpp] view plain copy print ?

    1. if (img.isContinuous()) 
    2.         nc = img.rows*img.cols*img.channels(); 
    3. uchar* data = img.ptr<uchar>(0); 
    4. for (int i=0; i<nc; i++) 
    5.         data[i] = 255; 
    if (img.isContinuous())
    {
            nc = img.rows*img.cols*img.channels();
    }
    uchar* data = img.ptr<uchar>(0);
    for (int i=0; i<nc; i++)
    {
            data[i] = 255;
    }
               

    更低級的指針操作就是使用Mat裡的data指針,之前我稱之為暴力青年,使用方法如下:

    [cpp] view plain copy print ?

    1. uchar* data = img.data; 
    2. // img.at(i, j) 
    3. data = img.data + i * img.step + j * img.elemSize(); 
    uchar* data = img.data;
    // img.at(i, j)
    data = img.data + i * img.step + j * img.elemSize();
               

    3.用疊代器iterator掃描圖像

    和C++STL裡的疊代器類似,Mat的疊代器與之是相容的。是MatIterator_。聲明方法如下:

    [cpp] view plain copy print ?

    1. cv::MatIterator_<Vec3b> it; 
    cv::MatIterator_<Vec3b> it;
               

    或者是:

    [cpp] view plain copy print ?

    1. cv::Mat_<Vec3b>::iterator it; 
    cv::Mat_<Vec3b>::iterator it;
               

    掃描圖像的方法如下:

    [cpp] view plain copy print ?

    1. Mat_<Vec3b>::iterator it = img.begin<Vec3b>(); 
    2. Mat_<Vec3b>::iterator itend = img.end<Vec3b>(); 
    3. for (; it!=itend; it++) 
    4.          (*it)[0] = 255; 
    Mat_<Vec3b>::iterator it = img.begin<Vec3b>();
    Mat_<Vec3b>::iterator itend = img.end<Vec3b>();
    for (; it!=itend; it++)
    {
             (*it)[0] = 255;
    }
               

    4.高效的scan image方案總結

    還是用我們之前使用過的getTickCount、getTickFrequency函數測試速度。這裡我就不一一列舉我測試的結果了,直接上結論。測試發現,好的編寫風格可以提高50%的速度!要想減少程式運作的時間,必要的優化包括如下幾個方面:

    (1)記憶體配置設定是個耗時的工作,優化之;

    (2)在循環中重複計算已經得到的值,是個費時的工作,優化之;舉例:

    [cpp] view plain copy print ?

    1. int nc = img.cols * img.channels(); 
    2. for (int i=0; i<nc; i++) 
    3. {.......} 
    4. //************************** 
    5. for (int i=0; i<img.cols * img.channels(); i++) 
    6. {......} 
    int nc = img.cols * img.channels();
    for (int i=0; i<nc; i++)
    {.......}
    //**************************
    for (int i=0; i<img.cols * img.channels(); i++)
    {......}
               

    後者的速度比前者要慢上好多。

    (3)使用疊代器也會是速度變慢,但疊代器的使用可以減少程式錯誤的發生幾率,考慮這個因素,可以酌情優化

    (4)at操作要比指針的操作慢很多,是以對于不連續資料或者單個點處理,可以考慮at操作,對于連續的大量資料,不要使用它

    (5)掃描連續圖像的做法可能是把W*H的衣服圖像看成是一個1*(w*h)的一個一維數組這種辦法也可以提高速度。短的循環比長循環更高效,即使他們的操作數是相同的

    以上的這些優化可能對于大家的程式運作速度提高并不明顯,但它們畢竟是個得到速度提升的好的程式設計政策,希望大家能多采納。

    還有就是利用多線程也可以高效提高運作速度。OpenMP和TBB是兩種流行的APT,不過對于多線程的東西,我是有些迷糊的,呵呵

    5.整行整列像素值的指派

    對于整行或者整列的資料,可以考慮這種方式處理

    [cpp] view plain copy print ?

    1. img.row(i).setTo(Scalar(255)); 
    2. img.col(j).setTo(Scalar(255)); 
    img.row(i).setTo(Scalar(255));
    img.col(j).setTo(Scalar(255));
               

    這節就先介紹這麼多攻略吧~希望大家喜歡

    參考資料

              1.http://www.opencv.org.cn/opencvdoc/2.3.2/html/doc/tutorials/core/mat%20-%20the%20basic%20image%20container/mat%20-%20the%20basic%20image%20container.html

              2http://blog.sina.com.cn/s/blog_73ee929c01010yor.html

繼續閱讀