每天都在用cv::Mat
,卻一直沒弄清楚它的內部存儲結構。特別是當我們存儲不同類型、不同通道數的數據時,Mat內部到底如何組織這些數據。
要不是今天遇到的一個特殊需求,恐怕我也不會去想這些麻煩事...
從特殊需求看Mat內存結構
需求是:存儲long
類型的數據到Mat
中。
看起來是個挺正常的需求,但Mat
偏偏沒有提供long
對應的type
。所謂的type
,就是Mat中自定義的CV_8U
、CV_32S
、CV_64F
等等常量,分別對應C++中的基本數據類型unsigned char
、int
、double
。爲什麼OpenCV非要額外定義這些常量,我也不知道(可能是爲了避免不同平臺C++編譯器導致的數據類型長度不一致?)。但既然設定了這樣的規則,我們就要遵守。假使違反了這個規則,會出現什麼事情呢?
比如,如果我們創建CV_8U
類型的Mat
,卻向裏面存浮點數,像這樣:
Mat mat(3, 3, CV_8U);
mat.at<float>(0, 0) = 1.0f;
float
類型佔用4個字節,而CV_8U
類型的每個元素只擁有1字節的空間。可想而知,上面的代碼雖然運行不會報錯,但1.0
這個浮點數會佔據mat
中前四個元素的空間。如果我們接下來繼續對mat
賦值:
mat.at<float>(0, 1) = 2.0f;
2.0
這個浮點數就會覆蓋掉1.0
的後三個字節的數據。我們把這兩個元素打印出來:
cout << "mat.at<float>(0, 0) = " << mattt.at<float>(0, 0) << endl;
cout << "mat.at<float>(0, 1) = " << mattt.at<float>(0, 1) << endl;
現在,奇蹟發生了!!打印結果竟然是對的。
mat.at<float>(0, 0) = 1
mat.at<float>(0, 1) = 2
這結果着實讓我吃驚了一把。這意味着第二個浮點數佔用的空間並沒有和第一個浮點數重疊。當我百思不得其解的時候,又順手嘗試了下面的代碼:
Mat mat(3, 3, CV_8U);
mat.at<float>(0, 0) = 1.0f;
mat.at<float>(1, 0) = 2.0f;
再把結果打印出來:
mat.at<float>(0, 0) = 1.17549e-38
mat.at<float>(1, 0) = 2
終於,錯誤出現了。第二個浮點數覆蓋掉了第一個浮點數的部分數據,導致第一個浮點數紊亂。爲了探究出現這兩種現象的原因,不妨看一眼Mat
類的at
方法的源碼。
template<typename _Tp> inline
_Tp& Mat::at(int i0, int i1)
{
CV_DbgAssert(dims <= 2);
CV_DbgAssert(data);
CV_DbgAssert((unsigned)i0 < (unsigned)size.p[0]);
CV_DbgAssert((unsigned)(i1 * DataType<_Tp>::channels) < (unsigned)(size.p[1] * channels()));
CV_DbgAssert(CV_ELEM_SIZE1(traits::Depth<_Tp>::value) == elemSize1());
return ((_Tp*)(data + step.p[0] * i0))[i1];
}
前五行都是異常檢測,不必細究。直接看最後一行,data
是一個uchar
類型的指針,指向Mat
數據塊的首地址。step.p[0]
是矩陣第0維的長度,也就是矩陣每一行所佔用的字節數。於是data + step.p[0] * i0
得到的是第i0行(行號從0開始)的首地址。請注意,該計算結果與數據類型_Tp
無關。接着,將該地址強制轉換爲_Tp*
,然後將其作爲一個_Tp
類型的數組,按照下標索引元素。由此可以得出結論,用at
獲取元素的方法,每行的第0個元素的地址固定不動,後面的元素按照指定數據類型所佔用的空間,依次排開。由於Mat
內部存儲空間是連續的,所以第一行的數據依次排開就會影響到第二行。但第一行內部的數據卻不會互相影響。當然,如果你非要較真,先mat.at<float>(0, 0) = 1.0f
,再mat.at<uchar>(0, 1) = 1
,顯然第0行第1個數仍然會影響第0行第0個數。
Mat的使用經驗
好了,現在我們可以總結一下使用Mat
的一些經驗。
1. 儘量按照與type對應的數據類型存取數據。
如果你不清楚數據類型對應的type是什麼,以float
爲例,可以用DataType<float>::type
來獲取。
2. Mat要求在存取數據時指定數據類型。
無論是使用at
方法,還是ptr
方法,都需要指定讀取的數據類型。有時候你可能納悶,明明我創建Mat
的時候已經指定過數據類型,爲什麼讀取的時候還要再指定一次。個人認爲,這可能是OpenCV的一個設計缺陷,但也可能是爲了提高靈活性而故意爲之。我們回到文章最初的那個問題,如何在Mat
中存儲long
類型的數據?
3. 在Mat中存儲long類型的數據
現在,我們來試試靈活地使用Mat
。既然創建Mat
時聲明的類型與讀取時的類型可以不一致,那麼我們完全可以創建一個double
類型的Mat
,然後用long
類型來讀取它。double
和long
的長度都是8字節(在64位計算機上),所以不用考慮數據覆蓋的問題。
Mat mat(3, 3, CV_64F);
mat.at<long>(0, 0) = 999999999999;
mat.at<long>(1, 0) = 555555;
cout << "mat.at<long>(0, 0) = " << mat.at<long>(0, 0) << endl;
cout << "mat.at<long>(1, 0) = " << mat.at<long>(1, 0) << endl;
輸出結果如下:
mat.at<long>(0, 0) = 999999999999
mat.at<long>(1, 0) = 555555
4. 在Mat中存儲自定義類型的數據
我們可以把上面這種用法推廣,用Mat
來存儲自定義類型的數據。比如,把一個長度爲8個字節的結構體存儲到Mat
中。
struct Person {
int age;
float salary;
};
ostream& operator<< (ostream& o, const Person& person)
{
return o << "{ age: " << person.age << ", salary: " << person.salary << " }";
}
int main(int argc, char **argv)
{
Mat mat(3, 3, CV_64F);
mat.at<Person>(0 ,0) = Person {24, 300000.0f};
mat.at<Person>(1, 0) = Person {30, 1000000.0f};
cout << "mat.at<Person>(0, 0) = " << mat.at<Person>(0, 0) << endl;
cout << "mat.at<Person>(1, 0) = " << mat.at<Person>(1, 0) << endl;
}
輸出結果如下:
mat.at<Person>(0, 0) = { age: 24, salary: 300000 }
mat.at<Person>(1, 0) = { age: 30, salary: 1e+06 }
怎麼樣,很有趣吧。但需要注意的是,這裏的自定義類型Person
必須和聲明的類型CV_64F
具有相同的長度。
5. 更多用法?
Mat
還有更多神奇的用法等着我們去開發。現在我能想到的,比如long
或double
等數據轉byte array可以用Mat
作爲中介。再比如,我們常常用boost或protobuf對數據序列化,很麻煩,特別是protobuf,還需要定義.proto
原型文件。如果只是序列化基本數據類型或大小不超過8字節的自定義類型,我們可以用Mat
保存這些數據,然後用imwrite
將數據寫入到圖片即可。當然,熟悉OpenCV的同學可能知道imwrite
只支持CV_8U類型的數據,但這難不倒我們。只要設計好容量,放心往裏面存就行了。大家可以自己試試如何實現這個功能。
最後溫馨提示,如果讀不懂Mat::at
方法的源碼的同學,建議先看文末的參考資料,再回來重讀就可以了。