타겟 추적을 위한 카메라의 팬,틸트 제어

영상처리 2013. 12. 10. 13:22

팬/틸트 카메라에서 카메라 영상위에 임의의 한 점이 영상 중심에 오도록 카메라를 제어할 때 필요한 팬, 틸트 제어량을 계산하는 방법에 관한 글입니다.


참고로, 이 글은 [개발한 것들] - 가상 3D 영상 생성 프로그램(http://darkpgmr.tistory.com/85) 글에 대해 댓글로 문의주신 내용에 대한 답변으로 작성된 글입니다.


풀고자 하는 문제는 아래 그림과 같이 영상위에 특정 대상을 화면 중심에 오도록 하려면 카메라의 팬과 틸트를 얼마나 움직여야 하는가 입니다.



얼핏 쉽게 떠오르는 방법은 dy만큼 틸트(tilt)를 시키고, dx만큼 팬(pan)을 시키는 것이겠지만 실제로는 이렇게 하면 원하는 결과를 얻을 수 없습니다. 그 이유는 i) 카메라를 틸트시켰을 때 영상에서 점의 위치가 포물선 형태를 그리며 변한다는 점 그리고 ii) 팬을 시킬 때 회전축이 현재의 틸트값에 따라서 바뀐다는 점 때문입니다.


아래 그림과 같이 영상 위에 한 점 p가 있을 때, 카메라를 상하방향으로 틸트시키면 점 p가 파란색 선을 따라서 일직선으로 움직이는게 아니라 실제로는 붉은색 선을 따라 포물선 형태로 움직입니다.



언뜻 이해가 안갈수도 있는데, 그 이유를 설명해 보면 이렇습니다. 위 그림의 왼쪽 삼각형처럼 카메라의 초점(projection 중점)과 물체(p)와의 관계는 틸트와 관계없이 항상 고정되어 있습니다. 그런데 틸트가 변함에 따라서 이미지 평면에 의해 잘리는 부분만이 변하게 됩니다. 이 잘리는 부분의 길이가 x좌표를 결정하며 카메라의 틸트가 이 물체를 정면으로 바라볼 때 그 값은 최소가 됩니다. 또한 카메라가 다른 방향을 볼수록 그 값은 커집니다. 사각뿔을 상상하면서 사각뿔의 꼭지점이 카메라 초점, 밑면이 이미지 평면이라고 생각하고 사각뿔의 꼭지점을 고정시킨 상태에서 사각뿔을 위 아래로 회전시켜 보면 이해가 좀더 쉬울 것입니다. 이러한 성질은 틸트 뿐만 아니라 패닝(panning)에 대해서도 동일하게 적용됩니다.


이를 수식적으로 계산할 수도 있는데, 점 p가 상하 방향으로 화면 가운데 오도록 틸트를 맞추었을 때의 p의 x좌표를 dx라 한다면 해당 위치에서 카메라를 dt만큼 틸트시켰을 때의 x 좌표는 dx/cos(dt)가 됩니다.


다음으로 틸트에 따른 패닝의 효과를 살펴보면 틸트가 전혀 없는 상태에서 패닝(panning)이 일어나면 카메라 화면이 좌우로 이동하겠지만, 만일 틸트가 90도인 경우(천장을 바라보거나 바닥을 바라보는 경우)에 패닝을 시키면 영상 중심을 중심으로 원을 그리며 회전이 일어납니다. 이와 같이 카메라의 패닝은 현재 틸트값에 따라 전혀 다른 결과를 가져오기 때문에 항상 틸트값을 고려하여 계산해야 합니다.


팬, 틸트 카메라의 특성에 대한 내용은 이 정도로 하고 다시 원래의 문제로 돌아가서 영상위의 임의의 지점으로 카메라 중심을 이동시키기 위한 방법을 생각해 보겠습니다.



1. 카메라의 절대적인 팬, 틸트 값을 모르는 경우


기본적으로 현재의 틸트 값을 모르면 해당 위치로 가기 위해 필요한 이동량을 한번에 계산하는 것은 불가능합니다. 그 이유는 실제 팬/틸트 카메라에서 팬과 틸트의 물리적인 회전축이 각각 하나로 고정되어 있기 때문입니다. 현재 영상에서 상대적인 좌우 회전각과 상하 회전각을 계산하는 것은 가능하지만 실제 물리적인 회전축이 현재 영상의 좌우방향이 아니기 때문에 틸트를 모르면 이동량을 계산할 수 없습니다. 만일 임의의 방향으로 회전이 가능한 카메라가 있다면 이러한 문제가 발생하지 않을 것입니다.


