본문 바로가기
[Public] 신호처리/OpenCV 다루기

[OpenCV] #1 OpenCV에서 행렬 표현하기

by 차출발 2010. 2. 23.
반응형

일반적으로 행렬을 표현하기 앞서 OpenCV에서 사용하는 가장 일반적인 타입은 CVPoint 이다.

 typedef struct CvPoint
{
    int x;
    int y;
}
CvPoint

두 개의 X Y 좌표를 가지는 일반적인 구조체이다.
이 밖에도 유사한 타입이 있다.

CvPoint2D32f : 실수형 멤버 x와 y를 가지고 있다.
CvPoint3D32f : 실수형 멤버 x와 y 그리고 z를 가지고 있다.

CvSize : CvPoint 와 비슷 하다 이는 정수형 멤버 width 와 Height 를 가지고 있다.

CvRect : CvPoint와 CvSize 의 멤버들이 합쳐진 형태도 4가가지 (x, y, Width, Height)
CvScalar : 4개의 double 형 실수값을 배열 형태로 가지고 있다. 메모리 사용량이 문제가 되지 않는다면,
  1 2 3 차 실수 벡터를 표현 할 수 도 있다.

 구조체 멤버  용도 
CvPoint int x,  y 영상 내 한 점의 위치 
CvPoint2D32f float x, y  2차원 실수 공간에서의 좌표 
CvPoint3D32f  float x, y, z  3차원 실수 공간에서의 좌표 
CvSize int width, height  영상의 크기 
CvRect int x, y, width, height  영상의 일부 영역 
 CvScalar double val[4]  RGB 값 


자 이제 행렬을 표현해 보자.
OpenCV를 한번식 사용하면 꼭 나오는 구조체가 있다. IplImage 라는 구조체이다.
이는 영상이라는 데이터 타입인데 이는 다양한 데이터를 저장할 수 있는 데이터 타입이다.

OpenCV는 C 언어로 구현되어 있지만 구조상 객체지향적으로 되어 있다. 이는 IplImage 가  CvMat에 상속되어 있고 이는 다시 CvArr로 부터 상속된 구조를 갖는다.

여기서 한번 CvArr 과 CvMat 를 집고 넘어 가보자

먼저 CvArr은 다음과 같이 정의가 되어 있다.
typedef void CvArr;

잉 void?
다시 말해 타입이 결정이 안되 있다는 말인데
이는 함수 파라미터로 스이며 IplImage*, CvMat*, CvSeq* 타입등등으로 쓰인다.
이는 런타임시 처음 4Byte를 조사함으로써 결정된다.

그럼 CvMat를 보면

typedef struct CvMat
{
    int type;
    int step;
    /* for internal use only */
    int* refcount;
    int hdr_refcount;
    union
    {
        uchar* ptr;
        short* s;
        int* i;
        float* fl;
        double* db;
    } data;

#ifdef __cplusplus
    union
    {
        int rows;
        int height;
    };
    union
    {
        int cols;
        int width;
    };
#else
    int rows;
    int cols;
#endif

}
CvMat;

정의가 되어 있다.
여기에서는 행렬의 가로, 세로 타입 스텝 데이터를 가리키는 포인터 등등이 포함되어 있다.

그럼 이제 행렬을 생성할 수 있겟다.

행렬을 생성하는 방법중 가장 일반적인 방법은 cvCreateMat() 함수가 있다.

원형을 보면 다음과 같다.
cvCreateMat( int height, int width, int type )
{
    CvMat* arr = cvCreateMatHeader( height, width, type );
    cvCreateData( arr );

    return arr;
}

이 함수는 내부적으로 cvCreateMatHeader 와 cvCreateData를 순차적으로 호출함을 알 수 있다.


cvCreateMatHeader 함수는 메모리 할당 없이 아래와 같이 CvMat 구조만 생성함을 알 수 있다.

cvCreateMatHeader( int rows, int cols, int type )
{
    type = CV_MAT_TYPE(type);

    if( rows <= 0 || cols <= 0 )
        CV_Error( CV_StsBadSize, "Non-positive width or height" );

    int min_step = CV_ELEM_SIZE(type)*cols;
    if( min_step <= 0 )
        CV_Error( CV_StsUnsupportedFormat, "Invalid matrix type" );

    CvMat* arr = (CvMat*)cvAlloc( sizeof(*arr));

    arr->step = min_step;
    arr->type = CV_MAT_MAGIC_VAL | type | CV_MAT_CONT_FLAG;
    arr->rows = rows;
    arr->cols = cols;
    arr->data.ptr = 0;
    arr->refcount = 0;
    arr->hdr_refcount = 1;

    icvCheckHuge( arr );
    return arr;
}


