LBP算子特点

LBP(Local Binary Pattern),即局部二值模式,属于一种图像预处理算法,具有光照不变性和旋转不变性。

我目前做的项目是人脸表情识别,采用这种算法可以减少光照和人脸旋转对表情分类结果的影响,提升识别算法的鲁棒性(还没有完全的实践确认)。

LBP的发展过程

八邻域LBP

取一个像素点的周围8个邻域点,根据邻域点和中心像素点之间的相对大小关系,将高于中心像素点的邻域点取为1,低于中心像素点的邻域点取为0,并将其全部连接成一个8位二进制数,将此二进制数作为中心像素点的LBP算子值,下面用一个例子说明:

如图所示,中心像素点的LBP算子值就是00011101 = 29。

圆形LBP

随着算法的使用,Ojala等人感觉8邻域LBP算法过于死板,于是他们改为选择一个任意大小的圆形邻域,作为邻域点的取值范围。

邻域点的取值公式如下:

\[x_{p} = x_{c} + R*cos(2πp/P)
\]

\[y_{p} = y_{c} + R*sin(2πp/P)
\]

其中\(x_c\), \(y_c\)是中心点的坐标值,R为圆形邻域的半径,P为邻域点的总个数,p取值为\((0, p-1)\)。

但是这个公式算出来的值并不一定是整数,需要用双线性插值把邻域点的像素值计算出来。

下面大概对双线性插值进行一点解释。

双线性插值

这个东西听起来很复杂(最少一开始我是不理解的),但是经过查阅资料后,我发现这其实是一个很简单的方法。

为了求未知点处的函数值,假设已知周围4个整数点函数值,就可以通过插值的方法确定P点的函数值。

假设未知点为\(P(x, y)\),周围4个整数点是\(Q_{11}(x_{11}, y_{11}), Q_{12}(x_{12}, y_{12}), Q_{21}(x_{21}, y_{21}), Q_{22}(x_{22}, y_{22})\),则公式如下:

\[F(R_{1}) = \frac{x_{12} - x}{x_{12} - x_{11}} * F(Q_{11}) + \frac{x - x_{11}}{x_{12} - x_{11}} * F(Q_{12})
\]

\[F(R_{2}) = \frac{x_{22} - x}{x_{22} - x_{21}} * F(Q_{21}) + \frac{x - x_{21}}{x_{22} - x_{21}} * F(Q_{22})
\]

\[F(P) = \frac{y_{21} - y}{y_{21} - y_{11}} * F(R_{1}) + \frac{y - y_{11}}{y_{21} - y_{11}} * F(R_{2})
\]

如下图所示:

旋转不变LBP算子

本身的LBP算子实现了光照不变性,但是前面所提到的旋转不变性并没有实现,那么是怎么实现的?

Ojala这些人就很强,他们是通过将一个像素点周围的全部取样点进行旋转,每旋转一次,计算一次LBP算子值,从中取最小值作为该像素点的LBP算子值。

这样即使图片发生了旋转,每个像素点周围的取样点旋转之后的最小值都是同一个值,这样就实现了旋转的不变性。

具体如下图所示:

LBP等价模式

然后Ojala等人这样还不满意,他们还觉得这个算子的维度太高了,有\(2^{n}\)维(比如8个取样点的话就有256维),于是他们就提出了一种等价模式。

这个模式有一个前提就是Ojala等人实验发现,大部分的像素点的LBP所对应的循环二进制数最多包含两次0到1或1到0的跳变。

然后,在这个前提下,Ojala等人就给了等价模式的定义:

当某个LBP所对应的循环二进制数从0到1或从1到0最多有两次跳变时,该LBP所对应的二进制就称为一个等价模式类;除等价模式以外的模式都归为一类,称为混合模式类。

这样,假设共有n个取样点,那么等价模式将有 \(n*(n-1)+2\)种,加上那一种混合模式,等价模式下的LBP算子将有 \(n*(n-1)+3\)维(如果有8个取样点,LBP的维度将从256维降低到59维,而这个维度的降低还不会导致大部分信息的损失,不得不说Ojala这些大佬太强了)

关于这个模式,我自己有一些想说的东西:

网上大多数关于LBP的描述中,这一部分都是简单的把概念和公式罗列出来,但是关于定义的理解却很容易让新手产生歧义。

