从OpenCV源码了解traincascade训练报错:Train dataset for temp stage can not be filled.

如果你在测试trainCascade时,资料目录没有放在项目目录中,很有可能触发下面的报错:

Train dataset for temp stage can not be filled.

我们很容易定位这个错误的来源,在cascadeClassifier.cpp中


bool CvCascadeClassifier::train( const string _cascadeDirName,
                                const string _posFilename,
                                const string _negFilename,
                                int _numPos, int _numNeg,
                                int _precalcValBufSize, int _precalcIdxBufSize,
                                int _numStages,
                                const CvCascadeParams& _cascadeParams,
                                const CvFeatureParams& _featureParams,
                                const CvCascadeBoostParams& _stageParams,
                                bool baseFormatSave,
                                double acceptanceRatioBreakValue )
{
    ...
    for( int i = startNumStages; i < numStages; i++ )
    {
        cout << endl << "===== TRAINING " << i << "-stage =====" << endl;
        cout << "<BEGIN" << endl;

        if ( !updateTrainingSet( requiredLeafFARate, tempLeafFARate ) )
        {
            cout << "Train dataset for temp stage can not be filled. "
                    "Branch training terminated." << endl;
            break;
        }
        ...
    }
    ...
}

进一步,updateTrainingSet中是在哪里返回false的呢?

bool CvCascadeClassifier::updateTrainingSet( double minimumAcceptanceRatio, double& acceptanceRatio)
{
    ...
    int negCount = fillPassedSamples( posCount, proNumNeg, false, minimumAcceptanceRatio, negConsumed );
    if ( !negCount )
        if ( !(negConsumed > 0 && ((double)negCount+1)/(double)negConsumed <= minimumAcceptanceRatio) )
            return false;
    ...
}

可见,触发这个错误的先决条件是if(!negCount), 也就是说,没有读取你的负样本。这样理解的话,你大概也猜得出来,要么文件名,要么哪个目录有问题。

如果你想通过搜索进一步去解决问题,得到的答案五花八门。

其实最便捷的办法,还是解析源码来得快,下面我们来走一遭。

首先,负本的数据是在哪里开始读取的?

我们看一下cascadeclassfier.cpp中的fillPassedSamples函数,

int CvCascadeClassifier::fillPassedSamples( int first, int count, bool isPositive, double minimumAcceptanceRatio, int64& consumed )
{
    int getcount = 0;
    Mat img(cascadeParams.winSize, CV_8UC1);
    for( int i = first; i < first + count; i++ )
    {
        for( ; ; )
        {
           ...
            bool isGetImg = isPositive ? imgReader.getPos( img ) :
                                           imgReader.getNeg( img );
            if( !isGetImg )
                return getcount;
            consumed++;

           ...
        }
    }
    return getcount;
}

其中有一个明显的读取操作:imgReader.getNeg( img ),那我们来追踪一下,

bool CvCascadeImageReader::NegReader::get( Mat& _img )
{
    ...
    if( img.empty() )
        if ( !nextImg() )
            return false;
    ...
}

接着找一个这个nextImg()函数,如下

bool CvCascadeImageReader::NegReader::nextImg()
{
    Point _offset = Point(0,0);
    size_t count = imgFilenames.size();
    for( size_t i = 0; i < count; i++ )
    {
        src = imread( imgFilenames[last++], 0 );
        ...
    }
    ...
}

嗯,终于看到了熟悉的imread(...),在loadsave.cpp中


Mat imread( const String& filename, int flags )
{
    ...
    /// load the data
    imread_( filename, flags, LOAD_MAT, &img );

    ...
    return img;
}


static void*
imread_( const String& filename, int flags, int hdrtype, Mat* mat=0 )
{
    ...    
    decoder = findDecoder( filename );
    ...

    /// if no decoder was found, return nothing.
    if( !decoder ){
        return 0;
    }
    ...
}

进一步imread_(...)调用了解码器findDecoder(...)函数,

static ImageDecoder findDecoder( const String& filename ) {

    size_t i, maxlen = 0;

    /// iterate through list of registered codecs
    for( i = 0; i < codecs.decoders.size(); i++ )
    {
        size_t len = codecs.decoders[i]->signatureLength();
        maxlen = std::max(maxlen, len);
    }

    /// Open the file
    FILE* f= fopen( filename.c_str(), "rb" );

    /// in the event of a failure, return an empty image decoder
    if( !f )
        return ImageDecoder();
    。。。
}

好了,打开文件的源码就是他了:FILE* f= fopen( filename.c_str(), "rb" )。这里你可以慢慢分析到底是文件名哪个地方不对。如果你要往回追究的话,这个文件名是来源于nextImg调用的数组imgFilenames[last++]的,而这个数组是在CvCascadeImageReader::NegReader::create中初始化的,

bool CvCascadeImageReader::NegReader::create( const string _filename, Size _winSize )
{
    string str;
    std::ifstream file(_filename.c_str());
    if ( !file.is_open() )
        return false;

    while( !file.eof() )
    {
        std::getline(file, str);
        str.erase(str.find_last_not_of(" \n\r\t")+1);
        if (str.empty()) break;
        if (str.at(0) == '#' ) continue; /* comment */
        imgFilenames.push_back(str);
    }
    file.close();

    winSize = _winSize;
    last = round = 0;
    return true;
}

好了,前因后果都明白了,相信问题就简单了。

不过为了方便,我举个简单的例子,假设你的项目目录为/apps/trainCascade/opencv_traincascade.vcproj,在/apps/dataCascade/建立了你的数据和图片文件,目录文件如../dataCascade/neg.txt,其内容是

neg/001.pgm

neg/002.pgm

......

负本图片放在这里

app/dataCascade/neg/001.pgm

app/dataCascade/neg/001.pgm

......

那对不起, 找不到,为什么?

因为imgFilenames.push_back(str);这句,把目录文件中的字符串如“neg/001.pgm”保存起来,当读取的时候,这个地址就会被当成相对于项目的地址,也就是app/trainCascade/neg/001.pgm, 这当然是找不到的,因为是在项目的上一级../data中放的dataCascade目录。解决办法就是,把你的neg.txt的内容改成下面这样的形式,

../dataCascade/neg/001.pgm

../dataCascade/neg/002.pgm

......

这样,就可以啦!

 

 

posted @ 2018-08-17 22:53  SpaceVision  阅读(81)  评论(0编辑  收藏  举报