在opencv3.2中,objdetect模块设计了快速的目标检测方法。其特征提取使用简单的haar特征,该特征可以使用积分图的方法进行快速提取;训练过程采用经典的ad-boost增强算法可将多个简单的弱分类器构建成强分类器;目标检测或者具体的人脸检测过程中,采用级联的多个强分类器,极大加速了目标检测过程,达到实时检测目的。
本文将以人脸检测为例,详细解析opencv本部分源码。
class CascadeClassifier
{
public:
CascadeClassifier();//无参数构造函数,new自动调用该函数分配初试内存
CascadeClassifier( const String& filename );//带参数构造函数,参数为模型XML的绝对名称
~CascadeClassifier();
bool empty() const;//是否导入参数,只创建了该对象而没有加载或者加载失败时都是空的
bool load( const String& filename );//加载分类器,参数为XML的绝对名称,新老版本的分类器均可
//注:load 会优先尝试按照新版本格式read分类器,若读取失败,则按老版本格式cvLoad分类器.
bool read(const FileNode& node);//load内部调用read解析XML中的内容,该函数只能读取新格式的分类器
//多尺度检测主函数
void detectMultiScale( InputArray Image,//当前被检测图像或视频帧
std::vector & objects,// 输出矩形
double scaleFactor = 1.1,// 缩放比例,必须大于1
int minNeighbors = 3, //合并窗口时最小neighbor
int flags = 0,
Size minSize = Size(),//限制目标最小尺度
Size maxSize = Size() );//限制目标最大尺度
Ptr cc;//通过该指针调用内部功能实现类
}
(1)初始化功能实现类CascadeClassifierImpl,加载分类器
bool CascadeClassifier::load( const String& filename )
{
// CascadeClassifier 中使用 CascadeClassifierImpl类 实现具体功能
// 初始化 cc
cc = makePtr<CascadeClassifierImpl>();
if(!cc->load(filename))
cc.release();
return !empty();
}
(2)多尺度目标检测实现,对于新老格式的分类器定义了不同的检测函数,这里主要介绍利用新格式检测方法
void CascadeClassifierImpl::detectMultiScale( InputArray _image, std::vector & objects,
std::vector<int>& rejectLevels,
std::vector<double>& levelWeights,
double scaleFactor, int minNeighbors,
int flags, Size minObjectSize, Size maxObjectSize,
bool outputRejectLevels )
{ //detectMultiScale 多尺度检测函数入口
CV_INSTRUMENT_REGION()
CV_Assert( scaleFactor > 1 && _image.depth() == CV_8U );
if( empty() )
return;
if( isOldFormatCascade() )//老格式分类器
{// 。。。省略,不关注
}
else//新格式分类器
{
detectMultiScaleNoGrouping( _image, objects, rejectLevels, levelWeights, scaleFactor,
minObjectSize, maxObjectSize,outputRejectLevels );
const double GROUP_EPS = 0.2;
if( outputRejectLevels )
groupRectangles( objects, rejectLevels, levelWeights, minNeighbors, GROUP_EPS );
else
groupRectangles( objects, minNeighbors, GROUP_EPS )
}
}
(3)新格式所调用的 detectMultiScaleNoGrouping,计算金字塔层数,通过积分图计算haar特征图,并利用CascadeClassifierInvoker类和parallel_for_ 实现并行检测
void CascadeClassifierImpl::detectMultiScaleNoGrouping( InputArray _image,
std::vector &candidates,
std::vector<int>& rejectLevels,
double scaleFactor,
bool outputRejectLevels )
{
CV_INSTRUMENT_REGION();
Size imgsz = _image.size();
if( maxObjectSize.height == 0 || maxObjectSize.width == 0 )
maxObjectSize = imgsz;//若未限制目标最大尺度,则定义为图像大小
if( (imgsz.height < originalWindowSize.height) || (imgsz.width < originalWindowSize.width) )
return;//若输入图像小于最小检测尺寸,则放弃检测
std::vector<float> all_scales, scales;
for( double factor = 1; ; factor *= scaleFactor )
{//将最小检测尺寸尝试放大,计算出金字塔所有的层,存入all_scales
Size windowSize( cvRound(originalWindowSize.width*factor),
cvRound(originalWindowSize.height*factor) );
if( windowSize.width > imgsz.width || windowSize.height > imgsz.height )
break;
all_scales.push_back((float)factor);
}
for( size_t index = 0; index < all_scales.size(); index++)
{//结合尺寸范围的限制筛选出待检测层,存入scales
Size windowSize( cvRound(originalWindowSize.width*all_scales[index]),
cvRound(originalWindowSize.height*all_scales[index]) );
if( windowSize.width > maxObjectSize.width || windowSize.height > maxObjectSize.height)
break;
if( windowSize.width < minObjectSize.width || windowSize.height < minObjectSize.height )
continue;
scales.push_back(all_scales[index]);
}
Mat grayImage;
_InputArray gray;
if (_image.channels() > 1)//需转化为灰度图处理
cvtColor(_image, grayImage, COLOR_BGR2GRAY);
else if (_image.isMat())// 输入类型格式需转换为_InputArray
grayImage = _image.getMat();
else
_image.copyTo(grayImage);
gray = grayImage;
//计算当前图像的金字塔积分图,积分图通过拼块方式存入Mat类型的sbuf对象中
if( !featureEvaluator->setImage(gray, scales) )
return;
size_t i, nscales = scales.size();
cv::AutoBuffer<int> stripeSizeBuf(nscales);
int* stripeSizes = stripeSizeBuf;
// 。。。(省略部分参数设置的代码)
// CascadeClassifierInvoker类实现具体的检测过程,候选目标位置存入candidates中
CascadeClassifierInvoker invoker(*this, (int)nscales, nstripes, s, stripeSizes,
candidates, rejectLevels, levelWeights,
outputRejectLevels, currentMask, &mtx);
//通过 parallel_for_ 并行检测目标
parallel_for_(Range(0, nstripes), invoker);
}
(4)并行计算类CascadeClassifierInvoker,通过重载()运算符定义。对特征金子塔逐层 逐行逐列 判断(runAt)是否候选目标位置
class CascadeClassifierInvoker : public ParallelLoopBody
{
public:
//。。。
void operator()(const Range& range) const//()运算符定义并行计算的子函数
{
Ptr evaluator = classifier->featureEvaluator->clone();
double gypWeight = 0.;
Size origWinSize = classifier->data.origWinSize;
for( int scaleIdx = 0; scaleIdx < nscales; scaleIdx++ )//循环特征金字塔每一层
{
const FeatureEvaluator::ScaleData& s = scaleData[scaleIdx];
float scalingFactor = s.scale;
int yStep = s.ystep;
int stripeSize = stripeSizes[scaleIdx];
int y0 = range.start*stripeSize;
Size szw = s.getWorkingSize(origWinSize);
int y1 = std::min(range.end*stripeSize, szw.height);
Size winSize(cvRound(origWinSize.width * scalingFactor),
cvRound(origWinSize.height * scalingFactor));
// 滑动窗口,对每个窗口使用runAt判断是否候选目标位置
for( int y = y0; y < y1; y += yStep )
{
for( int x = 0; x < szw.width; x += yStep )
{
//调用runAt实现当前块的分类result=1表示通过了所有的分类器 <=0表示失败未通过的级数
int result = classifier->runAt(evaluator, Point(x, y), scaleIdx, gypWeight);
if( result > 0 )
{
mtx->lock();//临界区,将通过分类器的候选区域push_back保存
rectangles->push_back(Rect(cvRound(x*scalingFactor),
cvRound(y*scalingFactor),
winSize.width, winSize.height));
mtx->unlock();
}
if( result == 0 )
x += yStep;
}
}
}
}
//。。。
}
(5)runAt (可选HAAR特征或者 LBP特征检测器)判断当前窗口是否包含待检测目标
int CascadeClassifierImpl::
runAt( Ptr& evaluator, Point pt, int scaleIdx, double& weight )
{
// miexp: runAt (HAAR LBP)判断当前像素点所代表窗口是否目标
assert( !oldCascade &&
(data.featureType == FeatureEvaluator::HAAR ||
data.featureType == FeatureEvaluator::LBP ||
data.featureType == FeatureEvaluator::HOG) );
if( !evaluator->setWindow(pt, scaleIdx) )//从sbuf中拿出当前窗口特征,放入特征检测器
return -1;
if( data.maxNodesPerTree == 1 )//树桩,只有一个结点
{
if( data.featureType == FeatureEvaluator::HAAR )//HAAR特征检测器
return predictOrderedStump( *this, evaluator, weight );
else if( data.featureType == FeatureEvaluator::LBP )//LBP特征检测器
return predictCategoricalStump( *this, evaluator, weight );
else//opencv3.2 no support HOG
return -2;
}
else
{
if( data.featureType == FeatureEvaluator::HAAR )
return predictOrdered( *this, evaluator, weight );
else if( data.featureType == FeatureEvaluator::LBP )
return predictCategorical( *this, evaluator, weight );
else
return -2;
}
}
(1)单node的HAAR特征检测器
template<class FEval>//HaarEvaluator
inline int predictOrderedStump( CascadeClassifierImpl& cascade,
Ptr &_featureEvaluator, double& sum )
{
CV_Assert(!cascade.data.stumps.empty());
FEval& featureEvaluator = (FEval&)*_featureEvaluator;
const CascadeClassifierImpl::Data::Stump* cascadeStumps = &cascade.data.stumps[0];
const CascadeClassifierImpl::Data::Stage* cascadeStages = &cascade.data.stages[0];
int nstages = (int)cascade.data.stages.size();
double tmp = 0;
// miexp:遍历每个强分类器 一共22个 级联 cascade
for( int stageIdx = 0; stageIdx < nstages; stageIdx++ )
{
const CascadeClassifierImpl::Data::Stage& stage = cascadeStages[stageIdx];
tmp = 0;
int ntrees = stage.ntrees;
// miexp:每个强分类器中有若干个弱分类器,遍历每个弱分类器,得分求和作为强分类器得分
for( int i = 0; i < ntrees; i++ )
{
const CascadeClassifierImpl::Data::Stump& stump = cascadeStumps[i];
// 求得当前块的HAAR特征通过弱分类器的取值,通过HaarEvaluator类的()重载实现
double value = featureEvaluator(stump.featureIdx);
tmp += value < stump.threshold ? stump.left : stump.right;
}
// miexp: 若当前强分类器得分小于阈值,则提前截止 cascade核心思想
if( tmp < stage.threshold )
{
sum = (double)tmp;
return -stageIdx;
}
cascadeStumps += ntrees;
}
sum = (double)tmp;
return 1;
}
class HaarEvaluator : public FeatureEvaluator
{
//。。。
// ( )重载 HAAR特征通过弱分类器的求值函数
// 独立运算单元
float operator()(int featureIdx) const
{ return optfeaturesPtr[featureIdx].calc(pwin) * varianceNormFactor; }
}
#include
using namespace std;
using namespace cv;
int main()
{
Mat image;
CascadeClassifier cascade;
vector faces;
cascade.load("haarcascade_frontalface_alt2.xml");
Size minSize=Size(80,80);
VideoCapture cap(0);//摄像头
while (cv::waitKey(30) != 27)
{
cap >> image;
cascade.detectMultiScale(image, faces, 1.05, 2 , 0|2, minSize);//检测
for (size_t i = 0; i < faces.size(); i++)
rectangle(image, faces[i], Scalar(0,255,0),5);
imshow("detectedFaces", image);
}
return 0;
}