轉載注明出處即可。
在進入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 );//尚未去看 [email protected] 這個地方還是很棒的,很多檢測方法的合并裡,我覺得這個最魯棒,都能拿去一試,推薦大家讀一下,這裡就不再展開了。
}
}
可以看到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的應用,好像沒用到?[email protected] 嗯哪,沒用到
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特征的計算部分,可參見
分類器是如何做檢測的?——【續】檢測中Haar和LBP特征的計算