OpenCV中CascadeClassifier類實現多尺度檢測原始碼解析
阿新 • • 發佈:2019-01-05
級聯分類器檢測類CascadeClassifier,在2.4.5版本中使用Adaboost的方法+LBP、HOG、HAAR進行目標檢測,載入的是使用traincascade進行訓練的分類器
class CV_EXPORTS_W CascadeClassifier
{
public:
CV_WRAP CascadeClassifier(); // 無引數建構函式,new自動呼叫該函式分配初試記憶體
CV_WRAP CascadeClassifier( const string& filename ); // 帶引數建構函式,引數為XML的絕對名稱
virtual ~CascadeClassifier(); // 解構函式,無需關心
CV_WRAP virtual bool empty() const; // 是否匯入引數,只建立了該物件而沒有載入或者載入失敗時都是空的
CV_WRAP bool load( const string& filename ); // 載入分類器,引數為XML的絕對名稱,函式內部呼叫read讀取新格式的分類器,讀取成功後直接返回,讀取失敗後呼叫cvLoad讀取舊格式的分類器,讀取成功返回true,否則返回false
virtual bool read( const FileNode& node ); // load內部呼叫read解析XML中的內容,也可以自己建立節點然後呼叫Read即可,但是該函式只能讀取新格式的分類器,不能讀取舊格式的分類器
// 多尺度檢測函式
CV_WRAP virtual void detectMultiScale( const Mat& image, // 影象,cvarrtoMat實現IplImage轉換為Mat,必須為8位,內部可自行轉換為灰度影象
CV_OUT vector<Rect>& objects, // 輸出矩形,注意vector不是執行緒安全的
double scaleFactor=1.1, // 縮放比例,必須大於1
int minNeighbors=3, // 合併視窗時最小neighbor,每個候選矩陣至少包含的附近元素個數
int flags=0, // 檢測標記,只對舊格式的分類器有效,與cvHaarDetectObjects的引數flags相同,預設為0,可能的取值為CV_HAAR_DO_CANNY_PRUNING(CANNY邊緣檢測)、CV_HAAR_SCALE_IMAGE(縮放影象)、CV_HAAR_FIND_BIGGEST_OBJECT(尋找最大的目標)、CV_HAAR_DO_ROUGH_SEARCH(做粗略搜尋);如果尋找最大的目標就不能縮放影象,也不能CANNY邊緣檢測
Size minSize=Size(), // 最小檢測目標
Size maxSize=Size() ); // 最大檢測目標
// 最好不要在這裡設定最大最小,可能會影響合併的效果,因此可以在檢測完畢後自行判斷結果是否滿足要求
CV_WRAP virtual void detectMultiScale( const Mat& image,
CV_OUT vector<Rect>& objects,
vector<int>& rejectLevels,
vector<double>& levelWeights,
double scaleFactor=1.1,
int minNeighbors=3, int flags=0,
Size minSize=Size(),
Size maxSize=Size(),
bool outputRejectLevels=false );
// 上述引數多了rejectLevels和levelWeights以及outputRejectLevels引數,只有在 outputRejectLevels為true的時候才可能輸出前兩個引數
// 還有就是在使用舊分類器的時候必須設定flags為CV_HAAR_SCALE_IMAGE,可以通過haarcascade_frontalface_alt.xml檢測人臉嘗試
bool isOldFormatCascade() const; // 是否是舊格式的分類器
virtual Size getOriginalWindowSize() const; // 初始檢測視窗大小,也就是訓練的視窗
int getFeatureType() const; // 獲取特徵型別
bool setImage( const Mat& ); // 設定影象,計算影象的積分圖
virtual int runAt( Ptr<FeatureEvaluator>& feval, Point pt, double& weight ); // 計算某檢測視窗是否為目標
// 儲存強分類器資料
class Data
{
public:
struct CV_EXPORTS DTreeNode // 節點
{
int featureIdx; // 對應的特徵編號
float threshold; // for ordered features only 節點閾值
int left; // 左子樹
int right; // 右子樹
};
struct CV_EXPORTS DTree // 弱分類器
{
int nodeCount; // 弱分類器中節點個數
};
struct CV_EXPORTS Stage // 強分類器
{
int first; // 在classifier中的起始位置
int ntrees; // 該強分類器中的弱分類器數
float threshold; // 強分類器閾值
};
bool read(const FileNode &node); // 讀取強分類器
bool isStumpBased; // 是否只有樹樁
int stageType; // BOOST,boostType:GAB、RAB等
int featureType; // HAAR、HOG、LBP
int ncategories; // maxCatCount,LBP為256,其餘為0
Size origWinSize;
vector<Stage> stages;
vector<DTree> classifiers;
vector<DTreeNode> nodes;
vector<float> leaves;
vector<int> subsets;
};
Data data;
Ptr<FeatureEvaluator> featureEvaluator;
Ptr<CvHaarClassifierCascade> oldCascade;
// 關於mask這塊參考《OpenCV目標檢測之MaskGenerator》
public:
class CV_EXPORTS MaskGenerator
{
public:
virtual ~MaskGenerator() {}
virtual cv::Mat generateMask(const cv::Mat& src)=0;
virtual void initializeMask(const cv::Mat& /*src*/) {};
};
void setMaskGenerator(Ptr<MaskGenerator> maskGenerator);
Ptr<MaskGenerator> getMaskGenerator();
void setFaceDetectionMaskGenerator();
protected:
Ptr<MaskGenerator> maskGenerator;
}
注意:當在不同的分類器之間切換的時候,需要手動釋放,因為read內部沒有釋放上一次讀取的分類器資料!
關於新舊格式的分類器參考《OpenCV儲存解讀之Adaboost分類器》
使用CascadeClassifier檢測目標的過程
1) load分類器並呼叫empty函式檢測是否load成功
// 讀取stages
bool CascadeClassifier::Data::read(const FileNode &root)
{
static const float THRESHOLD_EPS = 1e-5f;
// load stage params
string stageTypeStr = (string)root[CC_STAGE_TYPE];
if( stageTypeStr == CC_BOOST )
stageType = BOOST;
else
return false;
printf("stageType: %s\n", stageTypeStr.c_str());
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;
printf("featureType: %s\n", featureTypeStr.c_str());
origWinSize.width = (int)root[CC_WIDTH];
origWinSize.height = (int)root[CC_HEIGHT];
CV_Assert( origWinSize.height > 0 && origWinSize.width > 0 );
isStumpBased = (int)(root[CC_STAGE_PARAMS][CC_MAX_DEPTH]) == 1 ? true : false;
printf("stumpBased: %d\n", isStumpBased);
// load feature params
FileNode fn = root[CC_FEATURE_PARAMS];
if( fn.empty() )
return false;
// LBP的maxCatCount=256,其餘特徵都等於0
ncategories = fn[CC_MAX_CAT_COUNT]; // ncategories=256/0
int subsetSize = (ncategories + 31)/32,// subsetSize=8/0 // 強制型別轉換取整,不是四捨五入
nodeStep = 3 + ( ncategories>0 ? subsetSize : 1 ); //每組數值個數,nodeStep=11/4
printf("subsetSize: %d, nodeStep: %d\n", subsetSize, nodeStep);
// load stages
fn = root[CC_STAGES];
if( fn.empty() )
return false;
stages.reserve(fn.size());
classifiers.clear();
nodes.clear();
FileNodeIterator it = fn.begin(), it_end = fn.end();
for( int si = 0; it != it_end; si++, ++it )
{
FileNode fns = *it;
Stage stage;
stage.threshold = (float)fns[CC_STAGE_THRESHOLD] - THRESHOLD_EPS;
fns = fns[CC_WEAK_CLASSIFIERS];
if(fns.empty())
return false;
stage.ntrees = (int)fns.size();
stage.first = (int)classifiers.size();
printf("stage %d: ntrees: %d, first: %d\n", si, stage.ntrees, stage.first);
stages.push_back(stage);
classifiers.reserve(stages[si].first + stages[si].ntrees);
FileNodeIterator it1 = fns.begin(), it1_end = fns.end();
for( ; it1 != it1_end; ++it1 ) // weak trees
{
FileNode fnw = *it1;
FileNode internalNodes = fnw[CC_INTERNAL_NODES];
FileNode leafValues = fnw[CC_LEAF_VALUES];
if( internalNodes.empty() || leafValues.empty() )
return false;
// 弱分類器中的節點
DTree tree;
tree.nodeCount = (int)internalNodes.size()/nodeStep;
classifiers.push_back(tree);
nodes.reserve(nodes.size() + tree.nodeCount);
leaves.reserve(leaves.size() + leafValues.size());
if( subsetSize > 0 ) // 針對LBP
subsets.reserve(subsets.size() + tree.nodeCount*subsetSize);
FileNodeIterator internalNodesIter = internalNodes.begin(), internalNodesEnd = internalNodes.end();
// 儲存每一個node
for( ; internalNodesIter != internalNodesEnd; ) // nodes
{
DTreeNode node;
node.left = (int)*internalNodesIter; ++internalNodesIter;
node.right = (int)*internalNodesIter; ++internalNodesIter;
node.featureIdx = (int)*internalNodesIter; ++internalNodesIter;
// 針對LBP,獲取8個數值
if( subsetSize > 0 )
{
for( int j = 0; j < subsetSize; j++, ++internalNodesIter )
subsets.push_back((int)*internalNodesIter);
node.threshold = 0.f;
}
else
{
node.threshold = (float)*internalNodesIter; ++internalNodesIter;
}
nodes.push_back(node);
}
// 儲存葉子節點
internalNodesIter = leafValues.begin(), internalNodesEnd = leafValues.end();
for( ; internalNodesIter != internalNodesEnd; ++internalNodesIter ) // leaves
leaves.push_back((float)*internalNodesIter);
}
}
return true;
}
// 讀取stages與features
bool CascadeClassifier::read(const FileNode& root)
{
// load stages
if( !data.read(root) )
return false;
// load features,參考《影象特徵->XXX特徵之OpenCV-估計》
featureEvaluator = FeatureEvaluator::create(data.featureType);
FileNode fn = root[CC_FEATURES];
if( fn.empty() )
return false;
return featureEvaluator->read(fn);
}
// 外部呼叫的函式
bool CascadeClassifier::load(const string& filename)
{
oldCascade.release();
data = Data();
featureEvaluator.release();
// 讀取新格式的分類器
FileStorage fs(filename, FileStorage::READ);
if( !fs.isOpened() )
return false;
if( read(fs.getFirstTopLevelNode()) )
return true;
fs.release();
// 讀取新格式失敗則讀取舊格式的分類器
oldCascade = Ptr<CvHaarClassifierCascade>((CvHaarClassifierCascade*)cvLoad(filename.c_str(), 0, 0, 0));
return !oldCascade.empty();
}
2) 呼叫detectMultiScale函式進行多尺度檢測,該函式可以使用老分類器進行檢測也可以使用新分類器進行檢測
2.1 如果load的為舊格式的分類器則使用cvHaarDetectObjectsForROC進行檢測,flags引數只對舊格式的分類器有效,參考《OpenCV函式解讀之cvHaarDetectObjects》
if( isOldFormatCascade() )
{
MemStorage storage(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;
}
2.2 新格式分類器多尺度檢測
for( double factor = 1; ; factor *= scaleFactor )
{
Size originalWindowSize = getOriginalWindowSize();
Size windowSize( cvRound(originalWindowSize.width*factor), cvRound(originalWindowSize.height*factor) );
Size scaledImageSize( cvRound( grayImage.cols/factor ), cvRound( grayImage.rows/factor ) );
Size processingRectSize( 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 );
// 計算步長
int yStep;
if( getFeatureType() == cv::FeatureEvaluator::HOG )
{
yStep = 4;
}
else
{
yStep = factor > 2. ? 1 : 2;
}
// 並行個數以及大小,按照列進行並行處理
int stripCount, stripSize;
// 是否採用TBB進行優化
#ifdef HAVE_TBB
const int PTS_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;
#endif
// 呼叫單尺度檢測函式進行檢測
if( !detectSingleScale( scaledImage, stripCount, processingRectSize, stripSize, yStep, factor, candidates,
rejectLevels, levelWeights, outputRejectLevels ) )
break;
}
2.3 合併檢測結果
objects.resize(candidates.size());
std::copy(candidates.begin(), candidates.end(), objects.begin());
if( outputRejectLevels )
{
groupRectangles( objects, rejectLevels, levelWeights, minNeighbors, GROUP_EPS );
}
else
{
groupRectangles( objects, minNeighbors, GROUP_EPS );
}
單尺度檢測函式流程
2.2.1 根據所載入的特徵計算積分圖、積分直方圖等
// 計算當前影象的積分圖,參考《影象特徵->XXX特徵之OpenCV-估計》
if( !featureEvaluator->setImage( image, data.origWinSize ) )
return false;
2.2.2 根據是否輸出檢測級數並行目標檢測
vector<Rect> candidatesVector;
vector<int> rejectLevels;
vector<double> levelWeights;
Mutex mtx;
if( outputRejectLevels )
{
parallel_for_(Range(0, stripCount), CascadeClassifierInvoker( *this, processingRectSize, stripSize, yStep, factor,
candidatesVector, rejectLevels, levelWeights, true, currentMask, &mtx));
levels.insert( levels.end(), rejectLevels.begin(), rejectLevels.end() );
weights.insert( weights.end(), levelWeights.begin(), levelWeights.end() );
}
else
{
parallel_for_(Range(0, stripCount), CascadeClassifierInvoker( *this, processingRectSize, stripSize, yStep, factor,
candidatesVector, rejectLevels, levelWeights, false, currentMask, &mtx));
}
candidates.insert( candidates.end(), candidatesVector.begin(), candidatesVector.end() );
CascadeClassifierInvoker函式的operator()實現具體的檢測過程
// 對於沒有並行時range.start=0,range.end=1
void operator()(const Range& range) const
{
Ptr<FeatureEvaluator> evaluator = classifier->featureEvaluator->clone();
Size winSize(cvRound(classifier->data.origWinSize.width * scalingFactor),
cvRound(classifier->data.origWinSize.height * scalingFactor));
// strip=processingRectSize.height
int y1 = range.start * stripSize; // 0
int y2 = min(range.end * stripSize, processingRectSize.height); // processSizeRect.height也就是可以處理的高度,已經減去視窗高度
for( int y = y1; y < y2; y += yStep )
{
for( int x = 0; x < processingRectSize.width; x += yStep )
{
if ( (!mask.empty()) && (mask.at<uchar>(Point(x,y))==0)) {
continue;
}
// result=1表示通過了所有的分類器 <=0表示失敗的級數
// gypWeight表示返回的閾值
double gypWeight;
int result = classifier->runAt(evaluator, Point(x, y), gypWeight);
// 輸出LOG
#if defined (LOG_CASCADE_STATISTIC)
logger.setPoint(Point(x, y), result);
#endif
// 當返回級數的時候可以最後三個分類器不通過
if( rejectLevels )
{
if( result == 1 )
result = -(int)classifier->data.stages.size();
// 可以最後三個分類器不通過
if( classifier->data.stages.size() + result < 4 )
{
mtx->lock();
rectangles->push_back(Rect(cvRound(x*scalingFactor), cvRound(y*scalingFactor), winSize.width, winSize.height));
rejectLevels->push_back(-result);
levelWeights->push_back(gypWeight);
mtx->unlock();
}
}
// 不返回級數的時候通過所有的分類器才儲存起來
else if( result > 0 )
{
mtx->lock();
rectangles->push_back(Rect(cvRound(x*scalingFactor), cvRound(y*scalingFactor),
winSize.width, winSize.height));
mtx->unlock();
}
// 如果一級都沒有通過那麼加大搜索步長
if( result == 0 )
x += yStep;
}
}
}
runAt函式實現某一檢測視窗的檢測
int CascadeClassifier::runAt( Ptr<FeatureEvaluator>& evaluator, Point pt, double& weight )
{
CV_Assert( oldCascade.empty() );
assert( data.featureType == FeatureEvaluator::HAAR ||
data.featureType == FeatureEvaluator::LBP ||
data.featureType == FeatureEvaluator::HOG );
// 設定某一點處的特徵,參考《影象特徵->XXX特徵之OpenCV-估計》
if( !evaluator->setWindow(pt) )
return -1;
// 如果為樹樁,沒有樹枝
if( data.isStumpBased )
{
if( data.featureType == FeatureEvaluator::HAAR )
return predictOrderedStump<HaarEvaluator>( *this, evaluator, weight );
else if( data.featureType == FeatureEvaluator::LBP )
return predictCategoricalStump<LBPEvaluator>( *this, evaluator, weight );
else if( data.featureType == FeatureEvaluator::HOG )
return predictOrderedStump<HOGEvaluator>( *this, evaluator, weight );
else
return -2;
}
// 每個弱分類器不止一個node
else
{
if( data.featureType == FeatureEvaluator::HAAR )
return predictOrdered<HaarEvaluator>( *this, evaluator, weight );
else if( data.featureType == FeatureEvaluator::LBP )
return predictCategorical<LBPEvaluator>( *this, evaluator, weight );
else if( data.featureType == FeatureEvaluator::HOG )
return predictOrdered<HOGEvaluator>( *this, evaluator, weight );
else
return -2;
}
}
predictOrdered*函式實現判斷當前檢測視窗的判斷
// HAAR與HOG特徵的多node檢測
template<class FEval>
inline int predictOrdered( CascadeClassifier& cascade, Ptr<FeatureEvaluator> &_featureEvaluator, double& sum )
{
int nstages = (int)cascade.data.stages.size();
int nodeOfs = 0, leafOfs = 0;
FEval& featureEvaluator = (FEval&)*_featureEvaluator;
float* cascadeLeaves = &cascade.data.leaves[0];
CascadeClassifier::Data::DTreeNode* cascadeNodes = &cascade.data.nodes[0];
CascadeClassifier::Data::DTree* cascadeWeaks = &cascade.data.classifiers[0];
CascadeClassifier::Data::Stage* cascadeStages = &cascade.data.stages[0];
// 遍歷每個強分類器
for( int si = 0; si < nstages; si++ )
{
CascadeClassifier::Data::Stage& stage = cascadeStages[si];
int wi, ntrees = stage.ntrees;
sum = 0;
// 遍歷每個弱分類器
for( wi = 0; wi < ntrees; wi++ )
{
CascadeClassifier::Data::DTree& weak = cascadeWeaks[stage.first + wi];
int idx = 0, root = nodeOfs;
// 遍歷每個節點
do
{
// 選擇一個node:root和idx初始化為0,即第一個node
CascadeClassifier::Data::DTreeNode& node = cascadeNodes[root + idx];
// 計算當前node特徵池編號下的特徵值
double val = featureEvaluator(node.featureIdx);
// 如果val小於node閾值則選擇左子樹,否則選擇右子樹
idx = val < node.threshold ? node.left : node.right;
} while( idx > 0 );
// 累加最終的葉子節點
sum += cascadeLeaves[leafOfs - idx];
nodeOfs += weak.nodeCount;
leafOfs += weak.nodeCount + 1;
}
// 判斷所有葉子節點累加和是否小於強分類器閾值,小於強分類器閾值則失敗
if( sum < stage.threshold )
return -si;
}
// 通過了所有的強分類器返回1,否則返回失敗的分類器
return 1;
}
// LBP特徵的多node檢測
template<class FEval>
inline int predictCategorical( CascadeClassifier& cascade, Ptr<FeatureEvaluator> &_featureEvaluator, double& sum )
{
int nstages = (int)cascade.data.stages.size();
int nodeOfs = 0, leafOfs = 0;
FEval& featureEvaluator = (FEval&)*_featureEvaluator;
size_t subsetSize = (cascade.data.ncategories + 31)/32;
int* cascadeSubsets = &cascade.data.subsets[0];
float* cascadeLeaves = &cascade.data.leaves[0];
CascadeClassifier::Data::DTreeNode* cascadeNodes = &cascade.data.nodes[0];
CascadeClassifier::Data::DTree* cascadeWeaks = &cascade.data.classifiers[0];
CascadeClassifier::Data::Stage* cascadeStages = &cascade.data.stages[0];
for(int si = 0; si < nstages; si++ )
{
CascadeClassifier::Data::Stage& stage = cascadeStages[si];
int wi, ntrees = stage.ntrees;
sum = 0;
for( wi = 0; wi < ntrees; wi++ )
{
CascadeClassifier::Data::DTree& weak = cascadeWeaks[stage.first + wi];
int idx = 0, root = nodeOfs;
do
{
CascadeClassifier::Data::DTreeNode& node = cascadeNodes[root + idx];
// c為0-255之間的數
int c = featureEvaluator(node.featureIdx);
// 獲取當前node的subset頭位置
const int* subset = &cascadeSubsets[(root + idx)*subsetSize]; // LBP:subsetSize=8
// 判斷選擇左子樹還是右子樹
idx = (subset[c>>5] & (1 << (c & 31))) ? node.left : node.right;
// c>>5表示將c右移5位,選擇高3位,0-7之間
// c&31表示低5位,1<<(c&31)選擇低5位後左移1位
// 將上面的數按位與,如果最後結果不為0表示選擇左子樹,否則選擇右子樹
}
while( idx > 0 );
sum += cascadeLeaves[leafOfs - idx];
nodeOfs += weak.nodeCount;
leafOfs += weak.nodeCount + 1;
}
if( sum < stage.threshold )
return -si;
}
return 1;
}
// HAAR與HOG特徵的單node檢測
template<class FEval>
inline int predictOrderedStump( CascadeClassifier& cascade, Ptr<FeatureEvaluator> &_featureEvaluator, double& sum )
{
int nodeOfs = 0, leafOfs = 0;
FEval& featureEvaluator = (FEval&)*_featureEvaluator;
float* cascadeLeaves = &cascade.data.leaves[0];
CascadeClassifier::Data::DTreeNode* cascadeNodes = &cascade.data.nodes[0];
CascadeClassifier::Data::Stage* cascadeStages = &cascade.data.stages[0];
int nstages = (int)cascade.data.stages.size();
// 遍歷每個強分類器
for( int stageIdx = 0; stageIdx < nstages; stageIdx++ )
{
CascadeCl