均值哈希算法(Average hash algorithm,AHA)第一次是從著名的阮一峯阮老師的博文《相似圖片搜索的原理》看到的。而此篇文章與阮老師也很類似Looks Like It - The Hacker Factor Blog 。這裏不對原諒做摘抄,有興趣的自己看一下,在此對學習過程中的心得和遇到過的問題做一下總結。
均值哈希算法,是感知哈希算法中最簡單的一種,基本原理是對圖片降頻。對於圖片,高頻有很多細節,如顏色、亮度、透明度等等,而低頻丟棄細節,只有圖像結構。
算法步驟
- 縮小尺寸。爲了保留結構去掉細節,去除大小、橫縱比的差異,把圖片統一縮放到8*8,共64個像素的圖片。
- 簡化色彩,轉化爲灰度圖。把縮放後的圖片轉化爲64級灰度圖。
- 計算平均值。計算進行灰度處理後圖片的所有像素點的平均值。
- 比較像素灰度值。遍歷灰度圖片每一個像素,如果大於平均值記錄爲1,否則爲0。
- 獲取指紋。將上一步的比較結果,組合在一起,就構成了一個64位的整數,這就是這張圖片的指紋。組合的次序並不重要,只要保證所有圖片都採用同樣次序就行了
可見,之所以稱之爲均值哈希算法,就是因爲這種哈希算法,是通過灰度值的平均值,與其他灰度值的差異性得出來的。
哈希值比較
得到指紋以後,就可以對比不同的圖片,看看圖片中有多少位是不一樣的。理論上,這等同於計算漢明距離(Hamming Distance)。如果不相同的數據位不超過5,就說明兩張圖很相似;如果大於10,說明這兩張是不同的圖片(不同的圖片,不代表不相似)。
算法優缺點
優點:
- 算法簡單,計算速度快。
- 圖片放大或縮小,改變縱橫比,或增加減少亮度、對比度、顏色,對hash值改變不會太大。
缺點:
- 算法對圖片的內容非常敏感,如果內容改變,很容易使得圖片的哈希值差別變大。
博文中的python代碼
在阮老師的博文中,給出了一段用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<<y)是核心之處,判斷了爲0還是爲1後,每後一位,向右移y位,這裏的(y,z)和後面的enumerate對應
#漢明距離計算函數
def hamming(h1, h2):
h, d = 0, h1 ^ h2
while d:
h += 1
d &= d - 1
return h
'''
主方法
用法:python imgHash.py image.jpg [dir]
第一個參數是要查找的圖片,第二個參數在哪個路徑下查找圖片
分別計算要查找圖片的均值哈希和路徑下所有圖片的均值哈希
'''
if __name__ == '__main__':
if len(sys.argv) <= 1 or len(sys.argv) > 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
結論:
文中的代碼也並未得出文中描述的結果。
java實現
下面附上我實驗過的代碼:
/**
* 均值哈希算法/Average hash algorithm/AHA
* <p>
* 最適用於縮略圖,放大圖搜索
* <p>
* 雖然均值哈希更簡單且更快速,但是在比較上更死板、僵硬。<br>
* 它可能產生錯誤的漏洞,如有一個伽馬校正或顏色直方圖被用於到圖像。<br>
* 這是因爲顏色沿着一個非線性標尺 - 改變其中“平均值”的位置,並因此改變哪些高於/低於平均值的比特數
* <p>
*
* @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%,就能匹配原圖,原理基本一致,都是根據圖片得到哈希值,再比較哈希。