1. 程式人生 > >OpenCV中CascadeClassifier類實現多尺度檢測原始碼解析

OpenCV中CascadeClassifier類實現多尺度檢測原始碼解析

級聯分類器檢測類CascadeClassifier,在2.4.5版本中使用Adaboost的方法+LBP、HOG、HAAR進行目標檢測,載入的是使用traincascade進行訓練的分類器 class CV_EXPORTS_W CascadeClassifier { public:     CV_WRAP CascadeClassifier(); // 無引數建構函式,new自動呼叫該函式分配初試記憶體     CV_WRAP CascadeClassifier( const string& filename ); // 帶引數建構函式,引數為XML的絕對名稱     virtual ~CascadeClassifier(); // 解構函式,無需關心     CV_WRAP virtual bool empty() const; // 是否匯入引數,只建立了該物件而沒有載入或者載入失敗時都是空的     CV_WRAP bool load( const string& filename ); // 載入分類器,引數為XML的絕對名稱,函式內部呼叫read讀取新格式的分類器,讀取成功後直接返回,讀取失敗後呼叫cvLoad讀取舊格式的分類器,讀取成功返回true,否則返回false     virtual bool read( const FileNode& node );   // load內部呼叫read解析XML中的內容,也可以自己建立節點然後呼叫Read即可,但是該函式只能讀取新格式的分類器,不能讀取舊格式的分類器     // 多尺度檢測函式     CV_WRAP virtual void detectMultiScale( const Mat& image,        // 影象,cvarrtoMat實現IplImage轉換為Mat,必須為8位,內部可自行轉換為灰度影象                                    CV_OUT vector<Rect>& objects,    // 輸出矩形,注意vector不是執行緒安全的                                    double scaleFactor=1.1,          // 縮放比例,必須大於1                                    int minNeighbors=3,              // 合併視窗時最小neighbor,每個候選矩陣至少包含的附近元素個數                                    int flags=0,                     // 檢測標記,只對舊格式的分類器有效,與cvHaarDetectObjects的引數flags相同,預設為0,可能的取值為CV_HAAR_DO_CANNY_PRUNING(CANNY邊緣檢測)、CV_HAAR_SCALE_IMAGE(縮放影象)、CV_HAAR_FIND_BIGGEST_OBJECT(尋找最大的目標)、CV_HAAR_DO_ROUGH_SEARCH(做粗略搜尋);如果尋找最大的目標就不能縮放影象,也不能CANNY邊緣檢測                                    Size minSize=Size(),             // 最小檢測目標                                    Size maxSize=Size() );           // 最大檢測目標     // 最好不要在這裡設定最大最小,可能會影響合併的效果,因此可以在檢測完畢後自行判斷結果是否滿足要求     CV_WRAP virtual void detectMultiScale( const Mat& image,                                    CV_OUT vector<Rect>& objects,                                    vector<int>& rejectLevels,                                    vector<double>& levelWeights,                                    double scaleFactor=1.1,                                    int minNeighbors=3, int flags=0,                                    Size minSize=Size(),                                    Size maxSize=Size(),                                    bool outputRejectLevels=false ); // 上述引數多了rejectLevels和levelWeights以及outputRejectLevels引數,只有在
outputRejectLevels為true的時候才可能輸出前兩個引數 // 還有就是在使用舊分類器的時候必須設定flags為CV_HAAR_SCALE_IMAGE,可以通過haarcascade_frontalface_alt.xml檢測人臉嘗試     bool isOldFormatCascade() const;        // 是否是舊格式的分類器     virtual Size getOriginalWindowSize() const;    // 初始檢測視窗大小,也就是訓練的視窗     int getFeatureType() const; // 獲取特徵型別     bool setImage( const Mat& );    // 設定影象,計算影象的積分圖     virtual int runAt( Ptr<FeatureEvaluator>& feval, Point pt, double& weight ); // 計算某檢測視窗是否為目標     // 儲存強分類器資料     class Data     {     public:         struct CV_EXPORTS DTreeNode // 節點         {             int featureIdx; // 對應的特徵編號             float threshold; // for ordered features only 節點閾值             int left; // 左子樹             int right; // 右子樹         };         struct CV_EXPORTS DTree // 弱分類器         {             int nodeCount; // 弱分類器中節點個數         };         struct CV_EXPORTS Stage // 強分類器         {             int first; // 在classifier中的起始位置             int ntrees; // 該強分類器中的弱分類器數             float threshold; // 強分類器閾值         };         bool read(const FileNode &node); // 讀取強分類器         bool isStumpBased;    // 是否只有樹樁         int stageType;      // BOOST,boostType:GAB、RAB等         int featureType;    // HAAR、HOG、LBP         int ncategories;    // maxCatCount,LBP為256,其餘為0         Size origWinSize;         vector<Stage> stages;         vector<DTree> classifiers;         vector<DTreeNode> nodes;         vector<float> leaves;         vector<int> subsets;     };     Data data;     Ptr<FeatureEvaluator> featureEvaluator;     Ptr<CvHaarClassifierCascade> oldCascade; // 關於mask這塊參考《OpenCV目標檢測之MaskGenerator》
public:     class CV_EXPORTS MaskGenerator     {     public:         virtual ~MaskGenerator() {}         virtual cv::Mat generateMask(const cv::Mat& src)=0;         virtual void initializeMask(const cv::Mat& /*src*/) {};     };     void setMaskGenerator(Ptr<MaskGenerator> maskGenerator);     Ptr<MaskGenerator> getMaskGenerator();     void setFaceDetectionMaskGenerator(); protected:     Ptr<MaskGenerator> maskGenerator; } 注意:當在不同的分類器之間切換的時候,需要手動釋放,因為read內部沒有釋放上一次讀取的分類器資料!
關於新舊格式的分類器參考《OpenCV儲存解讀之Adaboost分類器》 使用CascadeClassifier檢測目標的過程 1) load分類器並呼叫empty函式檢測是否load成功 // 讀取stages bool CascadeClassifier::Data::read(const FileNode &root) {     static const float THRESHOLD_EPS = 1e-5f;     // load stage params     string stageTypeStr = (string)root[CC_STAGE_TYPE];     if( stageTypeStr == CC_BOOST )         stageType = BOOST;     else         return false;     printf("stageType: %s\n", stageTypeStr.c_str());     string featureTypeStr = (string)root[CC_FEATURE_TYPE];     if( featureTypeStr == CC_HAAR )         featureType = FeatureEvaluator::HAAR;     else if( featureTypeStr == CC_LBP )         featureType = FeatureEvaluator::LBP;     else if( featureTypeStr == CC_HOG )         featureType = FeatureEvaluator::HOG;     else         return false;     printf("featureType: %s\n", featureTypeStr.c_str());     origWinSize.width = (int)root[CC_WIDTH];     origWinSize.height = (int)root[CC_HEIGHT];     CV_Assert( origWinSize.height > 0 && origWinSize.width > 0 );     isStumpBased = (int)(root[CC_STAGE_PARAMS][CC_MAX_DEPTH]) == 1 ? true : false;     printf("stumpBased: %d\n", isStumpBased);     // load feature params     FileNode fn = root[CC_FEATURE_PARAMS];     if( fn.empty() )         return false;     // LBP的maxCatCount=256,其餘特徵都等於0     ncategories = fn[CC_MAX_CAT_COUNT]; // ncategories=256/0     int subsetSize = (ncategories + 31)/32,// subsetSize=8/0 // 強制型別轉換取整,不是四捨五入         nodeStep = 3 + ( ncategories>0 ? subsetSize : 1 ); //每組數值個數,nodeStep=11/4     printf("subsetSize: %d, nodeStep: %d\n", subsetSize, nodeStep);     // load stages     fn = root[CC_STAGES];     if( fn.empty() )         return false;     stages.reserve(fn.size());     classifiers.clear();     nodes.clear();     FileNodeIterator it = fn.begin(), it_end = fn.end();     for( int si = 0; it != it_end; si++, ++it )     {         FileNode fns = *it;         Stage stage;         stage.threshold = (float)fns[CC_STAGE_THRESHOLD] - THRESHOLD_EPS;         fns = fns[CC_WEAK_CLASSIFIERS];         if(fns.empty())             return false;         stage.ntrees = (int)fns.size();         stage.first = (int)classifiers.size();         printf("stage %d: ntrees: %d, first: %d\n", si, stage.ntrees, stage.first);         stages.push_back(stage);         classifiers.reserve(stages[si].first + stages[si].ntrees);         FileNodeIterator it1 = fns.begin(), it1_end = fns.end();         for( ; it1 != it1_end; ++it1 ) // weak trees         {             FileNode fnw = *it1;             FileNode internalNodes = fnw[CC_INTERNAL_NODES];             FileNode leafValues = fnw[CC_LEAF_VALUES];             if( internalNodes.empty() || leafValues.empty() )                 return false;             // 弱分類器中的節點             DTree tree;             tree.nodeCount = (int)internalNodes.size()/nodeStep;             classifiers.push_back(tree);             nodes.reserve(nodes.size() + tree.nodeCount);             leaves.reserve(leaves.size() + leafValues.size());             if( subsetSize > 0 ) // 針對LBP                 subsets.reserve(subsets.size() + tree.nodeCount*subsetSize);             FileNodeIterator internalNodesIter = internalNodes.begin(), internalNodesEnd = internalNodes.end();             // 儲存每一個node             for( ; internalNodesIter != internalNodesEnd; ) // nodes             {                 DTreeNode node;                 node.left = (int)*internalNodesIter; ++internalNodesIter;                 node.right = (int)*internalNodesIter; ++internalNodesIter;                 node.featureIdx = (int)*internalNodesIter; ++internalNodesIter;                 // 針對LBP,獲取8個數值                 if( subsetSize > 0 )                 {                     for( int j = 0; j < subsetSize; j++, ++internalNodesIter )                         subsets.push_back((int)*internalNodesIter);                     node.threshold = 0.f;                 }                 else                 {                     node.threshold = (float)*internalNodesIter; ++internalNodesIter;                 }                 nodes.push_back(node);             }             // 儲存葉子節點             internalNodesIter = leafValues.begin(), internalNodesEnd = leafValues.end();             for( ; internalNodesIter != internalNodesEnd; ++internalNodesIter ) // leaves                 leaves.push_back((float)*internalNodesIter);         }     }     return true; } // 讀取stages與features bool CascadeClassifier::read(const FileNode& root) {     // load stages     if( !data.read(root) )         return false;     // load features,參考《影象特徵->XXX特徵之OpenCV-估計》     featureEvaluator = FeatureEvaluator::create(data.featureType);     FileNode fn = root[CC_FEATURES];     if( fn.empty() )         return false;     return featureEvaluator->read(fn); } // 外部呼叫的函式 bool CascadeClassifier::load(const string& filename) {     oldCascade.release();     data = Data();     featureEvaluator.release();     // 讀取新格式的分類器     FileStorage fs(filename, FileStorage::READ);     if( !fs.isOpened() )         return false;     if( read(fs.getFirstTopLevelNode()) )         return true;     fs.release();     // 讀取新格式失敗則讀取舊格式的分類器     oldCascade = Ptr<CvHaarClassifierCascade>((CvHaarClassifierCascade*)cvLoad(filename.c_str(), 0, 0, 0));     return !oldCascade.empty(); } 2) 呼叫detectMultiScale函式進行多尺度檢測,該函式可以使用老分類器進行檢測也可以使用新分類器進行檢測 2.1 如果load的為舊格式的分類器則使用cvHaarDetectObjectsForROC進行檢測,flags引數只對舊格式的分類器有效,參考《OpenCV函式解讀之cvHaarDetectObjects》     if( isOldFormatCascade() )     {         MemStorage storage(cvCreateMemStorage(0));         CvMat _image = image;         CvSeq* _objects = cvHaarDetectObjectsForROC( &_image, oldCascade, storage, rejectLevels, levelWeights, scaleFactor,                                               minNeighbors, flags, minObjectSize, maxObjectSize, outputRejectLevels );         vector<CvAvgComp> vecAvgComp;         Seq<CvAvgComp>(_objects).copyTo(vecAvgComp);         objects.resize(vecAvgComp.size());         std::transform(vecAvgComp.begin(), vecAvgComp.end(), objects.begin(), getRect());         return;     } 2.2 新格式分類器多尺度檢測      for( double factor = 1; ; factor *= scaleFactor )      {         Size originalWindowSize = getOriginalWindowSize();         Size windowSize( cvRound(originalWindowSize.width*factor), cvRound(originalWindowSize.height*factor) );         Size scaledImageSize( cvRound( grayImage.cols/factor ), cvRound( grayImage.rows/factor ) );         Size processingRectSize( scaledImageSize.width-originalWindowSize.width + 1, scaledImageSize.height-originalWindowSize.height + 1 );         if( processingRectSize.width <= 0 || processingRectSize.height <= 0 )             break;         if( windowSize.width > maxObjectSize.width || windowSize.height > maxObjectSize.height )             break;         if( windowSize.width < minObjectSize.width || windowSize.height < minObjectSize.height )             continue;         // 縮放影象         Mat scaledImage( scaledImageSize, CV_8U, imageBuffer.data );         resize( grayImage, scaledImage, scaledImageSize, 0, 0, CV_INTER_LINEAR );         // 計算步長         int yStep;         if( getFeatureType() == cv::FeatureEvaluator::HOG )         {             yStep = 4;         }         else         {             yStep = factor > 2. ? 1 : 2;         }         // 並行個數以及大小,按照列進行並行處理         int stripCount, stripSize;         // 是否採用TBB進行優化     #ifdef HAVE_TBB         const int PTS_PER_THREAD = 1000;         stripCount = ((processingRectSize.width/yStep)*(processingRectSize.height + yStep-1)/yStep + PTS_PER_THREAD/2)/PTS_PER_THREAD;         stripCount = std::min(std::max(stripCount, 1), 100);         stripSize = (((processingRectSize.height + stripCount - 1)/stripCount + yStep-1)/yStep)*yStep;     #else         stripCount = 1;         stripSize = processingRectSize.height;     #endif         // 呼叫單尺度檢測函式進行檢測         if( !detectSingleScale( scaledImage, stripCount, processingRectSize, stripSize, yStep, factor, candidates,             rejectLevels, levelWeights, outputRejectLevels ) )             break;     } 2.3 合併檢測結果     objects.resize(candidates.size());     std::copy(candidates.begin(), candidates.end(), objects.begin());     if( outputRejectLevels )     {         groupRectangles( objects, rejectLevels, levelWeights, minNeighbors, GROUP_EPS );     }     else     {         groupRectangles( objects, minNeighbors, GROUP_EPS );     } 單尺度檢測函式流程 2.2.1 根據所載入的特徵計算積分圖、積分直方圖等     // 計算當前影象的積分圖,參考《影象特徵->XXX特徵之OpenCV-估計》     if( !featureEvaluator->setImage( image, data.origWinSize ) )         return false; 2.2.2 根據是否輸出檢測級數並行目標檢測     vector<Rect> candidatesVector;     vector<int> rejectLevels;     vector<double> levelWeights;     Mutex mtx;     if( outputRejectLevels )     {         parallel_for_(Range(0, stripCount), CascadeClassifierInvoker( *this, processingRectSize, stripSize, yStep, factor,             candidatesVector, rejectLevels, levelWeights, true, currentMask, &mtx));         levels.insert( levels.end(), rejectLevels.begin(), rejectLevels.end() );         weights.insert( weights.end(), levelWeights.begin(), levelWeights.end() );     }     else     {          parallel_for_(Range(0, stripCount), CascadeClassifierInvoker( *this, processingRectSize, stripSize, yStep, factor,             candidatesVector, rejectLevels, levelWeights, false, currentMask, &mtx));     }     candidates.insert( candidates.end(), candidatesVector.begin(), candidatesVector.end() ); CascadeClassifierInvoker函式的operator()實現具體的檢測過程     // 對於沒有並行時range.start=0,range.end=1     void operator()(const Range& range) const     {         Ptr<FeatureEvaluator> evaluator = classifier->featureEvaluator->clone();         Size winSize(cvRound(classifier->data.origWinSize.width * scalingFactor),                       cvRound(classifier->data.origWinSize.height * scalingFactor));         // strip=processingRectSize.height         int y1 = range.start * stripSize; // 0         int y2 = min(range.end * stripSize, processingRectSize.height); // processSizeRect.height也就是可以處理的高度,已經減去視窗高度         for( int y = y1; y < y2; y += yStep )         {             for( int x = 0; x < processingRectSize.width; x += yStep )             {                 if ( (!mask.empty()) && (mask.at<uchar>(Point(x,y))==0)) {                     continue;                 }                 // result=1表示通過了所有的分類器 <=0表示失敗的級數                 // gypWeight表示返回的閾值                 double gypWeight;                 int result = classifier->runAt(evaluator, Point(x, y), gypWeight); // 輸出LOG #if defined (LOG_CASCADE_STATISTIC)                 logger.setPoint(Point(x, y), result); #endif                  // 當返回級數的時候可以最後三個分類器不通過                 if( rejectLevels )                 {                     if( result == 1 )                         result = -(int)classifier->data.stages.size();                     // 可以最後三個分類器不通過                     if( classifier->data.stages.size() + result < 4 )                     {                         mtx->lock();                         rectangles->push_back(Rect(cvRound(x*scalingFactor), cvRound(y*scalingFactor), winSize.width, winSize.height));                         rejectLevels->push_back(-result);                         levelWeights->push_back(gypWeight);                         mtx->unlock();                     }                 }                 // 不返回級數的時候通過所有的分類器才儲存起來                 else if( result > 0 )                 {                     mtx->lock();                     rectangles->push_back(Rect(cvRound(x*scalingFactor), cvRound(y*scalingFactor),                                                winSize.width, winSize.height));                     mtx->unlock();                 }                 // 如果一級都沒有通過那麼加大搜索步長                 if( result == 0 )                     x += yStep;             }         }     } runAt函式實現某一檢測視窗的檢測 int CascadeClassifier::runAt( Ptr<FeatureEvaluator>& evaluator, Point pt, double& weight ) {     CV_Assert( oldCascade.empty() );     assert( data.featureType == FeatureEvaluator::HAAR ||             data.featureType == FeatureEvaluator::LBP ||             data.featureType == FeatureEvaluator::HOG );     // 設定某一點處的特徵,參考《影象特徵->XXX特徵之OpenCV-估計》     if( !evaluator->setWindow(pt) )         return -1;     // 如果為樹樁,沒有樹枝     if( data.isStumpBased )     {         if( data.featureType == FeatureEvaluator::HAAR )             return predictOrderedStump<HaarEvaluator>( *this, evaluator, weight );         else if( data.featureType == FeatureEvaluator::LBP )             return predictCategoricalStump<LBPEvaluator>( *this, evaluator, weight );         else if( data.featureType == FeatureEvaluator::HOG )             return predictOrderedStump<HOGEvaluator>( *this, evaluator, weight );         else             return -2;     }     // 每個弱分類器不止一個node     else     {         if( data.featureType == FeatureEvaluator::HAAR )             return predictOrdered<HaarEvaluator>( *this, evaluator, weight );         else if( data.featureType == FeatureEvaluator::LBP )             return predictCategorical<LBPEvaluator>( *this, evaluator, weight );         else if( data.featureType == FeatureEvaluator::HOG )             return predictOrdered<HOGEvaluator>( *this, evaluator, weight );         else             return -2;     } } predictOrdered*函式實現判斷當前檢測視窗的判斷 // HAAR與HOG特徵的多node檢測 template<class FEval> inline int predictOrdered( CascadeClassifier& cascade, Ptr<FeatureEvaluator> &_featureEvaluator, double& sum ) {     int nstages = (int)cascade.data.stages.size();     int nodeOfs = 0, leafOfs = 0;     FEval& featureEvaluator = (FEval&)*_featureEvaluator;     float* cascadeLeaves = &cascade.data.leaves[0];     CascadeClassifier::Data::DTreeNode* cascadeNodes = &cascade.data.nodes[0];     CascadeClassifier::Data::DTree* cascadeWeaks = &cascade.data.classifiers[0];     CascadeClassifier::Data::Stage* cascadeStages = &cascade.data.stages[0];     // 遍歷每個強分類器     for( int si = 0; si < nstages; si++ )     {         CascadeClassifier::Data::Stage& stage = cascadeStages[si];         int wi, ntrees = stage.ntrees;         sum = 0;         // 遍歷每個弱分類器         for( wi = 0; wi < ntrees; wi++ )         {             CascadeClassifier::Data::DTree& weak = cascadeWeaks[stage.first + wi];             int idx = 0, root = nodeOfs;             // 遍歷每個節點             do             {                 // 選擇一個node:root和idx初始化為0,即第一個node                 CascadeClassifier::Data::DTreeNode& node = cascadeNodes[root + idx];                 // 計算當前node特徵池編號下的特徵值                 double val = featureEvaluator(node.featureIdx);                 // 如果val小於node閾值則選擇左子樹,否則選擇右子樹                 idx = val < node.threshold ? node.left : node.right;             } while( idx > 0 );             // 累加最終的葉子節點             sum += cascadeLeaves[leafOfs - idx];             nodeOfs += weak.nodeCount;             leafOfs += weak.nodeCount + 1;         }         // 判斷所有葉子節點累加和是否小於強分類器閾值,小於強分類器閾值則失敗         if( sum < stage.threshold )             return -si;     }     // 通過了所有的強分類器返回1,否則返回失敗的分類器     return 1; } // LBP特徵的多node檢測 template<class FEval> inline int predictCategorical( CascadeClassifier& cascade, Ptr<FeatureEvaluator> &_featureEvaluator, double& sum ) {     int nstages = (int)cascade.data.stages.size();     int nodeOfs = 0, leafOfs = 0;     FEval& featureEvaluator = (FEval&)*_featureEvaluator;     size_t subsetSize = (cascade.data.ncategories + 31)/32;     int* cascadeSubsets = &cascade.data.subsets[0];     float* cascadeLeaves = &cascade.data.leaves[0];     CascadeClassifier::Data::DTreeNode* cascadeNodes = &cascade.data.nodes[0];     CascadeClassifier::Data::DTree* cascadeWeaks = &cascade.data.classifiers[0];     CascadeClassifier::Data::Stage* cascadeStages = &cascade.data.stages[0];     for(int si = 0; si < nstages; si++ )     {         CascadeClassifier::Data::Stage& stage = cascadeStages[si];         int wi, ntrees = stage.ntrees;         sum = 0;         for( wi = 0; wi < ntrees; wi++ )         {             CascadeClassifier::Data::DTree& weak = cascadeWeaks[stage.first + wi];             int idx = 0, root = nodeOfs;             do             {                 CascadeClassifier::Data::DTreeNode& node = cascadeNodes[root + idx];                 // c為0-255之間的數                 int c = featureEvaluator(node.featureIdx);                 // 獲取當前node的subset頭位置                 const int* subset = &cascadeSubsets[(root + idx)*subsetSize]; // LBP:subsetSize=8                 // 判斷選擇左子樹還是右子樹                 idx = (subset[c>>5] & (1 << (c & 31))) ? node.left : node.right;                 // c>>5表示將c右移5位,選擇高3位,0-7之間                 // c&31表示低5位,1<<(c&31)選擇低5位後左移1位                 // 將上面的數按位與,如果最後結果不為0表示選擇左子樹,否則選擇右子樹             }             while( idx > 0 );             sum += cascadeLeaves[leafOfs - idx];             nodeOfs += weak.nodeCount;             leafOfs += weak.nodeCount + 1;         }         if( sum < stage.threshold )             return -si;     }     return 1; } // HAAR與HOG特徵的單node檢測 template<class FEval> inline int predictOrderedStump( CascadeClassifier& cascade, Ptr<FeatureEvaluator> &_featureEvaluator, double& sum ) {     int nodeOfs = 0, leafOfs = 0;     FEval& featureEvaluator = (FEval&)*_featureEvaluator;     float* cascadeLeaves = &cascade.data.leaves[0];     CascadeClassifier::Data::DTreeNode* cascadeNodes = &cascade.data.nodes[0];     CascadeClassifier::Data::Stage* cascadeStages = &cascade.data.stages[0];     int nstages = (int)cascade.data.stages.size();     // 遍歷每個強分類器     for( int stageIdx = 0; stageIdx < nstages; stageIdx++ )     {         CascadeCl