OpenCV 人臉檢測自學(3)

ybyly發表於2015-02-04
(本文是在別處看到了,留著以後用,那個網站老彈廣告)
  1. 從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函式進行訓練。


  1. bool CvCascadeClassifier::train( const String _cascadeDirName,//工作目錄  
  2.                                 const String _posFilename,//pos.vec  
  3.                                 const String _negFilename,//neg.txt  
  4.                                 int _numPos, int _numNeg,//5000,3000  
  5.                                 int _precalcValBufSize, int _precalcIdxBufSize,  
  6.                                 int _numStages,  
  7.                                 const CvCascadeParams& _cascadeParams,//BOOST, HAAR/LBP,width,height  
  8.                                 const CvFeatureParams& _featureParams,//裡面的引數好像是跟上面選的是Haar還是LBP有關,具體還不懂  
  9.                                 const CvCascadeBoostParams& _stageParams,//GAB,minHitRate,maxFalseAlarmRate,weightTrimRate,maxDepth,maxWeakCount  
  10.                                 bool baseFormatSave )  
  11. {  
  12.   
  13.     if ( !imgReader.create( _posFilename, _negFilename, _cascadeParams.winSize ) )//讀取正負樣本的資訊  
  14.   
  15.     if ( !load( dirName ) )  
  16.     {  
  17.         cascadeParams = _cascadeParams;//表明cascade是不是用boost,分類器基於的特徵型別是Haar還是LBP,正樣本的大小是多少  
  18.         featureParams = CvFeatureParams::create(cascadeParams.featureType);//根據featureType來決定返回的是new 一個CvHaarFeatureParams還是CvLBPFeatureParams  
  19.         featureParams->init(_featureParams);  
  20.         stageParams = new CvCascadeBoostParams;  
  21.         *stageParams = _stageParams;//這裡填寫了APP指定的GAB,minHitRate,maxFalseAlarmRate,weightTrimRate,maxDepth,maxWeakCount,個人認為都是一個訓練強分類器需要的一些引數  
  22.         featureEvaluator = CvFeatureEvaluator::create(cascadeParams.featureType);//根據featureType來決定返回的是new 一個CvHaarEvaluator還是CvLBPEvaluator  
  23.         featureEvaluator->init( (CvFeatureParams*)featureParams, numPos + numNeg, cascadeParams.winSize );//把featureEvaluator的裡面有關feature的指標都開闢記憶體,例如sum,title,cls等,然後根據image的長寬呼叫基類的init和子類的generateFeatrues來計算所有的Haar/LBP的features的形狀存到vector<Feature> features中  
  24.         stageClassifiers.reserve( numStages );  
  25.     }  
  26.   
  27.     double requiredLeafFARate = pow( (double) stageParams->maxFalseAlarm, (double) numStages ) /  
  28.                                 (double)stageParams->max_depth;//需要達到的FA的值  
  29.     double tempLeafFARate;  
  30.   
  31.     forint i = startNumStages; i < numStages; i++ )  
  32.     {  
  33.         cout << endl << "===== TRAINING " << i << "-stage =====" << endl;  
  34.         cout << "<BEGIN" << endl;  
  35.   
  36.         if ( !updateTrainingSet( tempLeafFARate ) )//從正負樣本集合裡挑選出numPos+numNeg個樣本到集合CvCascadeImageReader imgReader中,個人感覺很多時間耗在這裡,因為這裡對每一個樣本要進行predict,就是說利用當前強分類器已構建的弱分類器進行判斷是正樣本還是負樣本,只有被判斷是正樣本的情況才被加到TrainingSet中(第一次的時候當然是預設都是正樣本)。所以如果i比較大的時候,在構建負樣本的TrainingSet就特別費時。並且把積分影象等計算出放到evaluator的img中。  
  37.   
  38.         if( tempLeafFARate <= requiredLeafFARate )//check下是否可以中止訓練  
  39.   
  40.   
  41.         CvCascadeBoost* tempStage = new CvCascadeBoost;  
  42.         tempStage->train( (CvFeatureEvaluator*)featureEvaluator,  
  43.                            curNumSamples, _precalcValBufSize, _precalcIdxBufSize,  
  44.                           *((CvCascadeBoostParams*)stageParams) );//訓練啦啦啦啦啦,訓練一個強分類器  
  45.         stageClassifiers.push_back( tempStage );//訓練完了一個強分類器新增到結果中  
  46.   
  47.         cout << "END>" << endl;  
  48.   
  49.     //儲存params.xml檔案  
  50.   
  51.     //儲存stage0.xml,stage1.xml,stage2.xml。。。檔案  
  52.     }//for i [startNumStages, nStage]  
  53.     save( dirName + CC_CASCADE_FILENAME, baseFormatSave );//儲存cascade.xml檔案  
  54.     return true;  
  55. }  