만일 현재의 틸트 값을 모르는 경우에는 일단은 dx만큼 패닝을 시키거나 dy만큼 틸트를 시킨 후에 영상 추적 기술을 접목하여 오차를 계산하고 이를 바탕으로 틸트를 추정하여 이후 다시 한번 이동을 하는 방식으로 중점을 맞추는 방법 등이 가능할 것입니다.



2. 카메라의 절대적인 팬, 틸트 값을 알수 있는 경우


수식 도출을 위해 먼저 좌표계 정의가 필요하며 편의상 월드좌표계, 카메라좌표계, 팬, 틸트를 다음과 같이 정의합니다 (다른 식으로 정의해도 관계없으며 정의하기에 따라서 수식 등이 조금씩 달리질 수 있음)


<그림 1>


<그림 1>과 같이 월드좌표계와 카메라 좌표계를 정의합니다.

  • 월드 좌표계: 지면이 XY 평면, 위쪽이 Z축
  • 카메라 좌표계: 카메라 광학축 방향이 Zc, 오른쪽이 Xc, 아래쪽이 Yc

이 때, 카메라 좌표계의 원점과 월드좌표계 원점은 일치하는 것으로 설정합니다.


또한 월드 좌표계 내에서 카메라의 pan 및 tilt를 다음과 같이 정의합니다.

  • pan(팬): 카메라의 좌우 회전각. 광학축이 월드좌표계 Y축과 평행할때 0도, 왼쪽이 +, 오른쪽이 -
  • tilt(틸트): 카메라의 상하 회전각. 광학축이 월드좌표계 Y축과 평행할때 0도, 위쪽이 +, 아래쪽이 -


즉, 카메라의 pan, tilt는 카메라 광학축(Zc)이 Y축 방향일 때 0이고 카메라를 위로 들면 tilt가 증가, 왼쪽으로 돌리면 pan이 증가합니다.


현재 카메라의 팬 값을 p, 틸트 값을 t라 하고 이동하고자 하는 목표점의 영상좌표를 q(x, y) 라 하면 q를 영상 중심에 맞추기 위한 팬 값 p', 틸트 값 t'은 다음과 같이 계산합니다.


1. 영상좌표 (x, y)에 대한 정규 이미지 좌표 (u, v)를 계산한다.

u = (x-cx)/fx

v = (y-cy)/fy

=> 보다 정밀한 결과를 위해서는 여기에 추가적으로 렌즈왜곡보정까지 해 주어야 함(렌즈왜곡보정에 대해서는 카메라 왜곡보정 - 이론 및 실제 글 참조)


2. (u, v)에 대한 카메라 좌표 Xc를 구한다.

Xc = (u, v, 1)


3. Xc를 월드좌표 Xw로 변환한다. 

=> 3D 좌표계 변환 방법 (예: 월드좌표계 - 카메라 좌표계) 글의 수식 (2) 참조


4. Xw의 팬 각 p', 틸트 각 t'을 계산한다.

Xw = (a, b, c)로 가정하면,

p' = -atan2(a, b)

t' = atan2(c, sqrt(a*a+b*b))


5. 계산된 p', t' 위치로 카메라를 이동시키면 OK.

현재의 팬, 틸트가 p, t이므로 p'-p, t'-t 만큼 이동시키면 원하는 위치로 이동하게 됨


계산 원리는 카메라 초점(프로젝션 중심)과 목표점을 잊는 선을 월드 좌표계 상에서 구한 후, 이 선 방향의 팬과 틸트 값을 계산하는 방식입니다.


※ 위 방법은 카메라의 초점 위치가 팬, 틸트에 관계없이 고정된다는 가정하에 수식을 세운 것입니다. 그런데, 실제 팬/틸트 카메라는 카메라 초점 위치도 같이 변하기 때문에 이에 따른 오차가 있을 수 있으니 참고하시기 바랍니다. 만일 초점의 위치변화까지 고려한다면 좀더 복잡한 수식 도출이 필요합니다..


by 다크 프로그래머


OpenCV Haar/cascade training 무한루프 방지

영상처리 2013. 12. 2. 15:43

