一、LBP特征的背景介绍
LBP局部二值模式,英文全称:Local Binary Pattern,是一种用来描述图像局部特征的算子,LBP特征具有灰度不变性和旋转不变性等显著优点。LBP特征比较出名的应用是用在人脸识别和目标检测中,在计算机视觉开源库Opencv中有使用LBP特征进行人脸识别的接口,也有用LBP特征训练目标检测分类器的方法,Opencv实现了LBP特征的计算,但没有提供一个单独的计算LBP特征的接口。所以需要我们自己去写源代码;
二、LBP特征的原理
1、原始LBP特征描述及计算方法
原始的LBP算子定义在像素33的邻域内,以邻域中心像素为阈值,相邻的8个像素的灰度值与邻域中心的像素值进行比较,若周围像素大于中心像素值,则该像素点的位置被标记为1,否则为0。这样,33邻域内的8个点经过比较可产生8位二进制数,将这8位二进制数依次排列形成一个二进制数字,这个二进制数字就是中心像素的LBP值,LBP值共有28
种可能,因此LBP值有256种。中心像素的LBP值反映了该像素周围区域的纹理信息。
计算LBP特征的图像必须是灰度图,如果是彩色图,需要先转换成灰度图。
上述过程用图像表示为:
将上述过程用公式表示为:
(xc,yc)为中心像素的坐标,p为邻域的第p个像素,ip为邻域像素的灰度值,ic为中心像素的灰度值,s(x)
为符号函数
原始LBP特征计算代码(Opencv下):
int main(int argc, char** argv) {
src = imread("E:/tuku/text1.jpg");
if (src.empty()) {
cout << "can't find this picture...";
return -1;
}
imshow("input", src);
// convert to gray
cvtColor(src, gray_src, COLOR_BGR2GRAY);
int width = gray_src.cols;
int height = gray_src.rows;
//基本LBP演示
Mat lbgImage = Mat::zeros(gray_src.rows - 2, gray_src.cols - 2, CV_8UC1);
for (int row = 1; row < height - 1; row++) {
for (int col = 1; col < width - 1; col++) {
uchar c = gray_src.at<uchar>(row, col);
uchar code = 0;
code |= (gray_src.at<uchar>(row - 1, col - 1) > c) << 7;
code |= (gray_src.at<uchar>(row - 1 , col ) > c) << 6;
code |= (gray_src.at<uchar>(row - 1, col +1) > c) << 5;
code |= (gray_src.at<uchar>(row , col+1) > c) << 4;
code |= (gray_src.at<uchar>(row +1, col +1) > c) << 3;
code |= (gray_src.at<uchar>(row +1, col ) > c) << 2;
code |= (gray_src.at<uchar>(row +1, col - 1) > c) << 1;
code |= (gray_src.at<uchar>(row, col -1) > c) << 0;
lbgImage.at<uchar>(row-1, col-1) = code;
}
}
imshow("out put", lbgImage);
效果图:
2、LBP特征的改进版本
在原始的LBP特征提出以后,研究人员对LBP特征进行了很多的改进,因此产生了许多LBP的改进版本。
2.1 圆形LBP特征(Circular LBP or Extended LBP)
由于原始LBP特征使用的是固定邻域内的灰度值,因此当图像的尺度发生变化时,LBP特征的编码将会发生错误,LBP特征将不能正确的反映像素点周围的纹理信息,因此研究人员对其进行了改进[3]。基本的 LBP 算子的最大缺陷在于它只覆盖了一个固定半径范围内的小区域,这显然不能满足不同尺寸和频率纹理的需要。为了适应不同尺度的纹理特征,并达到灰度和旋转不变性的要求,Ojala 等对 LBP 算子进行了改进,将 3×3 邻域扩展到任意邻域,并用圆形邻域代替了正方形邻域,改进后的 LBP 算子允许在半径为 R 的圆形邻域内有任意多个像素点。从而得到了诸如半径为R的圆形区域内含有P个采样点的LBP算子:
这种LBP特征叫做Extended LBP,也叫Circular LBP。使用可变半径的圆对近邻像素进行编码,可以得到如下的近邻:
对于给定中心点(xc,yc),其邻域像素位置为(xp,yp),p∈P,其采样点(xp,yp)用如下公式计算
R是采样半径,p是第p个采样点,P是采样数目。由于计算的值可能不是整数,即计算出来的点不在图像上,我们使用计算出来的点的插值点。目的的插值方法有很多,Opencv使用的是双线性插值,双线性插值的公式如下:
# include<opencv2\opencv.hpp>
# include <iostream>
# include <math.h>
using namespace std;
using namespace cv;
Mat src, gray_src;
int current_radius = 3;
int max_count = 20;
void ELBP_Demo(int, void*);
int main(int argc, char** argv) {
src = imread("E:/tuku/text1.jpg");
if (src.empty()) {
cout << "can't find this picture...";
return -1;
}
imshow("input", src);
// convert to gray
cvtColor(src, gray_src, COLOR_BGR2GRAY);
int width = gray_src.cols;
int height = gray_src.rows;
//基本LBP演示
Mat lbgImage = Mat::zeros(gray_src.rows - 2, gray_src.cols - 2, CV_8UC1);
for (int row = 1; row < height - 1; row++) {
for (int col = 1; col < width - 1; col++) {
uchar c = gray_src.at<uchar>(row, col);
uchar code = 0;
code |= (gray_src.at<uchar>(row - 1, col - 1) > c) << 7;
code |= (gray_src.at<uchar>(row - 1 , col ) > c) << 6;
code |= (gray_src.at<uchar>(row - 1, col +1) > c) << 5;
code |= (gray_src.at<uchar>(row , col+1) > c) << 4;
code |= (gray_src.at<uchar>(row +1, col +1) > c) << 3;
code |= (gray_src.at<uchar>(row +1, col ) > c) << 2;
code |= (gray_src.at<uchar>(row +1, col - 1) > c) << 1;
code |= (gray_src.at<uchar>(row, col -1) > c) << 0;
lbgImage.at<uchar>(row-1, col-1) = code;//其实这里就相当于半径等于1
}
}
imshow("out put", lbgImage);
//ELBP演示
namedWindow("ELBP out put", 1);
createTrackbar("ELBP radius:", "ELBP out put", ¤t_radius, max_count, ELBP_Demo);
ELBP_Demo(0, 0);
waitKey(0);
return 0;
}
void ELBP_Demo(int, void*) {
int offset = current_radius * 2;
//LBP特征图像的行数和列数的计算要准确(这里确定它的大小)
Mat elbpImage = Mat::zeros(gray_src.rows - offset, gray_src.cols - offset,CV_8UC1);
int width = gray_src.cols;
int height = gray_src.rows;
int numNeighbors = 8;
for (int n = 0; n < numNeighbors; n++) {
//计算采样点对于中心点坐标的偏移量x,y
float x = static_cast<float>(current_radius*cos(2.0*CV_PI*n) / static_cast<float>(numNeighbors));
float y = static_cast<float>(current_radius*-sin(2.0*CV_PI*n) / static_cast<float>(numNeighbors));
//为双线性插值做准备
//对采样点偏移量分别进行上下取整
int fx = static_cast<int>(floor(x));//floor向下取整,即不大于要求值的最大的那个整数值
int fy = static_cast<int>(floor(y));
int cx = static_cast<int>(ceil(x));//向上取整计算,不小于最接近的整数。
int cy = static_cast<int>(ceil(y));
float tx = x - fx;
float ty = y - fy;
float w1 = (1 - tx)*(1 - ty);
float w2 = ty * (1 - tx);
float w3 = (1 - tx)*ty;
float w4 = tx * ty;
for (int row = current_radius; row < (height - current_radius); row++) {
for (int col = current_radius; col < (width - current_radius); col++) {
float t = w1 * gray_src.at<uchar>(row + fy, col + fx) + w2 * gray_src.at<uchar>(row + fy, col + cx) +
w3 * gray_src.at<uchar>(row + cy, col + fx) + w4 * gray_src.at<uchar>(row + cy, col + cx);
elbpImage.at<uchar>(row - current_radius, col - current_radius) +=
((t > gray_src.at<uchar>(row, col)) && (abs(t - gray_src.at<uchar>(row, col)) > std::numeric_limits<float>::epsilon())) << n;
}
}
//不知道此处是应该用|=还是+=?
}
imshow("ELBP out put", elbpImage);
}
效果图:
2.2 旋转不变LBP特征
从上面可以看出,上面的LBP特征具有灰度不变性,但还不具备旋转不变性,因此研究人员又在上面的基础上进行了扩展,提出了具有旋转不变性的LBP特征。
首先不断的旋转圆形邻域内的LBP特征,根据选择得到一系列的LBP特征值,从这些LBP特征值选择LBP特征值最小的作为中心像素点的LBP特征。具体做法如下图所示:
如上图所示(图片摘自网络),原始LBP得到的数值转化为二进制编码,对它进行循环移位操作,有8种情况(包括自身)。取其中最小的一个值,比如图中就对应着15,这个值是旋转不变的,因为对图像做旋转操作等价与上面8种移位的过程了,而8种情况都对应同一个值,即8个值中的最小值15,即拥有了旋转不变特性。
//旋转不变圆形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-2*radius,src.cols-2*radius,CV_8UC1);
Mat dst = _dst.getMat();
dst.setTo(0);
for(int k=0;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 = (1-tx) * (1-ty);
float w2 = tx * (1-ty);
float w3 = (1-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-1);
}
}
}
//进行旋转不变处理
for(int i=0;i<dst.rows;i++)
{
for(int j=0;j<dst.cols;j++)
{
unsigned char currentValue = dst.at<uchar>(i,j);
unsigned char minValue = currentValue;
for(int k=1;k<neighbors;k++)
{
//循环左移
unsigned char temp = (currentValue>>(neighbors-k)) | (currentValue<<k);
if(temp < minValue)
{
minValue = temp;
}
}
dst.at<uchar>(i,j) = minValue;
}
}
}
radius = 3,neighbors = 8,最后一幅是旋转不变LBP特征