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폴더내 파일항목들을 받아 사용해도 문제가 없을까요...??

  • markers2 2017.11.02 03:59 신고 ADDR 수정/삭제 답글

    안녕하세요 다크프로그래머님! 저는 이번에 학교 프로젝트의 일부로 얼굴인식을 진행하게 된 학부생 입니다...
    다름이 아니라 현재 opencv를 이용해서 샘플을 만들고 학습을 하려고 하는데 opencv_trainingcascade.exe 실행 시 무한루프에 빠지는 현상이 일어나는 듯 합니다...
    적은 개수로 먼저 시도만 해보려하여 bg폴더에 300x300사이즈의 사진 70장을 넣어두고 -w와 -h를 조절하면서 stage는 3 또는 5로 진행하였는데..

    numPos == 100, numNeg == 50, -w 와 -h을 실 사이즈와 같이 300으로 했을경우엔 assertion failed <_img.rows * _img.cols == vecSize> 에러가 발생하였고
    -w 와 -h을 각각 24로 하였을 경우엔 Bad argument <can not get new positive sample ~~~...> 라고 에러가 발생합니다..
    numPos == 70, numNeg == 35로 조절하여 실행하니.. 이제는 무한루프에 빠지는지 작동 중지됩니다...ㅠ ㅠ

    그래서 무한루프 방지를 위한 코드 수정을 했는데도... 여전합니다.... ㅠ ㅠ
    현재 외국에 나와 있어서 주변의 도움을 받기가 힘든 부분이라... 이렇게 댓글 남깁니다... ㅠ ㅠ
    numPos와 numNeg의 정도를 잘모르겠으며... -w와 -h는 실 이미지와 같은 값을 넣어줘야 하는건지..........
    너무 베이직한 질문일지요...

    • BlogIcon 다크pgmr 2017.11.02 16:14 신고 수정/삭제

      저도 이 프로그램을 이용해서 실제 학습을 시키거나 연구에 활용하는게 아니라서 참 뭐라고 답변을 드리기가 어렵네요.. 일단 제가 알기로는 numPos는 실제 샘플의 개수를 적고 numNeg는 이미지 개수에 관계없이 적당히 큰 수(?)를 적어주면 됩니다. 그리고 -w, -h는 실 이미지 크기가 아니라 모델의 크기이기 때문에 적당히 작은 크기를 적습니다. -w, -h에 너무 큰 값을 사용하면 내부 메모리 에러나 학습이 너무 오래 걸리는 등의 문제가 발생할 것입니다. 그리고 can not get new positive sample ~~ 이라는 에러 메시지는 어떨 때 발생하는지는 저도 잘 모르고 opencv 원본 소스코드를 확인해야 할 것 같습니다.. 직접 한번 확인해 보시면 좋을 것 같습니다.

  • markers2 2017.11.04 06:25 신고 ADDR 수정/삭제 답글

    ㅠㅠㅠ답변 감사합니다!!! 한번 여쭤보니 그래도 뭔가 답답함이 풀리는 듯 합니당....
    다행히도 어제 갑자기 잘 실행되어서 xml 파일까지 생성되었답니다!
    하지만.. 정확도를 올리기 위해 -numStages 10에 샘플 수만 더 늘렸는데
    stage 2에서 갑자기 "Opencv Error: Bad argument <Can not get new positive sample>" 에러가 뜨네요...ㅠ ㅠ
    이 에러는 제가 더 알아보도록 하겠습니다!! ㅎㅎ
    감사합니다 다크프로그래머님!!!! 한국 날씨가 많이 추워졌다고 하던데 감기 조심하세여! ^ ^