2.關於特徵,以LBP為例(Haar特徵網上例子比較多了)

2.1 LBP特徵的形狀,位置,數目的計算,都是在CvLBPEvaluator.cpp中

  1. void CvLBPEvaluator::init(const CvFeatureParams *_featureParams, int _maxSampleCount, Size _winSize)  
  2. {  
  3.     CV_Assert( _maxSampleCount > 0);  
  4.     sum.create((int)_maxSampleCount, (_winSize.width + 1) * (_winSize.height + 1), CV_32SC1);//LBP需要用到積分圖,_maxSampleCount是指所有正負樣本的數量和  
  5.     CvFeatureEvaluator::init( _featureParams, _maxSampleCount, _winSize );//呼叫基類的init  
  6. }  


  1. void CvFeatureEvaluator::init(const CvFeatureParams *_featureParams,  
  2.                               int _maxSampleCount, Size _winSize )  
  3. {  
  4.     CV_Assert(_maxSampleCount > 0);  
  5.     featureParams = (CvFeatureParams *)_featureParams;  
  6.     winSize = _winSize;  
  7.     numFeatures = 0;  
  8.     cls.create( (int)_maxSampleCount, 1, CV_32FC1 );//表示樣本的正負情況,所以就一列  
  9.     generateFeatures();//虛擬函式,呼叫子類的實現,這裡是計算feature的位置啊,大小啊等資料,還包括多少個feature等資訊,因為img的大小固定後這些東西就可以計算出來了所以先算出來為好。  
  10. }  

  1. void CvLBPEvaluator::generateFeatures()//把所有可能的LBPfeature的位置大小啥的都計算出來。  
  2. {  
  3.     int offset = winSize.width + 1;  
  4.     forint x = 0; x < winSize.width; x++ )  
  5.         forint y = 0; y < winSize.height; y++ )  
  6.             forint w = 1; w <= winSize.width / 3; w++ )//因為它要用3個block by 3個block的情況,所以w這除以3  
  7.                 forint h = 1; h <= winSize.height / 3; h++ )  
  8.                     if ( (x+3*w <= winSize.width) && (y+3*h <= winSize.height) )  
  9.                         features.push_back( Feature(offset, x, y, w, h ) );  
  10.     numFeatures = (int)features.size();  
  11. }  

  1. /************************************************************************/  
  2. /*  
  3. 個人理解這裡LBP的Feature是指的是liao的文章的Fig.1(b)。是9x9MB-LBP operator  
  4. CvLBPEvaluator::Feature::Feature( int offset, int x, int y, int _blockWidth, int _blockHeight )  
  5. {  
  6.     Rect tr = rect = cvRect(x, y, _blockWidth, _blockHeight);//我奇怪文章裡講到的block是正方形啊,為啥這裡w和h可以不一樣呢?  
  7.     CV_SUM_OFFSETS( p[0], p[1], p[4], p[5], tr, offset )//這裡按照長方形tr來計算p[0],p[1], p[4], p[5]相對於影象左上角的offset,以便以後對每個影象求積分的直接拿這4個數作為下標就很方便的得到積分值。  
  8.     tr.x += 2*rect.width;  
  9.     CV_SUM_OFFSETS( p[2], p[3], p[6], p[7], tr, offset )  
  10.     tr.y +=2*rect.height;  
  11.     CV_SUM_OFFSETS( p[10], p[11], p[14], p[15], tr, offset )  
  12.     tr.x -= 2*rect.width;  
  13.     CV_SUM_OFFSETS( p[8], p[9], p[12], p[13], tr, offset )  
  14. }  



2.2 LBP特徵的值的計算
這部分是跟載入正負樣本資料到訓練集合中結合到一起的,也就是CvCascadeClassifier::updateTrainingSet函式,這個函式還返回一個這次載入的負樣本的FA值,來決定是否退出訓練。


  1. bool CvCascadeClassifier::updateTrainingSet( double& acceptanceRatio)  
  2. {  
  3.     int64 posConsumed = 0, negConsumed = 0;  
  4.     imgReader.restart();  
  5.     int posCount = fillPassedSamples( 0, numPos, true, posConsumed );//這個fillPassSample就是之前說過的對輸入樣本只要被當前的強分類器判斷為正樣本的意思  
  6.     cout << "POS count : consumed   " << posCount << " : " << (int)posConsumed << endl;  
  7.   
  8.     int proNumNeg = cvRound( ( ((double)numNeg) * ((double)posCount) ) / numPos ); // apply only a fraction of negative samples. double is required since overflow is possible  
  9.     int negCount = fillPassedSamples( posCount, proNumNeg, false, negConsumed );//這裡就是之前說的認為耗時的地方。這裡針對負樣本,負樣本的集合一般圖片大小大於正樣本的大小,如果是負樣本的話,函式會去扣一塊跟正樣本大小相同的區域下來送到訓練集合中。  
  10.     curNumSamples = posCount + negCount;  
  11.     acceptanceRatio = negConsumed == 0 ? 0 : ( (double)negCount/(double)(int64)negConsumed );  
  12.     cout << "NEG count : acceptanceRatio    " << negCount << " : " << acceptanceRatio << endl;  
  13.     return true;  
  14. }  



  1. int CvCascadeClassifier::fillPassedSamples( int first, int count, bool isPositive, int64& consumed )  
  2. {  
  3.     int getcount = 0;  
  4.     Mat img(cascadeParams.winSize, CV_8UC1);  
  5.     forint i = first; i < first + count; i++ )  
  6.     {  
  7.         for( ; ; )  
  8.         {  
  9.             bool isGetImg = isPositive ? imgReader.getPos( img ) :  
  10.                                            imgReader.getNeg( img );//讀一個影象資料到img中,個人覺得需要注意的是這裡它不是說每次呼叫getNeg的時候就是讀下一個負樣本影象,在getNeg裡面它會對一個負樣本進行縮放操作來扣下來目標大小的影象。  
  11.             if( !isGetImg )  
  12.                 return getcount;  
  13.             consumed++;  
  14.   
  15.             featureEvaluator->setImage( img, isPositive ? 1 : 0, i );//計算這個img的積分圖等資訊  
  16.             if( predict( i ) == 1.0F )//這個predict應該是利用之前已有的weak 分類器來判斷是否為正樣本,所以對於isPositive為False的情況下,會一直去尋找第i個負樣本作為setImage  
  17.             {  
  18.                 getcount++;  
  19.                 break;  
  20.             }  
  21.         }  
  22.     }  
  23.     return getcount;  
  24. }  



  1. bool getNeg(Mat &_img) { return negReader.get( _img ); }  
  2. bool getPos(Mat &_img) { return posReader.get( _img ); }  


讀一個影象是從CvCascadeImageReader讀的影象,分為讀正樣本和負樣本,正樣本是通過vec檔案讀的,暫且不去管了,反正是固定大小的。負樣本的negReader的get為:

  1. bool CvCascadeImageReader::NegReader::get( Mat& _img )  
  2. {  
  3.     if( img.empty() )  
  4.         if ( !nextImg() )//nextImg根據neg.txt的內容讀取下一個負樣本圖片的內容  
  5.             return false;  
  6.   
  7.     Mat mat( winSize.height, winSize.width, CV_8UC1,  
  8.         (void*)(img.data + point.y * img.step + point.x * img.elemSize()), img.step );//這裡是把mat指向計算的img的data的那個地方,最後一個引數指明src的寬,從而mat是相當於從src上的那個位置扣下來的width x height的大小的圖片。  
  9.     mat.copyTo(_img);  
  10.   
  11.     if( (int)( point.x + (1.0F + stepFactor ) * winSize.width ) < img.cols )  
  12.         point.x += (int)(stepFactor * winSize.width);  
  13.     else  
  14.     {  
  15.         point.x = offset.x;  
  16.         if( (int)( point.y + (1.0F + stepFactor ) * winSize.height ) < img.rows )  
  17.             point.y += (int)(stepFactor * winSize.height);  
  18.         else  
  19.         {  
  20.             point.y = offset.y;  
  21.             scale *= scaleFactor;//這裡把之前縮小後的scale(w_neg/w_pos)乘以sqrt(2)  
  22.             if( scale <= 1.0F )//然後比較下這個scale有沒有達到原始影象(沒縮放的時候)的大小,要是沒有的話就按乘以sqrt(2)的方式放大img  
  23.                 resize( src, img, Size( (int)(scale*src.cols), (int)(scale*src.rows) ) );  
  24.             else  
  25.             {  
  26.                 if ( !nextImg() )//在這把下一個負樣本的image給read進來  
  27.                     return false;  
  28.             }  
  29.         }  
  30.     }  
  31.     return true;  
  32. }  


  1. void CvLBPEvaluator::setImage(const Mat &img, uchar clsLabel, int idx)//計算積分影象  
  2. {  
  3.     CV_DbgAssert( !sum.empty() );  
  4.     CvFeatureEvaluator::setImage( img, clsLabel, idx );  
  5.     Mat innSum(winSize.height + 1, winSize.width + 1, sum.type(), sum.ptr<int>((int)idx));  
  6.     integral( img, innSum );//計算這個讀出來的img的積分影象放到sum的第idx位置中  
  7. }  


3. 訓練

  1. bool CvCascadeBoost::train( const CvFeatureEvaluator* _featureEvaluator,//包含了sum,tilted,特徵的位置等資訊  
  2.                            int _numSamples,  
  3.                            int _precalcValBufSize, int _precalcIdxBufSize,  
  4.                            const CvCascadeBoostParams& _params )  
  5. {  
  6.     CV_Assert( !data );  
  7.     clear();  
  8.     data = new CvCascadeBoostTrainData( _featureEvaluator, _numSamples,  
  9.                                         _precalcValBufSize, _precalcIdxBufSize, _params );  
  10.     CvMemStorage *storage = cvCreateMemStorage();  
  11.     weak = cvCreateSeq( 0, sizeof(CvSeq), sizeof(CvBoostTree*), storage );  
  12.     storage = 0;  
  13.   
  14.     set_params( _params );  
  15.     if ( (_params.boost_type == LOGIT) || (_params.boost_type == GENTLE) )  
  16.         data->do_responses_copy();  
  17.   
  18.     update_weights( 0 );  
  19.   
  20.     cout << "+----+---------+---------+" << endl;  
  21.     cout << "|  N |    HR   |    FA   |" << endl;  
  22.     cout << "+----+---------+---------+" << endl;  
  23.   
  24.     do  
  25.     {  
  26.         CvCascadeBoostTree* tree = new CvCascadeBoostTree;  
  27.         if( !tree->train( data, subsample_mask, this ) )  
  28.         {  
  29.             delete tree;  
  30.             break;  
  31.         }  
  32.         cvSeqPush( weak, &tree );  
  33.         update_weights( tree );  
  34.         trim_weights();  
  35.         if( cvCountNonZero(subsample_mask) == 0 )  
  36.             break;  
  37.     }  
  38.     while( !isErrDesired() && (weak->total < params.weak_count) );  
  39.   
  40.     data->is_classifier = true;  
  41.     data->free_train_data();  
  42.     return true;  
  43. }  






相關文章