均值哈希算法(Average hash algorithm,AHA)第一次是从著名的阮一峰阮老师的博文《相似图片搜索的原理》看到的。而此篇文章与阮老师也很类似Looks Like It - The Hacker Factor Blog 。这里不对原谅做摘抄,有兴趣的自己看一下,在此对学习过程中的心得和遇到过的问题做一下总结。
均值哈希算法,是感知哈希算法中最简单的一种,基本原理是对图片降频。对于图片,高频有很多细节,如颜色、亮度、透明度等等,而低频丢弃细节,只有图像结构。
- 缩小尺寸。为了保留结构去掉细节,去除大小、横纵比的差异,把图片统一缩放到8*8,共64个像素的图片。
- 简化色彩,转化为灰度图。把缩放后的图片转化为64级灰度图。
- 计算平均值。计算进行灰度处理后图片的所有像素点的平均值。
- 比较像素灰度值。遍历灰度图片每一个像素,如果大于平均值记录为1,否则为0。
- 获取指纹。将上一步的比较结果,组合在一起,就构成了一个64位的整数,这就是这张图片的指纹。组合的次序并不重要,只要保证所有图片都采用同样次序就行了
可见,之所以称之为均值哈希算法,就是因为这种哈希算法,是通过灰度值的平均值,与其他灰度值的差异性得出来的。
得到指纹以后,就可以对比不同的图片,看看图片中有多少位是不一样的。理论上,这等同于计算汉明距离(Hamming Distance)。如果不相同的数据位不超过5,就说明两张图很相似;如果大于10,说明这两张是不同的图片(不同的图片,不代表不相似)。
优点:
缺点:
在阮老师的博文中,给出了一段用python写的源码,笔者本身并没有写过python代码,但根据上面算法的描述,开始并没有得出原文中相似的哈希结果,于是才反回来再看看这段python代码。遇到的问题或者说误解有:
imgHash.py如下
#!/usr/bin/python
#coding:utf-8
import glob
import os
import sys
#引入Python Imaging Library (PIL)
from PIL import Image
#支持的图片后缀,window中大小写不敏感,在此只取小写的,否则最后一行结果会打印两次
#EXTS = 'jpg', 'jpeg', 'JPG', 'JPEG', 'gif', 'GIF', 'png', 'PNG'
EXTS = 'jpg', 'gif', 'png'
#均值哈希算法函数
def avhash(im):
if not isinstance(im, Image.Image):
im = Image.open(im)#打开图片
im = im.resize((8, 8), Image.ANTIALIAS).convert('L')#缩小为8*8,平滑图(ANTIALIAS),转为灰度图(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)#计算哈希值,x|(z< 3:
print "Usage: %s image.jpg [dir]" % sys.argv[0]
else:
im, wd = sys.argv[1], '.' if len(sys.argv) < 3 else sys.argv[2]
h = avhash(im)
os.chdir(wd)
images = []
for ext in EXTS:
images.extend(glob.glob('*.%s' % ext))
seq = []
prog = int(len(images) > 50 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 "%d\t%s123" % (ham, f)
这段python代码,2.x版本的,因此不要用3.x版本来运行。笔者用Python 2.7运行成功。
由于代码中使用了PIL,需要先安装。Python Imaging Library (PIL)
现在放两张图片,用以测试:
输出:
C:\Users\Administrator>c:\Python27\python.exe c:\Python27\imgHash2.py c:\imagetest\imgHash\bg2011072103.jpg c:\imagetest\imgHash\
0 bg2011072103.jpg123
32 f.png123
之前提到过,缩小的图片和灰度图,以及哈希值有疑义,现在用该python代码,分别输出一下这两个图片以级hash值。将原来的代码做如下修改:
im = im.resize((8, 8), Image.ANTIALIAS).convert('L')#缩小为8*8,平滑图(ANTIALIAS),转为灰度图(L)
==>
im = im.resize((8, 8), Image.ANTIALIAS)
im.save("c:/imagetest/imgHash/88/hash88.jpg")
#缩小为8*8,平滑图(ANTIALIAS),转为灰度图(L)
im = im.convert('L')
im.save("c:/imagetest/imgHash/88/hash88_gray.jpg")
在
h = avhash(im)
之后加上:
print "avhash:%x"%h
输出结果:
avhash:175f2f63435be3e7
0 bg2011072103.jpg123
32 f.png123
结论:
文中的代码也并未得出文中描述的结果。
下面附上我实验过的代码:
/**
* 均值哈希算法/Average hash algorithm/AHA
*
* 最适用于缩略图,放大图搜索
*
* 虽然均值哈希更简单且更快速,但是在比较上更死板、僵硬。
* 它可能产生错误的漏洞,如有一个伽马校正或颜色直方图被用于到图像。
* 这是因为颜色沿着一个非线性标尺 - 改变其中“平均值”的位置,并因此改变哪些高于/低于平均值的比特数
*
*
* @author xuyanhua
* @data Jan 10, 2017 1:09:46 AM
*/
public class AHash {
/**
* 图片指纹
*
* @param imagePath
* @return
* @throws IOException
*/
public static long fingerprint(String imagePath) throws IOException {
BufferedImage srcImage = ImageIO.read(new File(imagePath));
/*
* 1.缩小尺寸. 为了保留结构去掉细节,去除大小、横纵比的差异,把图片统一缩放到8*8,共64个像素的图片
*/
BufferedImage image8x8 = ImageUtil.resize(srcImage, 8, 8);
/*
* 2.简化色彩,转化为灰度图. 把缩放后的图片转化为256阶的灰度图
*/
int width = image8x8.getWidth();
int height = image8x8.getHeight();
int[] grayPix = new int[64];
int i = 0;
int sum = 0;
for (int y = 0; y < height; y++) {
for (int x = 0; x < width; x++) {
int rgb = image8x8.getRGB(x, y);
int r = rgb >> 16 & 0xff;
int g = rgb >> 8 & 0xff;
int b = rgb >> 0 & 0xff;
int gray = (r * 30 + g * 59 + b * 11) / 100;
grayPix[i++] = gray;
sum += gray;
}
}
/* 3.计算平均值, 计算进行灰度处理后图片的所有像素点的平均值 */
int avg = sum / 64;
/*
* 4.比较像素灰度值,遍历灰度图片每一个像素,如果大于平均值记录为1,否则为0. 5.获取指纹
*/
long figure = 0;
for (i = 63; i >= 0; i--) {
long b = (long) (grayPix[i] > avg ? 1 : 0);
figure |= b << i;
}
return figure;
}
}
计算汉明距离:
public class HammingDistance {
/**
* 比较,计算汉明距离 如果不相同的数据位不超过5,就说明两张图片很相似;如果大于10,就说明这是两张不同的图片。
*
* @param file1
* @param file2
* @return
*/
public static int distance(long fg1, long fg2) {
int distance = 0;
long res = fg1 ^ fg2;
for (int i = 0; i < 64; i++) {
distance += (res >> i & 1);
}
return distance;
}
}
测试代码:
@Test
public void test6() throws IOException {
String find = "C:/imagetest/imgHash/bg2011072103.jpg";
long finger = AHash.fingerprint(find);
System.out.println(Long.toHexString(finger));
String find2 = "C:/imagetest/imgHash/88/bg2011072103.jpg";
long finger2 = AHash.fingerprint(find2);
System.out.println(HammingDistance.distance(finger, finger2)+"<-->bg2011072103.jpg");
String find3 = "C:/imagetest/imgHash/88/f.png";
long finger3 = AHash.fingerprint(find3);
System.out.println(HammingDistance.distance(finger, finger3)+"<-->f.png");
}
输出:
171f3f2343d3e3e7
0<-->bg2011072103.jpg
32<-->f.png
和文中的结果基本一致,hash值略有不同。
文中接着说到,实际应用中,往往采用更强大的pHash算法和SIFT算法,可以识别图片的变形,只要变形不超过25%,就能匹配原图,原理基本一致,都是根据图片得到哈希值,再比较哈希。