EasyPR源码剖析(6):车牌判断之LBP特征

时间:2023-11-10 20:00:20

一、LBP特征

LBP指局部二值模式,英文全称:Local Binary Pattern,是一种用来描述图像局部特征的算子,LBP特征具有灰度不变性和旋转不变性等显著优点。

原始的LBP算子定义在像素3*3的邻域内,以邻域中心像素为阈值,相邻的8个像素的灰度值与邻域中心的像素值进行比较,若周围像素大于中心像素值,则该像素点的位置被标记为1,否则为0。这样,3*3邻域内的8个点经过比较可产生8位二进制数,将这8位二进制数依次排列形成一个二进制数字,这个二进制数字就是中心像素的LBP值,LBP值共有2^8种可能,因此LBP值有256种。中心像素的LBP值反映了该像素周围区域的纹理信息。
备注:计算LBP特征的图像必须是灰度图,如果是彩色图,需要先转换成灰度图。
上述过程用图像表示为:
EasyPR源码剖析(6):车牌判断之LBP特征
EasyPR源码剖析(6):车牌判断之LBP特征

原始LBP特征的具体的代码如下:

 //原始LBP特征计算
template <typename _tp>
void getOriginLBPFeature(InputArray _src,OutputArray _dst)
{
Mat src = _src.getMat();
_dst.create(src.rows-,src.cols-,CV_8UC1);
Mat dst = _dst.getMat();
dst.setTo();
for(int i=;i<src.rows-;i++)
{
for(int j=;j<src.cols-;j++)
{
_tp center = src.at<_tp>(i,j);
unsigned char lbpCode = ;
lbpCode |= (src.at<_tp>(i-,j-) > center) << ;
lbpCode |= (src.at<_tp>(i-,j ) > center) << ;
lbpCode |= (src.at<_tp>(i-,j+) > center) << ;
lbpCode |= (src.at<_tp>(i ,j+) > center) << ;
lbpCode |= (src.at<_tp>(i+,j+) > center) << ;
lbpCode |= (src.at<_tp>(i+,j ) > center) << ;
lbpCode |= (src.at<_tp>(i+,j-) > center) << ;
lbpCode |= (src.at<_tp>(i ,j-) > center) << ;
dst.at<uchar>(i-,j-) = lbpCode;
}
}
}

在原始的LBP特征提出以后,研究人员对LBP特征进行了很多的改进,因此产生了许多LBP的改进版本。

圆形LBP特征(Circular LBP or Extended LBP)

由于原始LBP特征使用的是固定邻域内的灰度值,因此当图像的尺度发生变化时,LBP特征的编码将会发生错误,LBP特征将不能正确的反映像素点周围的纹理信息,因此研究人员对其进行了改进。基本的 LBP 算子的最大缺陷在于它只覆盖了一个固定半径范围内的小区域,这显然不能满足不同尺寸和频率纹理的需要。为了适应不同尺度的纹理特征,并达到灰度和旋转不变性的要求, 对 LBP 算子进行改进,将 3×3 邻域扩展到任意邻域,并用圆形邻域代替了正方形邻域,改进后的 LBP 算子允许在半径为 R 的圆形邻域内有任意多个像素点。从而得到了诸如半径为R的圆形区域内含有P个采样点的LBP算子:
EasyPR源码剖析(6):车牌判断之LBP特征

旋转不变LBP特征

从上面可以看出,上面的LBP特征具有灰度不变性,但还不具备旋转不变性,因此研究人员又在上面的基础上进行了扩展,提出了具有旋转不变性的LBP特征。首先不断的旋转圆形邻域内的LBP特征,根据选择得到一系列的LBP特征值,从这些LBP特征值选择LBP特征值最小的作为中心像素点的LBP特征。具体做法如下图所示:

EasyPR源码剖析(6):车牌判断之LBP特征
如图,通过对得到的LBP特征进行旋转,得到一系列的LBP特征值,最终将特征值最小的一个特征模式作为中心像素点的LBP特征。

 //旋转不变圆形LBP特征计算,声明时默认neighbors=8
