图片相似度计算

ZhuYuanxiang 2019-02-26 00:00:00
Categories: Tags:

图片相似度计算

背景

要识别两张图片是否相似,首先我们可能会区分这两张图是人物照,还是风景照等……对应的风景照是蓝天还是大海……做一系列的分类。

从机器学习的的角度来说,首先要提取图片的特征,将这些特征进行分类处理,训练并建立模型,然后在进行识别。

但是让计算机去区分这些图片分别是哪一类是很不容易的,不过计算机可以知道图像的像素值的,因此,在图像识别过程中,通过颜色特征来识别是相似图片是我们常用的(当然还有其特征还有纹理特征、形状特征和空间关系特征等,这些有分为直方图,颜色集,颜色局,聚合向量,相关图等来计算颜色特征),

为了得到两张相似的图片,在这里通过以下几种简单的计算方式来计算图片的相似度:

直方图

理论

比如有图像A和图像B,分别计算两幅图像的直方图,HistA,HistB,然后计算两个直方图的归一化相关系数(巴氏距离,直方图相交距离)等等。

这种思想是基于简单的数学上的向量之间的差异来进行图像相似程度的度量,这种方法是目前用的比较多的一种方法,第一,直方图能够很好的归一化,比如通常的256个bin条的。那么两幅分辨率不同的图像可以直接通过计算直方图来计算相似度很方便。而且计算量比较小。

这种方法的缺点:

  1. 直方图反映的是图像像素灰度值的概率分布,比如灰度值为200的像素有多少个,但是对于这些像素原来的位置在直方图中并没有体现,所以图像的骨架,也就是图像内部到底存在什么样的物体,形状是什么,每一块的灰度分布式什么样的这些在直方图信息中是被省略掉得。那么造成的一个问题就是,比如一个上黑下白的图像和上白下黑的图像其直方图分布是一模一样的,其相似度为100%。
  2. 两幅图像之间的距离度量,采用的是巴氏距离或者归一化相关系数,这种用分析数学向量的方法去分析图像本身就是一个很不好的办法。
  3. 就信息量的道理来说,采用一个数值来判断两幅图像的相似程度本身就是一个信息压缩的过程,那么两个256个元素的向量(假定直方图有256个bin条)的距离用一个数值表示那么肯定就会存在不准确性。

下面是一个基于直方图距离的图像相似度计算的Matlab Demo和实验结果.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
%计算图像直方图距离
%巴氏系数计算法

M=imread('1.jpg');
N=imread('2.jpg');
I=rgb2gray(M);
J=rgb2gray(N);

[Count1,x]=imhist(I);
[Count2,x]=imhist(J);
Sum1=sum(Count1);Sum2=sum(Count2);
Sumup = sqrt(Count1.*Count2);
SumDown = sqrt(Sum1*Sum2);
Sumup = sum(Sumup);
figure(1);
subplot(2,2,1);imshow(I);
subplot(2,2,2);imshow(J);
subplot(2,2,3);imhist(I);
subplot(2,2,4);imhist(J);
HistDist=1-sqrt(1-Sumup/SumDown)

通过上图可以看到这种计算图像相似度的方法确实存在很大的弊端。然而很多人也对于这种方法进行了修改,比如FragTrack算法,具体可以参见这篇论文《》。其中对图像分成横纵的小块,然后对于每一个分块搜索与之最匹配的直方图。来计算两幅图像的相似度,融入了直方图对应位置的信息。但是计算效率上很慢。

还有一种是计算一个图像外包多边形,一般得到跟踪图像的前景图后计算其外包多边形,根据外包多边形做Delauny三角形分解,然后计算每个三角形内部的直方图,对于这两个直方图组进行相似距离计算。这样就融入了直方图的位置信息。

操作

下面三张图片,分别是img1, img2, img3:

imgimg1

imgimg2

imgimg3

可以看出上面这三张图是相似的,颜色上是差不多的,最相似的是哪两张大家可以猜猜看,看和我们计算的是否一样。