예전에 올렸던 OpenCV Haar/cascade training 튜토리얼(http://darkpgmr.tistory.com/70) 글의 내용 중에서 무한루프 방지 부분에 대한 보완 내용입니다. 원래 글에 추가하기에는 내용이 좀 길어서 따로 포스팅합니다.


opencv의 haartraining 또는 cascade training을 수행할 때 종종 무한루프에 빠지게 되는데 이에 대한 원인 및 해결 방법에 대해서는 댓글로 일부 설명을 한 바 있습니다만(http://darkpgmr.tistory.com/70, http://darkpgmr.tistory.com/73) 이 글에서는 좀더 구체적인 코드를 적어보고자 합니다.


opencv에 있는 원본 소스코드를 분석하면서 나름 생각한 해결책이라 어디 다른데에 공개된 방법은 아닙니다. 그런 만큼 오류 가능성도 있으니 참고하시기 바라며 혹시 문제가 예상되는 부분이 있으면 댓글로 알려주시기 바랍니다.



1. OpenCV Haartraining 방식에서의 무한루프 방지


이전 방식인 Haartraining 방식을 사용할 경우에는 코드에서 두 부분을 수정해 주어야 합니다.


먼저 opencv\apps\haartraining\cvhaartraining.cpp 파일에 있는 icvGetNextFromBackgroundData(...) 함수의 구현 부분을 다음과 같이 수정합니다 (기존 파란색 코드 부분을 붉은색 코드로 수정).


static

void icvGetNextFromBackgroundData( CvBackgroundData* data,

 CvBackgroundReader* reader )

{

IplImage* img = NULL;

size_t datasize = 0;

int round = 0;

int i = 0;

CvPoint offset = cvPoint(0,0);


assert( data != NULL && reader != NULL );


if( reader->src.data.ptr != NULL )

{

cvFree( &(reader->src.data.ptr) );

reader->src.data.ptr = NULL;

}

if( reader->img.data.ptr != NULL )

{

cvFree( &(reader->img.data.ptr) );

reader->img.data.ptr = NULL;

}


#ifdef CV_OPENMP

#pragma omp critical(c_background_data)

#endif /* CV_OPENMP */

{

for( i = 0; i < data->count; i++ )

{

round = data->round;


//#ifdef CV_VERBOSE

//            printf( "Open background image: %s\n", data->filename[data->last] );

//#endif /* CV_VERBOSE */


// original code

/*

data->last = rand() % data->count;

data->last %= data->count;

img = cvLoadImage( data->filename[data->last], 0 );

if( !img )

continue;

data->round += data->last / data->count;

data->round = data->round % (data->winsize.width * data->winsize.height);

*/


//*** begin (modified code by dark)

img = cvLoadImage( data->filename[data->last++], 0 );

if( !img )

continue;

if( img->height<data->winsize.height || img->width < data->winsize.width )

continue;

data->round += data->last / data->count;

data->round = data->round % (data->winsize.width * data->winsize.height);

data->last %= data->count;

//*** end


offset.x = round % data->winsize.width;

offset.y = round / data->winsize.width;


offset.x = MIN( offset.x, img->width - data->winsize.width );

offset.y = MIN( offset.y, img->height - data->winsize.height );


if( img != NULL && img->depth == IPL_DEPTH_8U && img->nChannels == 1 &&

offset.x >= 0 && offset.y >= 0 )

{

break;

}

if( img != NULL )

cvReleaseImage( &img );

img = NULL;

}

}

if( img == NULL )

{

/* no appropriate image */


#ifdef CV_VERBOSE

printf( "Invalid background description file.\n" );

#endif /* CV_VERBOSE */


assert( 0 );

exit( 1 );

}

datasize = sizeof( uchar ) * img->width * img->height;

reader->src = cvMat( img->height, img->width, CV_8UC1, (void*) cvAlloc( datasize ) );

cvCopy( img, &reader->src, NULL );

cvReleaseImage( &img );

img = NULL;


//reader->offset.x = round % data->winsize.width;

//reader->offset.y = round / data->winsize.width;

reader->offset = offset;

reader->point = reader->offset;

reader->scale = MAX(

((float) data->winsize.width + reader->point.x) / ((float) reader->src.cols),

((float) data->winsize.height + reader->point.y) / ((float) reader->src.rows) );


reader->img = cvMat( (int) (reader->scale * reader->src.rows + 0.5F),

(int) (reader->scale * reader->src.cols + 0.5F),

CV_8UC1, (void*) cvAlloc( datasize ) );

cvResize( &(reader->src), &(reader->img) );

}



opencv의 원래 코드(파란색 부분)는 사실 문제의 소지가 있는 코드입니다. 자세히 보면 data->round 값이 절대로 증가할 수 없는 구조로서 항상 data->round 값이 0인 오류를 포함하고 있습니다. 이 부분만 위와 같이 수정해도 haartraining의 성능 및 무한루프 문제가 상당부분 개선될 것으로 생각됩니다.


완벽한 무한루프 방지를 위해 두번째로 수정해 줘야 할 부분은 icvGetHaarTrainingData(...)의 함수 구현 부분으로서 아래와 같이 수정해 줍니다 (붉은색 코드 부분을 새로 추가).


static

int icvGetHaarTrainingData( CvHaarTrainingData* data, int first, int count,

  CvIntHaarClassifier* cascade,

  CvGetHaarTrainingDataCallback callback, void* userdata,

  int* consumed, double* acceptance_ratio )

{

int i = 0;

ccounter_t getcount = 0;

ccounter_t thread_getcount = 0;

ccounter_t consumed_count;

ccounter_t thread_consumed_count;


/* private variables */

CvMat img;

CvMat sum;

CvMat tilted;

CvMat sqsum;


sum_type* sumdata;

sum_type* tilteddata;

float*    normfactor;


/* end private variables */


assert( data != NULL );

assert( first + count <= data->maxnum );

assert( cascade != NULL );

assert( callback != NULL );


// if( !cvbgdata ) return 0; this check needs to be done in the callback for BG


CCOUNTER_SET_ZERO(getcount);

CCOUNTER_SET_ZERO(thread_getcount);

CCOUNTER_SET_ZERO(consumed_count);

CCOUNTER_SET_ZERO(thread_consumed_count);


#ifdef CV_OPENMP

#pragma omp parallel private(img, sum, tilted, sqsum, sumdata, tilteddata, \

normfactor, thread_consumed_count, thread_getcount)

#endif /* CV_OPENMP */

{

sumdata    = NULL;

tilteddata = NULL;

normfactor = NULL;


CCOUNTER_SET_ZERO(thread_getcount);

CCOUNTER_SET_ZERO(thread_consumed_count);

int ok = 1;


img = cvMat( data->winsize.height, data->winsize.width, CV_8UC1,

cvAlloc( sizeof( uchar ) * data->winsize.height * data->winsize.width ) );

sum = cvMat( data->winsize.height + 1, data->winsize.width + 1,

CV_SUM_MAT_TYPE, NULL );

tilted = cvMat( data->winsize.height + 1, data->winsize.width + 1,

CV_SUM_MAT_TYPE, NULL );

sqsum = cvMat( data->winsize.height + 1, data->winsize.width + 1, CV_SQSUM_MAT_TYPE,

cvAlloc( sizeof( sqsum_type ) * (data->winsize.height + 1)

* (data->winsize.width + 1) ) );


//*** begin (prevent infinit loop)

int start_neg_index = cvbgdata->round * cvbgdata->count + cvbgdata->last;

bool neg_updated = false;

//*** end


#ifdef CV_OPENMP

#pragma omp for schedule(static, 1)

#endif /* CV_OPENMP */

for( i = first; (i < first + count); i++ )

{

if( !ok )

continue;

for( ; ; )

{

//*** prevent infinite loop

int neg_index = cvbgdata->round * cvbgdata->count + cvbgdata->last;

if(neg_index!=start_neg_index)

neg_updated = true;

if(neg_updated && neg_index==start_neg_index)

{

i = first + count - 1;

break;

}

//*** end


ok = callback( &img, userdata );

if( !ok )

break;


CCOUNTER_INC(thread_consumed_count);


sumdata = (sum_type*) (data->sum.data.ptr + i * data->sum.step);

tilteddata = (sum_type*) (data->tilted.data.ptr + i * data->tilted.step);

normfactor = data->normfactor.data.fl + i;

sum.data.ptr = (uchar*) sumdata;

tilted.data.ptr = (uchar*) tilteddata;

icvGetAuxImages( &img, &sum, &tilted, &sqsum, normfactor );

if( cascade->eval( cascade, sumdata, tilteddata, *normfactor ) != 0.0F )

{

CCOUNTER_INC(thread_getcount);

break;

}

}


#ifdef CV_VERBOSE

if( (i - first) % 500 == 0 )

{

fprintf( stderr, "%3d%%\r", (int) ( 100.0 * (i - first) / count ) );

fflush( stderr );

}

#endif /* CV_VERBOSE */

}


cvFree( &(img.data.ptr) );

cvFree( &(sqsum.data.ptr) );


#ifdef CV_OPENMP

#pragma omp critical (c_consumed_count)

#endif /* CV_OPENMP */

{

/* consumed_count += thread_consumed_count; */

CCOUNTER_ADD(getcount, thread_getcount);

CCOUNTER_ADD(consumed_count, thread_consumed_count);

}

} /* omp parallel */


if( consumed != NULL )

{

*consumed = (int)consumed_count;

}


if( acceptance_ratio != NULL )

{

/* *acceptance_ratio = ((double) count) / consumed_count; */

*acceptance_ratio = CCOUNTER_DIV(count, consumed_count);

}


return static_cast<int>(getcount);

}



위와 같이 수정한 자세한 이유는 설명치 않겠습니다. 간단히 설명하면 모든 가능한 조합에 대해 negative image들을 검사했음에도 불구하고 원하는 수 만큼의 negative sample을 획득하지 못한 경우에는 그냥 for 루프를 빠져나가도록 수정했다는 정도.. (원래의 opencv 구현은 이러한 경우에도 다시 같은 조합을 반복하여 탐색하기 때문에 무한루프에 빠지게 됨)


참고용으로 위 코드를 텍스트 파일로 첨부합니다.


haartraining_infinit_loop.txt




2. OpenCV Cascade Training 방식에서의 무한루프 방지


Cascade 방식에서도 무한루프 방지를 위해서 두 부분을 수정해 주면 됩니다.


먼저, opencv\apps\traincascade\imagestorage.h에서 CvCascadeImageReader 클래스에 다음과 같이 getNegIndex라는 함수를 멤버로 추가해 줍니다.


class CvCascadeImageReader

{

public:

bool create( const String _posFilename, const String _negFilename, Size _winSize );

void restart() { posReader.restart(); }

bool getNeg(Mat &_img) { return negReader.get( _img ); }

bool getPos(Mat &_img) { return posReader.get( _img ); }


//*** begin (prevent infinit loop)

int getNegIndex()

{

size_t count = negReader.imgFilenames.size();

return negReader.round*count + negReader.last;

}

//*** end


private:

class PosReader

{

public:

PosReader();

virtual ~PosReader();

bool create( const String _filename );

bool get( Mat &_img );

void restart();


short* vec;

FILE*  file;

int    count;

int    vecSize;

int    last;

int    base;

} posReader;


class NegReader

{

public:

NegReader();

bool create( const String _filename, Size _winSize );

bool get( Mat& _img );

bool nextImg();


Mat     src, img;

vector<String> imgFilenames;

Point   offset, point;

float   scale;

float   scaleFactor;

float   stepFactor;

size_t  last, round;

Size    winSize;

} negReader;

};




다음으로, cascadeclassifier.cpp 파일에 있는 CvCascadeClassifier::fillPassedSamples(...) 함수를 다음과 같이 수정해 줍니다.


int CvCascadeClassifier::fillPassedSamples( int first, int count, bool isPositive, int64& consumed )

{

//*** begin (prevent infinit loop)

int start_neg_index = imgReader.getNegIndex();

bool neg_updated = false;

//*** end


int getcount = 0;

Mat img(cascadeParams.winSize, CV_8UC1);

for( int i = first; i < first + count; i++ )

{

// if(i%10==0) cout << i-first << "/" << count << "[" << imgReader.getNegCount() << "]" << endl;

for( ; ; )

{

//*** prevent infinite loop

int neg_index = imgReader.getNegIndex();

if(neg_index!=start_neg_index)

neg_updated = true;

if(neg_updated && neg_index==start_neg_index)

return getcount;

//*** end


bool isGetImg = isPositive ? imgReader.getPos( img ) :

imgReader.getNeg( img );

if( !isGetImg )

return getcount;

consumed++;


featureEvaluator->setImage( img, isPositive ? 1 : 0, i );

if( predict( i ) == 1.0F )

{

getcount++;

break;

}

}

}

return getcount;

}


원리는 haartraining 방식의 경우와 동일합니다.


참고용으로 위 코드를 텍스트 파일로 첨부합니다.


cascadetraining_infinite_loop.txt


by 다크 프로그래머


영상추적 프로그램(DTracker)

개발한 것들 2013. 11. 29. 14:37

최근 몇년간 개인적으로 가장 많은 시간을 들여서 연구했던 것이 영상추적 알고리즘입니다. OpenCV meanshift / camshift를 이용한 추적 프로그램 예제코드와 그동안 제가 개발했던 영상추적기(실행파일만)를 순서대로 올립니다.



1. OpenCV의 MeanShift / CamShift 추적기


처음 영상추적 개발을 시작하면서 저도 가장 기본적이라고 할 수 있는 meanshift부터 시작했습니다. 당시에 직접 meanshift를 구현하여 돌려보았지만 성능이 그다지 만족스럽지는 않았습니다.


블로그에 올릴 목적으로 최근 OpenCV에서 제공하는 meanshift, camshift를 이용한 영상 추적기를 새로 구현해 보았습니다. 아래의 프로그램을 이용하면 meanshift, camshift 추적기의 사용법 및 성능, 장단점에 대해 쉽게 테스트할 수 있습니다 (소스코드 포함).


tracker_opencv.zip



사용법: 위 화면에 보는 것처럼 camshift 알고리즘, meanshift 알고리즘을 선택할 수 있고 사용할 색상모델을 선택할 수 있게 되어 있습니다. 마우스로 추적할 박스영역을 그리면 되고 스페이스바를 누르면 일시멈춤이 됩니다. 실행도중 언제든지 추적대상을 바꿀 수 있습니다.


OpenCV에서 제공하는 샘플코드 등에는 단일채널(hue, gray 등) 히스토그램만을 이용한 예가 나와 있지만 실제로는 HSV나 RGB의 3D 채널을 모두 이용할 경우에 추적 성능이 훨씬 좋습니다. 그 구체적인 방법에 대해서는 첨부 파일에 있는 소스코드를 참조하기 바랍니다.


다음은 위 프로그램을 적용한 데모 동영상들입니다 (OpenCV에서 제공하는 기본적인 tracker 들로서 일반적인 성능은 좋지 않습니다. 어떤 면이 강하고 또 어떤 경우에 잘 안되는지 아래 동영상들을 보면서 확인하시기 바랍니다).


opencv camshift demo - hsv 3d color model


opencv meanshift demo - rgb 3d color model


opencv meanshift demo - rgb 3d color model


실제 테스트를 해 보면 알겠지만 camshift의 경우 크기, 회전 변화를 추적할 수 있으며 특히 사람 얼굴을 추적하는데에 매우 뛰어난 성능을 보입니다. 하지만 camshift는 물체가 단일색일 경우 그리고 배경과 구분되는 색일 경우의 성능은 매우 뛰어나지만 물체에 여러 색상이 섞여있거나 배경과 유사한 색일 경우에는 오히려 meanshift보다 성능이 떨어집니다


meanshift나 camshift 모두 색상 히스토그램을 이용한 추적 방식이기 때문에 이에 따른 장단점은 명확합니다. 히스토그램의 특성상 위치정보는 고려하지 않기 때문에 물체의 형태가 변해도 추적이 가능한 반면, 다른 한편으로는 색이 배치된 위치정보를 잃어버리기 때문에 물체의 색 구성이 배경과 유사한 경우에는 추적에 실패하기 쉽습니다.


결론적으로 meanshift/camshift 등은 단순한 환경(공장자동화 응용, 배경도 단색, 물체도 단색)에서는 최고의 tracker이지만 일반적인 환경에서는 거의 적용이 힘들다고 볼 수 있습니다.



2. DTracker Ver.1 (히스토그램 기반)


처음에 시도해 보았던 meanshift가 일반적인 환경에서는 성능이 너무 떨어졌기에 이를 개선하고자 당시 여러 시도를 해 보았었는데 그 때 개발한 영상추적기가 dtracker v1입니다. meanshift와 마찬가지로 색상 히스토그램을 이용합니다만 meanshift보다는 훨씬 성능이 뛰어납니다. 사용한 색상 모델은 RGB 3D 모델입니다 (실행파일 첨부: 소스코드는 포함되지 않음).


dtracker_v1.zip


특징은 meanshift로 추적 가능한 것은 모두 추적할 수 있으며 배경이 유사한 경우에도 어느정도 추적이 가능합니다. 특히 서서 걸어가는 보행자를 추적할 때 성능이 좋은 편입니다. 그레이/칼라 영상 모두 가능하며 처리속도는 i7기준으로 200 ~ 1,000 fps 정도입니다.


다음은 dtracker v1 추적기를 적용한 데모 동영상들입니다.


dtracker(histogram) demo1


dtracker(histogram) demo2


dtracker(histogram) demo3


dtracker(histogram) demo4


dtracker(histogram) demo5


dtracker(histogram) demo6



3. DTracker Ver2 (Optical Flow 기반)


처음 개발했던 히스토그램 기반의 dtracker v1는 물체의 크기변화는 추적하지 못하며 다른 물체에 의해 가려지는 경우 추적 성능이 떨어지는 문제가 있었습니다 (예: 위 dtracker v1 데모동영상 6). 이러한 단점을 개선하고자 개발한 것이 dtracker v2인데 dtracker v2에서는 기존 히스토그램 방식을 버리고 optical flow 방식을 사용하였습니다 (테스트용 실행파일 첨부: 소스코드는 포함되지 않음).


dtracker_v2.zip


특징은 추적 대상의 크기변화, 회전변화를 추적할 수 있으며 부분적으로 가려지는 경우에도 추적이 가능합니다. 또한 추적 도중 흔들림이 거의 없고 초기 설정한 추적 영역을 정확하게 추적할 수 있습니다. 그레이/칼라 영상 모두 가능하며 처리속도는 i7 기준으로 40 ~ 300 fps 정도입니다.


다음은 dtracker v2 추적기를 적용한 데모 동영상들입니다.


dtracker(optical flow) demo1


dtracker(optical flow) demo2


dtracker(optical flow) demo3


dtracker(optical flow) demo4


dtracker(optical flow) demo5



4. 향후 개발할 tracker


1차 목표는 현재 개발된 히스토그램 방식과 optical flow 방식을 결합하는 것입니다. 형태가 고정된 물체의 경우 optical flow 방식이 훨씬 좋은 성능을 내지만 사람 등과 같이 형태가 변하는 경우에는 히스토그램 방식이 더 뛰어날 때가 많습니다. 각각의 장단점이 있으므로 이들 장점을 잘 결합하는게 1차 목표입니다.


2차 목표는 detector 기능을 추가하는 것입니다. 현재까지 개발한 tracker들은 detector 기능이 없습니다. 그래서 추적하던 물체가 완전히 사라졌다가 나타나는 경우에는 다시 복구하지 못하는 문제가 있습니다(예: dtracker v2 demo4). 개발한 tracker는 속도와 추적 정확도 면에서 기존 어떤 추적기와도 견줄 수 있지만 detector 기능이 없음으로 해서 요즘 나오는 tld tracker 등에 밀리고 있습니다. detection 기능만 보완되면 괜찮은 추적기가 완성될 것으로 생각합니다.


저의 궁극적인 목표는 추적할 대상 물체를 스스로 인식하고 그 경계까지 완벽하게 추적하는 것입니다. 예를 들어 초기에 물체의 정확한 영역이 아닌 배경까지 포함된 영역을 추적 대상으로 설정해 줘도 그 안에 포함된 실제 물체 영역만을 찾아내어 추적하는 추적기, 또는 초기에 물체의 일부 영역만 지정해줘도 알아서 전체 물체 영역을 식별하고 해당 물체를 추적해 주는 추적기를 개발하는 것입니다.



※ 동영상 출처


참고로 테스트에 사용한 동영상들의 원본 출처는 다음과 같습니다 (대부분 영상추적 분야에서 벤치마킹용으로 사용되는 공개 동영상들입니다).


[VTD] http://cv.snu.ac.kr/research/~vtd/

[FragT] http://www.cs.technion.ac.il/~amita/fragtrack/fragtrack.htm

[LOT] http://www.eng.tau.ac.il/~oron/LOT/LOT.html

[TLD] http://info.ee.surrey.ac.uk/Personal/Z.Kalal/tld.html



by 다크 프로그래머


'개발한 것들' 카테고리의 다른 글

카메라 캘리브레이션 프로그램 (DarkCamCalibrator)  (169) 2014.07.17
Ferns Detector  (18) 2013.08.28
가상 3D 영상 생성 프로그램  (16) 2013.08.01