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

原地址:http://blog.csdn.net/delltdk/article/details/9186875

在进入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

// stageType为BOOST类型

    string stageTypeStr = (string)root[CC_STAGE_TYPE];

    if( stageTypeStr == CC_BOOST)

        stageType= BOOST;

    else

        return false;

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

    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,还不清楚这里的意思

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

 

// load feature params

// 载入特征参数,HOG分类器下包括两个参数:maxCatCount和featSize,featSize很透明,就是特征的种类数,这里为36,是指每个block中4个cell、每个cell9个梯度方向的直方图。例如特征号为3时,计算的是当前窗口中划分为4个cell后第一个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结构中包含threshold、ntrees和first三个变量

        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();//ntrees和first指出该stage中包含的树的数目和起始位置

        stages.push_back(stage);//stage被保存在stage的vector(也就是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());//扩展存储node和leaves的vector结构空间

            if(subsetSize > 0 )

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

 

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

//遍历nodes

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

            {

                DTreeNodenode;//一个node中包含left、right、threshold和featureIdx四个变量。其中left和right是其对应的代号,left=0,right=-1;featureIdx指的是整个分类器中使用的特征池中某个特征的ID,比如共有108个特征,那么featureIdx就在0~107之间;threshold是node的。同时可以看到这里的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中保存相应每个node的left leaf和right leaf的值,因为每个weak tree只有一个node也就分别只有一个left leaf和right 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并读入到featureEvaluator的features中

            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   函数自身

//输入参数:image—Mat类型的图像

           objects—检测得到的矩形

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

           levelWeights—

          scaleFactor—图像缩放因子

           minNeighbors—

          flags—

           minObjectSize—最小检测窗口大小

           maxObjectSize—最大检测窗口大小

           outputRejectLevels—是否输出rejectLevels和levelWeights,默认为false

voidCascadeClassifier::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的应用尚不清楚

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

 

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

        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 );//尚未去看

    }

}

 

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

2.2  detectSingleScale函数

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

boolCascadeClassifier::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的应用,好像没用到?

 

    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

                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;//node和leaf的整体序号

    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);//这里node的featureIdx指出要计算的是哪一个特征,也就是xml中的哪一个rect,在生成一个HOGEvaluator时就会在operator中根据传入的featureIdx计算特征值,引用到HOGEvaluator中的calc函数

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

        }

 

        if( sum < stage.threshold )//如果总和大于stage的threshold则通过,小于则退出,并返回当前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特征的计算

转载于:https://www.cnblogs.com/lanye/p/3599375.html

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/377248.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

<MySQL>何时使用普通索引,何时使用唯一索引

如果能够保证业务代码不会写入重复数据&#xff0c;就可以继续往下看。 如果业务不能保证&#xff0c;那么必须创建唯一索引。 关于查询能力 普通索引和唯一索引在查询能力上是没有很大差别的。 如&#xff1a;select id from T where k5 1、普通索引查找到满足条件的第一个记…

Web版OutLook,利用POP接收邮件服务器邮件

一直想做一个Web版的OutLook&#xff0c;所以才萌生这个想法&#xff0c;其实以前也接触过这方面的东西。于是上网找了找&#xff0c;漫天的都是Jmail来接收&#xff0c;好吧&#xff0c;既然大家都在用我也就下载下来试试了。 什么&#xff0c;怎么总是报错呢&#xff1f;原来…

abs std::abs_ABS的完整形式是什么?

abs std::absABS&#xff1a;防抱死制动系统 (ABS: Anti-lock Braking System) ABS is an abbreviation of the Anti-lock Braking System. It is a safety anti-skid braking system that is used on a variety of aircraft, automobiles and other land vehicles, such as mo…

ubuntu 使用

shell 命令历史搜索 &#xff1a; ctrl r使能 session 选择界面&#xff1a;安装gnome-session-fallback安装lwqq转载于:https://www.cnblogs.com/JonnyLulu/p/3600263.html

汉字速查使用方法简介

《汉字速查》&#xff08;HanziSearcher&#xff09;是一个支持全汉字字典和词典的检索工具。其界面如下所示。 界面上方为工具栏。 左方为字典和词典检索栏。 右方在启动时显示版权信息和作者的联系方式&#xff0c;在执行检索时&#xff0c;显示检索结果。 检索方法 汉字速查…

android jni示例_Android服务示例

android jni示例A service is a component that runs in the background for supporting different types of operations that are long running. The user is not interacted with these. These perform task even if application is destroyed. Examples include handling of…

《MySQL——选错索引,该如何做》

如果不断地删除历史数据和新增数据&#xff0c;MySQL有时会选错索引。 选择索引是优化器的工作&#xff0c;优化器优化时会考虑的因素&#xff1a;扫描行数、是否需要排序、是否使用临时表 MySQL通过统计索引上的基数&#xff0c;作为索引的区分度。 统计方法时采样统计&#x…

