OpenCV源码解析:多尺度检测的训练trainCascade
(没时间一次性写完,更新中)
该项目在Opencv Application的一部分,项目名称为opencv_trainCascade,它即可以用来训练lbp特征分类器,也可以是haar特征,有人说lbp特征训练起来更快,我没有专门比较过,不作评论;根据个人经验,lbp在很多场合会更稳定,所以我一般会选lbp特征。这里和检测(detectMultiScale)对应,这里我们仍然以汽车外形检测为例。
关于样本收集的注意事项等,可参考此文:https://blog.csdn.net/u014587123/article/details/78507649
本例的汽车正负样本下载: https://download.csdn.net/download/tanmx219/10747369
检测样本下载: https://download.csdn.net/download/tanmx219/10623808
注意都是pgm格式,如果你想预览而看图器打不开,请到SpaceSoftwares.com上下载SpaceView看图工具。
如何训练?
准备工作:
正本数550
负本数 500
总样本数 1050 ( sample_count )
下载的样本中已经生成possamples.vec,你可以自己生成,命令(createSamples在Opencv-Application中)如下
createSamples.exe -info ../dataCascade/cars.info -vec ../dataCascade/possamples.vec -num 550 -w 100 -h 40
pause
vec文件格式是这样的,具体读取参考源码CvCascadeImageReader::PosReader::create(..)及PosReader::get(),这部分比较简单,后面不再详述。
bool CvCascadeImageReader::PosReader::create( const string _filename )
从vec文件中读取图片码流,其vec文件格式(文件结构)是这样的
图片个数(4字节)
图片尺寸(4字节,灰度图的字节数size=宽x高)
0(4字节)
0(1个字节的图片分隔符)
Data(共计size个字节)
0(1个字节的图片分隔符)
Data(共计size个字节)
0(1个字节的图片分隔符)
Data(共计size个字节)
…
bool CvCascadeImageReader::PosReader::get( Mat &_img )
读取vec文件中的一个image,放入到_img中。
vec文件格式 参考上面(CvCascadeImageReader::PosReader::create)的注释
训练时使用的命令如下
opencv_traincascade.exe -data ../dataCascade -vec ../dataCascade/possamples.vec -bg ../dataCascade/neg.info -numPos 550 -numNeg 500 -numStages 20 -precalcValbufSize 200 -precalcdxBufSize 1000 -featureType LBP -w 100 -h 40 -minHitRate 0.995 -maxFalseAlarmRate 0.5 -weightTrimRate 0.95 -maxDepth 1 -maxWeakCount 100 -mode ALL
pause
这里最重要的就是注意目录的存放和路径(根据实际存放的位置,要设成你自己实际使用的路径),如果在VS项目中直接运行,找不到文件的话,建议参考:
https://blog.csdn.net/tanmx219/article/details/81783803
trainCascade.exe 参数
-data:
文件夹名,用于保存训练生成的各种xml文件,该文件夹一定要事先创建好,否则系统会报错,在这里,我们定义该文件夹名为data,它已在前面创建好
-vec:
由opencv_createsamples.exe程序生成的正样本vec文件
-bg:
用于表示不含车牌照片的文本文件,即neg.txt
-numPos:
训练级联分类器的每一级分类器(即强分类器)时所用的正样本数目
-numNeg:
训练级联分类器的每一级分类器(即强分类器)时所用的负样本数目
-numStages:
最终得到的级联分类器的级数,我们设置为12
-precalcValBufSize:
用于存储预先计算特征值的内存空间大小,单位为MB,越大越好,但分配时不要超过可用内存,否则可能无法完成分配。
如果空间分配过小,就会根据空间的容纳能力,只计算前面能被容纳的部分特征,具体规则可参考
CvCascadeBoostTrainData::setData及precalculate函数,这部分能被容纳的特征的个数是numPrecalcVal;
后面的特征不会被事先计算好,而是在实际使用时才计算,可以参考CvCascadeBoostTrainData::get_cat_var_data函数。
-precalcIdxBufSize:
用于存储预先计算特征索引的内存空间大小,单位为MB,越大越好,但分配时不要超过可用内存,否则可能运行不了。
-stageType:
强分类器的类型,目前只实现了AdaBoost,因此唯一的值(缺省值)为BOOST
-featureType:
特征类型,HAAR(缺省值),LBP或HOG
-w:
正样本图像的宽,必须与opencv_createsamples.exe命令的参数一致,即58
-h:
正样本图像的高,必须与opencv_createsamples.exe命令的参数一致,即18
-bt:
AdaBoost的类型,DAB,RAB,LB或GAB(缺省值)
-minHitRate:
原理部分提到的每级分类器的最小识别率
-maxFalseAlarmRate:
原理部分提到的每级分类器的最大错误率
-weightTrimRate:
用于决策树的剪枝,缺省值为0.95
-maxDepth:
决策树的最大深度,缺省值为1,即该决策树为二叉树(树墩形)
-maxWeakCount:
强分类器所包含的最大决策树的数量,该值也与最大错误率有关,我们定义该值为150
-mode:
如果特征为HAAR,则该参数决定了使用哪种HAAR状特征(见图1),BASIC(缺省值)、CORE或ALL
CvCascadeClassifier(级联分类器)
CvCascadeParams cascadeParams; // 级联分类器参数
cv::Ptr<CvFeatureParams> featureParams; // 特征参数
cv::Ptr<CvCascadeBoostParams> stageParams; // 强分类器参数
cv::Ptr<CvFeatureEvaluator> featureEvaluator;// 特征计算
std::vector< cv::Ptr<CvCascadeBoost> > stageClassifiers; // 强分类器
CvCascadeImageReader imgReader; // 样本图像读取
int numStages, curNumSamples; // 强分类器数目,当前样本数目
int numPos, numNeg; // 正样本数目,负样本数目
123456789
CvCascadeParams(级联分类器参数)
static const int defaultStageType = BOOST; // 默认级联分类器类型
static const int defaultFeatureType = CvFeatureParams::HAAR; // 默认级联分类器特征
int stageType; // 强分类器类型
int featureType; // 特征类型
cv::Size winSize; // 级联分类器窗口信息
123456
CvFeatureParams(特征参数)
int maxCatCount; // 0 in case of numerical features // 最大特征数目
int featSize; // 1 in case of simple features (HAAR, LBP) and N_BINS(9)*N_CELLS(4) in case of Dalal's HOG features // 特征尺寸
123
CvCascadeBoostParams(强分类器参数)
float minHitRate; // 最小检测率
float maxFalseAlarm; // 最大误检率
123
CvFeatureEvaluator(特征计算)
int npos, nneg; //正样本数目,负样本数目
int numFeatures; // 特征数目
cv::Size winSize; // 窗口尺寸
CvFeatureParams *featureParams; // 特征参数
cv::Mat cls; // 设置样本类别矩阵
123456
CvCascadeBoost(强分类器)
float threshold; // 强分类器阈值
float minHitRate, maxFalseAlarm; // 强分类器最小检测率,最大误检率
123
CvBoostTree(弱分类器)使用CvDTree实现(ml模块中的随机树实现)
CvDTreeNode* root; // 根节点
CvMat* var_importance; // 重要变脸
CvDTreeTrainData* data; // 训练数据
CvMat train_data_hdr, responses_hdr; // 训练数据和响应数据
cv::Mat train_data_mat, responses_mat; // 训练数据矩阵和响应数据矩阵
int pruned_tree_idx; // 树裁剪索引
1234567
CvDTreeNode(弱分类器节点信息)
int class_idx; // 赋给节点的归一化的类别索引(从0到class_count-1),用在分类树和树集成中
int Tn; // 在有序排列的树中的树索引。用在剪枝过程中和之后。
double value; // 赋给节点的值。可以是一个类别标签,也可以是估计函数值
CvDTreeNode* parent; // 父节点
CvDTreeNode* left; // 左子树
CvDTreeNode* right; // 右子树
CvDTreeSplit* split; // 指向第一个分裂点(初始分裂点)的指针
int sample_count; // 在训练阶段所用到的样本数目
int depth; // 节点深度。根节点为0,子节点是父节点深度加1
int* num_valid; // 合法数目
int offset;
int buf_idx;
double maxlr;
// global pruning data
// 全局裁剪数据
int complexity;
double alpha;
double node_risk, tree_risk, tree_error;
// cross-validation pruning data
// 交叉验证裁剪数据
int* cv_Tn; // 如果该参数大于1,用叠一交叉验证来进行剪枝
double* cv_node_risk;
double* cv_node_error;// 节点错误率
开始训练(trainCascade)
本次调试中使用的参数:
-data ../dataCascade -vec ../dataCascade/possamples.vec -bg ../dataCascade/neg.info -numPos 550 -numNeg 500 -numStages 20 -precalcValBufSize 2048 -precalcIdxBufSize 1024 -featureType LBP -w 100 -h 40 -minHitRate 0.995 -maxFalseAlarmRate 0.5 -weightTrimRate 0.95 -maxDepth 1 -maxWeakCount 100 -mode ALL
这里要注意,如果你碰到问题如,
No.1 OpenCV(3.4.3) Error: Bad argument (> Can not get new positive sample. The most possible reason is insufficient count of samples in given vec-file.那么,请把numPos改小一点,如540,或500,也可以把minHitRate降低,详情可参考下面这两个链接。
http://answers.opencv.org/question/4368/traincascade-error-bad-argument-can-not-get-new-positive-sample-the-most-possible-reason-is-insufficient-count-of-samples-in-given-vec-file/
http://answers.opencv.org/question/776/error-in-parameter-of-traincascade/?answer=792#post-id-792
No.2 Infinite loop in opencv_traincascade CvCascadeClassifier::fillPassedSamples
死循环是因为副本太少!!!请添加副本。
https://stackoverflow.com/questions/22440647/infinite-loop-haar-lbp-hog-traincascade-of-opencv-stuck
https://stackoverflow.com/questions/14554281/infinite-loop-in-opencv-traincascade-cvcascadeclassifierfillpassedsamples
是不是出现死循环,我建议在CvCascadeClassifier::fillPassedSamples中添加简单的判断语句,例如本人添加了一句:
cout<< "consumed: " << consumed << endl;
如果你发现consumed不断输出,没有停下来,超过副本总数很多,基本就可以判断为死循环。
int CvCascadeClassifier::fillPassedSamples( int first, int count, bool isPositive, double minimumAcceptanceRatio, int64& consumed )
{
int getcount = 0;
Mat img(cascadeParams.winSize, CV_8UC1);
for( int i = first; i < first + count; i++ )
{
for( ; ; )
{
if( consumed != 0 && ((double)getcount+1)/(double)(int64)consumed <= minimumAcceptanceRatio )
return getcount;
bool isGetImg = isPositive ? imgReader.getPos( img ) :
imgReader.getNeg( img );
if( !isGetImg )
return getcount;
consumed++;
cout<< "consumed: " << consumed << endl; // 这里是本人添加的代码
featureEvaluator->setImage( img, isPositive ? 1 : 0, i );
if( predict( i ) == 1 )
{
getcount++;
printf("%s current samples: %d\r", isPositive ? "POS":"NEG", getcount);
break;
}
}
}
return getcount;
}