CascadeClassifier::detectMultiScale(const 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
// 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,还不清楚这里的意思
add @ 2015-10-22 :这里的意思是弱分类器是stump类型,stump就是树墩嘛,我理解就是只有一个split 两个叶节点的这么个树,它不是个树,也就是个树墩,外国人起名字还是很到位的。
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中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中保存相应每个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
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 );//将灰度图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 );//尚未去看 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;//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特征的计算部分,可参见
CascadeClassifier::detectMultiScale(const 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
// 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,还不清楚这里的意思
add @ 2015-10-22 :这里的意思是弱分类器是stump类型,stump就是树墩嘛,我理解就是只有一个split 两个叶节点的这么个树,它不是个树,也就是个树墩,外国人起名字还是很到位的。
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中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中保存相应每个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
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 );//将灰度图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 );//尚未去看 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;//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特征的计算部分,可参见