LPWSTR 类型的实参与const.char *类型形参不兼容

CString csPlus; CString csSummand; m_PlusNumber.GetWindowTextW(csPlus); m_Summand.GetWindowTextW(csSummand); int nPlus atoi(csPlus.GetBuffer(0)); //将编辑框文本转换成整数// int nPlus atoi(strcpy(csPlus.GetBuffer(10),"aa")); csPlus.ReleaseBu…

空间换时间,把递归的时间复杂度降低到O(2n)

递归算法的时间复杂度除非只有前两项&#xff0c;否则都不是线性的&#xff0c;并且相当耗费内存。我们用最常见的的fibonacci数列来说明&#xff1a; function fibonacci(n){if( n 0 || n 1){return n;} else {return fibonacci(n - 1) fibonacci(n - 2);} } 这是一种最常见…

scala char_Scala中的Char数据类型

scala charScala Char数据类型 (Scala Char Data Type) Character (char) in Scala is a data type that is equivalent to 16-bit unsigned integer. The character data type stores a single character. It can be an alphabet, numbers, symbols, etc. The character takes…

《MySQL——给长字符串加索引》

对于长字符串&#xff0c;可用如下方式建立索引&#xff1a; &#xff08;1&#xff09;前缀索引 &#xff08;2&#xff09;字符串倒叙前缀索引 &#xff08;3&#xff09;添加hash字段并在hash字段上加索引 &#xff08;4&#xff09;字段拆分&#xff08;一个字段可拆分为两…

[蓝桥杯历届试题] 欧拉与鸡蛋

大数学家欧拉在集市上遇到了本村的两个农妇&#xff0c;每人跨着个空篮子。她们和欧拉打招呼说两人刚刚卖完了所有的鸡蛋。 欧拉随便问&#xff1a;“卖了多少鸡蛋呢&#xff1f;” 不料一个说&#xff1a;“我们两人自己卖自己的&#xff0c;一共卖了150个鸡蛋&#xff0c;虽然…

Python元组练习

Here, we are covering following Python tuple exercises, 在这里&#xff0c;我们将介绍以下Python元组练习 &#xff0c; Creating & printing a tuple 创建和打印元组 Unpacking the tuple into strings 将元组解包成字符串 Create a tuple containing the letters of…

傻瓜教你看清MVC内部执行流程之ViewData数据传输,轻松学MVC--①目了然篇(待续)

1.首先在执行到Controller里面的action(方法)时,执行到最后会调用一个View()-->此方法是Controller的一个方法 源代码: View Code protected internal ViewResult View(){return View(null /* viewName */, null /* masterName */, null /* model */);} 2.然后继续调用自己…

《MySQL——count()逻辑》

count()用法 count()语义&#xff1a;该函数为一个聚合函数&#xff0c;对于返回的结果集一行行地判断&#xff0c;如果count函数地参数不是NULL&#xff0c;累计值就加1&#xff0c;否则不加。最后返回累计值。 所以count(*),count(主键id)和count(1)都表示返回满足条件地结果…

phpmailer 发送邮件

<?php /* 可用新浪和网易邮箱测试成功&#xff0c;但QQ不成功&#xff01; 下载 phpmailer 解压 http://phpmailer.worxware.com/要注意邮件服务器的端口号&#xff0c;默认是 25 不用修改&#xff0c;如果不是则要修改如下&#xff0c;在$mail->IsSMTP() ;下一行加上 $…

静态负载均衡和动态负载均衡_动态负载平衡

静态负载均衡和动态负载均衡动态负载平衡 (Dynamic Load Balancing) The algorithm monitors changes on the system workload and redistributes the work accordingly. 该算法监视系统工作负载的变化并相应地重新分配工作。 This algorithm works on three strategies: 该算…

poj 1088

题目&#xff1a;http://poj.org/problem?id1088 记忆化搜索&#xff0c;dp[r][c] max(dp[r - 1][c] , dp[r 1][c] , dp[r][c - 1] , dp[r][c 1]) 1 ( if (题目给的条件满足&#xff09;&#xff09; View Code 1 using namespace std;2 typedef long long ll;3 const in…

《MySQL——order by逻辑(全字段排序与rowid排序)》

创建一个表&#xff0c;然后使用查询语句&#xff1a; 查询城市是“杭州”的所有人名字&#xff0c;并且按照姓名排序返回前 1000 个人的姓名、年龄 create table t (id int(11) not null,city vachar(16) not null,name vachar(16) not null,age vachar(16) not null,addr va…

ruby 生成哈希值_哈希== Ruby中的运算符

ruby 生成哈希值In the last article, we have seen how we can compare two hash objects with the help of < operator? "<" method is a public instance method defined in Rubys library. 在上一篇文章中&#xff0c;我们看到了如何在<运算符的帮助下…