每天都在用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
方法的源码的同学,建议先看文末的参考资料,再回来重读就可以了。