OpenCV的级联分类器分为两部分,训练与检测。训练那块代码在apps目录下,有旧分类器haartraining和新分类器traincascade各自的代码。目前没去看,训练用OpenCV给的程序目前就足够了。
检测这块代码在以下3个文件中
E:\opencv\sources\modules\objdetect\include\opencv2\objdetect\objdetect.hpp;
定义了旧分类器计算Haar特征值的结构体:
CvHaarFeature(Haar特征结构体),
CvHaarClassifier(Haar特征分类器结构体),
CvHaarStageClassifier(Haar强分类器结构体),
CvHaarClassifierCascade(Haar强分类器级联结构体)
定义了类:
FeatureEvaluator(特征值计算类,新分类器使用),
CascadeClassifier(级联分类器)类
E:\opencv\sources\modules\objdetect\src\cascadedetect.hpp
定义了类,以下几个类都是继承自FeatureEvaluator:
HaarEvaluator(Haar特征值计算类),
LBPEvaluator(LBP特征值计算类),
HOGEvaluator(HOG特征值计算类)
定义了函数:
predictOrdered(计算并验证是否通过级联强分类器)
predictCategorical
predictOrderedStump
predictCategoricalStump
E:\opencv\sources\modules\objdetect\src\cascadedetect.cpp
检测时首先调用CascadeClassifier::load(const string& filename)函数载入分类器xml文件,如果是旧分类器,将xml中的信息读取到Ptr
//CascadeClassifier的Protected保护子类
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
vector
vector
vector
vector
};
Data data; //CascadeClassifier的保护子类对象成员,存放强级联分类器数据。
CascadeClassifier有两个多尺度检测函数的实现(如下),读取xml后可以调用这两个函数进行级联强分类器检测(使用不同的xml就是不同的检测,如人脸检测,眼睛,鼻子,嘴巴等检测)
CV_WRAP virtual void detectMultiScale( const Mat& image,//图像,
CV_OUT vector
double scaleFactor=1.1, //缩放比例,必须大于1
int minNeighbors=3 ,//合并窗口最小间距,每个候选矩阵至少需要包含的附近元素个数(一个目标至少要检测多少次)
int 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
vector
vector
double scaleFactor=1.1,
int minNeighbors=3, int flags=0,
Size minSize=Size(),
Size maxSize=Size(),
bool outputRejectLevels=false );//只有该值为true时,才能输出前两个新参数
调用时一般不需要返回通过的级联分类器级数,首先调用该无输出级数形式函数
void CascadeClassifier::detectMultiScale( const Mat& image, vector
double scaleFactor, int minNeighbors,
int flags, Size minObjectSize, Size maxObjectSize)
{
vector
vector
detectMultiScale( image, objects, fakeLevels, fakeWeights, scaleFactor,
minNeighbors, flags, minObjectSize, maxObjectSize, false );//构造输出级数容器,统一调用有级数输出的函数
}
void CascadeClassifier::detectMultiScale( const Mat& image, vector
vector
vector
double scaleFactor, int minNeighbors,
int flags, Size minObjectSize, Size maxObjectSize,//这个flags只有使用旧分类器xml文件时才有效,现在一般使用新分类器格式的xml
bool outputRejectLevels )//需要输出检测级数
{
const double GROUP_EPS = 0.2; //相似矩形框聚类参数
CV_Assert( scaleFactor > 1 && image.depth() == CV_8U ); //非真即为0,即这两个条件必须为真,检查缩放参数是否大于1,图像位深度是否为8位
if( empty() )//检测是否融入参数,即使创建了对象没有初始化加载也是空
return;
if( isOldFormatCascade() )//是否是旧格式的分类器
{
MemStorage storage(cvCreateMemStorage(0));
CvMat _image = image;
CvSeq* _objects = cvHaarDetectObjectsForROC( &_image, oldCascade, storage, rejectLevels, levelWeights, scaleFactor,
minNeighbors, flags, minObjectSize, maxObjectSize, outputRejectLevels );
vector
Seq
objects.resize(vecAvgComp.size());
std::transform(vecAvgComp.begin(), vecAvgComp.end(), objects.begin(), getRect());
return;
}
//新格式的分类器检测流程看下面
objects.clear();//清空一下检测结果存放容器
if (!maskGenerator.empty()) {//class CV_EXPORTS MaskGenerator在类CascadeClassifier中定义,用于生成mask,当检测窗口中的第一个点在mask矩阵中时跳过检测。Ptr
maskGenerator->initializeMask(image);//初始化mask生成器
}
if( maxObjectSize.height == 0 || maxObjectSize.width == 0 )
maxObjectSize = image.size();//默认检测目标最大为图像本身大小
Mat grayImage = image;
if( grayImage.channels() > 1 )//通道数大于1
{
Mat temp;
cvtColor(grayImage, temp, CV_BGR2GRAY);//转为灰度图
grayImage = temp;
}
Mat imageBuffer(image.rows + 1, image.cols + 1, CV_8U);//图像缓存,多加一行一列?
vector
for( double factor = 1; ; factor *= scaleFactor )//多尺度,每次放大ScaleFactor倍
{
Size originalWindowSize = getOriginalWindowSize();//
Size windowSize( cvRound(originalWindowSize.width*factor), cvRound(originalWindowSize.height*factor) );//检测窗口每次放大ScaleFactor倍
Size scaledImageSize( cvRound( grayImage.cols/factor ), cvRound( grayImage.rows/factor ) );
Size processingRectSize( scaledImageSize.width - originalWindowSize.width, scaledImageSize.height - originalWindowSize.height );//可供原始检测窗口左上顶点移动的矩形区域
if( processingRectSize.width <= 0 || processingRectSize.height <= 0 )//检测窗口可移动区域小于0,说明图像比20*20还小,退出
break;
if( windowSize.width > maxObjectSize.width || windowSize.height > maxObjectSize.height )//窗口大于最大检测目标,退出,如果没有设置最大检测窗口,当originalWindowSize*factor > image.size时退出,如20*20一直放大到比输入图像大时退出。
break;
if( windowSize.width < minObjectSize.width || windowSize.height < minObjectSize.height )//窗口小于于最小检测目标,跳过
continue;
Mat scaledImage( scaledImageSize, CV_8U, imageBuffer.data );//imageBuffer不是还没有数据?
resize( grayImage, scaledImage, scaledImageSize, 0, 0, CV_INTER_LINEAR );//灰度图缩小后赋给scaledImage
int yStep;//y步长
if( getFeatureType() == cv::FeatureEvaluator::HOG )//HOG特征步长设置为4
{
yStep = 4;
}
else
{
yStep = factor > 2. ? 1 : 2;//缩放倍数大于2时,步长为1,否则为2
}
int stripCount, stripSize;// 并行计算线程个数以及大小,分行并行处理
const int PTS_PER_THREAD = 1000;//预定动作时间标准系统也称预定时间标准系统(Predetermined Time System)
stripCount = ((processingRectSize.width/yStep)*(processingRectSize.height + yStep-1)/yStep + PTS_PER_THREAD/2)/PTS_PER_THREAD;//后面TBB加速要生成的并行线程数
stripCount = std::min(std::max(stripCount, 1), 100);
stripSize = (((processingRectSize.height + stripCount - 1)/stripCount + yStep-1)/yStep)*yStep;
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());
if( outputRejectLevels )
{
groupRectangles( objects, rejectLevels, levelWeights, minNeighbors, GROUP_EPS );
}
else
{
groupRectangles( objects, minNeighbors, GROUP_EPS );//合并检测结果,一个人脸可能被多处检测到,根据minNeighbors进行合并
}
}
单尺度检测函数
bool CascadeClassifier::detectSingleScale( const Mat& image, int stripCount, Size processingRectSize,
int stripSize, int yStep, double factor, vector
vector
{
if( !featureEvaluator->setImage( image, data.origWinSize ) )//对图像做积分图,Ptr
return false;
#if defined (LOG_CASCADE_STATISTIC)
logger.setImage(image);
#endif
Mat currentMask;
if (!maskGenerator.empty()) {
currentMask=maskGenerator->generateMask(image);//生成mask
}
vector
vector
vector
Mutex mtx;
if( outputRejectLevels )
{
parallel_for_(Range(0, stripCount), CascadeClassifierInvoker( *this, processingRectSize, stripSize, yStep, factor,
candidatesVector, rejectLevels, levelWeights, true, currentMask, &mtx));//class CascadeClassifierInvoker定义在单尺度检测函数之前
levels.insert( levels.end(), rejectLevels.begin(), rejectLevels.end() );
weights.insert( weights.end(), levelWeights.begin(), levelWeights.end() );
}
else
{
//这里是检测过程中的关键,使用parallel_for是为了TBB加速中使用,生成stripCount个平行线程(每个线程生成一个CascadeClassifierInvoker),在每个CascadeClassifierInvoker中分配当前缩放图像的N行做检测,这是TBB利用多线程做的加速计算
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() );
#if defined (LOG_CASCADE_STATISTIC)
logger.write();
#endif
return true;
}
CascadeClassifierInvoker级联分类器调用器类,实现了具体的单尺度目标检测
class CascadeClassifierInvoker : public ParallelLoopBody
{
public:
CascadeClassifierInvoker( CascadeClassifier& _cc, Size _sz1, int _stripSize, int _yStep, double _factor,
vector
{
classifier = &_cc;
processingRectSize = _sz1;
stripSize = _stripSize;
yStep = _yStep;
scalingFactor = _factor;
rectangles = &_vec;
rejectLevels = outputLevels ? &_levels : 0;
levelWeights = outputLevels ? &_weights : 0;
mask = _mask;
mtx = _mtx;
}
//没有并行时,range.start = 0,range.end =1;
void operator()(const Range& range) const//重载了()操作符
{
Ptr
Size winSize(cvRound(classifier->data.origWinSize.width * scalingFactor), cvRound(classifier->data.origWinSize.height * scalingFactor));
int y1 = range.start * stripSize;
int y2 = min(range.end * stripSize, processingRectSize.height);
//并行计算应该是用range将图像检测按几行为一个线程进行并行计算的
for( int y = y1; y < y2; y += yStep )
{
for( int x = 0; x < processingRectSize.width; x += yStep )
{
if ( (!mask.empty()) && (mask.at
continue;
}
double gypWeight;
int result = classifier->runAt(evaluator, Point(x, y), gypWeight);//result =1表示通过所有分类器,result<0表示失败的级数。
//输出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;
}
}
}
CascadeClassifier* classifier;
vector
Size processingRectSize;
int stripSize, yStep;
double scalingFactor;
vector
vector
Mat mask;
Mutex* mtx;
};
runAt()实现某一检测窗口的检测
int CascadeClassifier::runAt( Ptr
{
CV_Assert( oldCascade.empty() );
assert( data.featureType == FeatureEvaluator::HAAR ||
data.featureType == FeatureEvaluator::LBP ||
data.featureType == FeatureEvaluator::HOG );
if( !evaluator->setWindow(pt) )//设置当前检测窗口,改变了计算特征值需要的offset
return -1;
if( data.isStumpBased )//CART树只有树桩
{
if( data.featureType == FeatureEvaluator::HAAR )
return predictOrderedStump
else if( data.featureType == FeatureEvaluator::LBP )
return predictCategoricalStump
else if( data.featureType == FeatureEvaluator::HOG )
return predictOrderedStump
else
return -2;
}
else //根据不同的特征类型调用CascadeClassifier的友元函数predictOrdered()
{
if( data.featureType == FeatureEvaluator::HAAR )
return predictOrdered
else if( data.featureType == FeatureEvaluator::LBP )
return predictCategorical
else if( data.featureType == FeatureEvaluator::HOG )
return predictOrdered
else
return -2;
}
}
//----------------------------------------------predictOrdered 计算特征值并预测函数-------------------------------------
template
inline int predictOrdered( CascadeClassifier& cascade, Ptr
{
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和index初始化为0,即第一个node
CascadeClassifier::Data::DTreeNode& node = cascadeNodes[root + idx];
double val = featureEvaluator(node.featureIdx);//计算特征池编号下的值,featureIdx为xml文件取出来的所有特征池的编号,每个弱分类器都会指明使用哪个特征。这里的featureEvaluator使用不同类型的特征,则调用不同特征特征值计算器的实现函数
idx = val < node.threshold ? node.left : node.right;//如果val小于node阈值则选择左子树,否则右子树
}
while( idx > 0 );//CART树找到了最终的叶子节点
sum += cascadeLeaves[leafOfs - idx];//累加最终的叶子节点值,强分类器阈值由多个弱分类器的阈值累加得到,
nodeOfs += weak.nodeCount;
leafOfs += weak.nodeCount + 1;
}
if( sum < stage.threshold )//判断是否小于强分类器阈值,是则表示失败了,返回失败的强分类器级数,因此需要通过全部强分类器才算成功。
return -si;
}
return 1;
}
不同的特征类型特征值计算器,这里只看Haar特征值的计算
//---------------------------------------------- HaarEvaluator ---------------------------------------
class HaarEvaluator : public FeatureEvaluator//继承自FeatureEvaluator
{
public:
struct Feature
{
Feature();
float calc( int offset ) const;//计算特征值
void updatePtrs( const Mat& sum );//调用setImage时,调用该函数更新组成该特征的矩形框(如A+B、B,在xml中可以看到相应的rect)在积分图存储内存的起始位置指针
bool read( const FileNode& node );//读取特征池的特征到节点
bool tilted;//是否是标准特征,还是旋转45度的特征
enum { RECT_NUM = 3 };//Haar特征最多只要3个矩形即可描述
struct
{
Rect r;
float weight;//矩形的权重
} rect[RECT_NUM];
const int* p[RECT_NUM][4];//用于存储第一个rect、第二个rect、第三个rect的4个顶点积分值在积分图内存区域的起始指针,每个rect的像素和由4个顶点的积分值计算得到
};
HaarEvaluator();
virtual ~HaarEvaluator();
virtual bool read( const FileNode& node );
virtual Ptr
virtual int getFeatureType() const { return FeatureEvaluator::HAAR; }
virtual bool setImage(const Mat&, Size origWinSize);//计算积分图
virtual bool setWindow(Point pt);
double operator()(int featureIdx) const//重载()操作符
{ return featuresPtr[featureIdx].calc(offset) * varianceNormFactor; }//调用calc函数进行特征值计算,offset在调用setWindow移动窗口时已确定
virtual double calcOrd(int featureIdx) const
{ return (*this)(featureIdx); }
protected:
Size origWinSize;
Ptr
Feature* featuresPtr; // optimization
bool hasTiltedFeatures;
Mat sum0, sqsum0, tilted0;
Mat sum, sqsum, tilted;
Rect normrect;
const int *p[4];
const double *pq[4];
int offset;
double varianceNormFactor;
};
//检测窗口下Haar特征值的计算
#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)
//计算方法:不同的特征用统一的格式来描述特征值的计算,左边为统一格式,右边为不同特征下白色区域减去黑色区域的像素和表达式,如下
(A+B)*(-1)+B*2=B-A //------A+B是xml中特征的第一个rect,B为第二个rect,-1,2为各自的权重
(A+B+C)*(-1)+B*3 = 2*B-A-C //---------A+B+C、B为两个rect,-1,3为各自权重
(A+B+C+D)*(-1)+2*B + 2*C = B+C-(A+D) //-------A+B+C+D、B、C为三个rect,-1,2,2为各自权重,可以到xml特征池看看具体的特征矩形框
inline float HaarEvaluator::Feature :: calc( int _offset ) const
{
float ret = rect[0].weight * CALC_SUM(p[0], _offset) + rect[1].weight * CALC_SUM(p[1], _offset);//const int* p[RECT_NUM][4] 指针数组在这里用到
if( rect[2].weight != 0.0f )
ret += rect[2].weight * CALC_SUM(p[2], _offset);
return ret;
}
/********************计算完毕**************************/
//补充
bool HaarEvaluator::setImage( const Mat &image, Size _origWinSize )
{
int rn = image.rows+1, cn = image.cols+1;
origWinSize = _origWinSize;
normrect = Rect(1, 1, origWinSize.width-2, origWinSize.height-2);//这是要去掉边框作为计算内容?
if (image.cols < origWinSize.width || image.rows < origWinSize.height)
return false;
if( sum0.rows < rn || sum0.cols < cn )
{
sum0.create(rn, cn, CV_32S);
sqsum0.create(rn, cn, CV_64F);
if (hasTiltedFeatures)
tilted0.create( rn, cn, CV_32S);
}
sum = Mat(rn, cn, CV_32S, sum0.data);
sqsum = Mat(rn, cn, CV_64F, sqsum0.data);
if( hasTiltedFeatures )//旋转特征
{
tilted = Mat(rn, cn, CV_32S, tilted0.data);
integral(image, sum, sqsum, tilted);
}
else
integral(image, sum, sqsum);//计算正常Haar特征积分图sum,像素平方和积分图sqsum
const int* sdata = (const int*)sum.data;
const double* sqdata = (const double*)sqsum.data;
size_t sumStep = sum.step/sizeof(sdata[0]);//一行的数据长度/数据类型大小=每行的数据个数?
size_t sqsumStep = sqsum.step/sizeof(sqdata[0]);
CV_SUM_PTRS( p[0], p[1], p[2], p[3], sdata, normrect, sumStep );//存储normrect窗口的四个顶点积分图数据指针
CV_SUM_PTRS( pq[0], pq[1], pq[2], pq[3], sqdata, normrect, sqsumStep );
size_t fi, nfeatures = features->size();//特征池下的Haar特征个数
for( fi = 0; fi < nfeatures; fi++ )
featuresPtr[fi].updatePtrs( !featuresPtr[fi].tilted ? sum : tilted );//更新每个特征组成的矩形框(A+B、B)在积分图存储内存的起始位置指针,后面再加上offset移动该特征窗口
return true;
}
bool HaarEvaluator::setWindow( Point pt )
{
if( pt.x < 0 || pt.y < 0 ||
pt.x + origWinSize.width >= sum.cols ||
pt.y + origWinSize.height >= sum.rows )
return false;
size_t pOffset = pt.y * (sum.step/sizeof(int)) + pt.x;
size_t pqOffset = pt.y * (sqsum.step/sizeof(double)) + pt.x;
int valsum = CALC_SUM(p, pOffset);
double valsqsum = CALC_SUM(pq, pqOffset);
double nf = (double)normrect.area() * valsqsum - (double)valsum * valsum;
if( nf > 0. )
nf = sqrt(nf);
else
nf = 1.;
varianceNormFactor = 1./nf;
offset = (int)pOffset; //更新了offset
return true;
}
class CV_EXPORTS MaskGenerator //编写子类继承,重写虚函数,实现Mask
{
public:
virtual ~MaskGenerator() {}
virtual cv::Mat generateMask(const cv::Mat& src)=0;//单尺度检测函数调用该函数生成新的Mask
virtual void initializeMask(const cv::Mat& /*src*/) {};//多尺度检测函数调用该函数初始化MaskGenerator
};
void setMaskGenerator(Ptr
Ptr
void setFaceDetectionMaskGenerator();