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 다크 프로그래머