template <typename _tp>
void getRotationInvariantLBPFeature(InputArray _src,OutputArray _dst,int radius,int neighbors)
{
Mat src = _src.getMat();
//LBP特征图像的行数和列数的计算要准确
_dst.create(src.rows-*radius,src.cols-*radius,CV_8UC1);
Mat dst = _dst.getMat();
dst.setTo();
for(int k=;k<neighbors;k++)
{
//计算采样点对于中心点坐标的偏移量rx,ry
float rx = static_cast<float>(radius * cos(2.0 * CV_PI * k / neighbors));
float ry = -static_cast<float>(radius * sin(2.0 * CV_PI * k / neighbors));
//为双线性插值做准备
//对采样点偏移量分别进行上下取整
int x1 = static_cast<int>(floor(rx));
int x2 = static_cast<int>(ceil(rx));
int y1 = static_cast<int>(floor(ry));
int y2 = static_cast<int>(ceil(ry));
//将坐标偏移量映射到0-1之间
float tx = rx - x1;
float ty = ry - y1;
//根据0-1之间的x,y的权重计算公式计算权重,权重与坐标具体位置无关,与坐标间的差值有关
float w1 = (-tx) * (-ty);
float w2 = tx * (-ty);
float w3 = (-tx) * ty;
float w4 = tx * ty;
//循环处理每个像素
for(int i=radius;i<src.rows-radius;i++)
{
for(int j=radius;j<src.cols-radius;j++)
{
//获得中心像素点的灰度值
_tp center = src.at<_tp>(i,j);
//根据双线性插值公式计算第k个采样点的灰度值
float neighbor = src.at<_tp>(i+x1,j+y1) * w1 + src.at<_tp>(i+x1,j+y2) *w2 \
+ src.at<_tp>(i+x2,j+y1) * w3 +src.at<_tp>(i+x2,j+y2) *w4;
//LBP特征图像的每个邻居的LBP值累加,累加通过与操作完成,对应的LBP值通过移位取得
dst.at<uchar>(i-radius,j-radius) |= (neighbor>center) <<(neighbors-k-);
}
}
}
//进行旋转不变处理
for(int i=;i<dst.rows;i++)
{
for(int j=;j<dst.cols;j++)
{
unsigned char currentValue = dst.at<uchar>(i,j);
unsigned char minValue = currentValue;
for(int k=;k<neighbors;k++)
{
//循环左移
unsigned char temp = (currentValue>>(neighbors-k)) | (currentValue<<k);
if(temp < minValue)
{
minValue = temp;
}
}
dst.at<uchar>(i,j) = minValue;
}
}
}

Uniform Pattern LBP特征

为了解决二进制模式过多的问题,提高统计性,Ojala提出了采用一种“等价模式”(Uniform Pattern)来对LBP算子的模式种类进行降维。Ojala等认为,在实际图像中,绝大多数LBP模式最多只包含两次从1到0或从0到1的跳变。因此,Ojala将“等价模式”定义为:当某个LBP所对应的循环二进制数从0到1或从1到0最多有两次跳变时,该LBP所对应的二进制就称为一个等价模式类。如00000000(0次跳变),00000111(只含一次从0到1的跳变),10001111(先由1跳到0,再由0跳到1,共两次跳变)都是等价模式类。除等价模式类以外的模式都归为另一类,称为混合模式类,例如10010111(共四次跳变)。通过这样的改进,二进制模式的种类大大减少,而不会丢失任何信息。模式数量由原来的2^P减少为 P ( P-1)+2种,其中P表示邻域集内的采样点数。对于3×3邻域内8个采样点来说,二进制模式由原始的256种减少为58种,即它把值分为59类,58个uniform pattern为一类,其它的所有值为第59类。这样直方图从原来的256维变成59维。这使得特征向量的维数更少,并且可以减少高频噪声带来的影响。

 //等价模式LBP特征计算
template <typename _tp>
void getUniformPatternLBPFeature(InputArray _src,OutputArray _dst,int radius,int neighbors)
{
Mat src = _src.getMat();
//LBP特征图像的行数和列数的计算要准确
_dst.create(src.rows-*radius,src.cols-*radius,CV_8UC1);
Mat dst = _dst.getMat();
dst.setTo();
//LBP特征值对应图像灰度编码表,直接默认采样点为8位
uchar temp = ;
uchar table[] = {};
for(int i=;i<;i++)
{
if(getHopTimes(i)<)
{
table[i] = temp;
temp++;
}
}
//是否进行UniformPattern编码的标志
bool flag = false;
//计算LBP特征图
for(int k=;k<neighbors;k++)
{
if(k==neighbors-)
{
flag = true;
}
//计算采样点对于中心点坐标的偏移量rx,ry
float rx = static_cast<float>(radius * cos(2.0 * CV_PI * k / neighbors));
float ry = -static_cast<float>(radius * sin(2.0 * CV_PI * k / neighbors));
//为双线性插值做准备
//对采样点偏移量分别进行上下取整
int x1 = static_cast<int>(floor(rx));
int x2 = static_cast<int>(ceil(rx));
int y1 = static_cast<int>(floor(ry));
int y2 = static_cast<int>(ceil(ry));
//将坐标偏移量映射到0-1之间
float tx = rx - x1;
float ty = ry - y1;
//根据0-1之间的x,y的权重计算公式计算权重,权重与坐标具体位置无关,与坐标间的差值有关
float w1 = (-tx) * (-ty);
float w2 = tx * (-ty);
float w3 = (-tx) * ty;
float w4 = tx * ty;
//循环处理每个像素
for(int i=radius;i<src.rows-radius;i++)
{
for(int j=radius;j<src.cols-radius;j++)
{
//获得中心像素点的灰度值
_tp center = src.at<_tp>(i,j);
//根据双线性插值公式计算第k个采样点的灰度值
float neighbor = src.at<_tp>(i+x1,j+y1) * w1 + src.at<_tp>(i+x1,j+y2) *w2 \
+ src.at<_tp>(i+x2,j+y1) * w3 +src.at<_tp>(i+x2,j+y2) *w4;
//LBP特征图像的每个邻居的LBP值累加,累加通过与操作完成,对应的LBP值通过移位取得
dst.at<uchar>(i-radius,j-radius) |= (neighbor>center) <<(neighbors-k-);
//进行LBP特征的UniformPattern编码
if(flag)
{
dst.at<uchar>(i-radius,j-radius) = table[dst.at<uchar>(i-radius,j-radius)];
}
}
}
}
}
//计算跳变次数
int getHopTimes(int n)
{
int count = ;
bitset<> binaryCode = n;
for(int i=;i<;i++)
{
if(binaryCode[i] != binaryCode[(i+)%])
{
count++;
}
}
return count;
}

