opencv 人脸检测源码解析

opencv 人脸检测源码解析

在opencv3.2中,objdetect模块设计了快速的目标检测方法。其特征提取使用简单的haar特征,该特征可以使用积分图的方法进行快速提取;训练过程采用经典的ad-boost增强算法可将多个简单的弱分类器构建成强分类器;目标检测或者具体的人脸检测过程中,采用级联的多个强分类器,极大加速了目标检测过程,达到实时检测目的。
本文将以人脸检测为例,详细解析opencv本部分源码。

源码及解析

  • objdetect.hpp
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;//通过该指针调用内部功能实现类
}
  • cascadedetect.cpp

(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;
    }
}
  • objdetect.hpp

(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; }
}

demo

  • demo.cpp
#include 
using namespace std;
using namespace cv;

int main()
{
   Mat image;
   CascadeClassifier cascade;  
   vectorfaces; 
   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;
}

你可能感兴趣的:(人脸检测-源码)