在 python 中利用 OpenCV 中的 calcHist() 方法获取其直方图数据,返回的结果是一个列表:

1
2
3
4
5
6
import cv2

# 计算图img1的直方图
H1 = cv2.calcHist([img1], [1], None, [256], [0, 256])
# 对图片进行归一化处理
H1 = cv2.normalize(H1, H1, 0, 1, cv2.NORM_MINMAX, -1)

先计算img1的直方图,在对其归一化,最后在分别对img2,img3计算,做归一化,然后在利用python自带的compareHist()进行相似度的比较:

1
2
# 利用compareHist()进行比较相似度
similarity1 = cv2.compareHist(H1, H2, 0)

最后得到三张图片的直方图如下:

img

图像的x轴是指的图片的0255之间的像素变化,y轴指的是在这0255像素所占的比列。

我们可以明显的看出img2与img3的直方图的变化趋势是相符的有重合态的,运行结果如下:

1
2
3
img1和img2的相似度:0.817515
img1和img3的相似度:0.862289
img2和img3的相似度:0.863098

通过运行结果知道img2和img3是值是最为相似的

上面的是直接调用opencv中的方法来实现的,下面还有自己写的方法:

首先是将图片转化为RGB格式,在这里是用的pillow中的Image来对图片做处理的:

1
2
3
4
# 将图片转化为RGB
def make_regalur_image(img, size=(64, 64)):
gray_image = img.resize(size).convert('RGB')
return gray_image

在计算两图片的直方图:

1
2
3
4
5
# 计算直方图
def hist_similar(lh, rh):
assert len(lh) == len(rh)
hist = sum(1 - (0 if l == r else float(abs(l - r)) / max(l, r)) for l, r in zip(lh, rh)) / len(lh)
return hist

在计算其相似度:

1
2
3
4
# 计算相似度
def calc_similar(li, ri):
calc_sim = hist_similar(li.histogram(), ri.histogram())
return calc_sim

得到最终的运行结果:

1
2
3
img1和img2的相似度:0.525800
img1和img3的相似度:0.530099
img2和img3的相似度:0.468747

两种方法的的结果还是有点差距的,可以看到img1和img3的结果相似度高些。

不过两者的相似度计算方法如下:

$$
\frac1N\sum_{i=1}^N(1-\frac{|g_i-s_i|}{\max(g_i,s_i)})
$$
$g_i$和$s_i$分别指的是两条曲线的第$i$个点。

总结

利用直方图计算图片的相似度时,是按照颜色的全局分布情况来看待的,无法对局部的色彩进行分析,同一张图片如果转化成为灰度图时,在计算其直方图时差距就更大了。

为了解决这个问题,可以将图片进行等分,然后在计算图片的相似度。不过在这里我就不叙述了,大家自行探讨!!!

哈希算法

感知哈希(Hash)算法

感知哈希算法是一类算法的总称,包括:aHash、pHash、dHash。顾名思义,感知哈希不是以严格的方式计算 Hash 值,而是以更加相对的方式计算 Hash 值,因为“相似”与否,就是一种相对的判定。算法输出的值越小,相似度越高,取值为$0\sim64$,即

几种hash值的比较

平均值哈希算法(aHash)

该算法是基于比较灰度图每个像素与平均值来实现。

aHash 的 Hamming 距离步骤

感知哈希算法(pHash)

平均值哈希虽然简单,但是受均值影响大。如果对图像进行伽马校正或者进行直方图均值化都会影响均值,从而影响哈希值的计算。所以就有人提出更健壮的方法,通过离散余弦(DCT)进行低频提取。

离散余弦变换(DCT)是种图像压缩算法,它将图像从像素域变换到频率域。然后一般图像都存在很多冗余和相关性的,所以转换到频率域之后,只有很少的一部分频率分量的系数才不为0,大部分系数都为0(或者说接近于0)。

pHash的计算步骤

差异值哈希算法(dHash)

