参考https://blog.csdn.net/q1007729991/article/details/52995734 和https://blog.csdn.net/qq_37577735/article/details/80041586 这两篇博文,由这两篇描述知道,要了解LBP就要先学习插值,以双线性插值为例。
插值
首先看最简单的线性插值如下:
接下来就是将线性插值用在双线性插值中,如下:
由这个推导的最后结果就可与下面所述f(i+u,j+v)对上,说明推导正确。可以明显看到对某个像素点的插值结果其实就是附近4个相邻像素点的加权。
到此,我们已明白双线性插值的意思。
LBP特征
接下来我们看LBP的原理,由文章开头发的链接中作者介绍可知:像素3*3的邻域内,以邻域中心像素为阈值,相邻的8个像素的灰度值与邻域中心的像素值进行比较,若周围像素大于中心像素值,则该像素点的位置被标记为1,否则为0。那么对于某个中心周围的8个邻域点,就有pow(2,8)=256种可能,取值范围就是[0,255],所以出来的LBP特征图就是一个灰度图。
可以由上图看到此时采样点组成了一个正方形,我觉得原始LBP可以叫做正方形LBP,自然此时不用插值。如作者给的opencv代码所述,就是从左上角顺时针一圈的结果:
//原始LBP特征计算
template <typename _tp>
void getOriginLBPFeature(InputArray _src,OutputArray _dst)
{
Mat src = _src.getMat();
_dst.create(src.rows-2,src.cols-2,CV_8UC1);
Mat dst = _dst.getMat();
dst.setTo(0);
for(int i=1;i<src.rows-1;i++)
{
for(int j=1;j<src.cols-1;j++)
{
_tp center = src.at<_tp>(i,j);
unsigned char lbpCode = 0;
lbpCode |= (src.at<_tp>(i-1,j-1) > center) << 7;
lbpCode |= (src.at<_tp>(i-1,j ) > center) << 6;
lbpCode |= (src.at<_tp>(i-1,j+1) > center) << 5;
lbpCode |= (src.at<_tp>(i ,j+1) > center) << 4;
lbpCode |= (src.at<_tp>(i+1,j+1) > center) << 3;
lbpCode |= (src.at<_tp>(i+1,j ) > center) << 2;
lbpCode |= (src.at<_tp>(i+1,j-1) > center) << 1;
lbpCode |= (src.at<_tp>(i ,j-1) > center) << 0;
dst.at<uchar>(i-1,j-1) = lbpCode;
}
}
}
可以看到左上角的那个点放在最高位上,顺时针以此类推。这个原始LBP很好理解,不用赘述。但作者后面说的圆形LBP中说了两个词:半径radius和采样点neighbors:
可以看到上图的LBP的右上角值就是radius,右下角值就是neighbors。可以看到不一定要求neighbors=8*radius,而是**<=**即可。以radius=1,neighbors=8为例:根据作者提供的代码:
template <typename _tp>
void getCircularLBPFeature(InputArray _src,OutputArray _dst,int radius,int neighbors)
{
Mat src = _src.getMat();
//LBP特征图像的行数和列数的计算要准确
_dst.create(src.rows-2*radius,src.cols-2*radius,CV_8UC1);
Mat dst = _dst.getMat();
dst.setTo(0);
//循环处理每个像素
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);
unsigned char lbpCode = 0;
for(int k=0;k<neighbors;k++)
{
//根据公式计算第k个采样点的坐标,这个地方可以优化,不必每次都进行计算radius*cos,radius*sin
float x = i + static_cast<float>(radius * \
cos(2.0 * CV_PI * k / neighbors));
float y = j - static_cast<float>(radius * \
sin(2.0 * CV_PI * k / neighbors));
//根据取整结果进行双线性插值,得到第k个采样点的灰度值
//1.分别对x,y进行上下取整
int x1 = static_cast<int>(floor(x));
int x2 = static_cast<int>(ceil(x));
int y1 = static_cast<int>(floor(y));
int y2 = static_cast<int>(ceil(y));
//2.计算四个点(x1,y1),(x1,y2),(x2,y1),(x2,y2)的权重
//将坐标映射到0-1之间
float tx = x - x1;
float ty = y - y1;
//根据0-1之间的x,y的权重计算公式计算权重
float w1 = (1-tx) * (1-ty);
float w2 = tx * (1-ty);
float w3 = (1-tx) * ty;
float w4 = tx * ty;
//3.根据双线性插值公式计算第k个采样点的灰度值
float neighbor = src.at<_tp>(x1,y1) * w1 + src.at<_tp>(x1,y2) *w2 \
+ src.at<_tp>(x2,y1) * w3 +src.at<_tp>(x2,y2) *w4;
//通过比较获得LBP值,并按顺序排列起来
lbpCode |= (neighbor>center) <<(neighbors-k-1);
}
dst.at<uchar>(i-radius,j-radius) = lbpCode;
}
}
}
可以看到使用了双线性插值,因为不再像原始LBP那样使用正方形,这里是圆形就必须插值,如下图所示0、2、4、6位置是插值所得:
由上图还可以看到,原始LBP与圆形LBP的二进制放的起始位置变了,比如最高位原始LBP在左上角,但圆形LBP最高位在正下方。但没关系,8个采样点组成了圆形所以是符合原理的。
突然看到https://blog.csdn.net/xhdmtx_hcf/article/details/80060963 这里也介绍了圆形LBP,但这个作者给的elbp_函数中,却不对,以中心点(1,1)为例,radius=1,neighbors=8:
from function elbp_():
1,1 bitpos:0 biliear:2,1 - 2,1 - 2,1 - 2,1
1,1 bitpos:1 biliear:1,0 - 2,0 - 1,1 - 2,1
1,1 bitpos:2 biliear:1,0 - 2,0 - 1,0 - 2,0
1,1 bitpos:3 biliear:0,0 - 1,0 - 0,1 - 1,1
1,1 bitpos:4 biliear:0,0 - 0,0 - 0,1 - 0,1
1,1 bitpos:5 biliear:0,1 - 1,1 - 0,2 - 1,2
1,1 bitpos:6 biliear:0,2 - 1,2 - 0,2 - 1,2
1,1 bitpos:7 biliear:1,1 - 2,1 - 1,2 - 2,2
from function getCircularLBPFeature():
1,1 bitpos:7 biliear:1,2 - 1,2 - 1,2 - 1,2
1,1 bitpos:6 biliear:0,1 - 1,1 - 0,2 - 1,2
1,1 bitpos:5 biliear:0,1 - 0,1 - 0,1 - 0,1
1,1 bitpos:4 biliear:0,0 - 1,0 - 0,1 - 1,1
1,1 bitpos:3 biliear:1,0 - 1,0 - 1,0 - 1,0
1,1 bitpos:2 biliear:1,0 - 2,0 - 1,1 - 2,1
1,1 bitpos:1 biliear:2,1 - 2,1 - 2,1 - 2,1
1,1 bitpos:0 biliear:1,1 - 2,1 - 1,2 - 2,2
看为什么elbp_函数中二进制位2的时候,怎么是由(1,0)、(2,0)这两个点插值的结果作为bitpos2的结果呢?!明显不对啊,然后这几个点画出来也不是圆形。再看getCircularLBPFeature()中二进制位2上的结果是由(1,0)、(2,0)、(1,1)、(2,1)插值得到的,就是符合我上面画的“圆形LBP”的图,的确组合成了圆形。