我第一次看的时候就对这个模式产生了一些误解,我无论如何计算都达不到上面给出的那个公式,甚至一度以为那个公式是错的,但是后来我才发现我忽视了上面加粗的字:“循环”。

“循环”就意味着等价模式只会有0次跳变或2次跳变,不可能存在1次跳变。即00000111是2次跳变,因为除了在第4位到第5位上有一次从0到1的跳变,在第7位到第0位上也发生了一次从1到0的跳变。只要意识到这个事情,上面的那个等价模式数量公式就很容易求出来了。

这样LBP算子的发展过程也就介绍完了,最后还是想要感叹一下那些人的聪明才智啊。对比之下,为什么自己就这么菜呢

自己在项目中要怎么使用LBP算子

自己的人脸表情识别算法是在github上找的一个项目,这个项目是通过深度学习CNN算法实现的,正常条件下的表情识别成功率很高,但是经过实验发现,在低光照或有遮挡物等情况下,识别率有大幅下降,所以针对这一情况,我选择了LBP算子来提升低光照下的算法的识别率。

Github项目链接如下:

人脸识别与卡通化

因为深度学习算法需要用大量的数据库进行训练,所以我认为要想使用LBP算子,必须要对数据库和待识别图像全部计算LBP才能实现识别的目的。

读取文件夹中的图片

要对训练库中的图像进行LBP处理,首先要对文件夹中的图片进行读取,而这一步并不是LBP相关的知识,只是os库的一些基本应用,所以不做过多解释,只是简单地将代码贴上来

from skimage.transform import rotate
from skimage.feature import local_binary_pattern
from skimage import data, io, data_dir, filters, feature
from skimage.color import label2rgb
import cv2
import numpy as np
import os
import glob as gb data_path = os.getcwd() + r'\data'
save_path = os.getcwd() + r"\LBP_data"
train_path = data_path + r'\train'
test_path = data_path + r'\test'
val_path = data_path + r'\val' train_LBP = save_path + r"\train"
test_LBP = save_path + r"\test"
val_LBP = save_path + r"\val" size = 100
part_size = 10
radius = 1
n_points = 8 if __name__ == '__main__':
if not os.path.exists(save_path):
os.makedirs(save_path) for save, origin_dir in [(train_LBP, train_path), (test_LBP, test_path), (val_LBP, val_path)]:
if not os.path.exists(save):
os.makedirs(save) for i in range(7):
sub_dir = origin_dir + '\\' + str(i)
sub_save = save + '\\' + str(i)
if not os.path.exists(sub_save):
os.makedirs(sub_save)
image_list = gb.glob(sub_dir + r'\*.jpg')
image_list.sort()
for image_dir in image_list:
(image_path, image_name) = os.path.split(image_dir)
image = cv2.imread(image_dir)
gray_image = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
lbp = local_binary_pattern(gray_image, radius, n_points, method='nri_uniform')
lbp *= 255.0
cv2.imwrite(sub_save + '\\' + image_name, lbp)

通过上面的代码,就实现了读取项目给出的图像数据库并进行LBP计算的目的

计算LBP算子并存为图片

这里一开始我是认为很简单的,就是通过上面的代码中的下面几行实现的,反而比读取图片还要简单

image = cv2.imread(image_dir)
gray_image = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
lbp = local_binary_pattern(gray_image, radius, n_points, method='nri_uniform')
lbp *= 255.0
cv2.imwrite(sub_save + '\\' + image_name, lbp)

这里的local_binary_pattern()函数是scikit-image库中的函数,直接就实现了LBP的功能。

本来我以为这是极好的,效果如下:



这个结果除了失真极其严重之外,还有一个致命的问题——这是一个二值化图像!

经过debug排查,我发现local_binary_pattern()这个函数的返回值是由整数0和1组成的矩阵,也就是说经过放大255倍之后,得到也只会是整数0和255组成的矩阵,这样产生的LBP算子图像也只会是二值化图像。

这显然不能满足我们的需要。。。所以我经过一番尝试无果后(主要是看不到第三方库中函数的定义),选择自己写一个计算LBP算子的函数

自己实现的LBP算子

既然要自己实现首先要有一个明确的实现流程,而我的实现流程如下:

