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大,所以该位为
9比中间的4大,所以该位为
1比中间的4小,所以该位为0
4比中间的4 ,也为
6比中间的4大,所以该位为
7比中间的4大,所以该位为
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 )
}

(更新中,未完待续)

猜你喜欢

转载自blog.csdn.net/tanmx219/article/details/83445197