原地址: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
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的指针 features,也就是存储了一系列Feature对象:
struct Feature
{
Feature();
float calc( int offset )const;
void updatePtrs( const vector&_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将是计算特征的核心,并且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&objects,vector&rejectLevels,vector&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 );
vectorvecAvgComp;
Seq(_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);
vectorcandidates;//每个尺度下的图像的检测结果装在该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
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;
#ifdefHAVE_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&candidates,vector&levels,vector&weights,booloutputRejectLevels)
{
if( !featureEvaluator->setImage(image,data.origWinSize ) )//setImage函数为特征计算做准备,
return false;
Mat currentMask;
if (!maskGenerator.empty()){
currentMask=maskGenerator->generateMask(image);
}//仍然不解mask的应用,好像没用到?
ConcurrentRectVectorconcurrentCandidates;//在每个平行粒子中访问的检测输出空间
vectorrejectLevels;
vectorlevelWeights;
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
return false;
hist.clear();//hist为存储Mat类型的vector
for( int bin = 0; bin
{
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
{
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 &_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
{
Ptrevaluator=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
{
for(intx = 0;x
{
//依然是尚未搞懂的mask
if( (!mask.empty())&& (mask.at(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(*this, evaluator, weight )this指针是当前CascadeClassifier的指针,evaluator是featureEvaluator的指针,weight为double类型。predictOrderedStump函数如下:
template
inline intpredictOrderedStump(CascadeClassifier&cascade,Ptr &_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
{
CascadeClassifier::Data::Stage&stage =cascadeStages[stageIdx];//遍历每个stage
sum= 0.0;//该stage中的叶节点的和
int ntrees = stage.ntrees;
for( int i = 0; i
{
CascadeClassifier::Data::DTreeNode&node =cascadeNodes[nodeOfs];//获取当前stage的各个node
doublevalue =featureEvaluator(node.featureIdx);//这里node的featureIdx指出要计算的是哪一个特征,也就是xml中的哪一个rect,在生成一个HOGEvaluator时就会在operator中根据传入的featureIdx计算特征值,引用到HOGEvaluator中的calc函数
sum+= cascadeLeaves[ value
}
if( sum
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特征的计算部分,可参见