るいすときのこの物語

オタクエンジニアの雑記

Goを使って画像の類似度を超簡単に行う / perceptual(image) hash


コンビニの有人レジがすごい混んでるのに、セルフレジを誰も使わない日本人を理解できない長谷川です。

概要

2つの画像の類似度を算出したい。

得られるハッシュ値は64bit 対象は静止画, 画像, 音声等のマルチメディアデータ コンテンツ内容が類似しているケースでハッシュを得た場合、例えば静止画画像の拡大、縮小といった加工の場合ハッシュ値が全く同じになる また、色調の修正やノイズが加わった場合も得られるハッシュ値間のハミング距離が近くなる 64bitのハッシュ値なので最も遠いハミング距離は64 (=全くコンテンツが異なっている) 逆にハミング距離が0であればperceptual hashで得られた結果上は同一コンテンツ こういった特徴があるため具体的にWebアプリケーションでの用途を考えると コンテンツの重複判定 類似画像検索 などに使えそうというのは上の特徴でわかるのではないでしょうか。

引用元:http://hideack.hatenablog.com/entry/2015/03/16/194336

計算方法もいくつか種類があります。 ・aHash:画像の平均輝度からの差分を使った方法 ・pHash:画像を離散コサイン変換(DCT)し周波数領域に変換後、低周波領域に対してaHashと同じ方法で算出する方法 ・dHash:隣接領域との差分を使った方法 ・wHash:pHashのDCTの代わりに離散ウェーブレット変換(DWT)を用いた方法 参考:http://tech.unifa-e.com/entry/2017/11/27/111546

ここでは aHash と dHash を使ってみようと思います。

使用したライブラリはこちら https://github.com/devedge/imagehash

 

実装

func main() {
    const hashLen = 8

    src, err := imagehash.OpenImg("/Users/luis/Desktop/src.jpg")
    if err != nil {
        panic(err)
    }

        srcAHash, err := imagehash.Ahash(src, hashLen) // return []byte
    if err != nil {
        panic(err)
    }

        srcHex := hex.EncodeToString(srcAHash)

        fmt.Println(srcHex)
        // fececc59d3c52d29a6ad852a23461131
}

imagehash.Ahash を imagehash.Dhash にするだけで aHash から dHash に切り替えることができます。

 

aHash と dHash の比較

こちらのサイトによると、dHash は aHash と同じ速度にも関わらず、精度が良好らしいです。

ちなみに2つの画像の distance(違う画像だと数値が大きい) を求めるには 2つの []byte をループさせて比較して値が違う場合に +1 すればいいだけです。

for i, _ := range laptop1AHash {
        if laptop1AHash[i] != laptop2AHash[i] {
                distance += 1
        }
}

 

試した画像

laptop1.jpg laptop2.jpg iqos.jpg

laptop1.jpg と laptop2.jpg は似たような画像(若干右にズレてます)、 iqos.jpg は全く違う画像を用いりました。

 

aHash

平均輝度の差分を使った超シンプルな方法 スマホで撮影した画像の場合、撮影時にタップした場所によって露出が変わるためスマホの画像を取り扱う場合には不適切なような気がする。

似たような画像
laptop1: febaa2a2a2a28282
laptop2: def2a2a2a2a282be
Distance: 3

アイコスと、1枚目
laptop1: febaa2a2a2a28282
iqos: f2f0f0c0d0f2e01c
Distance: 8

// 1000回ループ
richgo run main.go  41.88s user 0.86s system 186% cpu 22.967 total

 

dHash

隣接領域との差分を使った方法でシンプルで精度も、速度も良いらしい。 グレースケールに変換した後、9x8サイズに縮小する。 右隣のピクセルと値を比較して大きければ1 同じか小さければ0

0を黒、1を白に置換し、1pixelを1bitとして、16進数に変換することで値を取得できる。 aHash と違い、輝度ではなく実際にピクセルを見るので精度が良いと言われるのも分かる。

似たような画像
laptop1: bf004891010141db3872464646464652
laptop2: 7f11d131234382973246464646465652
Distance: 11

アイコスと、1枚目
laptop1: bf004891010141db3872464646464652
iqos: fececc59d3c52d29a6ad852a23461131
Distance: 15

// 1000回ループ
richgo run main.go  57.97s user 0.97s system 210% cpu 28.021 total

aHash と dHash の比較をしてみたけど精度にそこまでの差を見つけられなかった。 まあ今回用いた画像が悪かった。

distance だけを見ると、dHash の方が粒度が高いように見えるけど似たような画像と、アイコスと1枚目の差が 4 しかない。

しかし、aHash では差が 5 ある。 しかも1000回ループした結果では、aHash の方が16秒速い。

全く違う iqos.jpg でも真ん中にディスプレイがあり、dhash の手法だと白黒に変換したタイミングで同じような結果となってしまい、たまたま aHash と差が内容になってしまったと推測。

どちらを使うにしても distance の閾値を適切に設定する必要性がありますね。 もっといろんな画像で試してどっちを使うか決めたいと思います。