从Opencv教程上才发现下面的话。要是早点看到就好了,就不用看haartraining了,不过话说haartraining的网上的资料还是有不少的,但是traincascade就比较少了,所以只能自己硬着头皮看代码了。在程序的使用上跟haartraining差不多,代码流程部分在这记录下以后慢慢补充。
”OpenCV中有两个程序可以训练级联分类器: opencv_haartraining 和opencv_traincascade 。 opencv_traincascade 是一个新程序,根据OpenCV 2.x API 用C++ 编写。这二者主要的区别是opencv_traincascade 支持 Haar[Viola2001] 和 LBP[Liao2007] (Local Binary Patterns) 两种特征,并易于增加其他的特征。与Haar特征相比,LBP特征是整数特征,因此训练和检测过程都会比Haar特征快几倍。LBP和Haar特征用于检测的准确率,是依赖训练过程中的训练数据的质量和训练参数。训练一个与基于Haar特征同样准确度的LBP的分类器是可能的。“
下面按照我觉得需要理解的部分进行分析。
1. 总流程
Traincascade.cpp中调用cvCascadeClassifier的train函数进行训练。
bool CvCascadeClassifier::train( const String _cascadeDirName,//工作目录 const String _posFilename,//pos.vec const String _negFilename,//neg.txt int _numPos, int _numNeg,//5000,3000 int _precalcValBufSize, int _precalcIdxBufSize, int _numStages, const CvCascadeParams& _cascadeParams,//BOOST, HAAR/LBP,width,height const CvFeatureParams& _featureParams,//里面的参数好像是跟上面选的是Haar还是LBP有关,具体还不懂 const CvCascadeBoostParams& _stageParams,//GAB,minHitRate,maxFalseAlarmRate,weightTrimRate,maxDepth,maxWeakCount bool baseFormatSave ) { if ( !imgReader.create( _posFilename, _negFilename, _cascadeParams.winSize ) )//读取正负样本的信息 if ( !load( dirName ) ) { cascadeParams = _cascadeParams;//表明cascade是不是用boost,分类器基于的特征类型是Haar还是LBP,正样本的大小是多少 featureParams = CvFeatureParams::create(cascadeParams.featureType);//根据featureType来决定返回的是new 一个CvHaarFeatureParams还是CvLBPFeatureParams featureParams->init(_featureParams); stageParams = new CvCascadeBoostParams; *stageParams = _stageParams;//这里填写了APP指定的GAB,minHitRate,maxFalseAlarmRate,weightTrimRate,maxDepth,maxWeakCount,个人认为都是一个训练强分类器需要的一些参数 featureEvaluator = CvFeatureEvaluator::create(cascadeParams.featureType);//根据featureType来决定返回的是new 一个CvHaarEvaluator还是CvLBPEvaluator featureEvaluator->init( (CvFeatureParams*)featureParams, numPos + numNeg, cascadeParams.winSize );//把featureEvaluator的里面有关feature的指针都开辟内存,例如sum,title,cls等,然后根据image的长宽调用基类的init和子类的generateFeatrues来计算所有的Haar/LBP的features的形状存到vector<Feature> features中 stageClassifiers.reserve( numStages ); } double requiredLeafFARate = pow( (double) stageParams->maxFalseAlarm, (double) numStages ) / (double)stageParams->max_depth;//需要达到的FA的值 double tempLeafFARate; for( int i = startNumStages; i < numStages; i++ ) { cout << endl << "===== TRAINING " << i << "-stage =====" << endl; cout << "<BEGIN" << endl; if ( !updateTrainingSet( tempLeafFARate ) )//从正负样本集合里挑选出numPos+numNeg个样本到集合CvCascadeImageReader imgReader中,个人感觉很多时间耗在这里,因为这里对每一个样本要进行predict,就是说利用当前强分类器已构建的弱分类器进行判断是正样本还是负样本,只有被判断是正样本的情况才被加到TrainingSet中(第一次的时候当然是默认都是正样本)。所以如果i比较大的时候,在构建负样本的TrainingSet就特别费时。并且把积分图像等计算出放到evaluator的img中。 if( tempLeafFARate <= requiredLeafFARate )//check下是否可以中止训练 CvCascadeBoost* tempStage = new CvCascadeBoost; tempStage->train( (CvFeatureEvaluator*)featureEvaluator, curNumSamples, _precalcValBufSize, _precalcIdxBufSize, *((CvCascadeBoostParams*)stageParams) );//训练啦啦啦啦啦,训练一个强分类器 stageClassifiers.push_back( tempStage );//训练完了一个强分类器添加到结果中 cout << "END>" << endl; //保存params.xml文件 //保存stage0.xml,stage1.xml,stage2.xml。。。文件 }//for i [startNumStages, nStage] save( dirName + CC_CASCADE_FILENAME, baseFormatSave );//保存cascade.xml文件 return true; }
2.关于特征,以LBP为例(Haar特征网上例子比较多了)
2.1 LBP特征的形状,位置,数目的计算,都是在CvLBPEvaluator.cpp中
void CvLBPEvaluator::init(const CvFeatureParams *_featureParams, int _maxSampleCount, Size _winSize) { CV_Assert( _maxSampleCount > 0); sum.create((int)_maxSampleCount, (_winSize.width + 1) * (_winSize.height + 1), CV_32SC1);//LBP需要用到积分图,_maxSampleCount是指所有正负样本的数量和 CvFeatureEvaluator::init( _featureParams, _maxSampleCount, _winSize );//调用基类的init }
void CvFeatureEvaluator::init(const CvFeatureParams *_featureParams, int _maxSampleCount, Size _winSize ) { CV_Assert(_maxSampleCount > 0); featureParams = (CvFeatureParams *)_featureParams; winSize = _winSize; numFeatures = 0; cls.create( (int)_maxSampleCount, 1, CV_32FC1 );//表示样本的正负情况,所以就一列 generateFeatures();//虚函数,调用子类的实现,这里是计算feature的位置啊,大小啊等数据,还包括多少个feature等信息,因为img的大小固定后这些东西就可以计算出来了所以先算出来为好。 }
void CvLBPEvaluator::generateFeatures()//把所有可能的LBPfeature的位置大小啥的都计算出来。 { int offset = winSize.width + 1; for( int x = 0; x < winSize.width; x++ ) for( int y = 0; y < winSize.height; y++ ) for( int w = 1; w <= winSize.width / 3; w++ )//因为它要用3个block by 3个block的情况,所以w这除以3 for( int h = 1; h <= winSize.height / 3; h++ ) if ( (x+3*w <= winSize.width) && (y+3*h <= winSize.height) ) features.push_back( Feature(offset, x, y, w, h ) ); numFeatures = (int)features.size(); }
/************************************************************************/ /* 个人理解这里LBP的Feature是指的是liao的文章的Fig.1(b)。是9x9MB-LBP operator CvLBPEvaluator::Feature::Feature( int offset, int x, int y, int _blockWidth, int _blockHeight ) { Rect tr = rect = cvRect(x, y, _blockWidth, _blockHeight);//我奇怪文章里讲到的block是正方形啊,为啥这里w和h可以不一样呢? CV_SUM_OFFSETS( p[0], p[1], p[4], p[5], tr, offset )//这里按照长方形tr来计算p[0],p[1], p[4], p[5]相对于图像左上角的offset,以便以后对每个图像求积分的直接拿这4个数作为下标就很方便的得到积分值。 tr.x += 2*rect.width; CV_SUM_OFFSETS( p[2], p[3], p[6], p[7], tr, offset ) tr.y +=2*rect.height; CV_SUM_OFFSETS( p[10], p[11], p[14], p[15], tr, offset ) tr.x -= 2*rect.width; CV_SUM_OFFSETS( p[8], p[9], p[12], p[13], tr, offset ) }
bool CvCascadeClassifier::updateTrainingSet( double& acceptanceRatio) { int64 posConsumed = 0, negConsumed = 0; imgReader.restart(); int posCount = fillPassedSamples( 0, numPos, true, posConsumed );//这个fillPassSample就是之前说过的对输入样本只要被当前的强分类器判断为正样本的意思 cout << "POS count : consumed " << posCount << " : " << (int)posConsumed << endl; int proNumNeg = cvRound( ( ((double)numNeg) * ((double)posCount) ) / numPos ); // apply only a fraction of negative samples. double is required since overflow is possible int negCount = fillPassedSamples( posCount, proNumNeg, false, negConsumed );//这里就是之前说的认为耗时的地方。这里针对负样本,负样本的集合一般图片大小大于正样本的大小,如果是负样本的话,函数会去扣一块跟正样本大小相同的区域下来送到训练集合中。 curNumSamples = posCount + negCount; acceptanceRatio = negConsumed == 0 ? 0 : ( (double)negCount/(double)(int64)negConsumed ); cout << "NEG count : acceptanceRatio " << negCount << " : " << acceptanceRatio << endl; return true; }
int CvCascadeClassifier::fillPassedSamples( int first, int count, bool isPositive, 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 );//读一个图像数据到img中,个人觉得需要注意的是这里它不是说每次调用getNeg的时候就是读下一个负样本图像,在getNeg里面它会对一个负样本进行缩放操作来扣下来目标大小的图像。 if( !isGetImg ) return getcount; consumed++; featureEvaluator->setImage( img, isPositive ? 1 : 0, i );//计算这个img的积分图等信息 if( predict( i ) == 1.0F )//这个predict应该是利用之前已有的weak 分类器来判断是否为正样本,所以对于isPositive为False的情况下,会一直去寻找第i个负样本作为setImage { getcount++; break; } } } return getcount; }
bool getNeg(Mat &_img) { return negReader.get( _img ); } bool getPos(Mat &_img) { return posReader.get( _img ); }
bool CvCascadeImageReader::NegReader::get( Mat& _img ) { if( img.empty() ) if ( !nextImg() )//nextImg根据neg.txt的内容读取下一个负样本图片的内容 return false; Mat mat( winSize.height, winSize.width, CV_8UC1, (void*)(img.data + point.y * img.step + point.x * img.elemSize()), img.step );//这里是把mat指向计算的img的data的那个地方,最后一个参数指明src的宽,从而mat是相当于从src上的那个位置扣下来的width x height的大小的图片。 mat.copyTo(_img); if( (int)( point.x + (1.0F + stepFactor ) * winSize.width ) < img.cols ) point.x += (int)(stepFactor * winSize.width); else { point.x = offset.x; if( (int)( point.y + (1.0F + stepFactor ) * winSize.height ) < img.rows ) point.y += (int)(stepFactor * winSize.height); else { point.y = offset.y; scale *= scaleFactor;//这里把之前缩小后的scale(w_neg/w_pos)乘以sqrt(2) if( scale <= 1.0F )//然后比较下这个scale有没有达到原始图像(没缩放的时候)的大小,要是没有的话就按乘以sqrt(2)的方式放大img resize( src, img, Size( (int)(scale*src.cols), (int)(scale*src.rows) ) ); else { if ( !nextImg() )//在这把下一个负样本的image给read进来 return false; } } } return true; }
void CvLBPEvaluator::setImage(const Mat &img, uchar clsLabel, int idx)//计算积分图像 { CV_DbgAssert( !sum.empty() ); CvFeatureEvaluator::setImage( img, clsLabel, idx ); Mat innSum(winSize.height + 1, winSize.width + 1, sum.type(), sum.ptr<int>((int)idx)); integral( img, innSum );//计算这个读出来的img的积分图像放到sum的第idx位置中 }
bool CvCascadeBoost::train( const CvFeatureEvaluator* _featureEvaluator,//包含了sum,tilted,特征的位置等信息 int _numSamples, int _precalcValBufSize, int _precalcIdxBufSize, const CvCascadeBoostParams& _params ) { CV_Assert( !data ); clear(); data = new CvCascadeBoostTrainData( _featureEvaluator, _numSamples, _precalcValBufSize, _precalcIdxBufSize, _params ); CvMemStorage *storage = cvCreateMemStorage(); weak = cvCreateSeq( 0, sizeof(CvSeq), sizeof(CvBoostTree*), storage ); storage = 0; set_params( _params ); if ( (_params.boost_type == LOGIT) || (_params.boost_type == GENTLE) ) data->do_responses_copy(); update_weights( 0 ); cout << "+----+---------+---------+" << endl; cout << "| N | HR | FA |" << endl; cout << "+----+---------+---------+" << endl; do { CvCascadeBoostTree* tree = new CvCascadeBoostTree; if( !tree->train( data, subsample_mask, this ) ) { delete tree; break; } cvSeqPush( weak, &tree ); update_weights( tree ); trim_weights(); if( cvCountNonZero(subsample_mask) == 0 ) break; } while( !isErrDesired() && (weak->total < params.weak_count) ); data->is_classifier = true; data->free_train_data(); return true; }