LBP算子

LBP算子特点

LBP(Local Binary Pattern),即局部二值模式,属于一种图像预处理算法,具有光照不变性和旋转不变性。
我目前做的项目是人脸表情识别,采用这种算法可以减少光照和人脸旋转对表情分类结果的影响,提升识别算法的鲁棒性(还没有完全的实践确认)。

LBP的发展过程

八邻域LBP

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

八邻域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算子

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的功能。

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

测试用原图
测试用LBP算子图像

这个结果除了失真极其严重之外,还有一个致命的问题——这是一个二值化图像!
经过debug排查,我发现local_binary_pattern()这个函数的返回值是由整数0和1组成的矩阵,也就是说经过放大255倍之后,得到也只会是整数0和255组成的矩阵,这样产生的LBP算子图像也只会是二值化图像。
这显然不能满足我们的需要。。。所以我经过一番尝试无果后(主要是看不到第三方库中函数的定义),选择自己写一个计算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库中函数实现效果

scikit-image库中LBP算子结果图像

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

自己实现的LBP算子结果图像

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

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

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

posted @ 2019-11-23 22:02  ilk012  阅读(2054)  评论(0编辑  收藏  举报