相比 pHash,dHash 的速度要快的多,相比 aHash,dHash 在效率几乎相同的情况下的效果要更好,它是基于渐变实现的。

dHash 的 Hamming 距离步骤:

计算哈希值差异

算法代码

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
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
import cv2
import numpy as np
from PIL import Image

def ahash(img):
"""平均值 Hash 算法"""
# 将图片缩放为8*8的
image = cv2.resize(img, (8, 8), interpolation=cv2.INTER_CUBIC)
# 将图片转化为灰度图
gray = cv2.cvtColor(image, cv2.COLOR_RGB2GRAY)
# s为初始的像素和灰度值,ahash_str为初始的哈希值
s = 0
ahash_str = ''
# 遍历像素累加和
for i in range(8):
for j in range(8):
s = s + gray[i, j]
# 计算像素和的平均值
avg = s / 64
# 灰度大于平均值则该位 Hash 值为1,反之为0
# 计算得到整张图片的平均哈希值,此时得到的hash值为64位的01字符串
for i in range(8):
for j in range(8):
if gray[i, j] > avg:
ahash_str = ahash_str + '1'
else:
ahash_str = ahash_str + '0'
return ahash_str
# return split(ahash_str) # 返回带分割的字符串

def phash(image):
"""感知 Hash 算法"""
# 加载并调整图片为32*32的灰度图片
img = cv2.resize(image, (32, 32),cv2.COLOR_RGB2GRAY)
# 转换为灰度图
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
# 将灰度图转为浮点型,再进行dct变换
dct = cv2.dct(np.float32(gray))
# opencv实现的掩码操作
dct_roi = dct[0:8, 0:8]

phash_str='' # 初始的 Hash 值
avg=np.mean(dct_roi)
for i in range(dct_roi.shape[0]):
for j in range(dct_roi.shape[1]):
if dct_roi[i,j]>avg:
phash_str=phash_str+'1'
else:
phash_str=phash_str+'0'
return phash_str

def dhash(image):
"""差异值 Hash 算法"""
# 将图片转化为8*8
image = cv2.resize(image, (9, 8), interpolation=cv2.INTER_CUBIC)
# 将图片转化为灰度图
gray = cv2.cvtColor(image, cv2.COLOR_RGB2GRAY)
dhash_str = ''
for i in range(8):
for j in range(8):
if gray[i, j] > gray[i, j + 1]:
dhash_str = dhash_str + '1'
else:
dhash_str = dhash_str + '0'
return dhash_str

def compare_Hash(hash1, hash2):
"""计算两个哈希值之间的差异"""
n = 0

# 两个 Hash 字符串长度不同时不能比较,返回 -1
if len(hash1) != len(hash2):
return -1

# 两个 Hash 字符串长度相同时,遍历长度
for i in range(len(hash1)):
if hash1[i] != hash2[i]:
n = n + 1
return n

def split(hash_str):
"""每隔4位插入一个空格"""
result = ''
for i in range(0, 64, 4):
result += ''.join('%x' % int(ahash_str[i: i + 4], 2))
# print("ahash值:",result)
return result

def output_hash_compare(hash_func, title):
print("--->"+title+"<---")

Hash1=hash_func(img1)
Hash2=hash_func(img2)
Hash3=hash_func(img3)
print("img1 的 "+title+" 值:", Hash1)
print("img2 的 "+title+" 值:", Hash2)
print("img3 的 "+title+" 值:", Hash3)

hamming_12=compare_hash(Hash1, Hash2)
hamming_13=compare_hash(Hash1, Hash3)
hamming_23=compare_hash(Hash2, Hash3)
print("img1 和 img2 的 Hamming 距离:", hamming_12)
print("img1 和 img3 的 Hamming 距离:", hamming_13)
print("img2 和 img3 的 Hamming 距离:", hamming_23)

def main():
output_hash_compare(ahash,"aHash")
output_hash_compare(phash,"pHash")
output_hash_compare(dhash,"dHash")

