均值哈希算法(Average Hash Algorithm,简称aHash) 是哈希算法的一种,主要用来做相似图片的搜索工作。
2. 原理
均值哈希算法(aHash)首先将原图像缩小成一个固定大小的像素图像,然后将图像转换为灰度图像,通过缩小图像的每个像素与平均灰度值的比较,生成一组哈希值。最后,利用两组图像的哈希值的汉明距离来评估图像的相似度。
魔法: 概括地讲,均值哈希算法一共可细分六步:
缩小图像: 将目标图像缩小为一个固定的大小,通常为8x8像素,总共64个像素。作用是去除各种图像尺寸和图像比例的差异,只保留结构、明暗等基本信息,目的是确保图像的一致性,降低计算的复杂度。
图像灰度化: 将缩小的图像转换为灰度图像。
灰度平均值: 计算灰度图像的平均灰度值。减少计算量。
比较平均值: 遍历灰度图像的每个像素,比较每个像素的灰度值是否大于或小于平均值。对于大于等于平均值的像素,将其表示为1,对于小于平均值的像素,将其表示为0。最后,得到一个64位的二进制值(8x8像素的图像)。
生成哈希值: 由于64位二进制值太长,所以按每4个字符为1组,由2进制转成16进制。这样就转为一个长度为16的字符串。这个字符串也就是这个图像可识别的哈希值,也叫图像指纹,即这个图像所包含的特征。
哈希值比较: 通过比较两个图像的哈希值的汉明距离(Hamming Distance),就可以评估图像的相似度,距离越小表示图像越相似。
3. 实验 第一步:缩小图像
将目标图像缩小为一个固定的大小,通常为8x8像素,总共64个像素。作用是去除各种图像尺寸和图像比例的差异,只保留结构、明暗等基本信息,目的是确保图像的一致性,降低计算的复杂度。
1)读取原图 # 测试图片路径 img_path = 'img_test/apple-01.jpg' # 通过OpenCV加载图像 img = cv2.imread(img_path) # 通道重排,从BGR转换为RGB img_rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) 2)缩小原图 # 缩小图像:使用OpenCV的resize函数将图像缩放为8x8像素,采用Cubic插值方法进行图像重采样 img_resize = cv2.resize(img, (8, 8), cv2.INTER_CUBIC)cv2.INTER_NEAREST: 最近邻插值,也称为最近邻算法。它简单地使用最接近目标像素的原始像素的值。虽然计算速度快,但可能导致图像质量下降。
cv2.INTER_LINEAR: 双线性插值,通过对最近的4个像素进行线性加权来估计目标像素的值。比最近邻插值更精确,但计算成本略高。
cv2.INTER_CUBIC: 双三次插值,使用16个最近像素的加权平均值来估计目标像素的值。通常情况下,这是一个不错的插值方法,适用于图像缩小。
cv2.INTER_LANCZOS4: Lanczos插值,一种高质量的插值方法,使用Lanczos窗口函数。通常用于缩小图像,以保留图像中的细节和纹理。
第二步:图像灰度化将缩小的图像转换为灰度图像。也就是说,所有像素点总共只有64种灰度颜色。
# 图像灰度化:将彩色图像转换为灰度图像。 img_gray = cv2.cvtColor(img_resize, cv2.COLOR_BGR2GRAY) print(f"缩放8x8的图像中每个像素的颜色=\n{img_gray}")输出打印:
缩放8x8的图像中每个像素的颜色= [[253 253 253 253 253 253 253 253] [253 253 253 148 253 253 253 253] [253 253 253 215 178 253 253 253] [253 253 119 93 132 176 253 253] [253 253 61 61 53 130 253 253] [253 253 112 67 66 142 253 253] [253 253 252 54 54 253 253 253] [253 253 236 63 146 249 253 253]] 第三步:灰度平均值计算灰度图像的平均灰度值。减少计算量。
img_average = np.mean(img_gray) print(f"灰度图像中所有像素的平均值={img_average}")输出打印:
灰度图像中所有像素的平均值=209.890625 第四步:比较平均值遍历灰度图像的每个像素,比较每个像素的灰度值是否大于或小于平均值。对于大于等于平均值的像素,将其表示为1;对于小于平均值的像素,将其表示为0。最后,得到一组长64位的二进制字符串(8x8像素的图像)。因为对于机器而言,只认识0和1,所以这组64位的二进制就可以表示这张图像的结构和亮度分布。
# 遍历图像像素:嵌套循环遍历图像的所有像素,对比灰度图像的平均灰度值,转换为二进制的图像哈希值 img_hash_binary = [] for i in range(img_gray.shape[0]): for j in range(img_gray.shape[1]): if img_gray[i,j] >= img_average: img_hash_binary.append(1) else: img_hash_binary.append(0) print(f"对比灰度图像的平均像素值降噪(图像的二进制哈希值)数组={img_hash_binary}") # 将列表中的元素转换为字符串并连接起来,形成一组64位的图像二进制哈希值字符串 img_hash_binary_str = ''.join(map(str, img_hash_binary)) print(f"对比灰度图像的平均像素值降噪(图像的二进制哈希值)={img_hash_binary_str}")代码分解和含义如下:
初始化空列表:创建一个空的列表 img_hash_binary,用于存储图像的哈希值。
遍历图像像素:嵌套循环遍历图像的所有像素,其中 img_gray 是输入的灰度图像,img_gray.shape[0] 和 img_gray.shape[1] 分别表示图像的高度和宽度。
计算平均值:代码中使用变量 img_average 存储了一个平均值,用于与图像像素的灰度值进行比较。
根据亮度值生成哈希值:对于每个像素,代码比较像素的灰度值与平均值 (img_gray[i, j] >= img_average)。如果像素的灰度值大于或等于平均值,就将数字1添加到 img_hash_binary 列表中,表示该像素是亮的。如果像素的灰度值小于平均值,就将数字0添加到 img_hash_binary 列表中,表示该像素是暗的。
最终哈希值:完成循环后,img_hash_binary 列表将包含图像的二进制哈希值,其中每个元素代表一个像素的明暗情况。
输出打印:
对比灰度图像的平均像素值降噪(图像的二进制形式)数组=[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 1, 1, 1, 1, 1, 1, 0, 0, 1, 1, 1] 对比灰度图像的平均像素值降噪(图像的二进制形式)=1111111111101111111101111100001111000011110000111110011111100111或者,使用等价的 lambda 表达式。效果一样。
# lambda表达式 img_hash_binary_str = "" for i in range(8): img_hash_binary_str += ''.join(map(lambda i: '0' if i < img_average else '1', img_gray[i])) print(f"对比灰度图像的平均像素值降噪(图像的二进制哈希值)={img_hash_binary_str}")输出打印:
对比灰度图像的平均像素值降噪(图像的二进制形式)=1111111111101111111101111100001111000011110000111110011111100111 第五步:生成哈希值由于64位二进制值太长,所以按每4个字符为1组,由2进制转成16进制。这样就转为一个长度为16的字符串。这个字符串也就是这个图像可识别的哈希值,也叫图像指纹,即这个图像所包含的特征。
img_hash = "" for i in range(0, 64, 4): img_hash += "".join('%x' % int(img_hash_binary_str[i : i + 4], 2)) print(f"图像可识别的哈希值={img_hash}")代码分解和含义如下:
初始化为空字符串:创建一个空的字符串 img_hash,用于存储图像哈希值的十六进制表示。
遍历二进制哈希值:通过循环,代码以4位为一组遍历二进制哈希值 img_hash_binary_str。range(0, 64, 4) 确保代码在哈希值的每4位之间进行迭代。
将4位二进制转换为一个十六进制字符:在每次循环中,代码取出哈希值中的4位二进制(例如,img_hash_binary_str[i : i + 4]),然后使用’%x’ % int(…, 2) 将这4位二进制转换为一个十六进制字符。int(…, 2) 将二进制字符串转换为整数,‘%x’ 将整数转换为十六进制字符。
将十六进制字符追加到 img_hash:在每次循环中,得到的十六进制字符将被追加到 img_hash 字符串中。
最终哈希值:完成循环后,img_hash 将包含图像哈希值的十六进制表示,其中每个字符表示4位二进制。
输出打印:
图像可识别的哈希值=ffeff7c3c3c3e7e7 第六步:哈希值比较通过比较两个图像的哈希值的汉明距离(Hamming Distance),就可以评估图像的相似度,距离越小表示图像越相似。
def hamming_distance(s1, s2): # 检查这两个字符串的长度是否相同。如果长度不同,它会引发 ValueError 异常,因为汉明距离只适用于等长的字符串 if len(s1) != len(s2): raise ValueError("Input strings must have the same length") distance = 0 for i in range(len(s1)): # 遍历两个字符串的每个字符,比较它们在相同位置上的值。如果发现不同的字符,将 distance 的值增加 1 if s1[i] != s2[i]: distance += 1 return distance汉明距离: 两个长度相同的字符串在相同位置上的字符不同的个数。即一组二进制数据变成另一组数据所需要的步骤数。汉明距离越小,则相似度越高。汉明距离为0,即两张图片完全一样。
4. 测试 实验场景
我们来简单测试一下基于均值哈希算法的以图搜图 – 基于一张原图找最相似图片,看看效果如何。
这里,我准备了10张图片,其中9张是苹果,但形态不一,1张是梨子。
实验素材 实验代码 """ 以图搜图:均值哈希算法(Average Hash Algorithm,简称aHash)的原理与实现 测试环境:win10 | python 3.9.13 | OpenCV 4.4.0 | numpy 1.21.1 实验时间:2023-10-31 """ import os import cv2 import time import numpy as np import matplotlib.pyplot as plt def get_aHash(img_path): # 读取图像:通过OpenCV的imread加载RGB图像 img_rgb = cv2.cvtColor(cv2.imread(img_path), cv2.COLOR_BGR2RGB) # 缩小图像:使用OpenCV的resize函数将图像缩放为8x8像素,采用Cubic插值方法进行图像重采样 img_resize = cv2.resize(img_rgb, (8, 8), cv2.INTER_CUBIC) # 图像灰度化:将彩色图像转换为灰度图像 img_gray = cv2.cvtColor(img_resize, cv2.COLOR_BGR2GRAY) # print(f"缩放32x32的图像中每个像素的颜色=\n{img_gray}") # 灰度图像中所有像素的平均值 img_average = np.mean(img_gray) """ # # 比较平均值:嵌套循环遍历图像的所有像素,对比灰度图像的平均灰度值,转换为二进制的图像哈希值 # # img_gray:是灰度图像 # # img_gray.shape[0] 和 img_gray.shape[1] 分别表示图像的高度和宽度 # img_hash_binary = [] # for i in range(img_gray.shape[0]): # for j in range(img_gray.shape[1]): # if img_gray[i,j] >= img_average: # img_hash_binary.append(1) # else: # img_hash_binary.append(0) # print(f"对比灰度图像的平均像素值降噪(图像的二进制哈希值)数组={img_hash_binary}") # # 将列表中的元素转换为字符串并连接起来,形成一组64位的图像二进制哈希值字符串 # img_hash_binary_str = ''.join(map(str, img_hash_binary)) # print(f"对比灰度图像的平均像素值降噪(图像的二进制哈希值)={img_hash_binary_str}") # # 生成哈希值 # img_hash = "" # # 遍历二进制哈希值:通过循环,代码以4位为一组遍历二进制哈希值 img_hash_binary_str。 # # range(0, 64, 4) 确保代码在哈希值的每4位之间进行迭代。 # for i in range(0, 64, 4): # # 将4位二进制转换为一个十六进制字符 # # 在每次循环中,代码取出哈希值中的4位二进制(例如,img_hash_binary_str[i : i + 4]) # # 然后使用'%x' % int(..., 2)将这4位二进制转换为一个十六进制字符。 # # int(..., 2)将二进制字符串转换为整数,'%x'将整数转换为十六进制字符。 # # 将十六进制字符追加到 img_hash:在每次循环中,得到的十六进制字符将被追加到 img_hash 字符串中。 # img_hash += "".join('%x' % int(img_hash_binary_str[i : i + 4], 2)) # print(f"图像可识别的哈希值={img_hash}") """ # 遍历图像像素:嵌套循环遍历图像的所有像素,对比灰度图像的平均灰度值,转换为二进制的图像哈希值 img_hash_binary_str = '' for i in range(8): img_hash_binary_str += ''.join(map(lambda i: '0' if i < img_average else '1', img_gray[i])) # 图像可识别哈希值 img_hash = ''.join(map(lambda x:'%x' % int(img_hash_binary_str[x : x + 4], 2), range(0, 64, 4))) return img_hash # 汉明距离:计算两个等长字符串(通常是二进制字符串或位字符串)之间的汉明距离。用于确定两个等长字符串在相同位置上不同字符的数量。 def hamming_distance(s1, s2): # 检查这两个字符串的长度是否相同。如果长度不同,它会引发 ValueError 异常,因为汉明距离只适用于等长的字符串 if len(s1) != len(s2): raise ValueError("Input strings must have the same length") distance = 0 for i in range(len(s1)): # 遍历两个字符串的每个字符,比较它们在相同位置上的值。如果发现不同的字符,将 distance 的值增加 1 if s1[i] != s2[i]: distance += 1 return distance # -------------------------------------------------- 测试 -------------------------------------------------- time_start = time.time() # 指定测试图像库目录 img_dir = 'img_test' # 指定测试图像文件扩展名 img_suffix = ['.jpg', '.jpeg', '.png', '.bmp', '.gif'] # 获取当前执行脚本所在目录 script_dir = os.path.dirname(__file__) # 获取目标测试图像的全路径 img_org_path = os.path.join(script_dir, img_dir, 'apple-01.jpg') # 获取目标图像可识别哈希值(图像指纹) org_img_hash = get_aHash(img_org_path) print(f"目标图像:{os.path.relpath(img_org_path)},图片HASH:{org_img_hash}") # 获取测试图像库中所有文件 all_files = os.listdir(os.path.join(script_dir, img_dir)) # 筛选出指定后缀的图像文件 img_files = [file for file in all_files if any(file.endswith(suffix) for suffix in img_suffix)] img_hash_all = [] # 遍历测试图像库中的每张图像 for img_file in img_files: # 获取相似图像文件路径 img_path = os.path.join(script_dir, img_dir, img_file) # 获取相似图像可识别哈希值(图像指纹) img_hash = get_aHash(img_path) # 获取相似图像与目标图像的汉明距离 distance = hamming_distance(org_img_hash, img_hash) # 存储相似图像的相对路径、哈希值、汉明距离 img_hash_all.append((os.path.relpath(img_path), img_hash, distance)) for img in img_hash_all: print(f"图像:{img[0]},图像HASH:{img[1]},与图像目标的相似值(汉明距离):{img[2]}") time_end = time.time() print(f"耗时:{time_end - time_start}")输出打印:
目标图像:..\..\P1_Hash\01_aHash\img_test\apple-01.jpg,图片HASH:ffeff7c3c3c3e7e7 图像:..\..\P1_Hash\01_aHash\img_test\apple-01.jpg,图像HASH:ffeff7c3c3c3e7e7,与图像目标的相似值(汉明距离):0 图像:..\..\P1_Hash\01_aHash\img_test\apple-02.jpg,图像HASH:ffcfc3e3e3e3e7ff,与图像目标的相似值(汉明距离):8 图像:..\..\P1_Hash\01_aHash\img_test\apple-03.jpg,图像HASH:ffe7c3c3c3c7c7ff,与图像目标的相似值(汉明距离):7 图像:..\..\P1_Hash\01_aHash\img_test\apple-04.jpg,图像HASH:e7e7c3c3c3eff7ff,与图像目标的相似值(汉明距离):10 图像:..\..\P1_Hash\01_aHash\img_test\apple-05.jpg,图像HASH:f3f3e7c7c3c7c7e7,与图像目标的相似值(汉明距离):7 图像:..\..\P1_Hash\01_aHash\img_test\apple-06.jpg,图像HASH:ffffd981818189dd,与图像目标的相似值(汉明距离):13 图像:..\..\P1_Hash\01_aHash\img_test\apple-07.jpg,图像HASH:fff7f3e3e3e3f0ff,与图像目标的相似值(汉明距离):10 图像:..\..\P1_Hash\01_aHash\img_test\apple-08.jpg,图像HASH:000006fdf171f9f8,与图像目标的相似值(汉明距离):16 图像:..\..\P1_Hash\01_aHash\img_test\apple-09.jpg,图像HASH:ffcfe7c1c1c3e7ff,与图像目标的相似值(汉明距离):6 图像:..\..\P1_Hash\01_aHash\img_test\pear-001.jpg,图像HASH:fffbe5c1c3c3c3ef,与图像目标的相似值(汉明距离):8 耗时:0.09973335266113281简单的测试分析:
原图相似图片相似值(汉明距离)相似图片特点相似图片与原图Hash对比结果10张测试图片中,汉明距离在5以内1张;汉明距离在5以外9张。
从抽样简单测试结果看,平均哈希简单且计算速度快,但它对图像的细节变化比较敏感,容易受到局部图像的特性的干扰。
备注:如果汉明距离0,则表示这两张图片非常相似;如果汉明距离小于5,则表示有些不同,但比较相近;如果汉明距离大于10,则表明是完全不同的图片。
5. 总结
经过实验和测试,平均哈希算法优缺点明显。
特点: 传统,属于一种外观相似哈希算法。
优点: 简单、计算效率高,适用于快速图像相似性比较。
缺点: 对于图片的旋转和主体内容变换相对不敏感;对于复杂、多主体、多色调的图片较难相似,因为它只捕获了图片的平均亮度和粗略结构。
6. 实验问题
问题1: 为什么通过 cv2.imread(img_path) 加载的图像,显示出来之后,原图由红色变成了蓝色?
问题2: 为什么使用了 cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) ,但显示出来图像是彩色的?
7. 系列书签
OpenCV书签 #均值哈希算法的原理与相似图片搜索实验
OpenCV书签 #感知哈希算法的原理与相似图片搜索实验
OpenCV书签 #差值哈希算法的原理与相似图片搜索实验
OpenCV书签 #直方图算法的原理与相似图片搜索实验