LBP算子
LBP算子特点
LBP(Local Binary Pattern),即局部二值模式,属于一种图像预处理算法,具有光照不变性和旋转不变性。
我目前做的项目是人脸表情识别,采用这种算法可以减少光照和人脸旋转对表情分类结果的影响,提升识别算法的鲁棒性(还没有完全的实践确认)。
LBP的发展过程
八邻域LBP
取一个像素点的周围8个邻域点,根据邻域点和中心像素点之间的相对大小关系,将高于中心像素点的邻域点取为1,低于中心像素点的邻域点取为0,并将其全部连接成一个8位二进制数,将此二进制数作为中心像素点的LBP算子值,下面用一个例子说明:
如图所示,中心像素点的LBP算子值就是00011101 = 29。
圆形LBP
随着算法的使用,Ojala等人感觉8邻域LBP算法过于死板,于是他们改为选择一个任意大小的圆形邻域,作为邻域点的取值范围。
邻域点的取值公式如下:
其中\(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})\),则公式如下:
如下图所示:
旋转不变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的时间,这个效率是很低下的,所以如果有人能对我的代码提出改进意见,我将会非常感激。