前言
传统机器视觉在图像识别领域虽然比不了神经网络,但也有一些可使用的场景。最近做了Kaggle上的dog-breed-identification的比赛,感觉神经网络太容易过拟合了,导致结果不一定好,仅仅进行调参也很难防止,导致花的时间多,但结果并没有变得更好。对于简单干扰较小的情况,机器视觉还是占有优势的。
物体与场景识别中,通过纹理特征进行识别也是很常用的手段,图像的纹理是描述图像中重复和规律性的局部结构信息,如平滑度、粗糙度、周期性等,它可以反映图像内物体表面的空间排列和灰度变化。通过对纹理特征的分析,我们可以进行图像分类、目标识别、图像分割等操作。LBP算法就是以像素为单位的基础,通过灰度值编码,来提取图像的纹理特征的图像特征描述算法。
本算法使用的测试素材如下:
传统LBP算法
算法原理
在一幅图像中,原始的LBP算子定义了一个3×3的滑动模版,对于一个给定的像素点x,在滑动模版的范围内(四周)相邻的8个像素将与x进行减法操作,得到8个不同的相对灰度值。其中,若相对灰度值大于等于的灰度值,则编码为1,否则为0。从左上角的像素点开始按照顺时针拼接这些数(0或1),如此可以得到一个8位二进制数,将这个二进制数转化为10进制数作为x的灰度值。整个处理过程可用以下公式表达:
其中
I(c) - 为中心坐标的灰度值
I(p) - 周围八个像素点的灰度值
s(x)函数表达式如下:
整个过程示意图如下所示:
按照顺时针的方向,可以将上图中相对灰度值拼接为”01111100”,转化为10进制为”124”,将”124”作为上图中中间像素的值,当然如果算出的灰度值超过了图像灰度值范围,还需要进行归一化,将灰度值放入[0,255]的范围中。
代码实现
按照算法原理,可以将实现方法分割为以下步骤:
- 初始化图像:图像灰度化、边缘填充、转化数据类型(int8的数据类型支持范围太小)。
- 定义LBP算法:遍历图像所有像素点,按照LBP(x
c+yc)公式进行计算。
- 定义s(x)。
- 编码并转码:按照顺时针得到8为二进制数并将它转化为10进制。
- 展示图像:我们一般使用像素直方图将处理结果展示出来。
导入需要使用到的Python库:
1 2 3 4
| import cv2 import time import numpy as np import matplotlib.pyplot as plt
|
初始化图像
- 对图像进行灰度化处理。
cv2.imread(image, 0)
或cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
- 为了让图像的边缘像素也能进行LBP编码,我们需要在图像四周添加1像素的边缘填充。OpenCV中已经提供了相应的方法,
cv2.copyMakeBorder(image, 1, 1, 1, 1, borderType=cv2.BORDER_REPLICATE)
,其中borderType
为填充的方法,cv2.BORDER_REPLICATE
指的是以边缘像素进行填充。
- 转化图像中像素值的类型为
float
或者int32
。
代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| def init_image(input_path): """ 初始化图像 :param input_path: 图像路径 :return: 处理后的图像 """ image = cv2.imread(input_path, 0) image = cv2.copyMakeBorder(image, 1, 1, 1, 1, borderType=cv2.BORDER_REPLICATE) h, w = image.shape[0], image.shape[1] image = image.astype(np.int32) image_name = input_path.split("/")[-1] print(f"图像名:{image_name},高:{h}, 宽:{w}") return image
|
定义LBP算法并转码
我们需要循环图像中所有的像素点,计算部分按照LBP(xc+yc)公式编写即可。得到周围的8个LBP编码值后,我使用list
存储,后续拼接为string
类型,转化10进制数可以使用int(str, 2)
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39
| def LBP(image): """ LBP算法 :param image: 待处理图像 :param h: 图像的 :param w: :return: """ h, w = image.shape[0], image.shape[1] temp = np.zeros((h - 1, w - 1)) print("---图像处理中---") for i in range(1, h - 1): for j in range(1, w - 1): calculation_list = list() calculation_list.append(image[i - 1][j - 1] - image[i][j]) calculation_list.append(image[i][j - 1] - image[i][j]) calculation_list.append(image[i + 1][j - 1] - image[i][j])
calculation_list.append(image[i + 1][j] - image[i][j]) calculation_list.append(image[i + 1][j + 1] - image[i][j])
calculation_list.append(image[i][j + 1] - image[i][j]) calculation_list.append(image[i - 1][j + 1] - image[i][j]) calculation_list.append(image[i - 1][j] - image[i][j]) for k in range(8): calculation_list[k] = transform(calculation_list[k]) result_str = "".join([str(i) for i in calculation_list]) temp[i - 1][j - 1] = int(result_str, 2) temp = np.clip(temp, 0, 255).astype("uint8") temp = cv2.cvtColor(temp, cv2.COLOR_BGR2RGB) print("---图像处理完成---") return temp
|
其中transform
函数即为公式中的s(x)。
1 2 3 4 5
| def transform(num): if num > 0: return 1 else: return 0
|
展示处理结果
对于这种需要进行对比的算法,一般都会采用Matplotlib
进行图像的展示,这样会更利于结果的对比。使用Matplotlib
展示要注意颜色格式的不同,需要将常规的BGR
转化为RGB
格式,转化方法为:cv2.cvtColor(temp, cv2.COLOR_BGR2RGB)
。
对于像素直方图,我们可以使用plt.hist(image,nut)
进行绘制,其中:
- image: 需要绘制的值,这边可以直接使用
image.ravel()
将h*w
的矩阵转化为1*(h*w)
的格式。
- nut: 分组间隔
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| def show_result(image_list, title_list): """ 展示处理后的图像及其像素直方图 :param image_list: 图像列表 :param title_list: 标题列表 :return: None """ plt.figure(figsize=(8, 5), dpi=100) for i in range(4): plt.subplot(2, 2, i + 1) plt.title(title_list[i]) if i % 2 == 1: plt.xticks(range(0, 257, 16), range(0, 17)) plt.hist(image_list[int(i / 2)].ravel(), 16) plt.xlabel("Dimension") plt.ylabel("Value") else: plt.axis("off") plt.imshow(image_list[int(i / 2)]) plt.tight_layout() plt.show()
|
测试代码与结果展示
1 2 3 4 5 6 7 8 9 10 11 12
| if __name__ == '__main__': input_path1 = "input/lane1.jpg" image = init_image(input_path1) result1 = LBP(image)
input_path2 = "input/lane2.jpg" image = init_image(input_path2) result2 = LBP(image)
image_list = [result1, result2] title_list = ["lane1_LBP", "lane1_LBP_hist", "lane2_LBP", "lane2_LBP_hist"] show_result(image_list, title_list)
|
输出结果为:
处理的速度还是比较快的,可以看到除了14-15这个分段值有些不同,其余各数值排布的值几乎完全一致。这说明这两张照片所展示的场景非常相似。
Circular LBP算法
算法原理
我们已经知道了传统LBP算法的基本原理。我们可以想象一下,如果我们将图像旋转,编码后的值是否一样呢?答案是否定的,具体原因如下图:
可以看到现在拼接后的字符串为”00011111”,转化为10进制为”31”,而不是原来的”124”。图像只是旋转了90°,但图像的实际内容并没有任何变化,所以传统的LBP算法是不具备旋转不变形的,这对于识别来说是相当不利的。
而且由于传统LBP采用的是3×3的模版,因此对周围像素的依赖是比较大的,可能会受到噪声的干扰。而且在一些复杂场景下,也没法根据情况进行参数调整。
总的来说,传统LBP算法虽然有着灰度不变性,但由于LBP值是按照顺时针进行编码,因此并不具有旋转不变性,周围八个像素也不能满足不同尺寸和频率纹理的需要。为了解决这些问题,后续的学者们也提出了很多的方法,我们只展示较为简单的Circular LBP算法。
相比于传统LBP算法,Circular LBP是以R为半径的圆作为取点范围,在圆形区域内选择P个像素点替代中心像素点周围的八个像素点,其中第i个像素点的坐标计算公式如下:
其中
(xc,yc) - 中心点坐标
R - 圆形区域半径
P - 选取点的总个数。
如何取值的示意如下图所示。
对于计算后结果不为整数的坐标值,在图像中是无法找到的,这时候可以使用双线性插值的计算值作为该坐标点的像素值。
最后,为了保证图像的旋转不变性,还需要进行如下操作:
将所得二进制字符串按照顺时针旋转顺序不断更换起始点,选取其中转化为10进制的最小值作为该点的LBP值,示意图如下所示。
Circular LBP算法通过圆形邻域、插值和旋转不变性的增强,使得相似纹理区域的编码更加一致。这种一致性有助于我们从图像中提取出更稳定、更准确的纹理特征。
代码实现
在了解了原理之后,我们可以大致可以得到如下的实现步骤:
- 图像初始化处理:灰度化、边缘填充、转化数据类型
- 定义双线性插值:对于计算出非整数的坐标,可以采用双线性插值得到该坐标点的像素值
- 在2进制字符串中找到转码后10进制的最小值
- 定义Circlar LBP的计算:按照公式进行,有一些细节需要注意
- 结果展示与分析:相比传统LBP有什么优点,性能如何
初始化处理
因为使用的是以R为圆心的圆形范围,因此我们在进行边缘填充时,需要按照R的大小进行填充。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| def init_image(input_path, R): """ 初始化图像 :param input_path: 图像路径 :param R:范围圆的半径 :return:处理后的图像 """ image = cv2.imread(input_path, 0) h, w = image.shape[0], image.shape[1] image = cv2.copyMakeBorder(image, R, R, R, R, borderType=cv2.BORDER_REPLICATE) image = image.astype(np.int32) image_name = input_path.split("/")[-1] print(f"图像名:{image_name},高:{h}, 宽:{w}") return image
|
定义双线性插值
在计算坐标点时,由于包括三角函数计算,因此很容易得出非整数值。当计算出的像素点坐标无法在图像中找到时,就可以使用双线性插值的方法计算出该点灰度值。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| def bilinear_interpolation(xp, yp, image): """ 双线性插值 :param xp: x坐标 :param yp: y坐标 :param image: 输入的图像 :return: 计算后所得(x,y)的灰度值 """ h, w = image.shape[0], image.shape[1] x1, y1 = int(xp), int(yp) if x1 > w - 1: x2 = x1 - 1 else: x2 = x1 + 1 if y1 > h - 1: y2 = y1 - 1 else: y2 = y1 + 1 f_x1y1 = image[y1, x1] f_x1y2 = image[y2, x1] f_x2y1 = image[y1, x2] f_x2y2 = image[y2, x2] pixel_value = f_x1y1 * (x2 - xp) * (y2 - yp) + f_x2y1 * (xp - x1) * (y2 - yp) + \ f_x1y2 * (x2 - xp) * (yp - y1) + f_x2y2 * (xp - x1) * (yp - y1) return pixel_value
|
找到最小的转码值
按照顺时针方向,转化起点的过程,我采用的是不断交换首尾字符的方法,不断变换2进制字符串的起点,因此可以找到所有2进制字符串中转化为10进制后最小的那个值。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| def get_min(str, p): """ 得到最小值编码 :param str:二进制字符串 :param p:获取点的个数 :return:十进制最小的值 """ min = int(str, 2) for i in range(len(str) - 1): temp = str[1:p] temp = temp + str[0] str = temp if int(str, 2) < min: min = int(str, 2) return min
|
定义Circular_LBP算法
这个过程按照公式编写即可,但有四个点需要注意:
对于三角函数运算,需要现将计算所得弧度制其转化为角度制,可以使用np.radians()
方法。
在我们的坐标值计算公式中,索引都是以1为起点(数学坐标系)。因此为了对应图像坐标系,我们在计算坐标时进行减一操作,不然会出现索引越界的情况。
由于计算所得的xp
与yp
必定为float
类型,因此即便是整数,也会多显示一位小数0(如35.0),为此,我们需要使用num.is_integer()
的方法进行整数的判断,这样才能精确根据所得坐标值进行不同的操作。
因为计算复杂,因此使用time
模块进行计算运行时间,来估量程序的运行效率。使用案例为:
1 2 3 4 5 6 7 8 9
| start_time = time.time() """ 程序运行代码 """
end_time = time.time()
all_time = end_time - start_time
|
实现代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39
| def circular_extended_lbp(image_path, r, p): image = init_image(image_path, r) h, w = image.shape[0], image.shape[1] print(f"开始LBP处理,设定参数:半径:{r} 点个数:{p}") start_time = time.time() temp = np.zeros((h - r, w - r)) for i in range(r, h - r): for j in range(r, w - r): calculation_list = list() for k in range(p): radians = np.radians(2 * np.pi * k / float(p)) xp = j + r * np.cos(radians) - 1 yp = i - r * np.sin(radians) - 1 xp = init_integer(xp) yp = init_integer(yp) try: calculation_list.append(image[xp][yp] - image[i][j]) except: calculation_list.append(bilinear_interpolation(xp, yp, image) - image[i][j]) calculation_list[k] = transform(calculation_list[k]) result_str = "".join([str(i) for i in calculation_list]) temp[i - r][j - r] = get_min(result_str, p) temp = np.clip(temp, 0, 255).astype("uint8") temp = cv2.cvtColor(temp, cv2.COLOR_BGR2RGB) end_time = time.time() all_time = end_time - start_time print(f"LBP处理完毕,耗时:{int(all_time // 60)}m{int(all_time % 60)}s") print("-" * 100) return temp
|
其中init_integer
函数为:
1 2 3 4 5
| def init_integer(num): if num.is_integer(): return int(num) else: return num
|
展示结果
展示部分的代码与之前代码并无差别,因此不做展示,直接展示测试代码与结果。
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| if __name__ == '__main__': R = 2 P = 6 input_path = "./input/lane1.jpg" result_image1 = circular_extended_lbp(input_path, R, P)
input_path2 = "./input/lane2.jpg" result_image2 = circular_extended_lbp(input_path, R, P)
image_list = [result_image1, result_image2] title_list = ["lane1_circular_LBP", "lane1_circular_LBP_hist", "lane2_circular_LBP", "lane2_circular_LBP_hist"] show_result(image_list, title_list)
|
结果展示:
可以看出,用Circular LBP处理后的图像,像素直方图几乎完全一致,且分布较为单一。这说明Circular LBP通过圆形邻域、插值和旋转不变性的增强,使得相似纹理区域的编码更加一致。表现在结果上,即既能得出结论的同时,抵抗干扰的能力会比传统的LBP算法要强。最后根据运行时间可以看出,该程序的运行效率不高,还有优化的空间。