OpenCV Haar/cascade training 무한루프 방지

영상처리 2013.12.02 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 다크 프로그래머


저작자 표시 비영리 변경 금지
신고
  • neverabandon 2013.12.11 17:27 신고 ADDR 수정/삭제 답글

    메일 보내드렸었는데 올려주셨군요. 바빠서 이제야 봤습니다. 적용해보고 피드백 드릴께요. 진심으로 감사드립니다.

    • BlogIcon 다크pgmr 2013.12.12 11:06 신고 수정/삭제

      네, 피드백 기대하겠습니다. 제가 cascade detector를 실제로 사용하는 것은 아니라서 테스트는 많이 하지 못했습니다. 사용해 보시고 피드백을 주시면 저나 다른 분들에게도 많은 참고가 되리라 생각합니다.

  • 니나루 2014.03.25 17:49 신고 ADDR 수정/삭제 답글

    ㅎㅎㅎ 다크님 짱~!!
    이 글 참고해서 코드에 적용했습니다.
    감사감사요~^- ^//

  • 홍기덕쿵더러러 2015.05.19 20:21 신고 ADDR 수정/삭제 답글

    다크님 하르트레이닝부터해서 정말 많이배워요!
    근데 질문이 있는데 트레이닝을 돌릴 때 터미널 창에서
    *** 1 cluster ***
    POS: 40 79 0.506329
    여기서 더 진행 안하고 가만히 있는데
    이런 경우가 무한루프맞나요??
    위에 나온 것처럼 코드수정해서 돌리는데도 그대루네요.

    • BlogIcon 다크pgmr 2015.05.19 22:23 신고 수정/삭제

      그건 저도 잘 모르겠습니다. 코드를 직접 컴파일해 사용하시는 것이라면 디버깅을 통해 확인해 보시면 좋을 것 같습니다.

  • hihiz 2017.06.02 19:12 신고 ADDR 수정/삭제 답글

    다크프로그래머님 안녕하세요 궁금한점이 있는데 제가 opencv2.7버전을 사용하고 있습니다. haar training 무한루프관련해서 제가 설치한 opencv/apps 파일에 haartraining폴더가 없는경우에는 인터넷에서 haartraining폴더내 파일항목들을 받아 사용해도 문제가 없을까요...??