if __name__ == "__main__":
img1=cv2.imread(img1_file)
img2=cv2.imread(img2_file)
img3=cv2.imread(img3_file)
main()

最终的运行结果:

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
--->aHash<---
img1的aHash值:
img2的aHash值:
img3的aHash值:

img1和img2的Hamming距离:
img1和img3的Hamming距离:
img2和img3的Hamming距离:

--->pHash<---
img1的phash值:
img2的phash值:
img3的phash值:

img1和img2的Hamming距离:
img1和img3的Hamming距离:
img2和img3的Hamming距离:

--->dHash<---
img1的dHash值:
img2的dHash值:
img3的dHash值:

img1和img2的Hamming距离:
img1和img3的Hamming距离:
img2和img3的Hamming距离:

通过上面运行的结果可以看出来,img1和img2的相似度高一些。

余弦相似度(cosin)

把图片表示成一个向量,通过计算向量之间的余弦距离来表征两张图片的相似度。

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
40
41
42
from PIL import Image
from numpy import average, dot, linalg

def get_thum(image, size=(64, 64), greyscale=False):
"""1. 对图片进行统一化处理"""
# 利用image对图像大小重新设置, Image.ANTIALIAS为高质量的
image = image.resize(size, Image.ANTIALIAS)
if greyscale:
# 将图片转换为L模式,其为灰度图,其每个像素用8个bit表示
image = image.convert('L')
return image

# 2. 计算图片的余弦距离
def image_similarity_vectors_via_numpy(image1, image2):
image1 = get_thum(image1)
image2 = get_thum(image2)
images = [image1, image2]
vectors = []
norms = []
for image in images:
vector = []
for pixel_tuple in image.getdata():
vector.append(average(pixel_tuple))
vectors.append(vector)
# linalg=linear(线性)+algebra(代数),norm则表示范数
# 求图片的范数??
norms.append(linalg.norm(vector, 2))
a, b = vectors
a_norm, b_norm = norms
# dot返回的是点积,对二维数组(矩阵)进行计算
res = dot(a / a_norm, b / b_norm)
return res

im1=Image.open('001.jpg')
im2=Image.open('002.jpg')
im3=Image.open('003.jpg')
cosin_12=image_similarity_vectors_via_numpy(im1, im2)
print("img1 与 img2 的余弦相似度:",cosin_12)
cosin_13=image_similarity_vectors_via_numpy(im1, im3)
print("img1 与 img3 的余弦相似度:",cosin_13)
cosin_23=image_similarity_vectors_via_numpy(im2, im3)
print("img2 与 img3 的余弦相似度:",cosin_23)

最终运行结果:

1
2
3
img1 与 img2 的余弦相似度:
img1 与 img3 的余弦相似度:
img2 与 img3 的余弦相似度:

结果显示img1和img2的相似度高一些,和计算hash值的汉明距离得到的结果是相一致的。

SSIM(结构相似度量)

SSIM是一种全参考的图像质量评价指标,分别从亮度、对比度、结构三个方面度量图像相似性。SSIM取值范围[0, 1],值越大,表示图像失真越小。在实际应用中,可以利用滑动窗将图像分块,令分块总数为N,考虑到窗口形状对分块的影响,采用高斯加权计算每一窗口的均值、方差以及协方差,然后计算对应块的结构相似度SSIM,最后将平均值作为两图像的结构相似性度量,即平均结构相似性SSIM。

1
ssim1 = compare_ssim(img1, img2, multichannel=True)

这个是scikit-image库自带的一种计算方法

运行结果:

1
2
3
img1与img2的SSIM:
img1与img3的SSIM:
img2与img3的SSIM:

可以看到img1和img2的相似度高。

互信息(Mutual Information)

通过计算两个图片的互信息来表征他们之间的相似度,如果两张图片尺寸相同,还是能在一定程度上表征两张图片的相似性的。但是,大部分情况下图片的尺寸不相同,如果把两张图片尺寸调成相同的话,又会让原来很多的信息丢失,所以很难把握。经过实际验证,此种方法的确很难把握。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
from sklearn import metrics as mr
from scipy.misc import imread
import numpy as np

