【Python实例学习笔记】图像相似度计算--哈希算法
【Python实例学习笔记】图像相似度计算--哈希算法
- 一、哈希算法的实现步骤:
- 二、对每一步都进行注解的代码
一、哈希算法的实现步骤:
1、缩小尺寸:
将图像缩小到8*8的尺寸,总共64个像素。这一步的作用是去除图像的细节,只保留结构/明暗等基本信息,摒弃不同尺寸/比例带来的图像差异;
2、简化色彩:
将缩小后的图像,转为64级灰度,即所有像素点总共只有64种颜色;
3、计算平均值:
计算所有64个像素的灰度平均值;
4、比较像素的灰度:
将每个像素的灰度,与平均值进行比较,大于或等于平均值记为1,小于平均值记为0;
5、计算哈希值:
将上一步的比较结果,组合在一起,就构成了一个64位的整数,这就是这张图像的指纹。组合的次序并不重要,只要保证所有图像都采用同样次序就行了;
7、计算汉明距离
得到指纹以后,就可以对比不同的图像,看看64位中有多少位是不一样的。在理论上,这等同于”汉明距离”(Hamming distance,在信息论中,两个等长字符串之间的汉明距离是两个字符串对应位置的不同字符的个数)。如果不相同的数据位数不超过5,就说明两张图像很相似;如果大于10,就说明这是两张不同的图像。
二、对每一步都进行注解的代码
由于本人刚刚开始学习Python,所以,代码注解比较多。
这个Python代码也是在别人的参考代码,我为了学习找来写了注解并且做了一些修改,在Python 3.12.8中运行成功。修改的都是一些真的Python3的修改,参考文章是2013年的。
#!/usr/bin/python
# 指定脚本的解释器路径,表示使用 /usr/bin/python 来运行这个脚本
'''
导入所需的模块:
glob 用于文件模式匹配,
os 用于操作系统相关功能,
sys 用于访问命令行参数,
PIL(Python Imaging Library)中的 Image 用于图像处理。
'''
import glob
import os
import sys
from functools import reduce
from PIL import Image
# 定义支持的图像文件扩展名
EXTS = 'jpg', 'jpeg', 'JPG', 'JPEG', 'gif', 'GIF', 'png', 'PNG'
'''
该函数用于计算图像的平均哈希值(aHash)。
功能:
1. 将输入的图像调整为 8x8 像素大小,并转换为灰度图。
2. 计算图像的平均像素值。
3. 根据平均值,将每个像素值转换为二进制值(0 或 1)。
4. 将这些二进制值组合成一个 64 位的整数,作为图像的哈希值。
用途:
- 生成图像的指纹(哈希值),用于快速比较图像的相似性。
参数:
- im: 可以是一个 PIL.Image.Image 对象或图像文件路径。
返回值:
- 一个整数,表示图像的平均哈希值。
'''
def avhash(im):
# 如果输入不是 Image 对象,则打开图像文件
if not isinstance(im, Image.Image):
im = Image.open(im)
# 第一步,缩小尺寸。
# 第二步,简化色彩。
# 将图像调整为 8x8 像素,转换为灰度图
# im = im.resize((8, 8), Image.ANTIALIAS).convert('L')
im = im.resize((8, 8), Image.Resampling.LANCZOS).convert('L')
# 第三步,计算平均值。
# 计算图像的平均像素值
avg = reduce(lambda x, y: x + y, im.getdata()) / 64.
# return reduce(lambda x, (y, z): x | (z << y),
# enumerate(map(lambda i: 0 if i < avg else 1, im.getdata())),
# 0)
# 第四步,比较像素的灰度。
# 根据平均值,将每个像素值转换为 0 或 1,生成哈希值
# 获取图像的像素数据
pixels = im.getdata()
# 将每个像素值与平均值比较,生成二进制序列
#这就是这张图片的指纹,这个指纹是一个64位的二进制数,我们可以通过这个指纹来判断两张图片是否相似。
binary_sequence = map(lambda i: 0 if i < avg else 1, pixels)
# 第五步,计算哈希值。
# 枚举二进制序列,生成索引和值的元组序列。
# 生成的 (index, value) 序列,表示像素索引和二值化结果。
enumerated_sequence = enumerate(binary_sequence)
# 使用 reduce 函数将枚举后的序列累积为一个整数
hash_value = reduce(lambda x, yz: x | (yz[1] << yz[0]), enumerated_sequence, 0)
# 将哈希值转换为二进制字符串并打印
binary_hash = bin(hash_value)[2:].zfill(64)
print(f"Hash binary representation: {binary_hash}")
# 返回哈希值
return hash_value
def hamming(h1, h2):
# 计算两个哈希值之间的汉明距离(不同位的数量)
h, d = 0, h1 ^ h2
while d:
h += 1
#这是一种高效的位操作技巧,用于清除 d 中最低位的 1。
#例如:d = 0110,d - 1 = 0101,d & (d - 1) = 0100。
d &= d - 1
return h
'''
第一个参数是基准图片,
第二个参数是用来比较的其他图片所在的目录,
返回结果是两张图片之间不相同的数据位数量(汉明距离)。
'''
if __name__ == '__main__':
# 检查命令行参数的数量,如果不符合要求,打印用法说明
# if len(sys.argv) <= 1 or len(sys.argv) > 3:
# print("{} Usage: {} image.jpg [dir]".format(sys.argv[0], sys.argv[0]))
# print(f"{sys.argv[0]} Usage: {sys.argv[0]} image.jpg [dir]")
# else:
# # else则执行后面的语句,而执行内容可以多行,以缩进来区分表示同一范围。
# # 从命令行参数中读取两个值:im 和 wd
# # im 被设置为 sys.argv[1],即命令行中的第一个参数(脚本名称之后的第一个值)
# # wd 的值则取决于命令行参数的数量
# # 如果 len(sys.argv) < 3,即命令行参数的数量少于 3 个(只包括脚本名称和一个参数),那么 wd 被设置为字符串 '.'
# # 否则,如果命令行参数的数量大于或等于 3 个,那么 wd 被设置为 sys.argv[2],即命令行中的第二个参数
# im, wd = sys.argv[1], '.' if len(sys.argv) < 3 else sys.argv[2]
im = '/Users/jp/Desktop/comparePic/pic1/frame_0005.png'
wd = '/Users/jp/Desktop/comparePic/pic1'
# 计算基准图像的哈希值
h = avhash(im)
# 切换到工作目录
os.chdir(wd)
images = []
# 查找所有支持的图像文件
for ext in EXTS:
images.extend(glob.glob('*.%s' % ext))
seq = []
# 如果图像数量超过 50 且输出到终端,显示进度
prog = int(len(images) > 20 and sys.stdout.isatty())
for f in images:
# 计算每个图像与基准图像之间的汉明距离
seq.append((f, hamming(avhash(f), h)))
if prog:
perc = 100. * prog / len(images)
x = int(2 * perc / 5)
print ('\rCalculating... [' + '#' * x + ' ' * (40 - x) + ']'),
print ('%.2f%%' % perc, '(%d/%d)' % (prog, len(images))),
sys.stdout.flush()
prog += 1
if prog: print()
# 按汉明距离排序并打印结果
for f, ham in sorted(seq, key=lambda i: i[1]):
print("{}\t{}".format(ham, f))
参考文章链接:
https://www.ruanyifeng.com/blog/2011/07/principle_of_similar_image_search.html
https://www.ruanyifeng.com/blog/2011/07/imgHash.txt