다음 호출되는 cvCreateData 함수는 실제 데이터 저장을 위한 아래와 같이 메모리를 할당함을 알 수 있다.
cvCreateData( CvArr* arr )
{
    if( CV_IS_MAT_HDR( arr ))
    {
        size_t step, total_size;
        CvMat* mat = (CvMat*)arr;
        step = mat->step;

        if( mat->data.ptr != 0 )
            CV_Error( CV_StsError, "Data is already allocated" );

        if( step == 0 )
            step = CV_ELEM_SIZE(mat->type)*mat->cols;

        int64 _total_size = (int64)step*mat->rows + sizeof(int) + CV_MALLOC_ALIGN;
        total_size = (size_t)_total_size;
        if(_total_size != (int64)total_size)
            CV_Error(CV_StsNoMem, "Too big buffer is allocated" );
        mat->refcount = (int*)cvAlloc( (size_t)total_size );
        mat->data.ptr = (uchar*)cvAlignPtr( mat->refcount + 1, CV_MALLOC_ALIGN );
        *mat->refcount = 1;
    }
    else if( CV_IS_IMAGE_HDR(arr))
    {
        IplImage* img = (IplImage*)arr;

        if( img->imageData != 0 )
            CV_Error( CV_StsError, "Data is already allocated" );

        if( !CvIPL.allocateData )
        {
            img->imageData = img->imageDataOrigin = 
                        (char*)cvAlloc( (size_t)img->imageSize );
        }
        else
        {
            int depth = img->depth;
            int width = img->width;

            if( img->depth == IPL_DEPTH_32F || img->nChannels == 64 )
            {
                img->width *= img->depth == IPL_DEPTH_32F ? sizeof(float) : sizeof(double);
                img->depth = IPL_DEPTH_8U;
            }

            CvIPL.allocateData( img, 0, 0 );

            img->width = width;
            img->depth = depth;
        }
    }
    else if( CV_IS_MATND_HDR( arr ))
    {
        CvMatND* mat = (CvMatND*)arr;
        int i;
        size_t total_size = CV_ELEM_SIZE(mat->type);

        if( mat->data.ptr != 0 )
            CV_Error( CV_StsError, "Data is already allocated" );

        if( CV_IS_MAT_CONT( mat->type ))
        {
            total_size = (size_t)mat->dim[0].size*(mat->dim[0].step != 0 ?
                         mat->dim[0].step : total_size);
        }
        else
        {
            for( i = mat->dims - 1; i >= 0; i-- )
            {
                size_t size = (size_t)mat->dim[i].step*mat->dim[i].size;

                if( total_size < size )
                    total_size = size;
            }
        }
        
        mat->refcount = (int*)cvAlloc( total_size +
                                        sizeof(int) + CV_MALLOC_ALIGN );
        mat->data.ptr = (uchar*)cvAlignPtr( mat->refcount + 1, CV_MALLOC_ALIGN );
        *mat->refcount = 1;
    }
    else
        CV_Error( CV_StsBadArg, "unrecognized or unsupported array type" );
}

 이미 데이터가 있거나 메모리 할당을 할 단계가 아닌 경우를 위하여 cvCreateMatHeader 함수를 따로 사용한다.


행렬을 생성하는 또다른 방법으로는 C#같이 clone 함수를 이용하는 것이다.

cvCloneMat() 이함수는 이미 존재하는 행렬과 동일한 행렬을 생성한다.
이 함수는 아래와 같이 인자로 넘어온 자료와 동일한 헤더를 생성하고 동일한 데이터를 담고 있는 메모리를 할당하여 모두 복사하는 역할을 한다.

cvCloneMat( const CvMat* src )
{
    if( !CV_IS_MAT_HDR( src ))
        CV_Error( CV_StsBadArg, "Bad CvMat header" );

    CvMat* dst = cvCreateMatHeader( src->rows, src->cols, src->type );

    if( src->data.ptr )
    {
        cvCreateData( dst );
        cvCopy( src, dst );
    }

    return dst;
}


행렬을 생성하는 2가지 방법을 배우면서 메모리를 할당 해줬다. 그럼 당연히 해줘야하지 않겠는가?

그래서 준비한 함수는 cvReleaseMat 함수를 호출하여 아래와 같이 소멸시킨다.


cvReleaseMat( CvMat** array )
{
    if( !array )
        CV_Error( CV_HeaderIsNull, "" );

    if( *array )
    {
        CvMat* arr = *array;
        
        if( !CV_IS_MAT_HDR(arr) && !CV_IS_MATND_HDR(arr) )
            CV_Error( CV_StsBadFlag, "" );

        *array = 0;

        cvDecRefData( arr );
        cvFree( &arr );
    }
}

CvMat 구조체는 cvMat()라는 생성자를 지니고 있다.
이는 실제 메모리할당은 하지 않고 헤더만 생성하는데 만약 행렬의 데이터가 존재할 경우 행렬의 헤더가 이를 가리키게 하기 위함으로 행렬 사용에서 컴파일 타임에서 매커니즘을 지원해준다.

간단한 예제를 보면은

float vals[] = { 0.123, -0.300, -0.300, 0.123};
CvMat matData;
cvInitMatHeader(&matData, 2, 2, CV_32FC1, vals);

다르게 사용하면 
CvMat matData = cvMat(2,2, CV_32FC1, vals);

