opencv2 Mat类copyTo()函数的内存泄露问题

我们都知道OpenCV中存储图像常用的方法就是用Mat表示. 基本上讲 Mat 是一个类,由两个数据部分组成:矩阵头(包含矩阵尺寸,存储方法,存储地址等信息)和一个指向存储所有像素值的矩阵(根据所选存储方法的不同矩阵可以是不同的维数)的指针。矩阵头的尺寸是常数值,但矩阵本身的尺寸会依图像的不同而不同,通常比矩阵头的尺寸大数个数量级。

基于这个考虑,OpenCV使用引用计数机制。其思路是让每个 Mat 对象有自己的信息头,但共享同一个矩阵。这通过让矩阵指针指向同一地址而实现。而拷贝构造函数则 只拷贝信息头和矩阵指针 ,而不拷贝矩阵。例如以下一段代码:

Mat A, C;                                 // 只创建信息头部分
A = cv::imread("test.jpg", CV_LOAD_IMAGE_COLOR); // 这里为矩阵开辟内存
Mat B(A);                                 // 使用拷贝构造函数
C = A;                                    // 赋值运算符

以上代码中的所有Mat对象最终都指向同一个也是唯一一个数据矩阵。虽然它们的信息头不同,但通过任何一个对象所做的改变也会影响其它对象。实际上,不同的对象只是访问相同数据的不同途径而已。

现在你也许会问,如果矩阵属于多个 Mat 对象,那么当不再需要它时谁来负责清理?简单的回答是:最后一个使用它的对象。通过引用计数机制来实现。无论什么时候有人拷贝了一个 Mat 对象的信息头,都会增加矩阵的引用次数;反之当一个头被释放之后,这个计数被减一;当计数值为零,矩阵会被清理。

但某些时候你仍会想拷贝矩阵本身(不只是信息头和矩阵指针),这时可以使用函数 clone() 和我们即将讨论的函数 copyTo() 。如下:

cv::Mat testImage = cv::imread("test.jpg"); //读入图片
cv::Mat imageCopy; 
testImage.copyTo(imageCopy); //完整拷贝一份testImage

Mat类的两个拷贝函数,copyTo()和clone(),都是进行深复制,也就是会另外开辟一个内存存储被复制的数据区域,对复制得到的新矩阵进行释放release()不会影响原矩阵的数据. 

一般情况下使用copyTo函数进行图像(或者矩阵)复制,变量被销毁,那么开辟的空间也会被Mat的析构函数释放. 但是如果copyTo函数的拷贝对象不是局部变量, 而是全局变量或者句柄指针,则出现变量一直存在而每一次复制都会开辟新的空间无法释放,造成内存泄露问题. 我们看一下以下例子, 例子是一个简单的循环队列, 队列元素包含Mat对象:

队列头文件test_queue.h

#ifndef __OPT_QUENE_H__
#define __OPT_QUENE_H__

#include <opencv2/opencv.hpp>
#include <pthread.h>

#define MAX_QUEUE_SIZE 100

typedef struct output_rslt_tag
{
   cv::Mat stImage;                               /* 帧图像 */
   unsigned int uiFrameSeq; 
}Rslt_s;

typedef struct queue_tag{
    int front;
    int rear;
    Rslt_s data[MAX_QUEUE_SIZE];

    pthread_mutex_t q_lock;
    pthread_cond_t cond;
}stQueue_s;


/* 初始化 */
int queue_create(void **ppvHandle);

/* 判断是否为空 */
int queue_empty(void *pvHandle);

/* 判断队列是否满 */
int queue_full(void *pvHandle);

/* 求循环队列的长度 */
int queue_length(void *pvHandle);

/* 入队 */
int queue_push(void *pvHandle, Rslt_s *pstOptRslt);

/* 队头元素出队,返回其值 */
int queue_pop(void *pvHandle, Rslt_s *pstOptRslt);

/* 销毁队列 */
int queue_destroy(void *pvHandle);


#endif //__OPT_QUENE_H__

队列源文件: test_queue.cpp

#include "test_queue.h"

#include <stdlib.h>
#include <stdio.h>
#include <cmath>


