Opencv2.4.9源码分析——Cascade Classification
Posted zhaocj
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Opencv2.4.9源码分析——Cascade Classification相关的知识,希望对你有一定的参考价值。
上一篇文章我们介绍了级联分类器的原理,下面我们给出级联分类器的源码分析。
训练级联分类器的源码在opencv/sources/app/traincascade目录下。
首先我们给出级联分类器的特征类型的相关类和函数。
CvHaarFeatureParams、CvLBPFeatureParams和CvHOGFeatureParams分别表示HAAR状特征、LBP特征和HOG特征的参数类,它们都继承于CvFeatureParams:
class CvFeatureParams : public CvParams
public:
//级联分类器能够使用三种特征类型用于训练样本:HAAR、LBP和HOG
enum HAAR = 0, LBP = 1, HOG = 2 ;
//缺省构造函数,赋值maxCatCount为0,featSize为1
CvFeatureParams();
//初始化maxCatCount和featSize
virtual void init( const CvFeatureParams& fp );
//表示向params.xml文件写入一些信息:maxCatCount和featSize的值
virtual void write( cv::FileStorage &fs ) const;
//表示从params.xml文件内读取一些信息:maxCatCount和featSize的值
virtual bool read( const cv::FileNode &node );
//构建相应的特征参数,即级联分类器具体使用哪种特征,如果输入参数featureType=0,则应用HAAR,该函数返回CvHaarFeatureParams的指针;如果featureType=1,则应用LBP,该函数返回CvLBPFeatureParams的指针;如果featureType=2,则应用HOG,该函数返回CvHOGFeatureParams的指针。
static cv::Ptr<CvFeatureParams> create( int featureType );
//表示样本特征的类别数,如果该值为0,则表示特征是数值的形式,对于HAAR和HOG,该值为0,对于LBP,该值被赋值为256
int maxCatCount; // 0 in case of numerical features
//表示一个特征所包含的特征成分的数量,HAAR和LBP为1,HOG为36,即HOG特征包含36(9×4)个特征成分
int featSize; // 1 in case of simple features (HAAR, LBP) and N_BINS(9)*N_CELLS(4) in case of Dalal's HOG features
;
CvHaarEvaluator、CvLBPEvaluator和CvHOGEvaluator分别表示HAAR状特征、LBP特征和HOG特征的计算评估特征值的类,它们都继承于CvFeatureEvaluator类:
class CvFeatureEvaluator
public:
virtual ~CvFeatureEvaluator() //析构函数
//初始化一些变量:featureParams、winSize、numFeatures、cls,另外还调用了generateFeatures函数,而CvFeatureEvaluator类的子类的init函数主要完成各自特征的初始化参数的任务
virtual void init(const CvFeatureParams *_featureParams,
int _maxSampleCount, cv::Size _winSize );
//确保样本图像img的尺寸正确,并且设置该图像的类别标签clsLabel,而CvFeatureEvaluator类的子类的setImage函数主要完成积分图像或梯度方向直方图的计算
virtual void setImage(const cv::Mat& img, uchar clsLabel, int idx);
//表示向xml文件写入一些内容
virtual void writeFeatures( cv::FileStorage &fs, const cv::Mat& featureMap ) const = 0;
//重载( )运算符,CvFeatureEvaluator类的子类CvHaarEvaluator和CvHOGEvaluator完成了第sampleIdx个样本图像的第featureIdx个特征的特征值的计算,而CvHOGEvaluator完成的是第sampleIdx个样本图像的第featureIdx个特征成分的方向直方图的计算
virtual float operator()(int featureIdx, int sampleIdx) const = 0;
//构建相应特征的特征值计算,即级联分类器具体使用哪种特征,如果输入参数type=0,则应用HAAR,该函数返回CvHaarEvaluator的指针;如果type=1,则应用LBP,该函数返回CvLBPEvaluator的指针;如果type=2,则应用HOG,该函数返回CvHOGEvaluator的指针。
static cv::Ptr<CvFeatureEvaluator> create(int type);
//得到numFeatures值
int getNumFeatures() const return numFeatures;
//得到CvFeatureParams:: maxCatCount值
int getMaxCatCount() const return featureParams->maxCatCount;
//得到CvFeatureParams:: featSize值
int getFeatureSize() const return featureParams->featSize;
//得到cls值
const cv::Mat& getCls() const return cls;
//得到矩阵cls中的第si个元素的值,即得到第si个样本的类别标签
float getCls(int si) const return cls.at<float>(si, 0);
protected:
//虚函数,执行子类的generateFeatures函数,产生HAAR、LBP或HOG的各自特征
virtual void generateFeatures() = 0;
int npos, nneg; //分别表示正、负样本的数量
int numFeatures; //表示特征数量
//表示正样本图像的尺寸大小,负样本图像的尺寸是根据正样本图像的尺寸进行剪切
cv::Size winSize;
CvFeatureParams *featureParams; //特征变量
cv::Mat cls; //表示样本的类别标签,即样本的响应值
;
在HAAR、LBP和HOG这三种特征的参数类和评估类中,最重要的是评估类中的setImage函数、generateFeatures函数和重载( )运算符,它们的作用分别是计算积分图像或梯度方向直方图、产生特征,以及计算特征值。下面我们就分别给出这三种特征的相关函数的解释。
HAAR状特征:
void CvHaarEvaluator::setImage(const Mat& img, uchar clsLabel, int idx)
//img表示待处理的样本图像
//clsLabel表示img图像的类别标签,即img图像是正样本还是负样本
//idx表示img在所有样本图像的索引值
//sum和tilted分别表示所有样本图像的积分图像和旋转积分图像,normfactor表示特征值的归一化因子,这三个变量在init函数中定义,这里再次确认这三个变量是否定义好
CV_DbgAssert( !sum.empty() && !tilted.empty() && !normfactor.empty() );
//调用父类的setImage函数,确定图像img尺寸大小是否正确,并赋值类别标签
CvFeatureEvaluator::setImage( img, clsLabel, idx);
//sum和titled包含的单一图像的积分图像和旋转积分图像都是以相量的形式存储的,在这里都需转换为矩阵的形式,以便于积分图像的计算
Mat innSum(winSize.height + 1, winSize.width + 1, sum.type(), sum.ptr<int>((int)idx));
Mat innTilted(winSize.height + 1, winSize.width + 1, tilted.type(), tilted.ptr<int>((int)idx));
Mat innSqSum;
//计算图像img的灰度积分图像innSum,灰度平方的积分图像innSqSum,旋转的积分图像innTilted
integral(img, innSum, innSqSum, innTilted);
//得到特征值的归一化因子,一个样本图像只有一个归一化因子
normfactor.ptr<float>(0)[idx] = calcNormFactor( innSum, innSqSum );
void CvHaarEvaluator::generateFeatures()
//得到HAAR状特征的模式,即HAAR状特征模板是BASIC、CORE还是ALL,该值由用户定义。BASIC为图1中的(a)、(b)、(e)、(i)和(n),CORE为BASIC模式再加上图1中的(f)、(j)和(m),ALL为图1中的全部模板
int mode = ((const CvHaarFeatureParams*)((CvFeatureParams*)featureParams))->mode;
//偏移量,就是积分图像的宽(在程序中,积分图像的宽和高都要比原图像的宽和高多1),用于计算图像中的某一点的像素从图像左上角开始扫描的位置,如坐标(x,y)在图像的位置是x+y×offset,这是因为积分图像是以相量的形式存储的,必须通过这种形式才能从相量中提取出积分图像中某一像素的积分值
int offset = winSize.width + 1;
//遍历样本图像的所有像素
//x和y表示HAAR状特征模板在样本图像中左上角的坐标
for( int x = 0; x < winSize.width; x++ )
for( int y = 0; y < winSize.height; y++ )
//得到以(x, y)为左上角坐标的所有不同大小、不同类型的HAAR状特征模板
for( int dx = 1; dx <= winSize.width; dx++ )
for( int dy = 1; dy <= winSize.height; dy++ )
// haar_x2
//图1(a)
//判断HAAR状特征矩形模板是否超出了样本图像的边界
if ( (x+dx*2 <= winSize.width) && (y+dy <= winSize.height) )
//得到一个HAAR状特征模板,把它放入表示特征模板变量的features向量队列中,Feature是CvHaarEvaluator类中的一个类,该类在后面有详细介绍
features.push_back( Feature( offset, false,
x, y, dx*2, dy, -1,
x+dx, y, dx , dy, +2 ) );
// haar_y2
//图1(b)
if ( (x+dx <= winSize.width) && (y+dy*2 <= winSize.height) )
features.push_back( Feature( offset, false,
x, y, dx, dy*2, -1,
x, y+dy, dx, dy, +2 ) );
// haar_x3
//图1(e)
if ( (x+dx*3 <= winSize.width) && (y+dy <= winSize.height) )
features.push_back( Feature( offset, false,
x, y, dx*3, dy, -1,
x+dx, y, dx , dy, +3 ) );
// haar_y3
//图1(i)
if ( (x+dx <= winSize.width) && (y+dy*3 <= winSize.height) )
features.push_back( Feature( offset, false,
x, y, dx, dy*3, -1,
x, y+dy, dx, dy, +3 ) );
if( mode != CvHaarFeatureParams::BASIC )
// haar_x4
//图1(f)
if ( (x+dx*4 <= winSize.width) && (y+dy <= winSize.height) )
features.push_back( Feature( offset, false,
x, y, dx*4, dy, -1,
x+dx, y, dx*2, dy, +2 ) );
// haar_y4
//图1(j)
if ( (x+dx <= winSize.width ) && (y+dy*4 <= winSize.height) )
features.push_back( Feature( offset, false,
x, y, dx, dy*4, -1,
x, y+dy, dx, dy*2, +2 ) );
// x2_y2
//图1(n)
if ( (x+dx*2 <= winSize.width) && (y+dy*2 <= winSize.height) )
features.push_back( Feature( offset, false,
x, y, dx*2, dy*2, -1,
x, y, dx, dy, +2,
x+dx, y+dy, dx, dy, +2 ) );
if (mode != CvHaarFeatureParams::BASIC)
//图1(m)
if ( (x+dx*3 <= winSize.width) && (y+dy*3 <= winSize.height) )
features.push_back( Feature( offset, false,
x , y , dx*3, dy*3, -1,
x+dx, y+dy, dx , dy , +9) );
if (mode == CvHaarFeatureParams::ALL)
// tilted haar_x2
//图1(c)
if ( (x+2*dx <= winSize.width) && (y+2*dx+dy <= winSize.height) && (x-dy>= 0) )
features.push_back( Feature( offset, true,
x, y, dx*2, dy, -1,
x, y, dx, dy, +2 ) );
// tilted haar_y2
//图1(d)
if ( (x+dx <= winSize.width) && (y+dx+2*dy <= winSize.height) && (x-2*dy>= 0) )
features.push_back( Feature( offset, true,
x, y, dx, 2*dy, -1,
x, y, dx, dy, +2 ) );
// tilted haar_x3
//图1(g)
if ( (x+3*dx <= winSize.width) && (y+3*dx+dy <= winSize.height) && (x-dy>= 0) )
features.push_back( Feature( offset, true,
x, y, dx*3, dy, -1,
x+dx, y+dx, dx, dy, +3 ) );
// tilted haar_y3
//图1(k)
if ( (x+dx <= winSize.width) && (y+dx+3*dy <= winSize.height) && (x-3*dy>= 0) )
features.push_back( Feature( offset, true,
x, y, dx, 3*dy, -1,
x-dy, y+dy, dx, dy, +3 ) );
// tilted haar_x4
//图1(h)
if ( (x+4*dx <= winSize.width) && (y+4*dx+dy <= winSize.height) && (x-dy>= 0) )
features.push_back( Feature( offset, true,
x, y, dx*4, dy, -1,
x+dx, y+dx, dx*2, dy, +2 ) );
// tilted haar_y4
//图1(l)
if ( (x+dx <= winSize.width) && (y+dx+4*dy <= winSize.height) && (x-4*dy>= 0) )
features.push_back( Feature( offset, true,
x, y, dx, 4*dy, -1,
x-dy, y+dy, dx, 2*dy, +2 ) );
numFeatures = (int)features.size(); //得到HAAR状特征的数量
得到一个HAAR状特征矩形模板:
CvHaarEvaluator::Feature::Feature( int offset, bool _tilted,
int x0, int y0, int w0, int h0, float wt0,
int x1, int y1, int w1, int h1, float wt1,
int x2, int y2, int w2, int h2, float wt2 )
//offset表示偏移量,即积分图像的宽
//_tilted表示是否倾斜
//x0、y0、w0、h0和wt0,x1、y1、w1、h1和wt1以及x2、y2、w2、h2和wt2分别表示HAAR状特征模板内三个不同矩形的左上角坐标、宽、高和它的权重,HAAR状特征模板最多包含3个矩形,其中x2、y2、w2、h2和wt2这5个参数缺省为0,用于表示此时的HAAR状特征模板内只有2个矩形
tilted = _tilted; //赋值,表示该模板是否倾斜
//给HAAR状特征模板内的三个矩形左上角坐标、宽、高和权重赋值
rect[0].r.x = x0;
rect[0].r.y = y0;
rect[0].r.width = w0;
rect[0].r.height = h0;
rect[0].weight = wt0;
rect[1].r.x = x1;
rect[1].r.y = y1;
rect[1].r.width = w1;
rect[1].r.height = h1;
rect[1].weight = wt1;
rect[2].r.x = x2;
rect[2].r.y = y2;
rect[2].r.width = w2;
rect[2].r.height = h2;
rect[2].weight = wt2;
if( !tilted ) //模板不倾斜
//CV_HAAR_FEATURE_MAX等于3,表示HAAR状特征模板内矩形的最多数量
for( int j = 0; j < CV_HAAR_FEATURE_MAX; j++ ) //遍历HAAR状特征内的所有矩形
if( rect[j].weight == 0.0F ) //权值为0表示没有该矩形
break;
//CV_SUM_OFFSETS为宏定义,作用是把模板内的矩形rect的4个顶点坐标转换为以图像左上角为起点开始扫描的位置(如坐标(x,y)在图像的位置是x+y×offset),并把它们赋值到fastRect结构内
CV_SUM_OFFSETS( fastRect[j].p0, fastRect[j].p1, fastRect[j].p2, fastRect[j].p3, rect[j].r, offset )
else //模板倾斜45度
for( int j = 0; j < CV_HAAR_FEATURE_MAX; j++ )
if( rect[j].weight == 0.0F )
break;
//CV_TILTED_OFFSETS为宏定义,作用是把模板内的倾斜矩形rect的4个顶点坐标转换为以图像左上角为起点开始扫描的位置(如坐标(x,y)在图像的位置是x+y×offset),并把它们赋值到fastRect结构内
CV_TILTED_OFFSETS( fastRect[j].p0, fastRect[j].p1, fastRect[j].p2, fastRect[j].p3, rect[j].r, offset )
inline float CvHaarEvaluator::operator()(int featureIdx, int sampleIdx) const
//计算第sampleIdx个样本图像的第featureIdx个特征的特征值
float nf = normfactor.at<float>(0, sampleIdx); //提取出该样本图像的归一化因子
//调用calc函数,计算特征值,并进行归一化处理
return !nf ? 0.0f : (features[featureIdx].calc( sum, tilted, sampleIdx)/nf);
计算HAAR状特征的特征值:
inline float CvHaarEvaluator::Feature::calc( const cv::Mat &_sum, const cv::Mat &_tilted, size_t y) const
//_sum表示第y个图像的积分图像
//_tilted表示第y个图像的旋转积分图像
//如果是计算倾斜的HAAR状特征的特征值,则img为_tilted,否则为_sum
const int* img = tilted ? _tilted.ptr<int>((int)y) : _sum.ptr<int>((int)y);
//计算HAAR状特征中的由矩形0和矩形1组成的特征的特征值
float ret = rect[0].weight * (img[fastRect[0].p0] - img[fastRect[0].p1] - img[fastRect[0].p2] + img[fastRect[0].p3] ) +
rect[1].weight * (img[fastRect[1].p0] - img[fastRect[1].p1] - img[fastRect[1].p2] + img[fastRect[1].p3] );
//如果HAAR状特征还有第3个矩形,则把该矩形也计算在内,一起构成特征的特征值
if( rect[2].weight != 0.0f )
ret += rect[2].weight * (img[fastRect[2].p0] - img[fastRect[2].p1] - img[fastRect[2].p2] + img[fastRect[2].p3] );
return ret; //返回特征值
LBP特征:
void CvLBPEvaluator::setImage(const Mat &img, uchar clsLabel, int idx)
//sum表示所有样本图像的积分图像,它在init函数中定义,这里再次确认该变量是否定义好
CV_DbgAssert( !sum.empty() );
//调用父类的setImage函数,确定图像img尺寸大小是否正确,并赋值类别标签
CvFeatureEvaluator::setImage( img, clsLabel, idx );
Mat innSum(winSize.height + 1, winSize.width + 1, sum.type(), sum.ptr<int>((int)idx));
integral( img, innSum ); //计算当前图像img的积分图像
void CvLBPEvaluator::generateFeatures()
int offset = winSize.width + 1; //得到偏移量,即积分图像的宽
//得到LBP特征,(x, y)为图7中p0点的坐标
for( int x = 0; x < winSize.width; x++ )
for( int y = 0; y < winSize.height; y++ )
//w和h为图7中区域0(当然也是其他区域)的宽和高
for( int w = 1; w <= winSize.width / 3; w++ )
for( int h = 1; h <= winSize.height / 3; h++ )
//判断LBP模板是否超出了样本图像的边界
if ( (x+3*w <= winSize.width) && (y+3*h <= winSize.height) )
//得到一个LBP特征,把它放入表示特征变量的features向量队列中,Feature是CvLBPEvaluator类中的一个类,该类在后面有详细介绍
features.push_back( Feature(offset, x, y, w, h ) );
numFeatures = (int)features.size(); //得到LBP特征的数量
得到一个LBP特征:
CvLBPEvaluator::Feature::Feature( int offset, int x, int y, int _blockWidth, int _blockHeight )
Rect tr = rect = cvRect(x, y, _blockWidth, _blockHeight); //给出区域0
//得到p0,p1,p4和p5的坐标的相对位置
CV_SUM_OFFSETS( p[0], p[1], p[4], p[5], tr, offset )
tr.x += 2*rect.width; //得到区域2
//得到p2,p3,p6和p7的坐标的相对位置
CV_SUM_OFFSETS( p[2], p[3], p[6], p[7], tr, offset )
tr.y +=2*rect.height; //得到区域8
//得到p10,p11,p14和p15的坐标的相对位置
CV_SUM_OFFSETS( p[10], p[11], p[14], p[15], tr, offset )
tr.x -= 2*rect.width; //得到区域6
//得到p8,p9,p12和p13的坐标的相对位置
CV_SUM_OFFSETS( p[8], p[9], p[12], p[13], tr, offset )
virtual float operator()(int featureIdx, int sampleIdx) const
//计算第sampleIdx个样本图像的第featureIdx个特征的特征值
return (float)features[featureIdx].calc( sum, sampleIdx); //调用calc函数
计算一个LBP特征的特征值:
inline uchar CvLBPEvaluator::Feature::calc(const cv::Mat &_sum, size_t y) const
const int* psum = _sum.ptr<int>((int)y); //得到第y个图像的积分图像
//得到区域4(即中心区域)的区域值
int cval = psum[p[5]] - psum[p[6]] - psum[p[9]] + psum[p[10]];
//计算特征值,8个区域分布对应于8位二进制编码的一位
//区域0的值对应于二进制LBP编码的值
return (uchar)((psum[p[0]] - psum[p[1]] - psum[p[4]] + psum[p[5]] >= cval ? 128 : 0) | // 0
//区域1的值对应于二进制LBP编码的值
(psum[p[1]] - psum[p[2]] - psum[p[5]] + psum[p[6]] >= cval ? 64 : 0) | // 1
//区域2的值对应于二进制LBP编码的值
(psum[p[2]] - psum[p[3]] - psum[p[6]] + psum[p[7]] >= cval ? 32 : 0) | // 2
//区域5的值对应于二进制LBP编码的值
(psum[p[6]] - psum[p[7]] - psum[p[10]] + psum[p[11]] >= cval ? 16 : 0) | // 5
//区域8的值对应于二进制LBP编码的值
(psum[p[10]] - psum[p[11]] - psum[p[14]] + psum[p[15]] >= cval ? 8 : 0) | // 8
//区域7的值对应于二进制LBP编码的值
(psum[p[9]] - psum[p[10]] - psum[p[13]] + psum[p[14]] >= cval ? 4 : 0) | // 7
//区域6的值对应于二进制LBP编码的值
(psum[p[8]] - psum[p[9]] - psum[p[12]] + psum[p[13]] >= cval ? 2 : 0) | // 6
//区域3的值对应于二进制LBP编码的值
(psum[p[4]] - psum[p[5]] - psum[p[8]] + psum[p[9]] >= cval ? 1 : 0)); // 3
HOG特征:
void CvHOGEvaluator::setImage(const Mat &img, uchar clsLabel, int idx)
//hist表示所有样本图像的梯度方向直方图,每个cell都有9个不同梯度方向的hist,表示cell内像素的梯度方向被分配到不同的bin中,hist在init函数中定义,这里再次确认该变量是否定义好
CV_DbgAssert( !hist.empty());
//调用父类的setImage函数,确定图像img尺寸大小是否正确,并赋值类别标签
CvFeatureEvaluator::setImage( img, clsLabel, idx );
vector<Mat> integralHist; //表示梯度方向直方图的积分图像
for (int bin = 0; bin < N_BINS; bin++) //遍历9个不同的bin,N_BINS为9
//把表示图像img的hist放到integralHist相量队列中
integralHist.push_back( Mat(winSize.height + 1, winSize.width + 1, hist[bin].type(), hist[bin].ptr<float>((int)idx)) );
//定义integralNorm,表示梯度幅度的积分图像
Mat integralNorm(winSize.height + 1, winSize.width + 1, normSum.type(), normSum.ptr<float>((int)idx));
//计算图像img的梯度方向bin和梯度幅度的积分图像
integralHistogram(img, integralHist, integralNorm, (int)N_BINS);
分别计算图像img的梯度方向bin和梯度幅度的积分图像——histogram和norm:
void CvHOGEvaluator::integralHistogram(const Mat &img, vector<Mat> &histogram, Mat &norm, int nbins) const
//确保样本图像的数据类型为CV_8U或CV_8UC3
CV_Assert( img.type() == CV_8U || img.type() == CV_8UC3 );
int x, y, binIdx;
Size gradSize(img.size()); //得到样本图像的尺寸
//得到某一bin的尺寸,也就相当于是样本图像的尺寸
Size histSize(histogram[0].size());
Mat grad(gradSize, CV_32F); //表示样本图像像素的梯度幅度
Mat qangle(gradSize, CV_8U); //表示样本图像像素的梯度角度的bin索引值
AutoBuffer<int> mapbuf(gradSize.width + gradSize.height + 4); //开辟一块内存空间
//分别表示图像行和列的映射
int* xmap = (int*)mapbuf + 1;
int* ymap = xmap + gradSize.width + 2;
//BORDER_REPLICATE为1,表示通过复制的方式映射图像的行和列
const int borderType = (int)BORDER_REPLICATE;
//得到图像行和列的映射值
for( x = -1; x < gradSize.width + 1; x++ )
xmap[x] = borderInterpolate(x, gradSize.width, borderType);
for( y = -1; y < gradSize.height + 1; y++ )
ymap[y] = borderInterpolate(y, gradSize.height, borderType);
int width = gradSize.width;
AutoBuffer<float> _dbuf(width*4); //开辟一块内存空间
float* dbuf = _dbuf; //指针赋值
Mat Dx(1, width, CV_32F, dbuf); //表示样本图像的某一行像素的水平梯度Gx
Mat Dy(1, width, CV_32F, dbuf + width); //表示样本图像的某一行像素的垂直梯度Gy
Mat Mag(1, width, CV_32F, dbuf + width*2); //表示样本图像的某一行像素的梯度幅度
Mat Angle(1, width, CV_32F, dbuf + width*3); //表示样本图像的某一行像素的梯度角
//表示每一bin所表示的角度范围的倒数,为9/π
float angleScale = (float)(nbins/CV_PI);
for( y = 0; y < gradSize.height; y++ ) //遍历样本图像的行
const uchar* currPtr = img.data + img.step*ymap[y]; //表示当前行
const uchar* prevPtr = img.data + img.step*ymap[y-1]; //表示前一行
const uchar* nextPtr = img.data + img.step*ymap[y+1]; //表示后一行
float* gradPtr = (float*)grad.ptr(y); //梯度幅度矩阵的首地址
uchar* qanglePtr = (uchar*)qangle.ptr(y); //梯度角度的bin矩阵的首地址
for( x = 0; x < width; x++ ) //遍历当前行的所有像素
//得到当前像素的水平导数,式5的前一项
dbuf[x] = (float)(currPtr[xmap[x+1]] - currPtr[xmap[x-1]]);
//得到当前像素的垂直导数,式5的后一项
dbuf[width + x] = (float)(nextPtr[xmap[x]] - prevPtr[xmap[x]]);
//通过调用cartToPolar函数(直角坐标转换为极坐标),得到当前行的所有像素的梯度幅度和梯度角度,即式6和式7
cartToPolar( Dx, Dy, Mag, Angle, false );
for( x = 0; x < width; x++ ) //遍历当前行的所有像素
float mag = dbuf[x+width*2]; //得到当前像素的梯度幅度
float angle = dbuf[x+width*3]; //得到当前像素的梯度角度
angle = angle*angleScale - 0.5f; //即θ×9/π
//向下取整,得到当前像素的梯度角所属的bin索引
int bidx = cvFloor(angle);
angle -= bidx;
//把梯度角限制在0~π范围内
if( bidx < 0 ) //说明梯度角在-π~0之间
bidx += nbins; //调整到0~π范围内
else if( bidx >= nbins ) //说明梯度角在π~2π之间
bidx -= nbins; //调整到0~π范围内
qanglePtr[x] = (uchar)bidx; //给梯度角度的bin赋值
gradPtr[x] = mag; //给梯度幅度赋值
//对梯度幅度图像grad进行积分图像处理,结果为norm
integral(grad, norm, grad.depth());
float* histBuf; //表示直方图
const float* magBuf; //表示梯度幅度
const uchar* binsBuf; //表示bin
//分别得到不同数据的步长
int binsStep = (int)( qangle.step / sizeof(uchar) );
int histStep = (int)( histogram[0].step / sizeof(float) );
int magStep = (int)( grad.step / sizeof(float) );
for( binIdx = 0; binIdx < nbins; binIdx++ ) //遍历所有bin
histBuf = (float*)histogram[binIdx].data; //得到当前bin的直方图
magBuf = (const float*)grad.data; //得到样本图像的梯度幅度
binsBuf = (const uchar*)qangle.data; //得到样本图像的梯度角度的bin
memset( histBuf, 0, histSize.width * sizeof(histBuf[0]) ); //直方图第一行清零
histBuf += histStep + 1; //第二行
for( y = 0; y < qangle.rows; y++ ) //遍历所有的行
histBuf[-1] = 0.f; //行首元素赋值为0
float strSum = 0.f; //用于表示当前行的幅度累加
for( x = 0; x < qangle.cols; x++ ) //遍历当前行的所有像素
if( binsBuf[x] == binIdx ) //如果当前像素的bin等于当前遍历的bin
strSum += magBuf[x]; //当前行的在x之前的像素梯度幅度累加
//得到属于当前bin的直方图的幅度积分图像
histBuf[x] = histBuf[-histStep + x] + strSum;
//分别指向下一行
histBuf += histStep;
binsBuf += binsStep;
magBuf += magStep;
void CvHOGEvaluator::generateFeatures()
int offset = winSize.width + 1; //得到偏移量,即积分图像的宽
Size blockStep;
int x, y, t, w, h;
//遍历不同大小的cell,t表示cell的尺寸,它的变化趋势为8,16,32……
for (t = 8; t <= winSize.width/2; t+=8) //t = size of a cell. blocksize = 4*cellSize
//表示block在样本图像的扫描步长,即每隔4个像素移动一次block
blockStep = Size(4,4);
//此时cell的大小为t×t,而block的大小为2t×2t
w = 2*t; //width of a block
h = 2*t; //height of a block
//以blockStep为步长扫描样本图像
for (x = 0; x <= winSize.width - w; x += blockStep.width)
for (y = 0; y <= winSize.height - h; y += blockStep.height)
//得到一个HOG特征,即一个block
features.push_back(Feature(offset, x, y, t, t));
//此时cell的大小为t×2t,而block的大小为2t×4t
w = 2*t;
h = 4*t;
for (x = 0; x <= winSize.width - w; x += blockStep.width)
for (y = 0; y <= winSize.height - h; y += blockStep.height)
features.push_back(Feature(offset, x, y, t, 2*t));
//此时cell的大小为2t×t,而block的大小为4t×2t
w = 4*t;
h = 2*t;
for (x = 0; x <= winSize.width - w; x += blockStep.width)
for (y = 0; y <= winSize.height - h; y += blockStep.height)
features.push_back(Feature(offset, x, y, 2*t, t));
numFeatures = (int)features.size(); //HOG特征的数量
得到一个HOG特征:
CvHOGEvaluator::Feature::Feature( int offset, int x, int y, int cellW, int cellH )
//4个cell组成1个block
rect[0] = Rect(x, y, cellW, cellH); //cell0
rect[1] = Rect(x+cellW, y, cellW, cellH); //cell1
rect[2] = Rect(x, y+cellH, cellW, cellH); //cell2
rect[3] = Rect(x+cellW, y+cellH, cellW, cellH); //cell3
for (int i = 0; i < N_CELLS; i++) //遍历1个block内的4个cell
//得到rect[i]所表示的cell矩形的4个顶点的相对左上角像素的位置
CV_SUM_OFFSETS(fastRect[i].p0, fastRect[i].p1, fastRect[i].p2, fastRect[i].p3, rect[i], offset);
与HAAR和LBP不同,HOG的( )重载运算符的第一个输入参数为特征变量索引,HOG的一个特征就是一个block,而特征变量是指block中的某个cell的某个bin的值:
inline float CvHOGEvaluator::operator()(int varIdx, int sampleIdx) const
//得到该特征变量varIdx索引所对应的特征block索引
int featureIdx = varIdx / (N_BINS * N_CELLS); //求商,N_BINS为9,N_CELLS为4
int componentIdx = varIdx % (N_BINS * N_CELLS); //求余数
//return features[featureIdx].calc( hist, sampleIdx, componentIdx);
//得到特征变量索引varIdx所对应的值
return features[featureIdx].calc( hist, normSum, sampleIdx, componentIdx);
计算某个特征block内第featComponent个特征成分的值:
inline float CvHOGEvaluator::Feature::calc( const std::vector<cv::Mat>& _hists, const cv::Mat& _normSum, size_t y, int featComponent ) const
float normFactor;
float res;
int binIdx = featComponent % N_BINS; //得到featComponent所对应的bin索引
int cellIdx = featComponent / N_BINS; //得到featComponent所对应的cell索引
//得到第y个样本图像梯度幅度的第binIdx个bin的直方图的积分图像
const float *phist = _hists[binIdx].ptr<float>((int)y);
//得到当前特征block内第cellIdx个cell内第binIdx个bin的所有像素梯度幅度之和
res = phist[fastRect[cellIdx].p0] - phist[fastRect[cellIdx].p1] - phist[fastRect[cellIdx].p2] + phist[fastRect[cellIdx].p3];
//得到第y个样本图像的梯度幅度的积分图像
const float *pnormSum = _normSum.ptr<float>((int)y);
//得到当前特征block内所有像素的梯度幅度之和
normFactor = (float)(pnormSum[fastRect[0].p0] - pnormSum[fastRect[1].p1] - pnormSum[fastRect[2].p2] + pnormSum[fastRect[3].p3]);
//归一化
res = (res > 0.001f) ? ( res / (normFactor + 0.001f) ) : 0.f; //for cutting negative values, which apper due to floating precision
//返回当前特征block内的第featComponent个特征成分的归一化的值
return res;
下面介绍级联分类器的强分类器的程序部分。
表示级联分类器的每级AdaBoost强分类器的参数结构为:
struct CvCascadeBoostParams : CvBoostParams
//识别率和错误率的定义见原理部分
float minHitRate; //强分类器的最小识别率
float maxFalseAlarm; //强分类器的最大错误率
CvCascadeBoostParams();
CvCascadeBoostParams( int _boostType, float _minHitRate, float _maxFalseAlarm,
double _weightTrimRate, int _maxDepth, int _maxWeakCount );
virtual ~CvCascadeBoostParams()
//表示向xml文件写入一些内容:弱分类器的数量、阈值、用到的特征
void write( cv::FileStorage &fs ) const;
//表示从xml文件内读取一些内容:弱分类器的数量、阈值、用到的特征
bool read( const cv::FileNode &node );
virtual void printDefaults() const; //向终端输出一些必要的信息
virtual void printAttrs() const; //向终端输出一些用到的参变量
//用于扫描执行训练样本命令时所附带的一些参数,从而为CvCascadeBoostParams赋值
virtual bool scanAttr( const std::string prmName, const std::string val);
;
AdaBoost强分类器的类CvCascadeBoost中的训练样本的函数train:
bool CvCascadeBoost::train( const CvFeatureEvaluator* _featureEvaluator,
int _numSamples,
int _precalcValBufSize, int _precalcIdxBufSize,
const CvCascadeBoostParams& _params )
//_featureEvaluator表示特征评估
//_numSamples表示训练样本数
//_precalcValBufSize表示缓存大小,用于存储预先计算的特征值,单位为MB
//_precalcIdxBufSize表示缓存大小,用于存储预先计算的特征索引,单位为MB
//_params表示AdaBoost所需的参数
bool isTrained = false; //表示该函数的返回标志变量
CV_Assert( !data ); //确保训练样本数据变量准备好
clear(); //释放一些全局变量
//实例化CvCascadeBoostTrainData结构,主要是调用CvCascadeBoostTrainData::setData函数设置AdaBoost强分类器的数据,在setData函数内还调用了precalculate函数,它的作用是提前并行计算所有训练图像的特征值,并存储在valCache矩阵中,提前计算特征值的数量由输入参数_precalcValBufSize决定,而那些没有计算完的特征值是在构建决策树时调用CvCascadeBoostTrainData::get_ord_var_data函数(HAAR状特征和HOG特征调用此函数)或CvCascadeBoostTrainData::get_cat_var_data函数(LBP特征调用此函数)来完成,因此原则上_precalcValBufSize和_precalcIdxBufSize值越大,真正用于构建决策树的时间就越短
data = new CvCascadeBoostTrainData( _featureEvaluator, _numSamples,
_precalcValBufSize, _precalcIdxBufSize, _params );
CvMemStorage *storage = cvCreateMemStorage(); //开辟一块内存空间
//weak表示弱分类器队列,用于存放AdaBoost强分类器的各个弱分类器
weak = cvCreateSeq( 0, sizeof(CvSeq), sizeof(CvBoostTree*), storage );
storage = 0;
set_params( _params ); //设置CvCascadeBoostParams参数
//如果AdaBoost强分类器的类型为Logit AdaBoost或Gentle AdaBoost,则需要生成一个样本响应值的副本
if ( (_params.boost_type == LOGIT) || (_params.boost_type == GENTLE) )
data->do_responses_copy();
//这里是第一次调用update_weights()函数,它的作用是初始化权值
update_weights( 0 );
cout << "+----+---------+---------+" << endl;
cout << "| N | HR | FA |" << endl;
cout << "+----+---------+---------+" << endl;
do //进入AdaBoost迭代中
//实例化CvCascadeBoostTree类,tree表示弱分类器,也就是一个决策树
CvCascadeBoostTree* tree = new CvCascadeBoostTree;
//训练决策树,CvCascadeBoostTree类没有实现train函数,则调用它的父类CvBoostTree中的train函数
if( !tree->train( data, subsample_mask, this ) )
//如果没有得到该次迭代的决策树,则退出迭代循环
delete tree; //删除该弱分类器
break; //退出循环
cvSeqPush( weak, &tree ); //把决策树放入弱分类器队列中
update_weights( tree ); //更新样本权值,用于下次迭代循环
//裁剪去掉那些权值太小的训练样本数据,权值越小,该样本被分类错误的可能性就越低
trim_weights();
//计算样本中没有被置1的数量,如果为零,则说明下次迭代将没有训练样本
if( cvCountNonZero(subsample_mask) == 0 )
break; //退出迭代
//如果错误率满足了要求,或者达到了决策树的最大数量,则退出迭代循环
while( !isErrDesired() && (weak->total < params.weak_count) );
if(weak->total > 0) //得到了AdaBoost强分类器
data->is_classifier = true; //重新赋值,表示是分类树
data->free_train_data(); //释放一些矩阵变量
isTrained = true; //标志变量
else //没有得到AdaBoost强分类器
clear();
return isTrained; //返回标志变量
计算错误率:
bool CvCascadeBoost::isErrDesired()
int sCount = data->sample_count, //训练样本的数量
//numPos和numNeg分别表示正、负样本的数量,numPosTrue表示正样本中被正确分类的数量,numFalse表示负样本中被错误分类为正样本的数量
numPos = 0, numNeg = 0, numFalse = 0, numPosTrue = 0;
vector<float> eval(sCount);
for( int i = 0; i < sCount; i++ ) //遍历所有样本
//得到正训练样本集
if( ((CvCascadeBoostTrainData*)data)->featureEvaluator->getCls( i ) == 1.0F )
//得到当前样本的预测结果,并且正样本数量累计
eval[numPos++] = predict( i, true );
icvSortFlt( &eval[0], numPos, 0 ); //对正样本的预测结果按从小到大进行排序
//计算识别率对应于正样本的阈值索引,
int thresholdIdx = (int)((1.0F - minHitRate) * numPos);
threshold = eval[ thresholdIdx ]; //得到识别率对应的阈值
numPosTrue = numPos - thresholdIdx; //得到分类正确的正样本的数量
//把那些样本预测值与阈值十分接近的样本也算作是分类正确的样本
for( int i = thresholdIdx - 1; i >= 0; i--)
if ( abs( eval[i] - threshold) < FLT_EPSILON )
numPosTrue++;
float hitRate = ((float) numPosTrue) / ((float) numPos); //计算当前强分类器的识别率
for( int i = 0; i < sCount; i++ ) //遍历所有样本
//得到负训练样本集
if( ((CvCascadeBoostTrainData*)data)->featureEvaluator->getCls( i ) == 0.0F )
numNeg++; //负样本数量累计
if( predict( i ) ) //负样本被预测为正样本
numFalse++; //错误分类的负样本数量累计
float falseAlarm = ((float) numFalse) / ((float) numNeg); //计算当前强分类器的错误率
//在终端输出信息
cout << "|"; cout.width(4); cout << right << weak->total; //弱分类器数量
cout << "|"; cout.width(9); cout << right << hitRate; //识别率
cout << "|"; cout.width(9); cout << right << falseAlarm; //错误率
cout << "|" << endl;
cout << "+----+---------+---------+" << endl;
//当前错误率与设置的错误率比较,前者大则返回false,否则true
return falseAlarm <= maxFalseAlarm;
最后,我们给出最终构成的级联分类器。
级联分类器所需参数的类——CvCascadeParams:
class CvCascadeParams : public CvParams
public:
//表示级联分类器每个级的类型,即强分类器的类型,目前只实现了强分类器是AdaBoost这一种类型
enum BOOST = 0 ;
static const int defaultStageType = BOOST;
//表示特征类型,缺省的类型是HAAR
static const int defaultFeatureType = CvFeatureParams::HAAR;
CvCascadeParams(); //缺省构造函数
//构造函数,_stageType表示强分类器类型,_featureType表示特征类型
CvCascadeParams( int _stageType, int _featureType );
//表示向params.xml文件写入一些内容:强分类器类型,特征类型,正样本图像的宽、高
void write( cv::FileStorage &fs ) const;
//表示从params.xml文件内读取一些内容:强分类器类型,特征类型,正样本图像的宽、高
bool read( const cv::FileNode &node );
void printDefaults() const; //向终端输出一些必要的信息
//向终端输出强分类器类型,特征类型,正样本图像的宽、高等信息
void printAttrs() const;
//用于扫描执行训练样本命令时所附带的一些参数,从而为CvCascadeParams赋值
bool scanAttr( const std::string prmName, const std::string val );
int stageType; //级联分类器的强分类器类型
int featureType; //级联分类器的特征类型
cv::Size winSize; //正样本图像的尺寸大小
;
级联分类器的类CvCascadeClassifier中最重要的函数是train:
bool CvCascadeClassifier::train( const string _cascadeDirName,
const string _posFilename,
const string _negFilename,
int _numPos, int _numNeg,
int _precalcValBufSize, int _precalcIdxBufSize,
int _numStages,
const CvCascadeParams& _cascadeParams,
const CvFeatureParams& _featureParams,
const CvCascadeBoostParams& _stageParams,
bool baseFormatSave )
//_cascadeDirName表示文件目录名,用于存放训练好的级联分类器的xml文件
//_posFilename和_negFilename分别表示正、负样本的文件名
//_numPos和_numNeg分别表示每级分类器的正、负训练样本集的数量
//_precalcValBufSize表示缓存大小,用于存储预先计算的特征值,单位为MB
//_precalcIdxBufSize表示缓存大小,用于存储预先计算的特征值的索引,单位为MB
//_numStages表示级联分类器的级数
//_cascadeParams、_featureParams和_stageParams分别表示级联、特征和AdaBoost所需的参数
//baseFormatSave表示级联分类器文件的存储格式类型,如果为false,则以老的格式存储用HAAR特征训练的级联分类器,否则是以新的格式存储用LBP和HOG特征训练的级联分类器
// Start recording clock ticks for training time output
const clock_t begin_time = clock(); //读取当前的系统时间,用于计时
//判断这三个输入参数是否存在
if( _cascadeDirName.empty() || _posFilename.empty() || _negFilename.empty() )
CV_Error( CV_StsBadArg, "_cascadeDirName or _bgfileName or _vecFileName is NULL" );
//得到级联分类器文件所在的目录名
string dirName;
if (_cascadeDirName.find_last_of("/\\\\") == (_cascadeDirName.length() - 1) )
dirName = _cascadeDirName;
else
dirName = _cascadeDirName + '/';
//赋值
numPos = _numPos;
numNeg = _numNeg;
numStages = _numStages;
//读取存有正、负样本的文件,得到一些信息,如正样本的数量,正样本图像的大小等
if ( !imgReader.create( _posFilename, _negFilename, _cascadeParams.winSize ) )
cout << "Image reader can not be created from -vec " << _posFilename
<< " and -bg " << _negFilename << "." << endl;
return false;
//加载在此次训练之前已生成的级联分类器,如果现在还没有级联分类器,则进入if语句,为一些变量赋值
if ( !load( dirName ) )
cascadeParams = _cascadeParams; //级联参数
//由特征类型(HAAR、LBP还是HOG),得到相应的特征参数类
featureParams = CvFeatureParams::create(cascadeParams.featureType);
featureParams->init(_featureParams); //初始化特征参数
stageParams = new CvCascadeBoostParams; //实例化AdaBoost参数类
*stageParams = _stageParams; //赋值
//由特征类型(HAAR、LBP还是HOG),得到相应的特征评估类
featureEvaluator = CvFeatureEvaluator::create(cascadeParams.featureType);
//初始化特征评估类,这里关键是调用了前面介绍过的generateFeatures函数,得到了不同的特征
featureEvaluator->init( (CvFeatureParams*)featureParams, numPos + numNeg, cascadeParams.winSize );
//为级联分类器相量队列预留numStages个空间大小
stageClassifiers.reserve( numStages );
//在终端显示一些信息
cout << "PARAMETERS:" << endl;
cout << "cascadeDirName: " << _cascadeDirName << endl;
cout << "vecFileName: " << _posFilename << endl;
cout << "bgFileName: " << _negFilename << endl;
cout << "numPos: " << _numPos << endl;
cout << "numNeg: " << _numNeg << endl;
cout << "numStages: " << numStages << endl;
cout << "precalcValBufSize[Mb] : " << _precalcValBufSize << endl;
cout << "precalcIdxBufSize[Mb] : " << _precalcIdxBufSize << endl;
cascadeParams.printAttrs();
stageParams->printAttrs();
featureParams->printAttrs();
//得到已有的级联分类器的级别数量
int startNumStages = (int)stageClassifiers.size();
//在终端显示目前级联分类器的级别数量
if ( startNumStages > 1 )
cout << endl << "Stages 0-" << startNumStages-1 << " are loaded" << endl;
else if ( startNumStages == 1)
cout << endl << "Stage 0 is loaded" << endl;
//得到级联分类器的最大错误率,它等于所有强分类器错误率的乘积,再除以决策树(弱分类器)的深度
double requiredLeafFARate = pow( (double) stageParams->maxFalseAlarm, (double) numStages ) /
(double)stageParams->max_depth;
double tempLeafFARate; //表示当前得到的级联分类器的错误率
//训练级联分类器的各个级的强分类器
for( int i = startNumStages; i < numStages; i++ )
cout << endl << "===== TRAINING " << i << "-stage =====" << endl;
cout << "<BEGIN" << endl;
//updateTrainingSet函数用于得到当前要训练的强分类器的正、负训练样本集,并且调用setImage函数得到这些样本图像的积分图像,还得到了当前级联分类器的错误率
//当前强分类器的正训练样本集是用当前级联分类器从正样本集中,预测正确的那些样本构成,而负训练样本集则是用当前级联分类器从负样本集中,预测错误(即预测为正样本)的那些负样本构成
if ( !updateTrainingSet( tempLeafFARate ) )
cout << "Train dataset for temp stage can not be filled. "
"Branch training terminated." << endl;
break;
//如果当前级联分类器的错误率满足要求,则退出训练迭代
if( tempLeafFARate <= requiredLeafFARate )
cout << "Required leaf false alarm rate achieved. "
"Branch training terminated." << endl;
break;
//实例化CvCascadeBoost,表示当前要训练的AdaBoost强分类器
CvCascadeBoost* tempStage = new CvCascadeBoost;
//训练级联分类器的AdaBoost强分类器
bool isStageTrained = tempStage->train( (CvFeatureEvaluator*)featureEvaluator,
curNumSamples, _precalcValBufSize, _precalcIdxBufSize,
*((CvCascadeBoostParams*)stageParams) );
cout << "END>" << endl;
if(!isStageTrained) //如果训练不成功,则退出循环
break;
//把当前训练得到的AdaBoost强分类器添加进级联分类器内
stageClassifiers.push_back( tempStage );
// save params
//i等于0表示还没有为级联分类器建立任何xml存储文件,因此需要建立文件用于保存级联分类器
if( i == 0)
//params.xml文件,用于存储级联分类器的参数信息,如强分类器的类别,特征类别,正样本图像的高和宽,AdaBoost类型,最小识别率,最大错误率,决策树的深度,弱分类器的数量等
std::string paramsFilename = dirName + CC_PARAMS_FILENAME;
FileStorage fs( paramsFilename, FileStorage::WRITE); //定义该文件
if ( !fs.isOpened() ) //打开文件
cout << "Parameters can not be written, because file " << paramsFilename
<< " can not be opened." << endl;
return false;
fs << FileStorage::getDefaultObjectName(paramsFilename) << "";
writeParams( fs ); //写入参数信息
fs << "";
// save current stage
//保存当前级的AdaBoost强分类器
char buf[10];
sprintf(buf, "%s%d", "stage", i );
//文件名为stage?.xml,其中?代表级联分类器的级数,如stage0.xml、stage1.xml等,即每一个强分类器就有一个这样的xml文件
string stageFilename = dirName + buf + ".xml";
FileStorage fs( stageFilename, FileStorage::WRITE ); //定义该文件
if ( !fs.isOpened() ) //打开该文件
cout << "Current stage can not be written, because file " << stageFilename
<< " can not be opened." << endl;
return false;
fs << FileStorage::getDefaultObjectName(stageFilename) << ""以上是关于Opencv2.4.9源码分析——Cascade Classification的主要内容,如果未能解决你的问题,请参考以下文章
Opencv2.4.9源码分析——Cascade Classification
Opencv2.4.9源码分析——Cascade Classification