作者 | a1131825850瘋子
來源 | Python爬蟲scrapy
1.背景
要識(shí)別兩張圖片是否相似,首先我們可能會(huì)區(qū)分這兩張圖是人物照,還是風(fēng)景照等......對(duì)應(yīng)的風(fēng)景照是藍(lán)天還是大海......做一系列的分類。
從機(jī)器學(xué)習(xí)的的角度來說,首先要提取圖片的特征,將這些特征進(jìn)行分類處理,訓(xùn)練并建立模型,然后在進(jìn)行識(shí)別。
但是讓計(jì)算機(jī)去區(qū)分這些圖片分別是哪一類是很不容易的,不過計(jì)算機(jī)可以知道圖像的像素值的,因此,在圖像識(shí)別過程中,通過顏色特征來識(shí)別是相似圖片是我們常用的(當(dāng)然還有其特征還有紋理特征、形狀特征和空間關(guān)系特征等,這些有分為直方圖,顏色集,顏色局,聚合向量,相關(guān)圖等來計(jì)算顏色特征),
為了得到兩張相似的圖片,在這里通過以下幾種簡單的計(jì)算方式來計(jì)算圖片的相似度:
直方圖計(jì)算圖片的相似度
通過哈希值,漢明距離計(jì)算
通過圖片的余弦距離計(jì)算
通過圖片結(jié)構(gòu)度量計(jì)算
一、直方圖計(jì)算圖片的相似度
上三張圖片,分別是img1.png, img2.jpg,img.png:
可以看出上面這三張圖是挺相似的,在顏色上是差不多的,最相似的是哪兩張大家可以猜猜看,看和我們計(jì)算的是否一樣。
在python中利用opencv中的calcHist()方法獲取其直方圖數(shù)據(jù),返回的結(jié)果是一個(gè)列表:
# 計(jì)算圖img1的直方圖H1 = cv2.calcHist([img1], [1], None, [256], [0, 256])H1 = cv2.normalize(H1, H1, 0, 1, cv2.NORM_MINMAX, -1) # 對(duì)圖片進(jìn)行歸一化處理
先計(jì)算img1的直方圖,在對(duì)其歸一化,最后在分別對(duì)img2,img3計(jì)算,做歸一化,然后在利用python自帶的compareHist()進(jìn)行相似度的比較:
利用compareHist()進(jìn)行比較相似度similarity1 = cv2.compareHist(H1, H2, 0)
最后得到三張圖片的直方圖如下:
圖像的x軸是指的圖片的0~255之間的像素變化,y軸指的是在這0~255像素所占的比列。
我們可以明顯的看出img2與img3的直方圖的變化趨勢是相符的有重合態(tài)的,運(yùn)行結(jié)果如下:
通過運(yùn)行結(jié)果知道img2和img3是值是最為相似的(代碼calcImage.py)
上面的是直接調(diào)用opencv中的方法來實(shí)現(xiàn)的,下面還有自己寫的方法:
首先是將圖片轉(zhuǎn)化為RGB格式,在這里是用的pillow中的Image來對(duì)圖片做處理的:
# 將圖片轉(zhuǎn)化為RGBdef make_regalur_image(img, size=(64, 64)): gray_image = img.resize(size).convert('RGB') return gray_image
在計(jì)算兩圖片的直方圖:
# 計(jì)算直方圖def hist_similar(lh, rh): assert len(lh) == len(rh) hist = sum(1 - (0 if l == r else float(abs(l - r)) / max(l, r)) for l, r in zip(lh, rh)) / len(lh) return hist
在計(jì)算其相似度:
# 計(jì)算相似度def calc_similar(li, ri): calc_sim = hist_similar(li.histogram(), ri.histogram())returncalc_sim
得到最終的運(yùn)行結(jié)果:
兩種方法的的結(jié)果還是有點(diǎn)差距的,可以看到img1和img3的結(jié)果相似度高些。
不過兩者的相似度計(jì)算方法如下:
gi和si分別指的是兩條曲線的第i個(gè)點(diǎn)。
總結(jié):
利用直方圖計(jì)算圖片的相似度時(shí),是按照顏色的全局分布情況來看待的,無法對(duì)局部的色彩進(jìn)行分析,同一張圖片如果轉(zhuǎn)化成為灰度圖時(shí),在計(jì)算其直方圖時(shí)差距就更大了。
為了解決這個(gè)問題,可以將圖片進(jìn)行等分,然后在計(jì)算圖片的相似度。不過在這里我就不敘述了,大家自行探討!??!
二、哈希算法計(jì)算圖片的相似度
在計(jì)算之前我們先了解一下圖像指紋和漢明距離:
圖像指紋:
圖像指紋和人的指紋一樣,是身份的象征,而圖像指紋簡單點(diǎn)來講,就是將圖像按照一定的哈希算法,經(jīng)過運(yùn)算后得出的一組二進(jìn)制數(shù)字。
漢明距離:
假如一組二進(jìn)制數(shù)據(jù)為101,另外一組為111,那么顯然把第一組的第二位數(shù)據(jù)0改成1就可以變成第二組數(shù)據(jù)111,所以兩組數(shù)據(jù)的漢明距離就為1。簡單點(diǎn)說,漢明距離就是一組二進(jìn)制數(shù)據(jù)變成另一組數(shù)據(jù)所需的步驟數(shù),顯然,這個(gè)數(shù)值可以衡量兩張圖片的差異,漢明距離越小,則代表相似度越高。漢明距離為0,即代表兩張圖片完全一樣。
感知哈希算法是一類算法的總稱,包括aHash、pHash、dHash。顧名思義,感知哈希不是以嚴(yán)格的方式計(jì)算Hash值,而是以更加相對(duì)的方式計(jì)算哈希值,因?yàn)?ldquo;相似”與否,就是一種相對(duì)的判定。
幾種hash值的比較:
aHash:平均值哈希。速度比較快,但是常常不太精確。
pHash:感知哈希。精確度比較高,但是速度方面較差一些。
dHash:差異值哈希。精確度較高,且速度也非常快
1. 平均哈希算法(aHash):
該算法是基于比較灰度圖每個(gè)像素與平均值來實(shí)現(xiàn)。
aHash的hanming距離步驟:
先將圖片壓縮成8*8的小圖
將圖片轉(zhuǎn)化為灰度圖
計(jì)算圖片的Hash值,這里的hash值是64位,或者是32位01字符串
將上面的hash值轉(zhuǎn)換為16位的
通過hash值來計(jì)算漢明距離
# 均值哈希算法def ahash(image): # 將圖片縮放為8*8的 image = cv2.resize(image, (8, 8), interpolation=cv2.INTER_CUBIC) # 將圖片轉(zhuǎn)化為灰度圖 gray = cv2.cvtColor(image, cv2.COLOR_RGB2GRAY) # s為像素和初始灰度值,hash_str為哈希值初始值 s = 0 # 遍歷像素累加和 for i in range(8): for j in range(8): s = s + gray[i, j] # 計(jì)算像素平均值 avg = s / 64 # 灰度大于平均值為1相反為0,得到圖片的平均哈希值,此時(shí)得到的hash值為64位的01字符串 ahash_str = '' for i in range(8): for j in range(8): if gray[i, j] > avg: ahash_str = ahash_str + '1' else: ahash_str = ahash_str + '0' result = '' for i in range(0, 64, 4): result += ''.join('%x' % int(ahash_str[i: i + 4], 2)) # print("ahash值:",result) return result
2.感知哈希算法(pHash):
均值哈希雖然簡單,但是受均值影響大。如果對(duì)圖像進(jìn)行伽馬校正或者進(jìn)行直方圖均值化都會(huì)影響均值,從而影響哈希值的計(jì)算。所以就有人提出更健壯的方法,通過離散余弦(DCT)進(jìn)行低頻提取。
離散余弦變換(DCT)是種圖像壓縮算法,它將圖像從像素域變換到頻率域。然后一般圖像都存在很多冗余和相關(guān)性的,所以轉(zhuǎn)換到頻率域之后,只有很少的一部分頻率分量的系數(shù)才不為0,大部分系數(shù)都為0(或者說接近于0)。
pHash的計(jì)算步驟:
縮小圖片:32 * 32是一個(gè)較好的大小,這樣方便DCT計(jì)算轉(zhuǎn)化為灰度圖
計(jì)算DCT:利用Opencv中提供的dct()方法,注意輸入的圖像必須是32位浮點(diǎn)型,所以先利用numpy中的float32進(jìn)行轉(zhuǎn)換
縮小DCT:DCT計(jì)算后的矩陣是32 * 32,保留左上角的8 * 8,這些代表的圖片的最低頻率
計(jì)算平均值:計(jì)算縮小DCT后的所有像素點(diǎn)的平均值。
進(jìn)一步減小DCT:大于平均值記錄為1,反之記錄為0.
得到信息指紋:組合64個(gè)信息位,順序隨意保持一致性。
最后比對(duì)兩張圖片的指紋,獲得漢明距離即可。
# phashdef phash(path): # 加載并調(diào)整圖片為32*32的灰度圖片 img = cv2.imread(path) img1 = cv2.resize(img, (32, 32),cv2.COLOR_RGB2GRAY) # 創(chuàng)建二維列表 h, w = img.shape[:2] vis0 = np.zeros((h, w), np.float32) vis0[:h, :w] = img1 # DCT二維變換 # 離散余弦變換,得到dct系數(shù)矩陣 img_dct = cv2.dct(cv2.dct(vis0)) img_dct.resize(8,8) # 把list變成一維list img_list = np.array().flatten(img_dct.tolist()) # 計(jì)算均值 img_mean = cv2.mean(img_list) avg_list = ['0' if i<img_mean else '1' for i in img_list] return ''.join(['%x' % int(''.join(avg_list[x:x+4]),2) for x in range(0,64,4)])
3. 差異值哈希算法(dHash):
相比pHash,dHash的速度要快的多,相比aHash,dHash在效率幾乎相同的情況下的效果要更好,它是基于漸變實(shí)現(xiàn)的。
dHash的hanming距離步驟:
先將圖片壓縮成9*8的小圖,有72個(gè)像素點(diǎn)
將圖片轉(zhuǎn)化為灰度圖
計(jì)算差異值:dHash算法工作在相鄰像素之間,這樣每行9個(gè)像素之間產(chǎn)生了8個(gè)不同的差異,一共8行,則產(chǎn)生了64個(gè)差異值,或者是32位01字符串。
獲得指紋:如果左邊的像素比右邊的更亮,則記錄為1,否則為0.
通過hash值來計(jì)算漢明距離
# 差異值哈希算法def dhash(image): # 將圖片轉(zhuǎn)化為8*8 image = cv2.resize(image, (9, 8), interpolation=cv2.INTER_CUBIC) # 將圖片轉(zhuǎn)化為灰度圖 gray = cv2.cvtColor(image, cv2.COLOR_RGB2GRAY) dhash_str = '' for i in range(8): for j in range(8): if gray[i, j] > gray[i, j + 1]: dhash_str = dhash_str + '1' else: dhash_str = dhash_str + '0' result = '' for i in range(0, 64, 4): result += ''.join('%x' % int(dhash_str[i: i + 4], 2)) # print("dhash值",result)returnresult
4. 計(jì)算哈希值差異
#計(jì)算兩個(gè)哈希值之間的差異def campHash(hash1, hash2): n = 0 # hash長度不同返回-1,此時(shí)不能比較 if len(hash1) != len(hash2): return -1 # 如果hash長度相同遍歷長度 for i in range(len(hash1)): if hash1[i] != hash2[i]: n = n + 1 return n
最終的運(yùn)行結(jié)果:
aHash:
dhash:
p_hsah:
通過上面運(yùn)行的結(jié)果可以看出來,img1和img2的相似度高一些。
三、余弦相似度(cosin)
把圖片表示成一個(gè)向量,通過計(jì)算向量之間的余弦距離來表征兩張圖片的相似度。
1. 對(duì)圖片進(jìn)行歸一化處理
# 對(duì)圖片進(jìn)行統(tǒng)一化處理def get_thum(image, size=(64, 64), greyscale=False): # 利用image對(duì)圖像大小重新設(shè)置, Image.ANTIALIAS為高質(zhì)量的 image = image.resize(size, Image.ANTIALIAS) if greyscale: # 將圖片轉(zhuǎn)換為L模式,其為灰度圖,其每個(gè)像素用8個(gè)bit表示 image = image.convert('L') return image
2. 計(jì)算余弦距離
# 計(jì)算圖片的余弦距離def image_similarity_vectors_via_numpy(image1, image2): image1 = get_thum(image1) image2 = get_thum(image2) images = [image1, image2] vectors = [] norms = [] for image in images: vector = [] for pixel_tuple in image.getdata(): vector.Append(average(pixel_tuple)) vectors.append(vector) # linalg=linear(線性)+algebra(代數(shù)),norm則表示范數(shù) # 求圖片的范數(shù)?? norms.append(linalg.norm(vector, 2)) a, b = vectors a_norm, b_norm = norms # dot返回的是點(diǎn)積,對(duì)二維數(shù)組(矩陣)進(jìn)行計(jì)算 res = dot(a / a_norm, b / b_norm)returnres
最終運(yùn)行結(jié)果:
結(jié)果顯示img1和img2的相似度高一些,和計(jì)算hash值的漢明距離得到的結(jié)果是相一致的。
四、圖片SSIM(結(jié)構(gòu)相似度量)
SSIM是一種全參考的圖像質(zhì)量評(píng)價(jià)指標(biāo),分別從亮度、對(duì)比度、結(jié)構(gòu)三個(gè)方面度量圖像相似性。SSIM取值范圍[0, 1],值越大,表示圖像失真越小。在實(shí)際應(yīng)用中,可以利用滑動(dòng)窗將圖像分塊,令分塊總數(shù)為N,考慮到窗口形狀對(duì)分塊的影響,采用高斯加權(quán)計(jì)算每一窗口的均值、方差以及協(xié)方差,然后計(jì)算對(duì)應(yīng)塊的結(jié)構(gòu)相似度SSIM,最后將平均值作為兩圖像的結(jié)構(gòu)相似性度量,即平均結(jié)構(gòu)相似性SSIM。
ssim1 = compare_ssim(img1, img2, multichannel=True)
這個(gè)是scikit-image庫自帶的一種計(jì)算方法
運(yùn)行結(jié)果:
可以看到img1和img2的相似度高。
好了,以上就是到目前為止我接觸到的圖片相似度的計(jì)算方法,肯定還有許多我沒有接觸到的計(jì)算方法,大家有需要的可以參考一下,有其他方法的大家可以留言一起探討!!!