OpenCV3.4.0學習筆記(一)——cv::Mat的內存結構與訪問

cv::Mat的內存結構與訪問

cv::Mat 是新版opencv主打的也是最爲常用的一種數據類型, 可以用於存儲任意維度的多通道數組。

本文目的在於記錄學習過程中得到關於 cv::Mat 內存結構,成員變量的一些認識。從數組、指針的角度解釋 cv::Mat ,提供從最底層操作 cv::Mat 的任一內容的方法。

首先,cv::Mat 被認爲是一個多維數組,那麼對任何數組最重要的操作就是數組任意元素的讀和寫。數組的讀寫操作也即數組的訪問操作。以下先來講講 Mat 的元素訪問操作,方便進一步延伸到探討 Mat 中數據內存結構的問題。

現有的常見的數組元素訪問方式有三種:

  1. at函數方式訪問
  2. iteration迭代器方式訪問
  3. ptr指針訪問

以下分別給出三種訪問方式的代碼範例

at函數方式訪問
	//定義2行2列,CV_8UC1型的 Mat
	cv::Mat mat(2,2,CV_8UC1);
	//at成員函數方式訪問 mat 元素
	uchar p = mat.at<uchar>(0,1); //讀0行1列元素
	mat.at<uchar>(1,0) = 128;	//寫1行0列元素

at函數方法比較直觀,且爲最保守的做法,但是需要函數調度開銷,效率較低。

iteration迭代器方式訪問
	//定義2行2列,CV_8UC1型的 Mat
	cv::Mat mat(2,2,CV_8UC1);
	//定義mat的數據迭代器
	cv::MatIterator_<uchar> iter = mat.begin();
	//迭代器方式訪問
	for (int i = 0; i < 2; i++)
		cout << *(iter++) << endl;

迭代器訪問也涉及函數調用,效率一般。

ptr指針訪問
	//定義2行2列,CV_8UC3型的 Mat
	cv::Mat roi(2,2,CV_8UC3);
	//遍歷roi所有元素
	for (int i = roi.rows-1; i >= 0; i--)
	{
		//roi中行元素的首地址儲存可能不連續
		unsigned char *ptr = roi.ptr<unsigned char>(i);
		for (int j = roi.cols-1; j >= 0; j--)
		{
			ptr[3 * j + 0] = 0; //roi(i,j)的第一通道元素
			ptr[3 * j + 1] = 255; //roi(i,j)的第二通道元素
			ptr[3 * j + 2] = 0; //roi(i,j)的第三通道元素
		}
	}

mat最快的訪問方式,也是opencv推薦的訪問方式(二維數組)。這裏需要注意,mat數據中,不同行的儲存可能是不連續的。行間是否連續可以通過 roi.isContinus() 來獲得。

現在問題來了——內存連續性?:

對於高維數組想要採用最高效的指針訪問方式【ptr方式】
怎麼判斷內存連續不連續?只是第一維(行)不連續?第二維呢?第三維?
要去到第幾維ptr才能使用指針連續偏移去訪問數組元素?

opencv給出了高維數組的更一般化的訪問方法:

data+step訪問方法

&amp;mat(i0,i1,...in)=mat.data+i0step[0]+i1step[1]+...+instep[n] \&amp; mat(i_0,i_1,...i_n)=mat.data+i_0*step[0]+i_1*step[1]+...+i_n*step[n]
對應代碼:

	extern int* index; //需要訪問元素的下標構成的數組
	matdataType* elementPointer ; //被訪問元素的指針
	uchar* tempPointer = mat.data; //臨時指針,用於偏移到被訪問指針
	for(int i=0;i<mat.dims,i++)
		tempPointer += index[i]*mat.step[i]; //偏移指針
	elementPointer = (matdataType*)tempPointer; //地址強制轉化

可見,mat.step[m]記錄了第m維度中,i_m 每增加1,地址的偏移量(以byte爲計數單位)。當mat被創建,step數組也同時被確定了下來,也即第m維度中,i_m 每增加1,地址的偏移量(以byte爲計數單位)同時也是一個固定值了。

更直觀地標書,方便起見,討論當 m=0,此時的step[0]即爲行維度座標增1,地址的偏移量(以byte爲計數單位),而 無關於你是第幾行

也就是說: 行存儲就算不連續,但是相鄰行間數據內存首地址間隔保持固定!!!

不只是行,這個可以推廣到更高的維度,直到第m維度,第m+1維度的數據存儲不管連續與否,數據首地址都是等間隔的。

而這個就是cv::Mat數據內存結構的特點。

以下爲第m+1維的數據連續儲存的判斷

step[m] == step[m+1]*size[m+1]

cv::Mat數據內存結構總結

cv::Mat中 數組data[][]…[m] 與 data[][]…[m+1] 在內存上不一定連續儲存,但是同一維度內,地址增1的內存首地址偏移量爲常量,數學表達即
data[][]...[m+1]data[][]...[m]=step[m],m=1,2,...,size[m] data[][]...[m+1] - data[][]...[m] = step[m] , \forall m =1,2,...,size[m]
且始終有以下關係存在:
step[m]&gt;=step[m+1]size[m+1] step[m] &gt;= step[m+1]*size[m+1]
mat.data配合mat.step數組對數據進行訪問是最通用的方式。
&amp;mat(i0,i1,...in)=mat.data+i0step[0]+i1step[1]+...+instep[n] \&amp; mat(i_0,i_1,...i_n)=mat.data+i_0*step[0]+i_1*step[1]+...+i_n*step[n]
這一訪問方式可以訪問任意維度cv::Mat任意位置的元素,但是系列的乘法操作會佔用較多的時間。

當cv::Mat內存是連續的 (mat.isContinus() == true) ,採用 mat.data 自增方式遍歷所有mat元素是效率最高的訪問方式。

關於cv::Mat內存結構的延伸性思考

cv::Mat 的 data+step 這種內存尋址方式也是 cv::Mat() 實現獲取圖像roi(region of interest)引用的基礎。也即

cv::Mat cv::Mat(cv::Rect(x,y,h,w)); 
//本質爲修改本對象矩陣頭副本中的data,step,size,並將矩陣頭副本返回
//原data所指向的矩陣元素內容及其排列方式均不發生改變

換句話說,cv::Mat 的 data+step 這種內存尋址方式很大程度上是爲了提取roi時提高效率,不涉及新data內存開闢與內容複製而存在的。而在 一般的矩陣開闢過程中,內存的開闢一般是連續的

舉個例子對以上結論進行說明:

cv::Mat src(cv::Size(3,3),CV_8UC1); //本質爲新開闢data內存
cv::Mat a;
cv::Mat b;

//獲取圖像roi,本質爲修改本對象矩陣頭副本中的data,step,size,並將矩陣頭副本返回
//原data所指向的矩陣元素內容及其排列方式均不發生改變
a = src(cv::Range(0,1), cv::Range(0,1)); 
//本質爲新開闢data內存,重新構建矩陣並複製內容
b = src.clone();

cout << src.isContinuous() << endl; //輸出true
cout << a.isContinuous() << endl; //輸出false
cout << b.isContinuous() << endl; //輸出true

進一步地,我們還有如下結論:

當需要將 cv::Mat a 變成連續存儲的數據(不考慮新開闢空間造成的資源開銷)時,可以使用如下語句實現:

a = a.clone();

注:以上爲作者學習過程中自己認識的記錄,如有問題,敬請指正。

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