上一篇文章我们介绍了级联分类器的原理,下面我们给出级联分类器的源码分析。
训练级联分类器的源码在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) << "{";
tempStage->write( fs, Mat() ); //写入AdaBoost强分类器信息
fs << "}";
// Output training time up till now
//得到训练时间
float seconds = float( clock () - begin_time ) / CLOCKS_PER_SEC;
int days = int(seconds) / 60 / 60 / 24;
int hours = (int(seconds) / 60 / 60) % 24;
int minutes = (int(seconds) / 60) % 60;
int seconds_left = int(seconds) % 60;
cout << "Training until now has taken " << days << " days " << hours << " hours " << minutes << " minutes " << seconds_left <<" seconds." << endl; //向终端输出训练时间
} //级联分类器训练完毕
if(stageClassifiers.size() == 0) //如果没有得到级联分类器
{
cout << "Cascade classifier can't be trained. Check the used training parameters." << endl;
return false;
}
//把前面得到的params.xml、stage0.xml、stage1.xml、……文件整合为一个cascade.xml文件,该文件就是训练后最终得到xml文件
save( dirName + CC_CASCADE_FILENAME, baseFormatSave );
return true;
}
在训练AdaBoost强分类器之前,需更新正、负训练样本集:
bool CvCascadeClassifier::updateTrainingSet( double& acceptanceRatio)
//acceptanceRatio表示接受率,也就是级联分类器的错误率
{
//posConsumed和negConsumed分别表示正、负样本所消耗的数量,也就是说从这么多个样本中,才能得到本次用于训练的正、负样本
int64 posConsumed = 0, negConsumed = 0;
imgReader.restart(); //启动图像阅读器,即开始选择样本图像
//posCount为本次真正用于训练的正样本的数量,fillPassedSamples函数见后面分析
int posCount = fillPassedSamples( 0, numPos, true, posConsumed );
if( !posCount ) //没有得到正训练样本集,则退出
return false;
cout << "POS count : consumed " << posCount << " : " << (int)posConsumed << endl;
//计算需要的负样本数,它等于每级应该有的负样本数乘以真正的每级正样本数与每级应该有的正样本数之比,即保持了选取训练样本的正负样本比例不变
int proNumNeg = cvRound( ( ((double)numNeg) * ((double)posCount) ) / numPos ); // apply only a fraction of negative samples. double is required since overflow is possible
//negCount为本次真正用于训练的负样本的数量,即从negConsumed个负样本中得到了negCount个预测错误的样本
int negCount = fillPassedSamples( posCount, proNumNeg, false, negConsumed );
if ( !negCount )
return false;
//得到当前正、负训练样本的总数
curNumSamples = posCount + negCount;
//计算负样本的接受率,也就是级联分类器的错误率,表示负样本预测错误的比例
acceptanceRatio = negConsumed == 0 ? 0 : ( (double)negCount/(double)(int64)negConsumed );
cout << "NEG count : acceptanceRatio " << negCount << " : " << acceptanceRatio << endl;
return true;
}
选取样本fillPassedSamples函数:
int CvCascadeClassifier::fillPassedSamples( int first, int count, bool isPositive, int64& consumed )
//first表示选取样本的起始点
//count表示应该选取的样本数
//isPositive为true表示选取正样本,false表示选取负样本
//consumed表示消耗的样本数量
{
int getcount = 0; //表示真正用于训练的样本数
Mat img(cascadeParams.winSize, CV_8UC1); //定义图像
for( int i = first; i < first + count; i++ ) //开始选取
{
for( ; ; ) //死循环
{
//由isPositive的不同,得到一个正样本图像,或一个负样本图像
bool isGetImg = isPositive ? imgReader.getPos( img ) :
imgReader.getNeg( img );
if( !isGetImg ) //如果没有得到样本,则退出该函数
return getcount;
consumed++; //累加
//得到img图像的积分图像或直方图,该函数在前面已给出了详细的介绍
featureEvaluator->setImage( img, isPositive ? 1 : 0, i );
//利用已经得到的级联分类器,预测此次选取图像的类别,当预测的结果为正样本时,则进入该if语句,即表示该样本通过了当前的级联分类器,可以用于下一个强分类器的训练。如果fillPassedSamples函数是用于对正样本的选取,则进入该if语句表明预测正确;如果fillPassedSamples函数是用于对负样本的选取,则进入该if语句表明预测错误
//这里的预测predict函数比较简单,因为预测图像和样本图像的大小是相同的,只需依次通过级联分类器、AdaBoost强分类器、决策树的预测即可
if( predict( i ) == 1.0F )
{
getcount++; //累加
printf("%s current samples: %d\r", isPositive ? "POS":"NEG", getcount);
break; //退出死循环
}
}
}
return getcount; //返回
}
以上是级联分类器的训练源码。当级联分类器训练好后,就会得到一个cascade.xml文件,我们利用该文件就可以检测物体。
检测不同尺寸大小的物体的源码是在opencv/sources/modules/objdetect/src目录下的cascadedetect.cpp文件内,具体为CascadeClassifier类。它的一个构造函数为:
CascadeClassifier::CascadeClassifier(const string& filename)
{
load(filename);
}
其中输入参数filename即为训练生成的cascade.xml文件。
当CascadeClassifier类加载了cascade.xml文件后,就可以利用detectMultiScale函数检测输入图像中是否有待识别物体,该物体不必与训练时的样本图像大小相同,只需比它大即可,因为比样本图像小的物体是无法被检测到的:
void CascadeClassifier::detectMultiScale( const Mat& image, vector<Rect>& objects,
vector<int>& rejectLevels,
vector<double>& levelWeights,
double scaleFactor, int minNeighbors,
int flags, Size minObjectSize, Size maxObjectSize,
bool outputRejectLevels )
//image为待检测的输入图像,它的类型必须为CV_8U
//objects为检测得到的物体,是向量的形式,向量中的元素是一个矩形数据类型,矩形包括的区域就是一个物体
//scaleFactor表示检测窗口尺寸变化的因子,该参数的缺省值为1.1
//minNeighbors表示候选矩形的最小数量,小于该值,则舍弃该候选矩形,即该矩形所指示的区域不是待识别物体,这是因为在检测窗口扫描输入图像时,同一个物体一定会在不同的扫描位置被多次检测到,只有在被检测次数大于minNeighbors值,才被认为是待识别物体,该参数的缺省值为3
//flags用于旧版本的级联分类器,新版本的Opencv已不再使用该值,该参数的缺省值为0
//minObjectSize和maxObjectSize分别表示检测得到的物体的尺寸范围,只有物体的尺寸大小在这两个值之间才被确定为检测物体,这两个参赛的缺省值都为Size(),即相当于minObjectSize为训练样本的大小,maxObjectSize为输入图像的大小
//rejectLevels、levelWeights和outputRejectLevels这三个参数是用于最后如何处理objects中的矩形元素(即检测到的物体),在这里我们不做讨论
{
const double GROUP_EPS = 0.2; //阈值
//确保输入参数scaleFactor和image正确
CV_Assert( scaleFactor > 1 && image.depth() == CV_8U );
//判断是否已加载了xml文件,如果没有则退出
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<CvAvgComp> vecAvgComp;
Seq<CvAvgComp>(_objects).copyTo(vecAvgComp);
objects.resize(vecAvgComp.size());
std::transform(vecAvgComp.begin(), vecAvgComp.end(), objects.begin(), getRect());
return;
}
objects.clear(); //输入参数object向量清空
if (!maskGenerator.empty()) {
maskGenerator->initializeMask(image);
}
//如果输入参数maxObjectSize的宽或高设置的是0,则调整maxObjectSize的值为输入图像的尺寸
if( maxObjectSize.height == 0 || maxObjectSize.width == 0 )
maxObjectSize = image.size();
//如果输入图像是彩色图像,则需转换为灰度图像
Mat grayImage = image;
if( grayImage.channels() > 1 )
{
Mat temp;
cvtColor(grayImage, temp, CV_BGR2GRAY);
grayImage = temp;
}
Mat imageBuffer(image.rows + 1, image.cols + 1, CV_8U); //开辟一块内存空间
vector<Rect> candidates; //该参数用于存储得到的候选检测物体
//依据尺寸变化因子,改变检测窗口尺寸,并在该尺寸下检测物体
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, scaledImageSize.height - originalWindowSize.height );
//processingRectSize的宽和高必须都大于0
if( processingRectSize.width <= 0 || processingRectSize.height <= 0 )
break; //退出for循环
//检测窗口windowSize的大小必须在输入参数maxObjectSize和minObjectSize之间,这样就保证了得到的检测物体的尺寸在maxObjectSize和minObjectSize之间
if( windowSize.width > maxObjectSize.width || windowSize.height > maxObjectSize.height )
break; //退出for循环
if( windowSize.width < minObjectSize.width || windowSize.height < minObjectSize.height )
continue; //继续for循环,因为windowSize会随着的factor的增大而增大
//缩小输入图像,得到scaledImage
Mat scaledImage( scaledImageSize, CV_8U, imageBuffer.data );
resize( grayImage, scaledImage, scaledImageSize, 0, 0, CV_INTER_LINEAR );
//表示检测窗口在输入图像上扫描的步长,水平和垂直扫描的步长都是该值
int yStep;
if( getFeatureType() == cv::FeatureEvaluator::HOG ) //特征为HOG
{
yStep = 4;
}
else //其他特征的情况
{
yStep = factor > 2. ? 1 : 2;
}
//为了加快运行速度,在单一尺寸下,程序把被检测图像分割成stripCount个横条,每个横条的宽就是被检测图像的宽,而高是stripSize,这样就可以并行处理这些stripCount个横条
int stripCount, stripSize;
const int PTS_PER_THREAD = 1000;
stripCount = ((processingRectSize.width/yStep)*(processingRectSize.height + yStep-1)/yStep + PTS_PER_THREAD/2)/PTS_PER_THREAD; //得到stripCount值
//确保stripCount大于1,并小于100
stripCount = std::min(std::max(stripCount, 1), 100);
stripSize = (((processingRectSize.height + stripCount - 1)/stripCount + yStep-1)/yStep)*yStep; //得到stripSize值
//调用detectSingleScale函数,在单一尺寸下检测物体
if( !detectSingleScale( scaledImage, stripCount, processingRectSize, stripSize, yStep, factor, candidates,
rejectLevels, levelWeights, outputRejectLevels ) )
break; //检测不成功,则退出for循环
}
//使objects的数量(即检测到的物体)等于candidates的数量
objects.resize(candidates.size());
std::copy(candidates.begin(), candidates.end(), objects.begin()); //复制
if( outputRejectLevels )
{
groupRectangles( objects, rejectLevels, levelWeights, minNeighbors, GROUP_EPS );
}
else
{
//根据minNeighbors和GROUP_EPS,进一步调整objects,即剔除或合并一些候选检测物体
groupRectangles( objects, minNeighbors, GROUP_EPS );
}
}
在单一尺寸下检测物体:
bool CascadeClassifier::detectSingleScale( const Mat& image, int stripCount, Size processingRectSize,
int stripSize, int yStep, double factor, vector<Rect>& candidates,
vector<int>& levels, vector<double>& weights, bool outputRejectLevels )
{
//计算image图像的积分图像或梯度方向直方图
if( !featureEvaluator->setImage( image, data.origWinSize ) )
return false;
#if defined (LOG_CASCADE_STATISTIC)
logger.setImage(image);
#endif
Mat currentMask;
if (!maskGenerator.empty()) {
currentMask=maskGenerator->generateMask(image);
}
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向量中
candidates.insert( candidates.end(), candidatesVector.begin(), candidatesVector.end() );
#if defined (LOG_CASCADE_STATISTIC)
logger.write();
#endif
return true;
}
在并行处理中,调用了CascadeClassifierInvoker类中的重载( )运算符,该函数的关键内容是调用了CascadeClassifier::runAt函数,它的作用是基于不同的特征(HAAR、LBP或HOG)来预测当前位置是否为被检测物体。而在预测中,就用到了前面提到的cascade.xml文件,即读取cascade.xml文件内的值,来进行预测判断。