OpenCV源码解析之Mat类理解及内存管理
在OpenCV中,Mat是一个基础的类,也是最重要的类之一,它直接实现对图像的内存管理和数据操作。
Mat的常见属性
Mat类可以看作是一个数据结构,它以矩阵的形式来存储和管理数据,里面定义了大量的相关属性。而理解这些属性,是我们灵活运用OpenCV的基础。
属性 | 说明 |
data | uchar型的指针。Mat类分为了两个部分:矩阵头和指向矩阵数据部分的指针,data就是指向矩阵数据的指针。 |
dims | 矩阵的维度,例如5*6矩阵是二维矩阵,则dims=2,三维矩阵dims=3. |
rows | 矩阵的行数 |
cols | 矩阵的列数 |
size | 矩阵的大小,size(cols,rows) |
channels() | 矩阵元素拥有的通道数,例如常见的彩色图像,每一个像素由RGB |
type() | 表示了矩阵中元素的类型以及矩阵的通道个数,它是一系列的预定义的常量,其命名规则为CV_(位数)+(数据类型)+(通道数)如:,CV_8UC3 |
depth() | 矩阵中元素的一个通道的数据类型,这个值和type是相关的。例如 type为 CV_8UC3,一个3通道的16位的有符号整数。那么,depth则是CV_8UC |
elemSize() | 矩阵一个元素占用的字节数(不区分通道,即多个通道的总和) |
elemSize1() | 矩阵一个元素每个通道占用的字节数(区分通道,单个个通道的值) |
flags | 一个int型数字,保存了许多有用的信息,flags说明; |
注:本贴最初写于2018年7月,转眼已经过去了2年,貌似OpenCV中cv::Mat的结构也稍有改变。我贴下来的这张图是最新的OpenCV4.30版的,类是cv::Mat,比如读取一张480*640*3的标准的图片,结果如下(上表中后面有括号的如channels, step1, elemSize这些都是函数,不是参数,所以不能显示在参数列表里),
你可能对这个表没什么感觉,那么我们看下面的
Size和step的物理含义
我们平常在处理图像时,最喜欢用width,height, stride这样的术语。OpenCV不同,自己在这搞个了个的step,size等,其实理解起来都差不多。
Mat类中的定义
MatSize size:
MatStep step;
理解:
step1(i):每一维元素的通道数
step[i]:每一维元素的大小,单位字节
size[i]:每一维元素的个数
elemSize():每个元素大小,单位字节
elemSize1():每个通道大小,单位字节
若还是不明白,就看下面的例子,
构造一个3维数组,每一平面上元素的个数:8行,6列
int matSize[] = { 5,8,6 };
Mat mat1(3, matSize, CV_8UC3, Scalar::all(0));
对于该矩阵(mat1)的理解如下,
mat1.size[0] = 5 = 第0维的维度
mat1.size[1] = 8 = 第1维的维度
mat1.size[2] = 6 = 第2维的维度
mat1.step[0] = 144, 表示面的大小,表示一个面占8*6*3=144个byte
mat1.step[1] = 18, 表示线的大小,表示一根线6个点,6*3=18个byte
mat1.step[2] = 3, 表示点的大小,表示一个点占3个byte
mat1.step1(i) = mat1.step[i] / mat1.elemSize1()
elemSize = 3, 也就是每个元素(点)的大小是3个byte, CV_16UC3则是6个byte
elemSize1 = 1, 也就是通道的大小是一个byte,CV_16UC3则是2个byte
elemSize可以通过下面的函数得到,
inline size_t Mat::elemSize() const
{
return dims > 0 ? step.p[dims - 1] : 0;
}
elemSize1在源码中可以通过
inline size_t Mat::elemSize1() const
{
return CV_ELEM_SIZE1(flags);
}
得到,返回的是一个宏,定义如下
/** Size of each channel item,
0x8442211 = 1000 0100 0100 0010 0010 0001 0001 ~ array of sizeof(arr_type_elem) */
#define CV_ELEM_SIZE1(type) \
((((sizeof(size_t)<<28)|0x8442211) >> CV_MAT_DEPTH(type)*4) & 15)
Mat的动态内存管理的实现
Mat采用了跟STL相似的手法对内存进行动态的管理,不需要用户手动的管理内存。当实例析构的时候,内存会根据实际使用情况自动回收。
比如,
Mat image0 = imread("D:\\SpaceSoftwares\\ Lenna.jpg", 1);
Mat imgtest(image0);
这样的语句,imread会把lenna.jpg读取一个Mat中,并返回给image0; imgtest拿到的是image0的数据信息,至于图像数据本身,他们在内存中是共享的,执行Mat imgtest(image0)这句的构造函数是,
inline Mat::Mat()
: flags(MAGIC_VAL), dims(0), rows(0), cols(0), data(0), datastart(0), dataend(0),
datalimit(0), allocator(0), u(0), size(&rows), step(0)
{}
当image0析构的时候,如果imgtest还有效,图像内存就不会回收(反过来也是),如果大家都不需要这个内存中的图像数据了,这块内存才会被回收。所以,你完全不用担心内存泄露等问题。
OpenCV是如何实现这个内存共享的呢?
OpenCV是通过引用计数的方式来实现内存共享的。
首先, Mat维护了一个数据结构UmatData,其中有一个参数叫refcount,用来表示被多少个变量所引用(不禁想到了windows中com组件类的管理)。例如上面这个例子中,imgtest被创建时,实际 运行的构造函数就是
inline
Mat::Mat(const Mat& m)
: flags(m.flags), dims(m.dims), rows(m.rows), cols(m.cols), data(m.data),
datastart(m.datastart), dataend(m.dataend), datalimit(m.datalimit), allocator(m.allocator),
u(m.u), size(&rows), step(0)
{
if( u )
CV_XADD(&u->refcount, 1);
if( m.dims <= 2 )
{
step[0] = m.step[0]; step[1] = m.step[1];
}else
{
dims = 0;
copySize(m);
}
}
这个构造函数只不过进行了一些常规的参数拷贝,其中关键点是通过CV_XADD(&u->refcount, 1) 把计数器增加了1。这个计数器在Mat的其他构造函数中都是类似的,在Mat.create中则是通过下面的函数实现计算器加1,
inline
void Mat::addref()
{
if( u )
CV_XADD(&u->refcount, 1);
}
当Mat析构的时候,他会运行release对计数器进行减1的操作,当refcount为0的时候,就会通过deallocate()释放图像数据的内存。
inline
void Mat::release()
{
if( u && CV_XADD(&u->refcount, -1) == 1 )
deallocate();
u = NULL;
datastart = dataend = datalimit = data = 0;
for(int i = 0; i < dims; i++)
size.p[i] = 0;
#ifdef _DEBUG
flags = MAGIC_VAL;
dims = rows = cols = 0;
if(step.p != step.buf)
{
fastFree(step.p);
step.p = step.buf;
size.p = &rows;
}
#endif
}
这里有一个地方要注意理解,CV_XADD执行的就是_InterlockedExchangeAdd,它返回的是减操作进行之前的refcount值,所以这里判断条件是CV_XADD(&u->refcount, -1) == 1,而不是CV_XADD(&u->refcount, -1) == 0。
如果你要开辟新的空间,不想共享内存图片数据怎么办?
可以使用copyTo或clone()函数。
补充一点,如果你想强制回收内存,或者,在某种情况下你没有按常规方法处理Mat,当想要清空Mat时,可以使用cvReleaseMat(Mat) 来回收内存。cvReleaseMat(Mat)会调用cvDecRefData( arr )把计数器清零,用cvFree( &arr )来释放数据区。