对于opencv traincascade 读取负样本图片bg的方式不是很了解,为了更好的理解和使用opencv traincascade ,对这个很绕的fillpassedsamples源码进行分析。
最开始看源码觉得这个读取负样本图片的程序绕来绕去,结构非常不清晰。后来才发现代码写成那样是为了避免读取过程中的出现的各种问题。不过为了方便我现阶段的使用,还是有可以改动的地方。
要点如下:
1.正样本读取很简单,读取生成的vec文件,而且每个训练stage需要读取设定数量numPos的正样本文件,否则退出训练。所以要根据vec文件样本数量、nunStages和maxHiRate设置正确的的numPos。
2.负样本图片不含有正样本图片,负样本尺寸需大于或等于采样框winsize
3.负样本图片读取流程为:
step1 根据样本描述文件的顺序读取一幅负样本图片,然后马上缩小到winsize到2winsize(不是指面积,指单个维度的倍数)之间的尺度(得到一个缩小系数)。
step2 进行滑动负样本采样,采样一幅图结束即根据放大系数放大图片,进行下一轮滑动负样本采样。
step3 读取样本描述文件的下一幅图片。
step4 样本描述文件中所有负样本图片读取完之后如果样本数不达要求会重新再读取一遍,但是此时(然后马上缩小到winsize到2winsize)这个尺度会发生改变,进而导致不同轮次负样本图片读取到的负样本有区别。
4.尽量不要训练时读取负样本文件图片多个轮次(这个轮次最多只能winsize.height*winsize.width,否则成为重复读取),因为与前一轮的样本区别实际上比较小。
5.为了方便得知轮次,bool CvCascadeImageReader::NegReader::nextImg()函数中我加了一个语句。
round += last / count;//整型除整型,读取到末位图片round才+1.记录遍历次数
if(last / count==1)
printf(" current round: %d\r", round);//空格是必须的
函数说明:
//读取正负样本
//其中正样本从生成的vec文件中读取
//负样本从负样本背景图片中读取
//负样本背景图片必须大于或等于样本图片得尺寸,如果大于样本图片,会继续缩放滑动采样
//在级联分类器的每一个训练stage,会重新读取正样本图片与负样本图片
//并丢弃,之前阶段训练的stage已经正确分类的正样本与负样本图片
//所以读取的正样本图片会增加,有效负样本的读取会越来越慢
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++;
//用之前的stage给当前样本分类并纪录
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;
}
//负样本读取 预定义变量
CvCascadeImageReader::NegReader::NegReader()
{
src.create( 0, 0 , CV_8UC1 );//原图
img.create( 0, 0, CV_8UC1 );//经缩放后当前图
point = offset = Point( 0, 0 );//样本框滑动位置
scale = 1.0F;//当前缩放系数
scaleFactor = 1.4142135623730950488016887242097F;//每一步的缩放系数变换比率
stepFactor = 0.5F;//滑动步长与样本width之比
}
//读取负样本图片描述文件
//得到负样本图片路径
bool CvCascadeImageReader::NegReader::nextImg()
{
//读取文件图片,并进行缩小,赋给img
//文件图片按文档的图片列表读取完后会进行再一遍读取,但是读取方式会有所不同,与之前一遍的缩放系数、滑动采样偏移值会有所不同
//对遍历次数进行记录,round
//对读取的图片次序进行记录
Point _offset = Point(0,0);//滑动采样偏移值
size_t count = imgFilenames.size();//文件图片数量
//对所有文件图片进行遍历,直到读取到正常的图片, !src.empty() && src.type() == CV_8UC1 && _offset.x >= 0 && _offset.y >= 0
for( size_t i = 0; i < count; i++ )
{
src = imread( imgFilenames[last++], 0 );//src,读取的第last个文件图片
if( src.empty() ){
last %= count;//可能遍历多遍,last需为正常值,不能越界
continue;
}
round += last / count;//整型除整型,读取到末位图片round才+1.记录遍历次数
round = round % (winSize.width * winSize.height);//winSize.width * winSize.height的遍历次数之内所得到的相同文件图片的缩放图片才有区别,故round有界
last %= count;//可能遍历多遍,last需为正常值,不能越界
//不同的遍历次数。 _offset.x会有区别,_offset偏移限于winSize内
_offset.x = std::min( (int)round % winSize.width, src.cols - winSize.width );
_offset.y = std::min( (int)round / winSize.width, src.rows - winSize.height );
if( !src.empty() && src.type() == CV_8UC1
&& _offset.x >= 0 && _offset.y >= 0 )
break;
}
if( src.empty() )
return false; // no appropriate image
point = offset = _offset;
//_offset的区别会影响 第一次缩小系数,即每个文件图片第一个缩放图片的尺寸。尺寸从winsize到2winsize
scale = max( ((float)winSize.width + point.x) / ((float)src.cols),
((float)winSize.height + point.y) / ((float)src.rows) );
//缩放src到img
Size sz( (int)(scale*src.cols + 0.5F), (int)(scale*src.rows + 0.5F) );
resize( src, img, sz, 0, 0, INTER_LINEAR_EXACT );
return true;
}
bool CvCascadeImageReader::NegReader::get( Mat& _img )
{
//_img为读取的样本,img为当前缩放图片,src为当前文件图片
//src:负样本集合中原图的灰度图
//img:src经过缩放得到的下采样图片,用于滑动采样得到样本_img
//每一次读取的当前文件图片,第一个缩放图片都在nextImg()缩放到最小,在此函数中缩放更新只需要放大
CV_Assert( !_img.empty() );
CV_Assert( _img.type() == CV_8UC1 );
CV_Assert( _img.cols == winSize.width );
CV_Assert( _img.rows == winSize.height );
//当前缩放图片为空则读取下一幅文件图片
if( img.empty() )
if ( !nextImg() )//若不存在下一幅文件图片则停止
return false;
//在当前缩放图像 通过mat构造函数截取样本图片 point决定滑动采样方式
Mat mat( winSize.height, winSize.width, CV_8UC1,
(void*)(img.ptr(point.y) + point.x * img.elemSize()), img.step );
mat.copyTo(_img);//样本返回给输入引用
//从图片 左上到右下 的采样
//列到达边界则根据offset.x更新,未到达边界则继续滑动
if( (int)( point.x + (1.0F + stepFactor ) * winSize.width ) < img.cols )
point.x += (int)(stepFactor * winSize.width);
else
{
point.x = offset.x;////列到达边界则根据offset.x更新
////行到达边界则根据offset.y更新,未到达边界则继续滑动
if( (int)( point.y + (1.0F + stepFactor ) * winSize.height ) < img.rows )
point.y += (int)(stepFactor * winSize.height);
else
{
point.y = offset.y;
//行需要更新使说明 图片以滑动采样完, 需要更新缩放 得到新的缩放图片
scale *= scaleFactor;//NegReader::NegReader()中赋值为1.414
if( scale <= 1.0F )//放大, 如果放大到原图像界限的大小则此 原图片已采样完。需要读取新的文件图片
resize( src, img, Size( (int)(scale*src.cols), (int)(scale*src.rows) ), 0, 0, INTER_LINEAR_EXACT );
else
{
if ( !nextImg() )
return false;
}
}
}
return true;
}