img1 = imread('1.jpg')
img2 = imread('2.jpg')

img2 = np.resize(img2, (img1.shape[0], img1.shape[1], img1.shape[2]))

img1 = np.reshape(img1, -1)
img2 = np.reshape(img2, -1)
print(img2.shape)
print(img1.shape)
mutual_infor = mr.mutual_info_score(img1, img2)

print(mutual_infor)

数学上的矩阵分解

图像本身就是一个矩阵,可以依靠数学上矩阵分解的一些知识来获取矩阵中一些代表这个矩阵元素值和分布的一些鲁棒性特征来对图像的相似度进行计算。

最常用的一般是SVD分解和NMF分解。

SVD分解

下面简单介绍下SVD分解的一些性质,如果需要探究的更深入一点网上有一些相关文献,读者可以去探究的更清楚:

  1. 奇异值的稳定性

  2. 奇异值的比例不变性

  3. 奇异值的旋转不变性

  4. 奇异值的压缩性

综上所述,可以看出奇异值分解是基于整体的表示。图像奇异值特征向量不但具有正交变换、旋转、位移、镜像映射等代数和几何上的不变性,而且具有良好的稳定性和抗噪性,广泛应用于模式识别与图像分析中。

对图像进行奇异值分解的目的是:

但是由于奇异值分解得到的奇异矢量中有负数存在所以不能很好的解释其物理意义。

非负矩阵分解(NMF)

NMF的主要思想是将非负矩阵分解为可以体现图像主要信息的基矩阵与系数矩阵,并且可以对基矩阵赋予很好的解释,比如对人脸的分割,得到的基向量正是人的“眼睛”,“鼻子”等主要概念特征,源图像表示为这些特征的加权组合。所以NMF算法也在人脸识别等场合中发挥着巨大的作用。

下面一个实验说明了SVD+NMF数学上的这些分解在图像相似度判定方面的应用,这个跟我目前的课题有关细节方面就不再透露更多了。

当然基于数学上的矩阵特征值计算的还有很多方法比如Trace变换,不变矩计算等等,当然如果有需要这方面资料的同学可以找我,我可以进行相关的帮助。

基于特征点

每一幅图像都有自己的特征点,这些特征点表征图像中比较重要的一些位置,比较类似函数的拐点那种,通常比较常用的有Harris角点和Sift特征点。那么将得到的图像角点进行比较,如果相似的角点数目较多,那么可以认为这两幅图像的相似程度较高。这里主要介绍基于Sift算子。

对于Sift的原理和代码可以参见David Lower的网站。

David G Lowe Sift网站

那么我们就可以通过找到匹配点的个数来判断两幅图像是否一致,这个算法的好处是对于一个物体,两个不同角度下得到的照片依然可以找到很多的匹配点,我也一直认为是一个综合来说结果相对较为准确的方法,但是由于每个特征点需要计算一个长度不小的特征值,也造成了该算法的时间消耗比较大。所以不常用于实时的视频处理。这个算法还有一个好处就是可以通过找到的匹配特征点进行图像校正。关于使用Sift做图像校正请参见我的另外一篇博文。可见Sift对于抗旋转和噪声的效果确实很好。对于Sift也不能全部相信,一般使用RANSAC对于错误匹配点去除可以达到更好的效果,当然目前也有很多对SIFT进行改进的算法。希望有这方面研究的可以多多交流。

参考文献

python图像识别———图片相似度计算 - 云+社区 - 腾讯云 (tencent.com)

(10条消息) Python计算图片之间的相似度_~小疯子~的博客-CSDN博客_python 图片相似度

(10条消息) python OpenCV 图片相似度 5种算法_追求卓越,做到专业-CSDN博客_python图片相似度识别算法

(10条消息) 图像相似度计算_数学屌丝走在it的道路上-CSDN博客