OpenCV源码解析:目标检测trainCascade算法剖析之LBP基础
本文重点讲解LBP特征及OpenCV中LBP特征的基本处理。
目标检测,也叫目标提取,是一种基于目标几何和统计特征的图像分割。用级联分类器实现目标检测在AI人工智能识别中应用十分广泛。
正样本的选取原则
正样本的尺寸不是必须一致的,从源码可以看到,这个是可以在输入图片文件的尺寸时设置大小从而实现在CreateSamples中进行裁剪的(参考cvCreateTrainingSamplesFromInfo中resize调整图片大小)。不过我建议你最好事先把尺寸统一处理好,除非你真的知道从图片的那个像素点开始裁剪。
数据来源尽可能做到多样化,比如样本为车,车的姿态场景应稍丰富些。同一正样本目标的图像太多会使局部特征过于明显,造成这个目标的训练过拟合,影响检测精度,不利于训练器泛化使用。
这里的输入文件名叫car.info,共计550行,可从这里下载的
https://github.com/TutorProgramacion/opencv-traincascade/tree/master/image_dataset
这里也行,里面已经生成possample.vec文件:
汽车正负样本下载: https://download.csdn.net/download/tanmx219/10747369
检测样本下载: https://download.csdn.net/download/tanmx219/10623808
内容是这样的,其中0,0,100,40表示从(0,0)这个像素点开始裁剪,宽为100,高为40个像素,
pos/pos-532.pgm 1 0 0 100 40
pos/pos-166.pgm 1 0 0 100 40
pos/pos-76.pgm 1 0 0 100 40
pos/pos-193.pgm 1 0 0 100 40
pos/pos-0.pgm 1 0 0 100 40
关于负样本的准备
原则上负样本图片中不能包含正样本目标;每个负样本之间应尽量保证各不相同,即确保负样本的多样性;
负样本的尺寸不是必须相同的,但负样本的尺寸不能小于正样本矢量集图像的宽和高;
整体上来说,负样本的准备是很简单的。
这里的负样本文件名称为neg.info。
使用openCV_createSamples
选择好样本之后,就要生成OpenCV生vec向量文件,然后再进行训练以得到级联模型(xml),最后进行目标识别。
这里我们先讲OpenCV生vec向量文件的过程,理一下源码。
先说一下最后生成的那个vec文件,其文件结构是这样的
图片个数(4字节)
图片尺寸(4字节,灰度图的字节数size=宽x高)
0(4字节)
0(1个字节的图片分隔符)
Data(共计size个字节)
0(1个字节的图片分隔符)
Data(共计size个字节)
0(1个字节的图片分隔符)
Data(共计size个字节)
…
可见,vec是一个十分简单的文件,生成过程同样很简单,如果自己编译,可以看到在OpenCV的工程目标下,有一个opencv_createSampels的项目,编译后可以得到opencv_createsamples.exe这个文件,我的目录结构是这样的
运行
opencv_createSamples -info ../dataCascade/cars.info -vec ../dataCascade/possamples.vec -num 550 -w 100 -h 40
pause
程序main里实质调用的函数是cvCreateTrainingSamplesFromInfo
其中
icvWriteVecHeader负责写入文件头,
icvWriteVecSample负责写入文件分隔符
参考:https://docs.opencv.org/3.3.0/dc/d88/tutorial_traincascade.html
积分图
也叫区域求和表,定义
在点 (x, y)处的面积和是该点左边和上边全部像素的和(包括该点本身在内);即,每个像素点对应的积分值,是该点左上角所有像素值的和。
有了积分表之后,就可以快速地求得任意面积的大小,如图,
假设ABCD为4个点对应的积分,那么ABCD这个区域内的像素值的和就是
Sum = D – B – C + A
在OpenCV3.4.1的源码中,有一个宏定义,
#define CALC_SUM_OFS_(p0, p1, p2, p3, ptr) \
((ptr)[p0] - (ptr)[p1] - (ptr)[p2] + (ptr)[p3])
该宏完成一个简单的积分块计算,也就是I(p0) + I(p3) – I(p1) – I(p2), 其中I(p0)表示取p0这个点的积分值,你可以对照上面的图,把p0,p1,p2,p3分别等价于A、B、C、D这4个点。
什么时LBP?
LBP有很多种形式,最常用的如下面所示的九宫格,示意图中计算的,是最中间那个4所在的位置的LBP值,其规则是,当一个值比中间这个4大时,该位高为1,否则为0。
5比中间的4大,所以该位为1
9比中间的4大,所以该位为1
1比中间的4小,所以该位为0
4比中间的4 ,也为1
6比中间的4大,所以该位为1
7比中间的4大,所以该位为1
2比中间的4小,所以该位为0
3比中间的4小,所以该位为0
这样,所有周边8个数比较过后,根据箭头的方向走一圈,得到这样一个8位的值11010011。这个值,就是中间4那个位置对应的LBP值。
计算LBP特征的函数CvLBPEvaluator::Feature::calc
作用:计算LBP码表,
该函数的功能和cascadeDetect.hpp中的LBPEvaluator::OptFeature::calc函数完全一致。
功能:
计算积分图的LBP特征(最简单的方块特征,8位,最大值255)
源码请和下面的示意图对照看,如前所述,注意LBP的方向。
inline int LBPEvaluator::OptFeature::calc( const int* p ) const
{
int cval = CALC_SUM_OFS_( ofs[5], ofs[6], ofs[9], ofs[10], p ); // center block character value
return (CALC_SUM_OFS_( ofs[0], ofs[1], ofs[4], ofs[5], p ) >= cval ? 128 : 0) | // block b0
(CALC_SUM_OFS_( ofs[1], ofs[2], ofs[5], ofs[6], p ) >= cval ? 64 : 0) | // block b1
(CALC_SUM_OFS_( ofs[2], ofs[3], ofs[6], ofs[7], p ) >= cval ? 32 : 0) | // block b2
(CALC_SUM_OFS_( ofs[6], ofs[7], ofs[10], ofs[11], p ) >= cval ? 16 : 0) | // block b5
(CALC_SUM_OFS_( ofs[10], ofs[11], ofs[14], ofs[15], p ) >= cval ? 8 : 0)| // block b8
(CALC_SUM_OFS_( ofs[9], ofs[10], ofs[13], ofs[14], p ) >= cval ? 4 : 0)| // block b7
(CALC_SUM_OFS_( ofs[8], ofs[9], ofs[12], ofs[13], p ) >= cval ? 2 : 0)| // block b6
(CALC_SUM_OFS_( ofs[4], ofs[5], ofs[8], ofs[9], p ) >= cval ? 1 : 0); // block b3
}
如上面的示意图,4表示最中间那个块,b0,b1,b2,b3 (|) b5, b6,b7,b8是其周围的8个块,
如果b0的积分值比4大,就置第8位bit为1, 否则为0
如果b1的积分值比4大,就置第7位bit为1, 否则为0
如果b2的积分值比4大,就置第6位bit为1, 否则为0
如果b5的积分值比4大,就置第5位bit为1, 否则为0
如果b8的积分值比4大,就置第4位bit为1, 否则为0
如果b7的积分值比4大,就置第3位bit为1, 否则为0
如果b6的积分值比4大,就置第2位bit为1, 否则为0
如果b3的积分值比4大,就置第1位bit为1, 否则为0
例如,如果得到的各位全是1,写成二进制就是1111 1111b,如果各位全是0,写成二进制就是0000 0000b。这样,就得到了一个完整的LBP值。
另一个函数我也把源码贴出来,原理上没有区别,只不过输入的参数不同
inline uchar CvLBPEvaluator::Feature::calc(const cv::Mat &_sum, size_t y) const
{
const int* psum = _sum.ptr<int>((int)y);
int cval = psum[p[5]] - psum[p[6]] - psum[p[9]] + psum[p[10]];
return (uchar)((psum[p[0]] - psum[p[1]] - psum[p[4]] + psum[p[5]] >= cval ? 128 : 0) | // 0
(psum[p[1]] - psum[p[2]] - psum[p[5]] + psum[p[6]] >= cval ? 64 : 0) | // 1
(psum[p[2]] - psum[p[3]] - psum[p[6]] + psum[p[7]] >= cval ? 32 : 0) | // 2
(psum[p[6]] - psum[p[7]] - psum[p[10]] + psum[p[11]] >= cval ? 16 : 0) | // 5
(psum[p[10]] - psum[p[11]] - psum[p[14]] + psum[p[15]] >= cval ? 8 : 0) | // 8
(psum[p[9]] - psum[p[10]] - psum[p[13]] + psum[p[14]] >= cval ? 4 : 0) | // 7
(psum[p[8]] - psum[p[9]] - psum[p[12]] + psum[p[13]] >= cval ? 2 : 0) | // 6
(psum[p[4]] - psum[p[5]] - psum[p[8]] + psum[p[9]] >= cval ? 1 : 0)); // 3
}
LBP特征的产生
上面说了LBP的原理和计算,现在看一下OpenCV中LBP的产生
void CvLBPEvaluator::generateFeatures()
作用:将图像划分成尽可能多的子方块(特征),其中特征的数量保存在numFeatures中。
说明:一幅宽为width,高为height的图像,
以(x,y)为起点,形成一个方块rect(x,y,x+3w,y+3h),内含3x3=9个子方块(九宫格),(w,h)是子方块的宽和高,w,h逐渐递增,最后得到的最大方块为rect(x,y,winSize.width,winSize.height),
当x=0,y=0时就是整幅图像的区域;所以,最大子方块的大小是 w <= winSize.width / 3, h <= winSize.height / 3
// 该函数一般在CvFeatureEvaluator::init初始化时调用,
// std::vector<Feature> features;定义在CvLBPEvaluator中。
void CvLBPEvaluator::generateFeatures()
{
int offset = winSize.width + 1; // 行距
for( int x = 0; x < winSize.width; x++ )
for( int y = 0; y < winSize.height; y++ )
for( int w = 1; w <= winSize.width / 3; w++ )
for( int h = 1; h <= winSize.height / 3; h++ )
if ( (x+3*w <= winSize.width) && (y+3*h <= winSize.height) )
features.push_back( Feature(offset, x, y, w, h ) );
numFeatures = (int)features.size();
}
其中的Feature构造函数
CvLBPEvaluator::Feature::Feature(...)
Feature描述的是构成的9个子方块,例如左上角第1个子方块,(x,y)为方块左上角坐标,_blockWidth, _blockHeight分别为方块的宽和高,单位是像素。像素点p[0]~p[15]共计16个点,描述了3x3=9个子方块。
源码是这样的,对照前面的示意图看很容易理解
#define CV_SUM_OFFSETS( p0, p1, p2, p3, rect, step ) \
/* (x, y) */ \
(p0) = (rect).x + (step) * (rect).y; \
/* (x + w, y) */ \
(p1) = (rect).x + (rect).width + (step) * (rect).y; \
/* (x, y + h) */ // 原注释(x + 2, y)是错误的 \
(p2) = (rect).x + (step) * ((rect).y + (rect).height); \
/* (x + w, y + h) */ \
(p3) = (rect).x + (rect).width + (step) * ((rect).y + (rect).height);
CvLBPEvaluator::Feature::Feature( int offset, int x, int y, int _blockWidth, int _blockHeight )
{
Rect tr = rect = cvRect(x, y, _blockWidth, _blockHeight);
CV_SUM_OFFSETS( p[0], p[1], p[4], p[5], tr, offset ) // 定义4个角点的位置, offset = image_width + 1
tr.x += 2*rect.width;
CV_SUM_OFFSETS( p[2], p[3], p[6], p[7], tr, offset )
tr.y +=2*rect.height;
CV_SUM_OFFSETS( p[10], p[11], p[14], p[15], tr, offset )
tr.x -= 2*rect.width;
CV_SUM_OFFSETS( p[8], p[9], p[12], p[13], tr, offset )
}
(更新中,未完待续)