はじめに
例えば機械学習用の画像を収集するとき、違うファイル名で同じ画像、というのは邪魔になります
あるいはデータの整理をしているとき、各所にコピーされた同じ画像を集約して、不要なものを削除したい、ということもあります
完全に同じ画像だけでなく、サイズ違いや縦横の移動など、少しだけ加工したものも取り除きたくなります
そんなとき、画像ハッシュを用いることで、類似画像を検出することが可能です
Python の場合は ImageHash モジュールを使うことで画像ハッシュを計算できます
本記事では Elixir Image を使って、画像ハッシュの中でも高速な dHash を計算し、類似画像を見つけます
実行環境はもちろん Livebook です
実装したノートブックはこちら
セットアップ
必要なモジュールをインストールします
Mix.install([
{:image, "~> 0.38"},
{:req, "~> 0.3"},
{:kino, "~> 0.11"}
])
画像の取得
元画像として、 ElixirConf EU のロゴ画像を取得します
original_img =
"https://www.elixirconf.eu/assets/images/drops.svg"
|> Req.get!()
|> Map.get(:body)
|> Image.from_binary!()
類似画像生成
比較対象としての類似画像を生成します
グレースケール
白黒画像にします
gray_img = Image.to_colorspace!(original_img, :bw)
リサイズ
半分のサイズに縮小します
resized_img = Image.resize!(original_img, 0.5)
回転
45度回転させます
rotated_img = Image.rotate!(original_img, 45)
切り取り
少しだけ外側を切り取ります
cropped_img = Image.crop!(original_img, 0.07, 0.07, 0.9, 0.9)
文字追加
画像内に文字を追加します
text_img = Image.Text.text!("Elixir", text_fill_color: :purple)
with_text_img = Image.compose!(original_img, text_img, x: 300, y: 100)
別画像
Phoenix のロゴを別の画像として取得します
other_img =
"https://hexdocs.pm/phoenix/assets/logo.png"
|> Req.get!()
|> Map.get(:body)
|> Image.from_binary!()
画像の一覧表示
元画像と他の画像を並べて表示します
img_list =
[
original_img,
gray_img,
resized_img,
rotated_img,
cropped_img,
with_text_img,
other_img
]
Kino.Layout.grid(img_list, columns: 4)
ハッシュの計算
以下のようにして dHash を計算することができます
{:ok, original_dhash} = Image.dhash(original_img)
結果は以下のようになります
{:ok, <<56, 126, 207, 217, 253, 103, 103, 124>>}
サイズを指定しない場合、 dHash は 64 桁のビット配列 = 8 桁のバイト配列になります
値を整数にしてみます
original_dhash_int = :binary.decode_unsigned(original_dhash, :big)
結果は 4070919648355772284
になります
16進数の文字列に変換してみます
Integer.to_string(original_dhash_int, 16)
結果は "387ECFD9FD67677C"
になります
2進数の文字列に変換してみます
original_dhash_int
|> Integer.to_string(2)
|> String.pad_leading(64, "0")
結果は "0011100001111110110011111101100111111101011001110110011101111100"
になります
別画像の dHash も取得し、2進数の文字列にします
other_img
|> Image.dhash()
|> elem(1)
|> :binary.decode_unsigned(:big)
|> Integer.to_string(2)
|> String.pad_leading(64, "0")
結果は "1111100001111111011111110111111100111111001111000000111000000000"
です
元画像と別画像の dHash を並べてみます
- 0011100001111110110011111101100111111101011001110110011101111100
- 1111100001111111011111110111111100111111001111000000111000000000
この dHash の値同士がどれくらい違っているか、は ハミング距離 で計算します
ハミング距離は各ビットの排他的論理和(XOR)の合計、つまり各桁の 0 1 を比較したとき、値が違う桁の数です
上の例だと、 XOR は以下のようになります
dHash | |
---|---|
元画像 | 0011100001111110110011111101100111111101011001110110011101111100 |
別画像 | 1111100001111111011111110111111100111111001111000000111000000000 |
XOR | 1100000000000001101100001010011011000010010110110110100101111100 |
XOR の 1 の個数は 27 なので、元画像 dHash と別画像 dHash のハミング距離は 27 になります
ハミング距離の計算
上記のような計算をするのは大変なので、 Elixir Image では Image.hamming_distance
に2つの画像を渡すだけで dHash のハミング距離を計算できるようにしてくれています
各画像についてハミング距離を計算し、画像とともに表示しましょう
img_list
|> Enum.map(fn img ->
hamming_distance =
original_img
|> Image.hamming_distance(img)
|> elem(1)
[hamming_distance, img]
|> Kino.Layout.grid(columns: 1)
end)
|> Kino.Layout.grid(columns: 4)
結果は以下のようになります
おおよその基準として、 10 以下ならほぼ同じ画像、 20 以下なら類似画像と判断できます
結果を確認してみましょう
比較対象 | ハミング距離 | 判定結果 | 補足 |
---|---|---|---|
元画像 | 0 | 同一画像 | 本当に全く同一の画像なので想定通り |
グレースケール | 0 | 同一画像 | 白黒にしても同じ dHash になりました |
リサイズ | 0 | 同一画像 | リサイズしても同じ dHash になりました |
回転 | 29 | 別画像 | 回転すると距離が大きくなりました |
切り抜き | 16 | 類似画像 | 切り抜き具合によりますが、近くなっています |
文字追加 | 1 | ほぼ同一画像 | 小さい文字を加えたくらいだと同じ画像と判定できます |
別画像 | 27 | 別画像 | 別画像なので想定通りです |
dHash の場合、実は計算の過程でグレースケール化、リサイズを実行しています
なので色の違いやサイズの違いは dHash では無いものと見做されます
逆に回転や切り抜きのような、概形が変わる場合は距離が大きくなります
aHash や pHash など、別の種類の画像ハッシュもあり、どの画像ハッシュを使うかによって「どういう画像を類似画像と見做すか」が違ってくるので注意しましょう
Elixir Image では現状 dHash のみサポートしています
まとめ
Elixir Image を使って、 dHash による画像の類似度を計算できました
「類似」の基準によっては別の画像ハッシュを使わないといけないので、その点は注意しましょう