1.? 概述
CascadeClassifier為OpenCV中cv namespace下用來做目標檢測的級聯分類器的一個類。該類中封裝的目標檢測機制,簡而言之是滑動窗口機制+級聯分類器的方式。OpenCV的早期版本中僅支持haar特征的目標檢測,分別在2.2和2.4.0(包含)之后開始支持LBP和HOG特征的目標檢測。
2.? 支持的特征
對于Haar、LBP和HOG,CascadeClassifier都有自己想對他們說的話:
1)? Haar:因為之前從OpenCV1.0以來,一直都是只有用haar特征的級聯分類器訓練和檢測(當時的檢測函數稱為cvHaarDetectObjects,訓練得到的也是特征和node放在一起的xml),所以在之后當CascadeClassifier出現并統一三種特征到同一種機制和數據結構下時,沒有放棄原來的C代碼編寫的haar檢測,仍保留了原來的檢測部分。另外,Haar在檢測中無論是特征計算環節還是判斷環節都是三種特征中最簡潔的,但是筆者的經驗中他的訓練環節卻往往是耗時最長的。
2)? LBP:LBP在2.2中作為人臉檢測的一種方法和Haar并列出現,他的單個點的檢測方法(將在下面看到具體討論)是三者中較為復雜的一個,所以當檢測的點數相同時,如果不考慮特征計算時間,僅計算判斷環節,他的時間是最長的。
3)? HOG:在2.4.0中才開始出現在該類中的HOG檢測,其實并不是OpenCV的新生力量,因為在較早的版本中HOG特征已經開始作為單獨的行人檢測模塊出現。比較起來,雖然HOG在行人檢測和這里的檢測中同樣是滑窗機制,但是一個是級聯adaboost,另一個是SVM;而且HOG特征為了加入CascadeClassifier支持的特征行列改變了自身的特征計算方式:不再有相鄰cell之間的影響,并且采用在Haar和LBP上都可行的積分圖計算,放棄了曾經的HOGCache方式,雖然后者的加速性能遠高于前者,而簡單的HOG特征也使得他的分類效果有所下降(如果用SVM分類器對相同樣本產生的兩種HOG特征做分類,沒有了相鄰cell影響的計算方式下的HOG特征不那么容易完成分類)。這些是HOG為了加入CascadeClassifier而做出的犧牲,不過你肯定也想得到OpenCV保留了原有的HOG計算和檢測機制。另外,HOG在特征計算環節是最耗時的,但他的判斷環節和Haar一樣的簡潔。
關于三種特征在三個環節的耗時有下表:
3.? 內部結構
從CascadeClassifier開始,以功能和結構來整體介紹內部的組織和功能劃分。
CascadeClassifer中的數據結構包括Data和FeatureEvaluator兩個主要部分。Data中存儲的是從訓練獲得的xml文件中載入的分類器數據;而FeatureEvaluator中是關于特征的載入、存儲和計算。在此之外還有檢測框架的邏輯部分,是在Data和Featureevaluator之上的一層邏輯。
3.1Data結構
先來看Data的結構,如下圖:
首先,在Data中存儲著一系列的DTreeNode,該結構體中記錄的是一個弱分類器。其中,feature ID表明它計算的是怎樣的一個特征,threshold1是它的閾值,據此判斷某個特征值應當屬于left還是right,后面的left leafvalue和right leaf value是左右葉節點的值。這里需要結合Stage的判斷環節來理解:
假設某個stage(也就是一個強分類器)中包含有num個弱分類器(也就是num個DTreeNode),按照下面的過程計算stage對某個采樣圖像im的結果。
1)? 初始化sum = 0
2)? for i = 1:num
計算
if f > threshold1
? sum = sum + leftVal
else
? sum = sum + rightVal
end
3)if sum > threshold2
? output = 1
else
? output = 0
? ? ? 其中,ID、threshold1、leftVal和rightVal是第i個弱分類器中的變量,featureExtract表示對im提取第ID個特征值,是該強分類器中的閾值,當結果為正時,輸出output=1,否則為0.
另外,可以從上圖看到stage結構中僅僅保存了第一個弱分類器的下標first、弱分類器數量num和自身的閾值threshold2,所有弱分類器或者說所有節點都是連續存儲在一個vector內的。
3.2 FeatureEvaluator
如果Data結構主要是在載入時保存分類器內部的數據,FeatureEvaluator則是負責特征計算環節。這是一個基類,在此之上衍生了HaarEvaluator、LBPEvaluator和HOGEvaluator三種特征各自的特征計算結構。每個Evaluator中都保存了一個vector,這是在read環節中從分類器中載入的特征池(feature pool),前面提到的feature ID對應的就是在這個vector內的下標。三種Evaluator中的Feature定義有所不同,因為計算特征所需的信息不同,具體如下:
Haar——Feature中保存的是3個包含權重的rect,如果要計算下圖的特征,
對應的rect為[(R2,-3),(R1,1)]和[(R1,1),(R2,-1)]。這里的R1對應于上圖中的紅色矩形,R2對應綠色矩形,圓括號內的第二個值為對應的權重。所有Haar特征的描述只需要至多3個加權矩形即可描述,所以HaarEvaluator的Feature中保存的是三個加權矩形;
LBP——Feature中僅保存一個rect,這里需要指出的是,LBP特征計算的不是一個3x3大小的區域中每個點與中心點的大小關系,而是一個3x3個相同大小的矩形區域之間的對比關系,這也是為什么LBP特征計算過程也用到積分圖方法的原因。如下圖所示,
Feature中保存的就是紅色的矩形位置,而我們要先提取上圖中9個矩形內的所有像素點的和,然后比較外圍8個矩形內的值和中間矩形內的值的關系,從而得到LBP特征。
HOG——與LBP中類似,Feature中同樣僅一個rect,HOG特征是在2x2個rect大小的范圍內提取出的,也就是說給出的rect是HOG計算過程中4個block里的左上角的block。
除此之外,Evaluator中還有另外一個很重要的數據結構——數據指針。這個結構在三種Evaluator中同樣不同,但他們所指向的都是積分圖中的一個值。在Haar和LBP中是先計算一個整圖的積分圖,而HOG中則是計算梯度方向和梯度幅值,然后按照梯度方向劃分的區間將梯度幅值圖映射成n個積分圖。每個特征的計算過程中要維護一系列指向積分圖中的指針,通過訪問積分圖快速計算某個矩形內的像素值的和,從而加速特征計算環節。這里暫不詳細展開。
3.3 檢測框架邏輯
這里的檢測框架簡而言之就是一個多尺度縮放+滑動窗口遍歷搜索的框架。在CascadeClassifier中包含detectMultiScale和detectSingleScale成員函數,分別對應多尺度和單尺度檢測,其中多尺度檢測中會調用單尺度的方法。
分類器僅能夠對某一固定size的采樣圖像做判斷,給出當前的采樣圖像是否為真實目標的“非正即負”的結果(size是由訓練數據決定的)。要找到某個圖像中的目標位置,就要以size大小的采樣窗口對圖像逐行逐列地掃描,然后對每個采樣圖像判斷是否為正,將結果以矩形位置保存下來就獲得了目標的位置。但是這僅僅是單尺度檢測,也就是說,一個以40x40大小訓練數據訓練獲得的分類器只能檢測當前圖像里40x40大小的目標,要檢測80x80大小的目標該如何做呢?可以把原圖像縮放到原來的1/2,這樣原圖中80x80大小的目標就變成40x40了,再做一次上面的掃描檢測過程,并且將得到的矩形換算到原圖中對應的位置,從而檢測到了80x80大小的目標。實際上,我們每次對原圖進行固定步長的縮放,形成一個圖像金字塔,對圖像金字塔的每一層都掃描檢測,這就是多尺度檢測的框架。
4.? 模塊功能
CascadeClassifier的使用中只要調用兩個外部接口,一個是read,另一個是detectMultiScale。
4.1? CascadeClassifier::read
4.1.1? 分類器的XML形式
read的過程就是對類的成員變量進行初始化的過程,經過這一步,Data結構按照之前已經討論的邏輯被填充。
先來看一下一個分類器的xml文件是怎樣組織的。
整體上它包括stageType、featureType、height、width、stageParams、featureParams、stages、features幾個節點。
這里的參數內容就不展開了,主要來看一下stage結構和feature在xml里是怎樣保存的,這樣訓練結束后你可以自己打開這個文件看一下就明白訓練了一個什么分類器出來了。
下面是一個stage的內部結構,maxWeakCount是stage包含的弱分類器個數,stageThreshold是該stage的閾值,也就是上面我們提到過的。接下來就是5個弱分類器了,每個弱分類器中包括internalNodes和leafValues兩個節點。前者分別是left和right標記、feature ID和threshold1。
這里可以解釋一下featureID到底是指在哪里的ID了。下圖是分類器中的features節點中保存的該分類器使用到的各種特征值,feature ID就是在這些中的ID,就是在這些之中的順序位置。圖中的特征是一個HOG特征,rect節點中的前四個數字代表我們提到的矩形,而最后的1表示要提取的特征值是block中提取的36維向量中的哪一個。當然,Haar和LBP特征的feature節點與此不同,不過也是類似的結構。
4.1.2? 讀取的過程
清楚了分類器的xml形式之后,就要從文件中讀取內容至cascadeClassifier中了。可以把這部分分為Data的讀取和features的讀取兩部分。
bool CascadeClassifier::read(constFileNode& root)
{
? if( !data.read(root) )//Data的讀取
? ? ? return false;
featureEvaluator = FeatureEvaluator::create(data.featureType);
? FileNode fn= root[CC_FEATURES];
? if( fn.empty() )
? ? ? return false;
? return featureEvaluator->read(fn);//features的讀取
}
4.1.2.1? Data的讀取
先來看看Data的讀取,這里以HOG特征的分類器為例,并且跳過stage的參數讀取部分,直接來看如何在Data中建立stage結構的。
// load stages
? ? fn = root[CC_STAGES];
? ? if( fn.empty() )
? ? ? ? return false;
? ? stages.reserve(fn.size());//先給vector分配空間出來
? ? classifiers.clear();
? ? nodes.clear();
? ? FileNodeIteratorit =fn.begin(),it_end= fn.end();
? ? for( int si = 0; it != it_end; si++, ++it )//遍歷stages
{
? ? //進入單個stage中
? ? ? ? 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;//一個節點包含nodeStep個值,計算得到當前的弱分類器中包含幾個節點,無論在哪種特征的分類器中這個值其實都可以默認為1
? ? ? ? ? ? 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 )//關于subsetSize的內容都是只在LBP分類器中使用
? ? ? ? ? ? ? ? subsets.reserve(subsets.size() +tree.nodeCount*subsetSize);
? ? ? ? ? ? FileNodeIteratorinternalNodesIter =internalNodes.begin(),internalNodesEnd= internalNodes.end();
//開始訪問節點內部
? ? ? ? ? ? for(; internalNodesIter != internalNodesEnd; )//nodes
? ? ? ? ? ? {
? ? ? ? ? ? ? ? DTreeNodenode;//一個node中包含left、right、threshold和featureIdx四個變量。其中left和right是其對應的代號,left=0,right=-1;featureIdx指的是整個分類器中使用的特征池中某個特征的ID,比如共有108個特征,那么featureIdx就在0~107之間;threshold是上面提到的。同時可以看到這里的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中查詢得到自己所屬的弱分類器都有哪些,從而構成一個強分類器的基礎。
4.1.2.2? features的讀取
特征的讀取最終將保留在featureEvaluator中的vector中。所以先來看一下Feature的定義,仍舊以HOG特征為例:
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
};
其中的CELL_NUM和BIN_NUM分別是HOG特征提取的過程中block內cell個數和梯度方向劃分的區間個數。也就是說,在一個block內將提取出CELL_NUM*BIN_NUM維度的HOG特征向量。rect[CELL_NUM]保存的是block的四個矩形位置,featComponent表明該特征是36維HOG特征中的哪一個值。而之后的pF與pN是重點:首先我們假設featComponent=10,那就是說要提取的特征值是該rect描述的block內提取的HOG特征的第10個值,而第一個cell中會產生9個值,那么第10個值就是第二個cell中的第一個值。通過原圖計算梯度和按照區間劃分的梯度積分圖之后,共產生9個積分圖,那么pF應當指向第1個積分圖內rect描述的block內的第二個cell矩形位置的四個點。
? ? ? ? 在featureEvaluator的read中,將對所有features遍歷填充到vector中。
在下面的代碼中只是讀取了參數,并沒有更新pF和pN指針,因為我們還沒有獲得梯度積分圖。
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.2? CascadeClassifier::detectMultiScale
這里的代碼的偽碼可以簡單寫成如下:
vector results;
for( doublefactor = 1; ;factor*= scaleFact;
{
MatscaledImage(scaledImageSize,CV_8U,imageBuffer.data);
? ? resize( grayImage,scaledImage,scaledImageSize,0, 0, CV_INTER_LINEAR );
? ? ? detectSingleScale( scaledImage,results );
}
oupRectangles( results );
簡單來說,多尺度檢測只是尺度縮放形成圖像金字塔然后在每個尺度上檢測之后將結果進行合并的過程。
在detectSingleScale中,使用OpenCV中的并行計算機制,以CascadeClassifierInvoker類對整圖掃描檢測。detectSingleScale的檢測過程仍以偽碼表達如下:
// detectSingleScale
featureEvaluator->setImage(image,data.origWinSize )
//CascadeClassifierInvoker
for( int y = 0; y
for(int x = 0; x
{
doublegypWeight;
int result=classifier->runAt(evaluator,Point(x, y), gypWeight);
results.push_back(R(x,y,W,H,scale));// R(x,y,W,H,scale)表示在scale尺度下檢測到的矩形(x,y,W,H)映射到原圖上時的矩形
}
可以看到上面的代碼中最重要的兩部分分別是setImage和runAt。
4.2.1? setImage
前面提到過,features的read部分僅僅把特征的參數讀取進入vector中,并沒有對指針們初始化,這正是setImage要做的工作。仍以HOG為例,setImage的偽碼如下:
vector hist;
Mat? ? norm;
integralHistogram( image,hist, norm );
for( featIdx= 0;featIdx < featCount;featIdx++ )
{
? ? featuresPtr[featIdx].updatePtrs( hist, norm );
}
integralHistogram的過程如下:首先計算image每個像素點的梯度幅值和梯度方向,梯度方向的區間為0~360°,劃分為9個區間,按照梯度方向所屬區間統計每個區間內image的梯度幅值的積分圖。也就是說,對于hist中的第一個Mat來說,先把所有梯度方向在0~40°之外的像素點的幅值置為0,然后計算梯度幅值圖的積分圖,保存為hist[0];第二個Mat對應40~80°的區間……這樣,得到一個包含9個Mat的hist,而norm則是9個Mat對應像素點的和。
接下來就是要根據hist和norm來更新每個Feature中的指針了,因為我們已經知道自己要計算的是一個在什么位置上的矩形、在那個區間上的特征,所以只要把指針更新到hist中的那個Mat上即可。注意,這里并沒有涉及到滑動窗口機制。
這樣在計算某個HOG特征值時,我們只要計算下面的式子即可:
HOG(i) = (pF[0]+pF[3]-pF[1]-pF[2] )/( pN[0]+pN[3]-pN[1]-pN[2] )
4.2.2 runAt
runAt函數調用了其他方法,但它的偽碼可以如下:
? ? ? ? setWindow( hist, cvPoint(x,y) );
for(intstageIdx= 0; stageIdx
{
stage= cascadeStages[stageIdx];//當前stage
sum= 0.0;
int ntrees = stage.ntrees;
for( int i = 0; i < ntrees; i++, nodeOfs++,leafOfs+= 2 )
{
node= cascadeNodes[nodeOfs];//當前node
doublevalue =featureEvaluator(node.featureIdx);//計算vector中的第featureIdx個特征的值
sum+= cascadeLeaves[ value< node.threshold? leafOfs : leafOfs+ 1 ];//根據node中的threshold得到左葉子或者右葉子的值,加到該stage中的總和
}
if( sum < stage.threshold )//如果總和大于stage的threshold則通過,小于則退出,并返回當前stage的相反數
return-stageIdx;
}
setWindow是根據當前的位置(x,y)計算Feature中的指針應當在積分圖上的偏移量,可以看到這里才是滑動窗口機制實現的真正部分,而不是在setImage中,setImage只是給出各個特征對應的指針相對位置,而不是真實位置。
后面在stage和node中的遍歷和檢測,正是體現弱分類器、強分類器和級聯分類器的概念。當stage中有一個不滿足時,立即退出不再進入下一級,這是級聯分類器的概念;弱分類器的判定僅僅給出一個分數,若干個弱分類器的分數的和作為強分類器的判定依據,這是強弱分類器的概念。
4.3 LBP的判定
這里的HOG的例子,與Haar很相似,只是特征計算環節有所不同,在判定環節都是根據某個閾值來判斷,但是LBP除了在特征計算環節不同以外,在判定環節也大不相同。訓練獲得的LBP分類器的node中包含8個數存儲在subset中,與node的存儲很類似。然后在判定階段按照下式
t = subset[c>>5]& (1 << (c & 31))
其中c是提取得到的LBP特征值。當t為0時,結果為左葉,為1時,結果為右葉。
4.4 groupRectangles
最終的結果由于在多尺度上獲得,因而矩形之間難免有重合、重疊和包含的關系,由于縮放尺度可能相對于目標大小比較小,導致同一個目標在多個尺度上被檢測出來,所以有必要進行合并。OpenCV的合并規則中有按照權重合并的,也有以MeanShift方法合并的,最簡單的一種是直接按照位置和大小關系合并。首先將所有矩形按照大小位置合并成不同的類別,然后將同一類別中的矩形合并成同一個矩形,當不滿足給出的閾值條件時,該矩形不會被保存下來。這一部分不是檢測的核心,不做詳細討論。
5.? 保留的Haar支持
之前已經說過,老版本的OpenCV中只支持Haar特征的分類器訓練和檢測,所以有大量性能表現優秀的老版本Haar分類器已經訓練獲得,如果新版本的CascadeClassifier不能支持這些分類器,那是非常遺憾的事。所以CascadeClassifier中在做新版本分類器載入之后,如果失敗將會按照老版本的分類器再做一次載入,并且保存到odlCascade指針中。在做檢測時,如果oldCascade不為空,則按照老版本的Haar分類器做檢測。這個過程完全是以C風格的代碼完成,是對OpenCV2.2之前版本的繼承。