我在学习过程中,发现几乎所有的地方都说不应该直接拿LBP算子值进行分类,而是应该将图片进行分块,而后统计每块中的LBP算子的直方图,最后将每块的直方图连接为一个矩阵来代表这个图像,再进行后续的分类等操作

这也就是流程图中分块相关操作的原因

图像的LBP前处理

因为训练库中的图像都是28*28的小图像,所以我首先要放大图像尺寸,以方便之后的分块和统计

LBP的主要实现是靠中心像素点和周围邻域点的像素值比较实现的,那么边缘处的像素点该怎么处理呢?

这里我选择的是将图像边界扩大,扩大边界的方法直接将边界处像素点像素值复制给边界,我认为这样能最大限度地减少人为扩大的边界对结果的影响

# 输入图像的预处理
def pre_image(image, size, radius):
# image 输入图像
# size 放大后图像尺寸
# radius LBP邻域半径值 gray_image = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
gray_image = cv2.resize(gray_image, (size, size))
gray_image = cv2.copyMakeBorder(gray_image, radius, radius, radius, radius, cv2.BORDER_REPLICATE)
return gray_image

计算单个像素点的LBP算子值

这里没什么好说的,都是上面发展过程中的实现方法

# 双线性插值
def twice_insert(image, x, y):
# image 输入图片
# x 像素点的x坐标
# y 像素点的y坐标 x1 = math.floor(x)
x2 = math.ceil(x)
y1 = math.floor(y)
y2 = math.ceil(y)
if x2 == x1:
pixel = image[x1][y1] if y1 == y2 else round((y2 - y) / (y2 - y1) * image[x][y1] + (y - y1) / (y2 - y1) * image[x][y2])
else:
temp1 = int((x2 - x) / (x2 - x1) * image[x1][y1] + (x - x1) / (x2 - x1) * image[x2][y1])
temp2 = int((x2 - x) / (x2 - x1) * image[x1][y2] + (x - x1) / (x2 - x1) * image[x2][y2])
pixel = temp1 if y1 == y2 else round((y2 - y) / (y2 - y1) * temp1 + (y - y1) / (y2 - y1) * temp2)
return pixel # 找寻最小二进制数,实现旋转不变性
def min_bin(data):
# data 输入的LBP算子值(列表字符串形式) min_lbp = int(''.join(data), 2)
for i in range(len(data) - 1):
for j in range(len(data) - 1):
data[j], data[j + 1] = data[j + 1], data[j]
tem_lbp = int(''.join(data), 2)
if tem_lbp < min_lbp:
min_lbp = tem_lbp
return min_lbp # 单像素点的lbp处理
def single_lbp(x_pos, y_pos, image, radius, n_points):
# x_pos 像素点的x坐标
# y_pos 像素点的y坐标
# image 输入图像
# radius LBP邻域半径
# n_points LBP邻域点个数 x_points = []
y_points = []
for i in range(n_points):
x_points.append(round(float(x_pos + radius * math.cos(2 * math.pi * i / n_points)), 5))
y_points.append(round(float(y_pos + radius * math.sin(2 * math.pi * i / n_points)), 5))
# round(): python浮点数计算误差需处理
pixels = []
for x, y in zip(x_points, y_points):
pixels.append(twice_insert(image, x, y))
data = []
for pixel in pixels:
if pixel >= image[x_pos][y_pos]:
data.append('1')
else:
data.append('0')
lbp = min_bin(data)
return lbp

图像分块统计直方图

