OpenCV源码解析之Mat类理解及内存管理

38 篇文章 1 订阅
37 篇文章 4 订阅

在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 )来释放数据区。

 

 

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值