概要
画像を扱う感覚をつかむため、類似画像を見つけるコードをrubyで書きました。
- mini_magickを使って画像をピクセル値の行列に変換する
- numo-narrayを使って画像の類似度をピクセルの行列間の差で表現する
- 類似画像を見つけるスクリプトを作って、類似度を確認する
背景
私自身は、画像処理は生まれてから一度も書いたことのない全くの初心者です。今後画像処理を使うにあたって、画像をデータとして扱う感覚をつかむために、類似画像検索スクリプトをつくりながら勉強しました。
完成コード
https://github.com/junara/simple_get_nearest_image
(MNISTの画像ファイルは入っていませんので、各自用意していください。私はこちらのサイトを見ながらやりました。)
環境
- ruby 2.5.1
- gem
- gem 'mini_magick'
- gem 'numo-narray'
画像をピクセル値の行列として扱う
いままで、自分で画像ファイルを扱ったことはないので、調べると、x, y, RGBで表現されることがわかりました。
rubyでやる場合はmini_magickを使えば良さそうですのでやってみました。
require 'mini_magick'
file = 'images/mnist1.png'
image= MiniMagick::Image.open(file)
image.get_pixels
=>
[[[0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0]], [[0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0]], [[0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0]], [[0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0]], [[0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0]], [[0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [3, 3, 3], [18, 18, 18], [18, 18, 18], [18, 18, 18], [126, 126, 126], [136, 136, 136], [175, 175, 175], [26, 26, 26], [166, 166, 166], [255, 255, 255], [247, 247, 247], [127, 127, 127], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0]], [[0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [30, 30, 30], [36, 36, 36], [94, 94, 94], [154, 154, 154], [170, 170, 170], [253, 253, 253], [253, 253, 253], [253, 253, 253], [253, 253, 253], [253, 253, 253], [225, 225, 225], [172, 172, 172], [253, 253, 253], [242, 242, 242], [195, 195, 195], [64, 64, 64], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0]], [[0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [49, 49, 49], [238, 238, 238], [253, 253, 253], [253, 253, 253], [253, 253, 253], [253, 253, 253], [253, 253, 253], [253, 253, 253], [253, 253, 253], [253, 253, 253], [251, 251, 251], [93, 93, 93], [82, 82, 82], [82, 82, 82], [56, 56, 56], [39, 39, 39], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0]], [[0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [18, 18, 18], [219, 219, 219], [253, 253, 253], [253, 253, 253], [253, 253, 253], [253, 253, 253], [253, 253, 253], [198, 198, 198], [182, 182, 182], [247, 247, 247], [241, 241, 241], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0]], [[0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [80, 80, 80], [156, 156, 156], [107, 107, 107], [253, 253, 253], [253, 253, 253], [205, 205, 205], [11, 11, 11], [0, 0, 0], [43, 43, 43], [154, 154, 154], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0]], [[0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [14, 14, 14], [1, 1, 1], [154, 154, 154], [253, 253, 253], [90, 90, 90], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0]], [[0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [139, 139, 139], [253, 253, 253], [190, 190, 190], [2, 2, 2], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0]], [[0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [11, 11, 11], [190, 190, 190], [253, 253, 253], [70, 70, 70], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0]], [[0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [35, 35, 35], [241, 241, 241], [225, 225, 225], [160, 160, 160], [108, 108, 108], [1, 1, 1], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0]], [[0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [81, 81, 81], [240, 240, 240], [253, 253, 253], [253, 253, 253], [119, 119, 119], [25, 25, 25], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0]], [[0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [45, 45, 45], [186, 186, 186], [253, 253, 253], [253, 253, 253], [150, 150, 150], [27, 27, 27], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0]], [[0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [16, 16, 16], [93, 93, 93], [252, 252, 252], [253, 253, 253], [187, 187, 187], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0]], [[0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [249, 249, 249], [253, 253, 253], [249, 249, 249], [64, 64, 64], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0]], [[0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [46, 46, 46], [130, 130, 130], [183, 183, 183], [253, 253, 253], [253, 253, 253], [207, 207, 207], [2, 2, 2], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0]], [[0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [39, 39, 39], [148, 148, 148], [229, 229, 229], [253, 253, 253], [253, 253, 253], [253, 253, 253], [250, 250, 250], [182, 182, 182], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0]], [[0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [24, 24, 24], [114, 114, 114], [221, 221, 221], [253, 253, 253], [253, 253, 253], [253, 253, 253], [253, 253, 253], [201, 201, 201], [78, 78, 78], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0]], [[0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [23, 23, 23], [66, 66, 66], [213, 213, 213], [253, 253, 253], [253, 253, 253], [253, 253, 253], [253, 253, 253], [198, 198, 198], [81, 81, 81], [2, 2, 2], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0]], [[0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [18, 18, 18], [171, 171, 171], [219, 219, 219], [253, 253, 253], [253, 253, 253], [253, 253, 253], [253, 253, 253], [195, 195, 195], [80, 80, 80], [9, 9, 9], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0]], [[0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [55, 55, 55], [172, 172, 172], [226, 226, 226], [253, 253, 253], [253, 253, 253], [253, 253, 253], [253, 253, 253], [244, 244, 244], [133, 133, 133], [11, 11, 11], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0]], [[0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [136, 136, 136], [253, 253, 253], [253, 253, 253], [253, 253, 253], [212, 212, 212], [135, 135, 135], [132, 132, 132], [16, 16, 16], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0]], [[0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0]], [[0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0]], [[0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0]]]
たしかに、それっぽいデータえられました。
画像の類似度をピクセルの行列間の差で表現
ピクセルは行列で扱えば良いことがわかり、実際にrubyで行列を得ることもできました。次は、これを実際に使ってみます。
今回は、類似画像をみつけることを目的にしています。
今回定義する類似画像を見つけるための指標は、各座標のRGB値の差の絶対値としました。
今回の定義を説明します。
例えば、2x2の画像を2つ想定します。
画像1
[[[0,0,0],[10,10,10]],[[10,10,10],[20,20,20]]]
画像2
[[[10,10,10],[0,0,0]],[[10,10,10],[20,20,20]]]
次に、画像1と画像2の類似度を計算します。以下の順序です。
- 画像1 - 画像2の差を取ると以下のようになります
[[[-10,-10,-10],[10,10,10]],[[0,0,0],[0,0,0]]]
- 次に絶対値をとります。
[[[10,10,10],[10,10,10]],[[0,0,0],[0,0,0]]]
- 最後に合計します
10 + 10 + 10 + 10 + 10 + 10 = 60
今回定義する類似度は、画像1と画像2の類似性は60と計算されます。
これをコードで表現します。
rubyで行列演算をするためのライブラリとして、gem 'numo-narray'を使います。
画像データをnumo-narrayの行列に変換するには以下の通りです。
# 画像 はファイルパスが入ります
image = MiniMagick::Image.open(画像)
array = Numo::DFloat.cast(image.pixels)
行列演算を使って、類似度を計算するには以下の通りです。
def diff_abs(array1, array2)
(array1 - array2).abs
end
def length(array1, array2)
diff_abs(array1, array2).sum
end
image1 = MiniMagick::Image.open(画像1)
array1 = Numo::DFloat.cast(image1.pixels)
image2 = MiniMagick::Image.open(画像2)
array2 = Numo::DFloat.cast(image2.pixels)
diff_abs(array1, array2) # これが類似度
類似画像を見つけるスクリプト
ここまでで、画像を行列とした取得し、取得した行列を元に類似度を計算する方法を定義しました。
最後に、これらをまとめて類似画像を見つけるスクリプトを書きます。
まず、画像から行列を取得するクラス
# frozen_string_literal: true
require 'mini_magick'
require 'numo/narray'
class AnalysedImage
include MiniMagick
attr_accessor :numo_array, :filename, :image
def initialize(file)
@image = MiniMagick::Image.open(file)
@numo_array = Numo::DFloat.cast(pixels)
@filename = file
end
def pixels
@image.get_pixels
end
end
画像間の差を取得するモジュールです。calc_length(target_file, dir)
で dir
内の全画像とtarget
画像との間の類似度をhashで取得し、execute
で上位xxx個(初期値は10)を表示します。
# frozen_string_literal: true
require './analysed_image'
require 'numo/narray'
module NearestImage
module_function
def execute(target_file, dir, top = 10)
data_array = calc_length(target_file, dir)
data_array.sort_by { |b| b[:length] }[0..(top - 1)]
end
def calc_length(target_file, dir)
ary = []
Dir.glob("#{dir}/**/*") do |item|
target_image = AnalysedImage.new(target_file)
reference_image = AnalysedImage.new(item)
temp_data_params = data_params(target_image, reference_image)
ary << temp_data_params
end
ary
end
def data_params(target_image, reference_image)
{ file: reference_image.filename, length: length(target_image.numo_array, reference_image.numo_array) }
end
def diff_abs(array1, array2)
(array1 - array2).abs
end
def length(array1, array2)
diff_abs(array1, array2).sum
end
end
実際にモジュールを動かすスクリプトは以下の通り。
# frozen_string_literal: true
require './nearest_image'
result = NearestImage.execute('images/mnist1.png', 'selected', 10)
result.each do |r|
p r
end
MNIST画像ファイル1000個について類似度トップ10を取得した結果は以下の通りです。10個中7個(うち1個はtarget画像自身)正解でした。targetとした画像自身も取得できていますし、単純なアルゴリズムですが、まあまあの精度・・・ですかね。
> bundle exec ruby test.rb
{:file=>"selected/mnist1.png", :length=>0.0}
{:file=>"selected/mnist833.png", :length=>55743.0}
{:file=>"selected/mnist965.png", :length=>61434.0}
{:file=>"selected/mnist653.png", :length=>61455.0}
{:file=>"selected/mnist772.png", :length=>61791.0}
{:file=>"selected/mnist50.png", :length=>62775.0}
{:file=>"selected/mnist131.png", :length=>64959.0}
{:file=>"selected/mnist126.png", :length=>65046.0}
{:file=>"selected/mnist800.png", :length=>65220.0}
{:file=>"selected/mnist133.png", :length=>65337.0}
以上。
所感
- ピクセルの行列にすると、画像がかなり身近になる感覚がえられる。これならなんでもできそう
- 行列演算とても楽ちんで速い