# 统计直方图
def statistics(part, n_points):
# part 一块图像中各像素点LBP值(列表形式)
# n_points LBP邻域点个数
hist = {}
for num in range(256):
binary = bin(num).replace('0b', '').zfill(n_points)
step = 0
for i in range(len(binary)):
j = 0 if i + 1 == len(binary) else i + 1
if binary[i] != binary[j]:
step += 1
if step <= 2:
hist[num] = 0
hist[256] = 0
for single in part:
binary = bin(single).replace('0b', '').zfill(n_points)
step = 0
for i in range(len(binary)):
j = 0 if i + 1 == len(binary) else i + 1
if binary[i] != binary[j]:
step += 1
if step <= 2:
hist[single] = hist[single] + 1
else:
hist[256] = hist[256] + 1
histogram = [sorted(hist.items(), key=lambda d: d[0])[i][1]for i in range(len(hist))]
return histogram # 对整幅图片进行分块,并统计LBP直方图
def part_lbp(image, size, part_size, radius, n_points):
# image 输入图像
# size 放大后图像大小
# part_size 每块图像大小
# radius LBP邻域半径
# n_points LBP邻域点个数
gray_image = pre_image(image, size, radius)
part_res = [[[]for m in range(size // part_size)]for n in range(size // part_size)]
for i in range(radius, size - radius):
for j in range(radius, size - radius):
part_i = (i - radius) // part_size
part_j = (j - radius) // part_size
part_res[part_i][part_j].append(single_lbp(i, j, gray_image, radius, n_points))
part_res = np.array(part_res)
part_res = part_res.flatten()
res = []
for part in part_res:
res.append(statistics(part, n_points))
return res

LBP结果可视化

为了能将LBP算子结果可视化,还是对整个图像部分快计算了LBP算子,并用cv2.imshow()显示,以方便进行对比

# 整幅图片的处理(显示LBP的图片,实际上要使用分块的LBP进行分类)
def lbp(image, size, radius, n_points):
# image 输入图像
# size 放大后图像大小
# radius LBP邻域半径
# n_points LBP邻域点个数
gray_image = pre_image(image, size, radius)
lbp = []
for i in range(radius, gray_image.shape[0] - radius):
row = []
for j in range(radius, gray_image.shape[1] - radius):
row.append(single_lbp(i, j, gray_image, radius, n_points))
lbp.append(row)
return lbp

测试用主程序

import cv2
import numpy as np
import matplotlib.pyplot as plt
import math size = 100
part_size = 10
radius = 1
n_points = 8 if __name__ == '__main__':
image = cv2.imread(img.png')
lbp = lbp(image, size, radius, n_points)
image_lbp = np.array(lbp)
res = part_lbp(image, size, part_size, radius, n_points)
image_res = np.array(res)
for data in image_res:
print(data)
plt.bar(x=range(1, n_points * (n_points - 1) + 4), height=data, color='blue', alpha=0.8)
plt.xlabel('pattern')
plt.ylabel('frequency')
plt.title('part_frequency_hist')
plt.show()
cv2.imshow('res', image_lbp)
cv2.waitKey(0)
cv2.destroyAllWindows()
cv2.imwrite('lbp.jpg', image_lbp)
print(res)

运行结果展示

图片第一部分LBP算子统计直方图展示(因不会将全部100张直方图同时展示,所以只展示第一部分的统计直方图)

和scikit-image库中的函数对比结果如下:

原图

scikit-image库中函数实现效果

自己的函数实现效果(因为将图像尺寸放大到了100*100,所以尺寸和原来不一致,但可以看出人脸表情相关信息基本没有丢失)

可以看出自己的函数是很好的实现了LBP算子的功能的,对比之下更加好奇库中函数到底实现的是个什么东西,可以说信息丢失很严重了

另外,这是对光照不变性的验证结果

最后,我还是要指出自己的算法的一个很重要的问题——就是有着极高的时间复杂度。。。大概处理一张图片要用1s的时间,这个效率是很低下的,所以如果有人能对我的代码提出改进意见,我将会非常感激。

LBP算子的更多相关文章

  1. 图像特征提取之LBP特征

    LBP(Local Binary Pattern,局部二值模式)是一种用来描述图像局部纹理特征的算子:它具有旋转不变性和灰度不变性等显著的优点.它是首先由T. Ojala, M.Pietik?inen ...

  2. 人脸识别经典算法二:LBP方法

    与第一篇博文特征脸方法不同,LBP(Local Binary Patterns,局部二值模式)是提取局部特征作为判别依据的.LBP方法显著的优点是对光照不敏感,但是依然没有解决姿态和表情的问题.不过相 ...

  3. 图像特征提取三大法宝:HOG特征,LBP特征,Haar特征(转载)

    (一)HOG特征 1.HOG特征: 方向梯度直方图(Histogram of Oriented Gradient, HOG)特征是一种在计算机视觉和图像处理中用来进行物体检测的特征描述子.它通过计算和 ...

  4. 转载:LBP的初步理解

    转自http://blog.csdn.net/ty101/article/details/8905394 本文的PDF版本,以及涉及到的所有文献和代码可以到下列地址下载: 1.PDF版本以及文献:ht ...

  5. 目标检测的图像特征提取之(二)LBP特征

    LBP(Local Binary Pattern,局部二值模式)是一种用来描述图像年提出,用于纹理特征提取.而且,提取的特征是图像的局部的纹理特征: 1.LBP特征的描述 原始的LBP算子定义为在3* ...

  6. 图像特征提取三大法宝:HOG特征,LBP特征,Haar特征

    (一)HOG特征 1.HOG特征: 方向梯度直方图(Histogram of Oriented Gradient, HOG)特征是一种在计算机视觉和图像处理中用来进行物体检测的特征描述子.它通过计算和 ...

  7. 机器视觉----LBP

    最近一直在做多视图的聚类与分裂,想要图片有更多的视图,就得对图片的特征进行抽取,那我们来聊聊图片的LBP特征. Local binary patterns (局部二值模式),是机器视觉中重要的一种特征 ...

  8. lbp特征提取(等价模式)

    LBP等价模式 考察LBP算子的定义可知,一个LBP算子可以产生多种二进制模式(p个采样点)如:3x3邻域有p=8个采样点,则可得到2^8=256种二进制模式:5x5邻域有p=24个采样点,则可得到2 ...

  9. LBP特征学习(附python实现)

    LBP的全称是Local Binary Pattern即局部二值模式,是局部信息提取中的一种方法,它具有旋转不变性和灰度不变性等显著的优点.在人脸识别领域有很多案例,此外,局部特征的算法还有 SIFT ...

随机推荐

  1. scala解析json —— json4s 解析json方法汇总

    使用json4s的框架,包括spark,flink 1.org.json4s 引入pom的方法 对于本地支持,引入以下依赖项添加到pom中 <dependency> <groupId ...

  2. python3中浮点数相减问题(大部分时候只需要关注整数的二进制形式就行了)

    首先直接上判断方法:一切判断直接将整数部分装化成二进制,如果位数相等则相减后的值是等于的,如果不相等,相减大的结果一定不等于你要比较的结果. 如66.6-60.6==6.0的情况,66.6的二进制:1 ...

  3. springboot整合RocketMq(非事务)

    1.配置文件 1.yml配置文件 rocketmq: #mq配置 producer: iseffect: true type: default # (transaction,default) tran ...

  4. Process Array

    http://xiaorui.cc/2016/05/10/%E6%BA%90%E7%A0%81%E5%88%86%E6%9E%90multiprocessing%E7%9A%84value-array ...

  5. vue PC端页面引入vue-quill-editor富文本插件

    项目需要:在添加新类别的弹框中,要在输入框中输入多条描述信息,不同的描述信息要换行输入,输入后点击确定传给后端,接口返回成功后点击查看刚添加的新类别时,描述框中展现多条换行的描述信息也要跟填写时一样( ...

  6. 为IIS服务器配置SSL,并设置为默认使用https协议访问网站

    要使网站支持https协议,需要SSL证书,我的服务器和域名都是在阿里云购买的,所以这里我演示阿里云获取SSL证书的方法 我先说下我的服务器环境:windows server 2012 + IIS8. ...

  7. Java中date和calendar的用法

    获取现在系统的时间和日期看起来是一件非常神奇的事情,但是当使用date和calendar之后发现仍然非常神奇. 1.date 使用date日期之前需要导入包: import java.text.Sim ...

  8. su - 运行替换用户和组标识的shell

    SYNOPSIS(总览) su [OPTION]... [-] [USER [ARG]...] DESCRIPTION(描述) 修改有效用户标识和组标识为USER的. -, -l, --login 使 ...

  9. Codeforces 351C Jeff and Brackets 矩阵优化DP

    题意:你要在纸上画一个长度为n * m的括号序列,第i个位置画左括号的花费是a[i % n], 画右括号的花费是b[i % n],问画完这个括号序列的最小花费.n <= 20, m <= ...

  10. PHP-在排序数组中查找元素的第一个和最后一个位置

    给定一个按照升序排列的整数数组 nums,和一个目标值 target.找出给定目标值在数组中的开始位置和结束位置. 你的算法时间复杂度必须是 O(log n) 级别. 如果数组中不存在目标值,返回 [ ...