圖片識別——均值哈希算法

   均值哈希算法(Average hash algorithm,AHA)第一次是從著名的阮一峯阮老師的博文《相似圖片搜索的原理》看到的。而此篇文章與阮老師也很類似Looks Like It - The Hacker Factor Blog 。這裏不對原諒做摘抄,有興趣的自己看一下,在此對學習過程中的心得和遇到過的問題做一下總結。

    均值哈希算法,是感知哈希算法中最簡單的一種,基本原理是對圖片降頻。對於圖片,高頻有很多細節,如顏色、亮度、透明度等等,而低頻丟棄細節,只有圖像結構。

算法步驟

  1. 縮小尺寸。爲了保留結構去掉細節,去除大小、橫縱比的差異,把圖片統一縮放到8*8,共64個像素的圖片。
  2. 簡化色彩,轉化爲灰度圖。把縮放後的圖片轉化爲64級灰度圖。
  3. 計算平均值。計算進行灰度處理後圖片的所有像素點的平均值。
  4. 比較像素灰度值。遍歷灰度圖片每一個像素,如果大於平均值記錄爲1,否則爲0。
  5. 獲取指紋。將上一步的比較結果,組合在一起,就構成了一個64位的整數,這就是這張圖片的指紋。組合的次序並不重要,只要保證所有圖片都採用同樣次序就行了

    可見,之所以稱之爲均值哈希算法,就是因爲這種哈希算法,是通過灰度值的平均值,與其他灰度值的差異性得出來的。

哈希值比較

    得到指紋以後,就可以對比不同的圖片,看看圖片中有多少位是不一樣的。理論上,這等同於計算漢明距離(Hamming Distance)。如果不相同的數據位不超過5,就說明兩張圖很相似;如果大於10,說明這兩張是不同的圖片(不同的圖片,不代表不相似)。

算法優缺點

優點:

  1. 算法簡單,計算速度快。
  2. 圖片放大或縮小,改變縱橫比,或增加減少亮度、對比度、顏色,對hash值改變不會太大。

缺點:

  1. 算法對圖片的內容非常敏感,如果內容改變,很容易使得圖片的哈希值差別變大。

博文中的python代碼

    在阮老師的博文中,給出了一段用python寫的源碼,筆者本身並沒有寫過python代碼,但根據上面算法的描述,開始並沒有得出原文中相似的哈希結果,於是才反回來再看看這段python代碼。遇到的問題或者說誤解有:

  1.   ,這張圖片,是算法的第一步,將原圖變爲8*8的圖片,但筆者寫的java代碼的縮小圖片,與該圖並不完全一致。
  2.   ,這張圖片,是算法的最後一步給出的,雖然是黑白的,但並不是灰度圖,將圖放大後,會看到只有黑白兩色,應該是二值化圖
  3. 文中給出的16進制哈希值:8f373714acfcf4d0。筆者的代碼未能得出相同結果。

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%,就能匹配原圖,原理基本一致,都是根據圖片得到哈希值,再比較哈希。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章