分类器是如何做检测的?——CascadeClassifier中的detectMultiScale函数解读

CascadeClassifier::detectMultiScaleconst Mat& image, vector<Rect>& objects, double scaleFactor=1.1,int minNeighbors, int flag

这里先将图像变成灰度图,对它应用直方图均衡化,做一些预处理的工作。接下来检测人脸,调用detectMultiScale函数,该函数在输入图像的不同尺度中检测物体。

1. image为输入的灰度图像

2. objects为得到被检测物体的矩形框向量组

3. scaleFactor为每一个图像尺度中的尺度参数,默认值为1.1

4. minNeighbors参数为每一个级联矩形应该保留的邻近个数(没能理解这个参数,-_-|||),默认为3

5. flags对于新的分类器没有用(但目前的haar分类器都是旧版的,CV_HAAR_DO_CANNY_PRUNING利用Canny边缘检测器来排除一些边缘很少或者很多的图像区域,CV_HAAR_SCALE_IMAGE就是按比例正常检测,CV_HAAR_FIND_BIGGEST_OBJECT只检测最大的物,CV_HAAR_DO_ROUGH_SEARCH只做初略检测。


——————————————————       ————————————————


原址: https://i-blog.csdnimg.cn/blog_migrate/908171b45817c288eb1996091860e26e.png

在进入detectMultiScal函数之前,首先需要对CascadeClassifier做初始化。

1.     初始化——read函数

CascadeClassifier的初始化很简单:

cv::CascadeClassifier classifier;

classifier.load(“cascade.xml”); //这里的xml是训练得到的分类器xml

 

CascadeClassifier类中既有load也有read函数,二者是相同的,load将引用read函数。

1.1   xml的结构

训练得到的分类器以xml形式保存,整体上它包括stageType、featureType、height、width、stageParams、featureParams、stages、features几个节点。


图1. 分类器的Xml文件整体结构

除stages和features外,其他主要是一些分类器的参数。

Stages中包含15个stage(训练程序设定),每个stage中包含多个weakClassifiers,而每个weakClassifier中又包含一个internalNodes和一个leafValues。internalNodes中四个变量代表一个node,分别为node中的left/right标记、特征池中的ID和threshold。leafValues中两个变量代表一个node,分别为left leaf的值和right leaf的值。


图2. 分类器的Xml文件具体结构

而features是分类器的特征池,每个特征包含一个矩形和要提取的特征序号(0~35)。


图3. features的具体结构

1.2   read的过程

下面是read代码,主要包括从xml中获取两部分内容:data和featureEvaluator的读取。

bool CascadeClassifier::read(constFileNode&root)

{

    if( !data.read(root) )//data成员变量的读取

        return false;

 

    // load features---特征的读取

    featureEvaluator= FeatureEvaluator::create(data.featureType);

    FileNodefn =root[CC_FEATURES];

    if( fn.empty() )

        return false;

 

    return featureEvaluator->read(fn);

}

1.2.1         data成员变量的读取

data的读取中同样可以分为两部分:分类器参数读取和stage分类树的建立。

首先是参数部分的获取。

static constfloatTHRESHOLD_EPS= 1e-5f;

 

// load stage params

// stageTypeBOOST类型

    string stageTypeStr = (string)root[CC_STAGE_TYPE];

    if( stageTypeStr == CC_BOOST)

        stageType= BOOST;

    else

        return false;

    // 这里以HOG特征分类器为例,featureType=2HOG

    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;

    //检测窗口的最小size,也就是正样本的size

    origWinSize.width = (int)root[CC_WIDTH];

    origWinSize.height = (int)root[CC_HEIGHT];

    CV_Assert(origWinSize.height> 0 &&origWinSize.width > 0 );

    //我训练得到的HOG分类器为true,还不清楚这里的意思

add @ 2015-10-22 :这里的意思是弱分类器是stump类型,stump就是树墩嘛,我理解就是只有一个split 两个叶节点的这么个树,它不是个树,也就是个树墩,外国人起名字还是很到位的。

    isStumpBased= (int)(root[CC_STAGE_PARAMS][CC_MAX_DEPTH])== 1 ?true : false;

 

// load feature params

// 载入特征参数,HOG分类器下包括两个参数:maxCatCountfeatSizefeatSize很透明,就是特征的种类数,这里为36,是指每个block4cell、每个cell9个梯度方向的直方图。例如特征号为3时,计算的是当前窗口中划分为4cell后第一个cell中所有点在120°方向(可能是,这要视起始角度而定)上分量的和,然后经过归一化后的值。对于第二个参数maxCatCount,这里为0,尚不清楚(这是指代表一个弱分类器的树的类别数量,用来计算一棵树的节点大小也就是nodeStep

    FileNode fn = root[CC_FEATURE_PARAMS];

    if( fn.empty() )

        return false;

 

    ncategories= fn[CC_MAX_CAT_COUNT];

    int subsetSize = (ncategories+ 31)/32,

       nodeStep = 3 + ( ncategories>0 ? subsetSize: 1 );

至此分类器参数读取完毕。

 

接下来是建立分类树,也就是stage部分的载入。

// load stages

    fn = root[CC_STAGES];

    if( fn.empty() )

        return false;

 

    stages.reserve(fn.size());//stages包含15个节点,fn.size()==15

    classifiers.clear();

    nodes.clear();

 

    FileNodeIteratorit =fn.begin(),it_end=fn.end();

 

    for( int si = 0; it != it_end; si++, ++it )//遍历stages

    {

        FileNodefns = *it;

        Stagestage;//stage结构中包含thresholdntreesfirst三个变量

        stage.threshold = (float)fns[CC_STAGE_THRESHOLD]-THRESHOLD_EPS;

        fns= fns[CC_WEAK_CLASSIFIERS];

        if(fns.empty())

            returnfalse;

        stage.ntrees = (int)fns.size();

        stage.first = (int)classifiers.size();//ntreesfirst指出该stage中包含的树的数目和起始位置

        stages.push_back(stage);//stage被保存在stagevector(也就是stages)中

        classifiers.reserve(stages[si].first +stages[si].ntrees);//相应地扩展classifiers的空间,它存储的是这些stage中的weak classifiers,也就是weak trees

 

        FileNodeIteratorit1 =fns.begin(),it1_end=fns.end();//遍历weak classifier

        for( ; it1 != it1_end;++it1 )// weaktrees

        {

            FileNodefnw = *it1;

            FileNodeinternalNodes =fnw[CC_INTERNAL_NODES];

            FileNodeleafValues =fnw[CC_LEAF_VALUES];

            if(internalNodes.empty()||leafValues.empty())

                returnfalse;

 

            DTreetree;

            tree.nodeCount = (int)internalNodes.size()/nodeStep;

            classifiers.push_back(tree);//一个弱分类器或者说一个weak tree中只包含一个int变量,用它在classifiers中的位置和自身来指出它所包含的node个数

 

            nodes.reserve(nodes.size() +tree.nodeCount);

            leaves.reserve(leaves.size() +leafValues.size());//扩展存储nodeleavesvector结构空间

            if(subsetSize > 0 )

                subsets.reserve(subsets.size() +tree.nodeCount*subsetSize);

 

            FileNodeIteratorinternalNodesIter =internalNodes.begin(),internalNodesEnd=internalNodes.end();

//遍历nodes

            for(; internalNodesIter != internalNodesEnd; )//nodes

            {

                DTreeNodenode;//一个node中包含leftrightthresholdfeatureIdx四个变量。其中leftright是其对应的代号,left=0right=-1featureIdx指的是整个分类器中使用的特征池中某个特征的ID,比如共有108个特征,那么featureIdx就在0~107之间;threshold是node中split的阈值,用来划分到左右节点的阈值。同时可以看到这里的HOG分类器中每个弱分类器仅包含一个node,也就是仅对某一个特征做判断,而不是多个特征的集合

                node.left = (int)*internalNodesIter; ++internalNodesIter;

                node.right = (int)*internalNodesIter; ++internalNodesIter;

                node.featureIdx = (int)*internalNodesIter; ++internalNodesIter;

                if(subsetSize > 0 )

                {

                    for(intj = 0;j <subsetSize;j++, ++internalNodesIter)

                        subsets.push_back((int)*internalNodesIter);

                    node.threshold = 0.f;

                }

                else

                {

                    node.threshold = (float)*internalNodesIter; ++internalNodesIter;

                }

                nodes.push_back(node);//得到的node将保存在它的vector结构nodes

            }

 

            internalNodesIter=leafValues.begin(),internalNodesEnd =leafValues.end();

 

            for(; internalNodesIter != internalNodesEnd; ++internalNodesIter)// leaves

                leaves.push_back((float)*internalNodesIter);//leaves中保存相应每个nodeleft leafright leaf的值,因为每个weak tree只有一个node也就分别只有一个left leafright leaf,这些将保存在leaves

        }

    }

通过stage树的建立可以看出最终是获取stages、classifiers、nodes和leaves四个vector变量。其中的nodes和leaves共同组成一系列有序节点,而classifiers中的变量则是在这些节点中查询来构成一个由弱分类器组,它仅仅是把这些弱分类器组合在一起,最后stages中每一个stage也就是一个强分类器,它在classifiers中查询得到自己所属的弱分类器都有哪些,从而构成一个强分类器的基础。

1.2.2         featureEvaluator的读取

完成data部分的载入后,接下来就是特征计算器(featureEvaluator)的载入了。上面每一个node中都会计算特征池中的某一个特征,这个特征以featureIdx出现在node中。现在来看看这些featureIdx背后的内容。

首先要创建某种特征类型的特征计算器,这里支持的是Haar、LBP和HOG三种。

featureEvaluator =FeatureEvaluator::create(data.featureType);

create中生成一个HaarEvaluator/LBPEvaluator/HOGEvaluator对象并返回指针而已。那HOGEvaluators中包含什么内容呢?

这里暂不提其他成员,先介绍一个vector<Feature>的指针 features,也就是存储了一系列Feature对象:

struct Feature

    {

        Feature();

        float calc( int offset )const;

        void updatePtrs( const vector<Mat>&_hist,constMat &_normSum);

        bool read( const FileNode&node); 

 

        enum { CELL_NUM = 4, BIN_NUM= 9 };

 

        Rectrect[CELL_NUM];

        int featComponent; //componentindex from 0 to 35

        const float* pF[4]; //for feature calculation

        const float* pN[4]; //for normalization calculation

};

这里的vector<Feature>将是计算特征的核心,并且featureEvaluator的读入部分主要就是对这个vector变量的内容作初始化,因此在此展示一下。

 

 featureEvaluator创建之后在xml中的features节点下开始读入。

bool HOGEvaluator::read( const FileNode& node)

{

    features->resize(node.size());//node.size()为整个分类器中使用到的特征数量,以我训练的HOG分类器为例包含108个特征

    featuresPtr= &(*features)[0];

    FileNodeIteratorit =node.begin(),it_end=node.end();

    for(inti = 0;it !=it_end;++it,i++)

    {

        if(!featuresPtr[i].read(*it))//遍历所有features并读入到featureEvaluatorfeatures

            returnfalse;

    }

    return true;

}

Feature的读入程序:

bool HOGEvaluator::Feature :: read(const FileNode&node )

{

    FileNodernode =node[CC_RECT];//rect节点下包括一个矩形和一个特征类型号featComponent

    FileNodeIteratorit =rnode.begin();

    it>> rect[0].x>> rect[0].y>> rect[0].width>> rect[0].height>> featComponent;//featComponent范围在[0,35]36类特征中的一个

    rect[1].x =rect[0].x +rect[0].width;

    rect[1].y =rect[0].y;

    rect[2].x =rect[0].x;

    rect[2].y =rect[0].y +rect[0].height;

    rect[3].x =rect[0].x +rect[0].width;

    rect[3].y =rect[0].y +rect[0].height;

    rect[1].width =rect[2].width =rect[3].width =rect[0].width;

rect[1].height=rect[2].height=rect[3].height=rect[0].height;

//xml中的rect存储的矩形信息与4个矩形之间的关系如下图4所示

 

    return true;

}


图4. Rect数组与xml中矩形的关系

这样经过特征读取这一步后,获得了一个特征池,池中每一个特征表示在图中某个矩形位置提取ID为0到35的某个特征量。

 

1.3   read的结果

read的结果一是初始化了分类器的特征类型、最小检测窗口size等参数;二是建立级联的分类器树;三是提取了xml中的特征池。

2.     detectMultiscale函数

在load分类器之后,可以调用该函数对一幅图像做多尺度检测。

2.1   函数自身

//输入参数:imageMat类型的图像

           objects—检测得到的矩形

           rejectLevels—如果不符合特征的矩形,返回级联分类器中符合的强分类器数

           levelWeights

          scaleFactor—图像缩放因子

           minNeighbors

          flags

           minObjectSize—最小检测窗口大小

           maxObjectSize—最大检测窗口大小

           outputRejectLevels—是否输出rejectLevelslevelWeights,默认为false

void CascadeClassifier::detectMultiScale(constMat&image,vector<Rect>&objects,vector<int>&rejectLevels,vector<double>&levelWeights,doublescaleFactor,intminNeighbors,intflags,SizeminObjectSize,SizemaxObjectSize,booloutputRejectLevels)

{

    const double GROUP_EPS =0.2;

 

    CV_Assert(scaleFactor > 1 &&image.depth()==CV_8U );//256灰度级且当前缩放因子大于1

 

    if( empty() )//没有载入

        return;

 

    if( isOldFormatCascade() )//这里是指haarTraining得到的分类器或者老版本的OpenCV,我不确定,但是这里可以跳过,因为训练与检测所使用的OpenCV版本是一致的

    {

        MemStoragestorage(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;

    }

 

    objects.clear();

//mask的应用尚不清楚 add @2015-10-22 我看2.4.9的实现里,压根儿就没有用,这儿只是留了空以备以后可以用,不知道3.0有了没有。

当拥有mask时,只在(i,j)|Mask(i,j)==1的位置才会做检测,想想看如果能够提前用一些处理方法获得mask,无疑将极大地提高检测速度。

    if (!maskGenerator.empty()){

        maskGenerator->initializeMask(image);

    }

 

 

    if( maxObjectSize.height== 0 || maxObjectSize.width == 0 )//很明显不能为0

        maxObjectSize= image.size();//默认最大检测size为图像size

 

    Mat grayImage = image;

    if( grayImage.channels()> 1 )//如果是三通道转换为灰度图

    {

        Mat temp;

        cvtColor(grayImage,temp,CV_BGR2GRAY);

        grayImage= temp;

    }

 

    Mat imageBuffer(image.rows + 1,image.cols + 1,CV_8U);

    vector<Rect>candidates;//每个尺度下的图像的检测结果装在该vector

 

    for( double factor = 1;; factor *= scaleFactor)//对每个尺度下图像检测

    {

        SizeoriginalWindowSize =getOriginalWindowSize();//最小检测窗口size

 

        SizewindowSize(cvRound(originalWindowSize.width*factor),cvRound(originalWindowSize.height*factor) );//当前检测窗口size

        SizescaledImageSize(cvRound(grayImage.cols/factor ),cvRound(grayImage.rows/factor ) );//缩放后图像size

        SizeprocessingRectSize(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 );//将灰度图resizescaledImage中,size为当前尺度下的缩放图像

 

        int yStep;//滑动窗口的滑动步长,xy方向上相同

        if( getFeatureType() == cv::FeatureEvaluator::HOG)

        {

            yStep= 4;

        }

        else

        {

            yStep= factor > 2. ? 1 : 2;//当缩放比例比较大时,滑动步长减小

        }

 

        int stripCount, stripSize;

 

    #ifdef HAVE_TBB

        const intPTS_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;//y方向上的滑动距离

    #endif

 

        if( !detectSingleScale(scaledImage,stripCount,processingRectSize,stripSize,yStep,factor,candidates,

            rejectLevels,levelWeights,outputRejectLevels) )//对单尺度图像做检测

            break;

    }

 

 

    objects.resize(candidates.size());

    std::copy(candidates.begin(),candidates.end(),objects.begin());//将每个尺度下的检测结果copy到输出vector

 

    if( outputRejectLevels )//默认为false,不输出rejectLevels

    {

        groupRectangles(objects,rejectLevels,levelWeights,minNeighbors,GROUP_EPS );

    }

    else

    {

        groupRectangles(objects,minNeighbors,GROUP_EPS );//尚未去看 add@2015-10-22 这个地方还是很棒的,很多检测方法的合并里,我觉得这个最鲁棒,都能拿去一试,推荐大家读一下,这里就不再展开了。

    }

}

 

可以看到detectMultiscale只是对detectSingleScale做了一次多尺度的封装。在单一尺度的图像中detectSingleScale是如何检测的呢?

2.2  detectSingleScale函数

//函数参数设置可以参见detectMultiScale函数

bool CascadeClassifier::detectSingleScale(constMat&image,intstripCount,SizeprocessingRectSize,intstripSize,intyStep,doublefactor,vector<Rect>&candidates,vector<int>&levels,vector<double>&weights,booloutputRejectLevels)

{

    if( !featureEvaluator->setImage(image,data.origWinSize ) )//setImage函数为特征计算做准备,

        return false;

 

    Mat currentMask;

    if (!maskGenerator.empty()){

        currentMask=maskGenerator->generateMask(image);

    }//仍然不解mask的应用,好像没用到?add@2015-10-22 嗯哪,没用到

 

    ConcurrentRectVectorconcurrentCandidates;//在每个平行粒子中访问的检测输出空间

    vector<int>rejectLevels;

    vector<double>levelWeights;

    if( outputRejectLevels )//这里选择的默认false,不返回

    {

        parallel_for(BlockedRange(0,stripCount),CascadeClassifierInvoker(*this,processingRectSize,stripSize,yStep,factor,

            concurrentCandidates,rejectLevels,levelWeights,true,currentMask));

        levels.insert(levels.end(),rejectLevels.begin(),rejectLevels.end() );

        weights.insert(weights.end(),levelWeights.begin(),levelWeights.end() );

    }

    else

    {

         parallel_for(BlockedRange(0,stripCount),CascadeClassifierInvoker(*this,processingRectSize,stripSize,yStep,factor,concurrentCandidates,rejectLevels,levelWeights,false,currentMask));//这里是检测过程中的关键,使用parallel_for是为了TBB加速中使用,生成stripCount个平行线程(每个线程生成一个CascadeClassifierInvoker),在每个CascadeClassifierInvoker中对当前图像做一次检测,这是TBB利用多线程做的加速计算

    }

    candidates.insert(candidates.end(),concurrentCandidates.begin(),concurrentCandidates.end() );//将检测结果加入到输出中

 

    return true;

}

2.2.1         featureEvaluators的setImage函数

   此处仍以HOG为例,其他两个特征的计算可能与之有所不同。

bool HOGEvaluator::setImage( const Mat& image,Size winSize)

{

    int rows = image.rows + 1;

    int cols = image.cols + 1;

    origWinSize= winSize;//最小检测窗口size

    if( image.cols <origWinSize.width||image.rows<origWinSize.height)

        return false;

    hist.clear();//hist为存储Mat类型的vector

    for( int bin = 0; bin < Feature::BIN_NUM;bin++)//BIN_NUM=9,梯度方向分为9个,所以统计得到的Mat个数应当为9

    {

        hist.push_back(Mat(rows,cols,CV_32FC1) );

    }

    normSum.create(rows,cols,CV_32FC1);//归一化的norm存储空间

 

    integralHistogram(image,hist,normSum,Feature::BIN_NUM );//计算归一化后的直方图

 

    size_t featIdx, featCount= features->size();

//遍历更新特征池中每个特征的HOG特征计算所需要的矩形四个顶点上对应积分图的指针

    for( featIdx = 0; featIdx< featCount; featIdx++)

    {

        featuresPtr[featIdx].updatePtrs(hist,normSum);

    }

    return true;

}

这里的updatePtrs函数是要根据梯度直方图和归一图来更新每个Feature中保存的四个指针,例如某Feature在xml中的形式为0 0 8 8 13,那么它所在的矩形就是cvRect(0,0,16,16),同时featComponent=13,binIdx=featComponent%9=4,cellIdx=featComponent/9=1.那么这个特征就是要计算矩形(8,0,8,8)中梯度方向160°方向上的分量总和。要计算这个特征我们只需要在hist中的第4个Mat中查找出矩形四个顶点上的值就可以了。而Feature中的四个float型指针正是指向hist中这四个值的指针。UpdatePtrs的作用就是要更新这四个指针。具体程序如下:

inline voidHOGEvaluator::Feature ::updatePtrs(constvector<Mat> &_hist,constMat&_normSum )

{

    int binIdx = featComponent% BIN_NUM;//计算要更新的角度

    int cellIdx = featComponent/ BIN_NUM;//计算要更新的cell是哪一个

    Rect normRect = Rect(rect[0].x,rect[0].y,2*rect[0].width,2*rect[0].height);

 

    const float* featBuf = (constfloat*)_hist[binIdx].data;

    size_t featStep = _hist[0].step /sizeof(featBuf[0]);

 

    const float* normBuf = (constfloat*)_normSum.data;

    size_t normStep = _normSum.step /sizeof(normBuf[0]);

 

    CV_SUM_PTRS(pF[0],pF[1],pF[2],pF[3],featBuf,rect[cellIdx],featStep);//更新四个直方积分图中的指针

    CV_SUM_PTRS(pN[0],pN[1],pN[2],pN[3],normBuf,normRect,normStep );//更新四个归一图中的指针

}

2.2.2         CascadeClassifierInvoker类的实例化

每个线程中会生成该类的一个对象,但是这里没有做TBB加速,因而是单线程。该对象的operator中对当前缩放尺度下的图像以滑窗形式扫描,在每个点上做分类器级联检测;如果有TBB加速,每个对象仅检测一行,通过多行一起扫描来加速。

void operator()(constBlockedRange&range)const

    {

        Ptr<FeatureEvaluator>evaluator=classifier->featureEvaluator->clone();//复制featureEvaluator的指针

 

        SizewinSize(cvRound(classifier->data.origWinSize.width*scalingFactor),cvRound(classifier->data.origWinSize.height*scalingFactor));//当前检测窗口的size,其实这里是通过缩放图像来做的,而不是窗口大小的改变

 

        int y1 = range.begin() *stripSize;//range的变化范围为[0,1)

        int y2 = min(range.end() *stripSize,processingRectSize.height);//y方向上的行数不可能超过滑动距离

        for( int y = y1;y <y2;y +=yStep )//遍历所有行

        {

            for(intx = 0;x <processingRectSize.width;x +=yStep )//遍历一行

            {

//依然是尚未搞懂的mask add @2015-10-22:)

                if( (!mask.empty())&& (mask.at<uchar>(Point(x,y))==0)) {

                    continue;

                }

 

                doublegypWeight;

                intresult =classifier->runAt(evaluator,Point(x,y),gypWeight);//在当前点提取每个stage中的特征并检验是否满足分类器,result是通过的stage个数的相反数,如果全部通过则为1

                if(rejectLevels )//默认为false

                {

                    if(result == 1 )

                        result =  -(int)classifier->data.stages.size();

                    if(classifier->data.stages.size() +result < 4 )

                    {

                        rectangles->push_back(Rect(cvRound(x*scalingFactor),cvRound(y*scalingFactor),winSize.width,winSize.height));

                        rejectLevels->push_back(-result);

                        levelWeights->push_back(gypWeight);

                    }

                }

                elseif(result> 0 )

                    rectangles->push_back(Rect(cvRound(x*scalingFactor),cvRound(y*scalingFactor),winSize.width,winSize.height));

                if(result == 0 )//保存当前的窗口

                    x+= yStep;

            }

          }

  }

这个程序中唯一需要解释的是CascadeClassifier::runAt函数。对于isStumpBased=true的HOG分类器,返回的结果是predictOrderedStump<HOGEvaluator>(*this, evaluator, weight )this指针是当前CascadeClassifier的指针,evaluator是featureEvaluator的指针,weight为double类型。predictOrderedStump函数如下:

 

template<classFEval>

inline intpredictOrderedStump(CascadeClassifier&cascade,Ptr<FeatureEvaluator> &_featureEvaluator,double&sum)

{

    int nodeOfs = 0, leafOfs= 0;//nodeleaf的整体序号

    FEval&featureEvaluator = (FEval&)*_featureEvaluator;

    float* cascadeLeaves = &cascade.data.leaves[0];//定义指向leaves首地址的指针

    CascadeClassifier::Data::DTreeNode*cascadeNodes = &cascade.data.nodes[0];//定义指向nodes首地址的指针

    CascadeClassifier::Data::Stage*cascadeStages = &cascade.data.stages[0];//定义指向stages首地址的指针

 

    int nstages = (int)cascade.data.stages.size();

    for( int stageIdx = 0; stageIdx < nstages;stageIdx++ )

    {

        CascadeClassifier::Data::Stage&stage =cascadeStages[stageIdx];//遍历每个stage

        sum= 0.0;//stage中的叶节点的和

 

        int ntrees = stage.ntrees;

        for( int i = 0; i < ntrees; i++, nodeOfs++,leafOfs+= 2 )

        {

            CascadeClassifier::Data::DTreeNode&node =cascadeNodes[nodeOfs];//获取当前stage的各个node

            doublevalue =featureEvaluator(node.featureIdx);//这里nodefeatureIdx指出要计算的是哪一个特征,也就是xml中的哪一个rect,在生成一个HOGEvaluator时就会在operator中根据传入的featureIdx计算特征值,引用到HOGEvaluator中的calc函数

            sum+= cascadeLeaves[ value< node.threshold? leafOfs : leafOfs+ 1 ];//根据node中的threshold得到左叶子或者右叶子的值,加到该stage中的总和

        }

 

        if( sum < stage.threshold )//如果总和大于stagethreshold则通过,小于则退出,并返回当前stage的相反数

            return-stageIdx;

    }

    return 1;

}

Feature中的calc很简单,因为前面已经更新了四个对应于矩形顶点处积分图的指针已经被更新,归一图中的指针也已经被更新。

这里表达的计算如下图所示:


图5. 积分图计算示意

要计算D中的值,在积分图中四个顶点的指针所指向的内容分别为A,A+B,A+C和A+B+C+D。因此中间两项与其余两项的差就是要求的D区域了。其中的offset变量是根据滑动窗口的位置确定的,代表上图中D矩形的左上顶点在全图中的位置。程序如下:

首先由如下定义

#define CALC_SUM_(p0,p1,p2,p3,offset)

    ((p0)[offset] - (p1)[offset] - (p2)[offset] + (p3)[offset])  

   

#define CALC_SUM(rect,offset)CALC_SUM_((rect)[0], (rect)[1],(rect)[2], (rect)[3],offset)

然后是Feature中的calc函数

inline floatHOGEvaluator::Feature ::calc(intoffset )const

{

    float res = CALC_SUM(pF,offset);

    float normFactor = CALC_SUM(pN,offset);

    res = (res > 0.001f) ? (res/ (normFactor + 0.001f) ) : 0.f;

    return res;

}


编后语: 此处均以HOG特征为例,有关Haar特征和LBP特征的计算部分,可参见

分类器是如何做检测的?——【续】检测中Haar和LBP特征的计算


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值