画像の特徴量にはSIFTやHoGがあったりするが、もう少し単純で扱いやすいxHashといった手法があります。
その中のdHash/aHashについて書いてきます。
目次
xHash共通の利点
SIFTやHoGは高次元で算出にもそれなりに時間がかかるが、xHashは基本小さくリサイズして画素ごとに演算して数十~数百のビット列にするだけです。
また、特徴量自体も数十~数百のビット列のため、比較演算が高速だったり、容量が小さいため特徴量の保持も容易にできます。
aHashの仕組み
流れは以下の感じです。ハッシュの大きさは8×8である必要がなく調整が可能です。
- 画像をリサイズ
- 一般的に8×8程度らしい
- 画像の平均輝度を算出
- 平均輝度より高ければ1, 低ければ0とする
- 一列に並べて8×8=64次元のビット列になる
メリット・デメリット
仕組み的に画像の輝度が全体的に変化した場合でも同じ特徴量になるため、輝度にロバスト性があります。
ただし、ガンマ補正やヒストグラム平坦化などで、色分布が変わると特徴量が変わってしまいます。
また、たまたま別の画像なのに特徴量が被るパターンが一定数見られます。
これらの要因からあまり良い精度とは言えません。
dHashの仕組み
流れは以下の感じです。ハッシュの大きさは8×8である必要がなく調整が可能です。
- 画像をリサイズ
- 一般的に8×8程度らしい
- 隣接する画素との差分を算出
- 隣接より輝度が高ければ1, 低ければ0とする
- 一列に並べて8×8=64次元のビット列になる
メリット・デメリット
隣接する画素との差分を比較しているため、位置情報をある程度考慮することができます。
そのため、ガンマ補正やヒストグラム平坦化にもめちゃくちゃ変わらなければロバスト性があり、ノイズにもロバスト性があります。
また、ごく少量であればトリミング、位置ずれにも耐性があります。
これについては原理的にハッシュのサイズが小さいほど強くなり、大きいほど弱くなります。
aHashにできてdHashにできない点は特になく、dHashは計算速度もそれなりで精度もそれなりな手法です。
Pythonの実装(imagehashライブラリ)
aHash/dHash程度なら自前で実装できるかもしれませんが、xHashはimagehashというライブラリが便利です。
https://github.com/JohannesBuchner/imagehash
pip install imagehash
imagehashを使ってビット列numpy配列を取得する実装はこんな感じです。
from PIL import Image
import imagehash
hash_size = 8 # 調整可能
image_path = <画像ファイルパス>
image = Image.open(image_path)
# (hash_size × hash_size, )のnumpy配列
ahash = imagehash.average_hash(image, hash_size).hash.flatten().astype('int8')
dhash = imagehash.dhash(image, hash_size).hash.flatten().astype('int8')
可視化したらこんな感じです。

類似度の計算
ハッシュ同士の配列比較計算 + numpyのcount_nonzero使えば類似度計算ができます。
import numpy as np
def calc_similarity(hash1: np.ndarray, hash2: np.ndarray):
assert hash1.shape[0] == hash1.shape[0] # 同じハッシュサイズであること
diff = np.count_nonzero(hash1 != hash2, axis=0) # ハッシュ間の距離
hash_len = hash1.shape[0]
return (hash_len - diff) / hash_len
ほかにも画像処理関連で色々な技術まとめているのでよかったら見てみてください!
補足:可視化スクリプト
一応載せときます。
from PIL import Image
import imagehash
import numpy as np
import cv2
def expand_bit_image(hash: np.ndarray, expand_rate=40):
assert hash.ndim == 2
result = np.zeros((hash.shape[0] * expand_rate, hash.shape[1] * expand_rate))
for i in range(hash.shape[0]):
for j in range(hash.shape[1]):
result[i * expand_rate:(i + 1) * expand_rate, j * expand_rate:(j + 1) * expand_rate] = hash[i, j]
return result
image_path = "画像ファイルパス"
image = Image.open(image_path)
# (hash_size, hash_size)のnumpy配列
ahash = imagehash.average_hash(image, hash_size).hash.astype('int8')
dhash = imagehash.dhash(image, hash_size).hash.astype('int8')
cv2.imwrite("ahash.jpg", expand_bit_image(ahash * 255))
cv2.imwrite("dhash.jpg", expand_bit_image(dhash * 255))