二、LBPH——图像的LBP特征向量

LBPH,Local Binary Patterns Histograms,即LBP特征的统计直方图,LBPH将LBP特征与图像的空间信息结合在一起。这种表示方法由Ahonen等人在论文[3]中提出,他们将LBP特征图像分成m个局部块,并提取每个局部块的直方图,然后将这些直方图依次连接在一起形成LBP特征的统计直方图,即LBPH。
一幅图像具体的计算LBPH的过程(以Opencv中的人脸识别为例):
1. 计算图像的LBP特征图像;
2. 将LBP特征图像进行分块,Opencv中默认将LBP特征图像分成8行8列64块区域;
3. 计算每块区域特征图像的直方图cell_LBPH,将直方图进行归一化,直方图大小为1*numPatterns;
4. 将上面计算的每块区域特征图像的直方图按分块的空间顺序依次排列成一行,形成LBP特征向量,大小为1*(numPatterns*64);
5. 用机器学习的方法对LBP特征向量进行训练,用来检测和识别目标。
举例说明LBPH的维度:
采样点为8个,如果用的是原始的LBP或Extended LBP特征,其LBP特征值的模式为256种,则一幅图像的LBP特征向量维度为:64*256=16384维,
而如果使用的UniformPatternLBP特征,其LBP值的模式为59种,其特征向量维度为:64*59=3776维,可以看出,使用等价模式特征,其特征向量的维度大大减少,
这意味着使用机器学习方法进行学习的时间将大大减少,而性能上没有受到很大影响。

 //计算LBP特征图像的直方图LBPH
Mat getLBPH(InputArray _src,int numPatterns,int grid_x,int grid_y,bool normed)
{
Mat src = _src.getMat();
int width = src.cols / grid_x;
int height = src.rows / grid_y;
//定义LBPH的行和列,grid_x*grid_y表示将图像分割成这么些块,numPatterns表示LBP值的模式种类
Mat result = Mat::zeros(grid_x * grid_y,numPatterns,CV_32FC1);
if(src.empty())
{
return result.reshape(,);
}
int resultRowIndex = ;
//对图像进行分割,分割成grid_x*grid_y块,grid_x,grid_y默认为8
for(int i=;i<grid_x;i++)
{
for(int j=;j<grid_y;j++)
{
//图像分块
Mat src_cell = Mat(src,Range(i*height,(i+)*height),Range(j*width,(j+)*width));
//计算直方图
Mat hist_cell = getLocalRegionLBPH(src_cell,,(numPattern-),true);
//将直方图放到result中
Mat rowResult = result.row(resultRowIndex);
hist_cell.reshape(,).convertTo(rowResult,CV_32FC1);
resultRowIndex++;
}
}
return result.reshape(,);
}
//计算一个LBP特征图像块的直方图
Mat getLocalRegionLBPH(const Mat& src,int minValue,int maxValue,bool normed)
{
//定义存储直方图的矩阵
Mat result;
//计算得到直方图bin的数目,直方图数组的大小
int histSize = maxValue - minValue + ;
//定义直方图每一维的bin的变化范围
float range[] = { static_cast<float>(minValue),static_cast<float>(maxValue + ) };
//定义直方图所有bin的变化范围
const float* ranges = { range };
//计算直方图,src是要计算直方图的图像,1是要计算直方图的图像数目,0是计算直方图所用的图像的通道序号,从0索引
//Mat()是要用的掩模,result为输出的直方图,1为输出的直方图的维度,histSize直方图在每一维的变化范围
//ranges,所有直方图的变化范围(起点和终点)
calcHist(&src,,,Mat(),result,,&histSize,&ranges,true,false);
//归一化
if(normed)
{
result /= (int)src.total();
}
//结果表示成只有1行的矩阵
return result.reshape(,);
}

上面的LBP特征都是较经典的LBP特征,除此之外,LBP特征还有大量的变种,如TLBP,DLBP,MLBP,MB-LBP,RGB-LBP等。