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 钱青

    原文作者:金戈大王
    原文地址: https://www.jianshu.com/p/8c1f449af66b
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