int queue_create(void **ppvHandle)
{
    stQueue_s* pstQueueInit;
    pstQueueInit = (stQueue_s*) malloc(sizeof(stQueue_s));
    if(pstQueueInit == NULL)
    {
        printf("Memory allocation error.\nQueue initialization failed.\n");
        return -1;
    }

    pstQueueInit->front = 0;
    pstQueueInit->rear = 0;

    
    /**thread lock initial**/
    pthread_mutex_init(&(pstQueueInit->q_lock), NULL);
    pthread_cond_init(&(pstQueueInit->cond), NULL);

    *ppvHandle = pstQueueInit;
    return 0;
}

int queue_empty(void *pvHandle)
{
    stQueue_s* pstQueue = (stQueue_s*)pvHandle;
    /**Return 1 if empty**/
    if( pstQueue->front == pstQueue -> rear)
    {
        return 1;
    }
    else
    {
        return 0;
    }

}

int queue_full(void *pvHandle)
{
    stQueue_s* pstQueue = (stQueue_s*)pvHandle;
    /**Return 1 if full**/
    if(pstQueue->front == (pstQueue->rear + 1)%MAX_QUEUE_SIZE)
    {
        return 1;
    }
    else
    {
        return 0;
    }
}

int queue_length(void *pvHandle)
{
    stQueue_s* pstQueue = (stQueue_s*)pvHandle;
    return  (pstQueue->rear - pstQueue->front + MAX_QUEUE_SIZE)%MAX_QUEUE_SIZE;
}

int queue_push(void *pvHandle, Rslt_s *pstOptRslt)
{
    stQueue_s* pstQueue = (stQueue_s*)pvHandle;

    if(pstQueue == NULL)
    {
        printf("Queue is not initialized, exit..\n");
        return -1;
    }

    if(pstOptRslt == NULL)
    {
        printf("Invalid INPUT element, exit..\n");
        return -1;
    }

    pthread_mutex_lock(&(pstQueue->q_lock));

    pstOptRslt->stImage.copyTo(pstQueue->data[pstQueue->rear].stImage); //拷贝图像
    pstQueue->data[pstQueue->rear].uiFrameSeq = pstOptRslt->uiFrameSeq;
    pstQueue->rear = (pstQueue->rear + 1)%MAX_QUEUE_SIZE;

    /**overwrite if queue is full**/
    if(pstQueue->rear == pstQueue->front)
    {
        pstQueue->front = pstQueue->rear + 1;
        printf("Warning: Data overwritten, Queue is full.. \n");
    }

    pthread_cond_signal(&(pstQueue->cond));
    pthread_mutex_unlock(&(pstQueue->q_lock));

    return 0;
}

int queue_pop(void *pvHandle, Rslt_s *pstOptRslt)
{
    stQueue_s* pstQueue = (stQueue_s*)pvHandle;

    if(pstQueue->front == pstQueue->rear)
    {
        printf("Empty queue, exit..\n");
        pthread_mutex_unlock(&pstQueue->q_lock);
        return AIA_FAIL;
    }

    if(pstOptRslt == NULL)
    {
        printf("Invalid INPUT element, exit.. \n");
        return AIA_FAIL;
    }

    pthread_mutex_lock(&pstQueue->q_lock);

    (pstQueue->data)[pstQueue->front].stImage.copyTo(pstOptRslt->stImage);
    pstOptRslt->uiFrameSeq = (pstQueue->data)[pstQueue->front].uiFrameSeq;

    pstQueue->front = (pstQueue->front + 1)%MAX_QUEUE_SIZE;

    pthread_mutex_unlock(&(pstQueue->q_lock));

    return 0;
}

int queue_destroy(void *pvHandle)
{
    stQueue_s* pstQueue = (stQueue_s*)pvHandle;
    if(pstQueue != NULL)
    {
        pthread_mutex_destroy(&(pstQueue->q_lock));
        pthread_cond_destroy(&(pstQueue->cond));
        free(pstQueue);
        pstQueue = NULL;
    }
    return 0;
}

在queue_push()函数中,pstOptRslt->stImage.copyTo(pstQueue->data[pstQueue->rear].stImage); 直接想句柄指针指向的数组内容拷贝数据,这个数据是在数组之外的空间中申请的新空间,而且由于句柄指针一直存在,这个空间不会释放,直到程序结束. 因此随着程序的持续运行, 不断的进行入队操作, 占用的空间会越来越大,造成内存泄露.


版权声明:本文为zjchenchujie原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。