편하게 사용하면 된다.

자 간단하게 행렬을 생성 했다면
이 행렬의 속성 정보를 가져오는 함수에 대하여 알아보자

int cvGetElemType(const CvArr* arr); 함수이다.
이 함수는 행렬 원소의 타입을 나타낸다.
보통 CV_8UC1,CV_32FC1, CV_64FC4 를 반환한다.

int cvGetDims(const CvArr* arr, int* sizes = NULL); 함수이다.
행렬의 차원수를 반환한다. 두 번째 인자는  2를 반환하지만 추후 n차원의 행렬도 다루게 된다.

int cvGetDimSize(const CvArr* arr, int index);함수이다.
위와 같지만 이는 행렬의 정보를 알려주다 즉 행렬의 높이와 너비 

이 3가지 함수를 사용하는 예를 들어보장

#include <stdio.h>
#include "cv.h"
#include "cxcore.h"
void main()
{
float vals[] = { 0.123, -0.300, -0.300, 0.123};
CvMat *matData = cvCreateMat(2, 1, CV_32FC1);

int ex1 = cvGetElemType(matData);
printf("%d\n", ex1);

int nptr[2];
int ex2 =  cvGetDims(matData, nptr);
printf("%d, %d\n",nptr[0], nptr[1]);
printf("%d\n",ex2);

int ex3 =  cvGetDimSize(matData, 1);
printf("%d\n",ex3);
}
결과는 
ex1 = CV_32FC1 = 5 (행렬의 타입 )

nptr[0] = 2, nptr[1] = 1 (인자값으로 행렬의 행 열 값)
ex2 = 2  (행렬의 차원 수)

ex3 = 1  (행렬 행 과 열)

가 된다.

이제 행렬을 생성하고 소멸하고 행렬의 속성을 알수가 있었다.
그럼 마지막으로 행렬에 접근을 해야하는 과정만 남았따.

행렬에 접근하는 3가지방법에 대하여 알아보자

1) 간편한 방법
  CV_MAT_ELEM() 와 CV_MAT_ELEM_PTR() 매크로 함수를 사용하는 것이다.
이 매크로 함수를 보면

#define CV_MAT_ELEM_PTR( mat, row, col )                 \
    CV_MAT_ELEM_PTR_FAST( mat, row, col, CV_ELEM_SIZE((mat).type) )


#define CV_MAT_ELEM( mat, elemtype, row, col )           \
    (*(elemtype*)CV_MAT_ELEM_PTR_FAST( mat, row, col, sizeof(elemtype)))

각각  정의 되어 있다 
즉 CV_MAT_ELEM지정한 타입의 포인터로 형변환을 한 후 역참조하여 값을 반환하고
    CV_MAT_ELEM_PTR은 이와 다름을 알수 있다.

한번 예를 들어 설정해보자
#include <stdio.h>
#include <assert.h>
#include "cv.h"
#include "cxcore.h"

void main()
{
float vals[] = { 0.123, -0.300, -0.300, 0.123};

CvMat matData1 = cvMat(2, 2, CV_32FC1, vals);

float ptr11 = CV_MAT_ELEM(matData1, float, 0, 0);
float ptr12 = CV_MAT_ELEM(matData1, float, 0, 1);
float ptr21 = CV_MAT_ELEM(matData1, float, 1, 0);
float ptr22 = CV_MAT_ELEM(matData1, float, 1, 1);

ptr11 = 1.000;
printf("%f, %f\n",ptr11, ptr12);
printf("%f, %f\n",ptr21, ptr22);

ptr11 = *(float*)CV_MAT_ELEM_PTR(matData1, 0, 0);
ptr12 = *(float*)CV_MAT_ELEM_PTR(matData1, 0, 1);
ptr21 = *(float*)CV_MAT_ELEM_PTR(matData1, 1, 0);
ptr22 = *(float*)CV_MAT_ELEM_PTR(matData1, 1, 1);

ptr11 = 1.000;
printf("%f, %f\n",ptr11, ptr12);
printf("%f, %f\n",ptr21, ptr22);
}
원서에서는 CV_MAT_ELEM_PTR 매크로를 써야한다고 작성되있으나 
CV_MAT_ELEM 매크로를 이용하여 행렬값을 설정 할 수도 있다.



여기 안에서 호출하는 CV_MAT_ELEM_PTR_FAST 를 보면

#define CV_MAT_ELEM_PTR_FAST( mat, row, col, pix_size )  \
    (assert( (unsigned)(row) < (unsigned)(mat).rows &&   \
             (unsigned)(col) < (unsigned)(mat).cols ),   \
     (mat).data.ptr + (size_t)(mat).step*(row) + (pix_size)*(col))

매번 행렬의 데이터 영역의 시작주소를 찾고 알고 싶은 원소의 주소 까지 offset을 계산하여 해당 원소에 접근하는 작업을 하는 것을 알 수 가 있다. 
이는 매번 원소를 차례대로 접근하기  때문에 상당히 비 효율적인 방법이다.

2) 엄격한 방법