cv::Mat的內存結構與訪問
cv::Mat 是新版opencv主打的也是最爲常用的一種數據類型, 可以用於存儲任意維度的多通道數組。
本文目的在於記錄學習過程中得到關於 cv::Mat 內存結構,成員變量的一些認識。從數組、指針的角度解釋 cv::Mat ,提供從最底層操作 cv::Mat 的任一內容的方法。
首先,cv::Mat 被認爲是一個多維數組,那麼對任何數組最重要的操作就是數組任意元素的讀和寫。數組的讀寫操作也即數組的訪問操作。以下先來講講 Mat 的元素訪問操作,方便進一步延伸到探討 Mat 中數據內存結構的問題。
現有的常見的數組元素訪問方式有三種:
- at函數方式訪問
- iteration迭代器方式訪問
- 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訪問方法
對應代碼:
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的內存首地址偏移量爲常量,數學表達即
且始終有以下關係存在:
mat.data配合mat.step數組對數據進行訪問是最通用的方式。
這一訪問方式可以訪問任意維度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();