cv::Mat內存結構

每天都在用cv::Mat,卻一直沒弄清楚它的內部存儲結構。特別是當我們存儲不同類型、不同通道數的數據時,Mat內部到底如何組織這些數據。

要不是今天遇到的一個特殊需求,恐怕我也不會去想這些麻煩事...

從特殊需求看Mat內存結構

需求是:存儲long類型的數據到Mat中。

看起來是個挺正常的需求,但Mat偏偏沒有提供long對應的type。所謂的type,就是Mat中自定義的CV_8UCV_32SCV_64F等等常量,分別對應C++中的基本數據類型unsigned charintdouble。爲什麼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類型來讀取它。doublelong的長度都是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還有更多神奇的用法等着我們去開發。現在我能想到的,比如longdouble等數據轉byte array可以用Mat作爲中介。再比如,我們常常用boost或protobuf對數據序列化,很麻煩,特別是protobuf,還需要定義.proto原型文件。如果只是序列化基本數據類型或大小不超過8字節的自定義類型,我們可以用Mat保存這些數據,然後用imwrite將數據寫入到圖片即可。當然,熟悉OpenCV的同學可能知道imwrite只支持CV_8U類型的數據,但這難不倒我們。只要設計好容量,放心往裏面存就行了。大家可以自己試試如何實現這個功能。

最後溫馨提示,如果讀不懂Mat::at方法的源碼的同學,建議先看文末的參考資料,再回來重讀就可以了。

參考資料

OpenCV中Mat屬性step,size,step1,elemSize,elemSize1 錢青

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