一、引子
有个粉丝博友“CP猫”前2天和我联系,说他也在研究CLAHE算法,遇到了OpenCV Mat对象的step属性访问的问题,问为啥一个象step这样的数组可以强制转为为一个整数输出,且输出值为数组的第一个元素,为此他昨天还专门写了篇博文《为什么OpenCV图像Mat矩阵的step属性能转换为整数输出?》。正好这近两个月来我一直断断续续的在研究CLAHE算法,在初始阶段恰好也有他这样的疑问,后来通过深入分析代码终于理解了,结合CP猫博主的疑问,老猿就此深入分析一下。
二、关于step属性的含义
在CP猫博友的文章《为什么OpenCV图像Mat矩阵的step属性能转换为整数输出?》介绍了一下step属性,原文是这么介绍的:“step是个数组,用于存储每一维元素的大小(单位字节),如step[0]就是一维元素的个数,具体到图像来说,step[0]就是图像每行像素占用的字节数,step[1]就是每列像素占用的字节数”。
以上说法不全对,其中有几个说法有问题:
- step不是一个数组,而是一个结构化的类型,提供了通过类似数组下标方式访问的方法,在访问上可以姑且认为是个数组。这个方法挺有用,随后详细介绍,大家可以好好品味一下;
- step中通过下标能访问的数据如果认为是一个数组,这个数组的每个元素并不是存储每一维元素的大小,step[1]更不是每列像素占用的字节数。
怎么解释step的含义呢?
step这里指出的是图像在各个梯级上的字节数大小,而这里的梯级指的是构成图像的各层次。
以三维的Mat数据布局为例:
三维图像由一个一个平面(第一级)构成,每一个平面由一行一行(第二级)像素构成,每行由一个一个像素点(第三级)构成。因此三维图像中step[0]是面占用空间的大小,step[1]是行占用空间的大小,step[2]是像素点占用空间的大小。
同理:二维图像由一行一行(第一级)构成,而每一行又由一个一个点(第二级)构成。step[0]是行占用空间的大小,step[1]是像素点占用空间的大小。
因此Mat中的step[0]就是每个图像构成要素的第一级在内存中占据的字节数量。例如,二维图像中step[0]就是每一行(第一级)在矩阵内存中,占据的字节的数量。也就是说step[i]就是第i+1级在矩阵内存中占据的字节的数量。
所以在CP猫博友的文章《为什么OpenCV图像Mat矩阵的step属性能转换为整数输出?》的代码案例中,输出信息为:
图像的分辨率为:1023×681
step =28FC40,stepCast =3069,step[0]=3069,step[1]=3
三、为什么step对象可以转换成整型呢?
弄清楚了step的含义,我们来分析CP猫博友的问题。要弄清楚这个问题,必须阅读OpenCV相关的源码。
3.1、MatStep相关源代码
Mat对象的step属性实际上是一个MatStep对象,MatStep的定义在源码的build\include\opencv2\core\Mat.hpp文件下,具体定义如下:
struct CV_EXPORTS MatStep
{
MatStep();
explicit MatStep(size_t s);
const size_t& operator[](int i) const;
size_t& operator[](int i);
operator size_t() const;
MatStep& operator = (size_t s);
size_t* p;
size_t buf[2];
protected:
MatStep& operator = (const MatStep&);
};
其相关实现代码在build\include\opencv2\core\Mat.inl.hpp内,具体代码如下:
/ MatStep
inline
MatStep::MatStep()
{
p = buf; p[0] = p[1] = 0;
}
inline
MatStep::MatStep(size_t s)
{
p = buf; p[0] = s; p[1] = 0;
}
inline
const size_t& MatStep::operator[](int i) const
{
return p[i];
}
inline
size_t& MatStep::operator[](int i)
{
return p[i];
}
inline MatStep::operator size_t() const
{
CV_DbgAssert( p == buf );
return buf[0];
}
inline MatStep& MatStep::operator = (size_t s)
{
CV_DbgAssert( p == buf );
buf[0] = s;
return *this;
}
扯一下,说实话,这代码看懂容易,但看了这C++代码让老猿觉得自己真low了,第一次知道结构可以象类一样的定义属性,也可以这样定义访问方法。
3.2、源代码分析
针对CP猫博友的问题,我们来看几个关键的源代码:
- MatStep的两个operator[]方法,这个是决定了MatStep对象可以下标访问的实现方法,就是重载下标访问符号"[]";
- MatStep的数据实际上就是放在类型为size_t大小为2的buf缓冲区中,因此它实际上是只能支持二维图像的处理;
- 类型转换没那么直观,实际上是通过重载operator size_t()重载size_t来访问的,size_t实际上vc定义的标准数据类型,其类型实际上就是无符号整型。具体定义如下:
#ifdef _WIN64
typedef unsigned __int64 size_t;
#else
typedef unsigned int size_t;
endif
因此CP猫博友在将step使用 static_cast<int>(img.step)
转换成int型时,实际上就调用了重载的size_t()操作符,因此返回了缓冲区的第一个元素。
备注:这个地方老猿没有细研究为什么int强制类型转换会触发size_t()操作,按理是触发int()操作,虽然size_t与int是有关系,但关系没有这么直接,但个人觉得不重要,就不进一步研究了。
三、小结
本文详细介绍了OpenCV Mat对象step属性含义,并基于OpenCV关于MatStep类型的源代码对step数据的访问机制进行了深入分析,从而解答了博友关于数组为什么强制类型转换会变为一个返回数组第一个元素的问题。
文章中列举的这样简单紧凑的源码,实现了数据的简捷高效访问,看了之后是否深受启发?