LoginSignup
20
7

Elixir Image で画像の dHash を計算し、類似画像を見つける

Last updated at Posted at 2023-11-06

はじめに

例えば機械学習用の画像を収集するとき、違うファイル名で同じ画像、というのは邪魔になります

あるいはデータの整理をしているとき、各所にコピーされた同じ画像を集約して、不要なものを削除したい、ということもあります

完全に同じ画像だけでなく、サイズ違いや縦横の移動など、少しだけ加工したものも取り除きたくなります

そんなとき、画像ハッシュを用いることで、類似画像を検出することが可能です

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!()

スクリーンショット 2023-10-31 15.39.17.png

類似画像生成

比較対象としての類似画像を生成します

グレースケール

白黒画像にします

gray_img = Image.to_colorspace!(original_img, :bw)

スクリーンショット 2023-10-31 15.40.41.png

リサイズ

半分のサイズに縮小します

resized_img = Image.resize!(original_img, 0.5)

スクリーンショット 2023-10-31 15.41.41.png

回転

45度回転させます

rotated_img = Image.rotate!(original_img, 45)

スクリーンショット 2023-10-31 15.42.44.png

切り取り

少しだけ外側を切り取ります

cropped_img = Image.crop!(original_img, 0.07, 0.07, 0.9, 0.9)

スクリーンショット 2023-10-31 15.43.47.png

文字追加

画像内に文字を追加します

text_img = Image.Text.text!("Elixir", text_fill_color: :purple)

with_text_img = Image.compose!(original_img, text_img, x: 300, y: 100)

スクリーンショット 2023-10-31 15.49.11.png

別画像

Phoenix のロゴを別の画像として取得します

other_img =
  "https://hexdocs.pm/phoenix/assets/logo.png"
  |> Req.get!()
  |> Map.get(:body)
  |> Image.from_binary!()

スクリーンショット 2023-10-31 15.51.37.png

画像の一覧表示

元画像と他の画像を並べて表示します

img_list =
  [
    original_img,
    gray_img,
    resized_img,
    rotated_img,
    cropped_img,
    with_text_img,
    other_img
  ]

Kino.Layout.grid(img_list, columns: 4)

スクリーンショット 2023-10-31 15.54.16.png

ハッシュの計算

以下のようにして 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)

結果は以下のようになります

スクリーンショット 2023-10-31 16.35.21.png

おおよその基準として、 10 以下ならほぼ同じ画像、 20 以下なら類似画像と判断できます

結果を確認してみましょう

比較対象 ハミング距離 判定結果 補足
元画像 0 同一画像 本当に全く同一の画像なので想定通り
グレースケール 0 同一画像 白黒にしても同じ dHash になりました
リサイズ 0 同一画像 リサイズしても同じ dHash になりました
回転 29 別画像 回転すると距離が大きくなりました
切り抜き 16 類似画像 切り抜き具合によりますが、近くなっています
文字追加 1 ほぼ同一画像 小さい文字を加えたくらいだと同じ画像と判定できます
別画像 27 別画像 別画像なので想定通りです

dHash の場合、実は計算の過程でグレースケール化、リサイズを実行しています

なので色の違いやサイズの違いは dHash では無いものと見做されます

逆に回転や切り抜きのような、概形が変わる場合は距離が大きくなります

aHash や pHash など、別の種類の画像ハッシュもあり、どの画像ハッシュを使うかによって「どういう画像を類似画像と見做すか」が違ってくるので注意しましょう

Elixir Image では現状 dHash のみサポートしています

まとめ

Elixir Image を使って、 dHash による画像の類似度を計算できました

「類似」の基準によっては別の画像ハッシュを使わないといけないので、その点は注意しましょう

20
7
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
20
7