類似画像をピクセル比較して見つける

# 概要
画像を扱う感覚をつかむため、類似画像を見つけるコードをrubyで書きました。

* [mini_magick](https://github.com/minimagick/minimagick)を使って画像をピクセル値の行列に変換する
* [numo-narray](https://github.com/ruby-numo/numo-narray)を使って画像の類似度をピクセルの行列間の差で表現する
* 類似画像を見つけるスクリプトを作って、類似度を確認する

# 背景

私自身は、画像処理は生まれてから一度も書いたことのない全くの初心者です。今後画像処理を使うにあたって、画像をデータとして扱う感覚をつかむために、類似画像検索スクリプトをつくりながら勉強しました。

# 完成コード
https://github.com/junara/simple_get_nearest_image
（MNISTの画像ファイルは入っていませんので、各自用意していください。私は[こちら](http://medfreak.info/?p=1081)のサイトを見ながらやりました。）

# 環境
* ruby 2.5.1
* gem 
   * gem 'mini_magick'
   * gem 'numo-narray'

# 画像をピクセル値の行列として扱う
いままで、自分で画像ファイルを扱ったことはないので、調べると、[x, y, RGB](https://home.hiroshima-u.ac.jp/java2/javaenshu-hosoku-4.pdf)で表現されることがわかりました。

rubyでやる場合は[mini_magick](https://github.com/minimagick/minimagick)を使えば良さそうですのでやってみました。

```ruby

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

```ruby

[[[0,0,0],[10,10,10]],[[10,10,10],[20,20,20]]]
```

画像2

```ruby
[[[10,10,10],[0,0,0]],[[10,10,10],[20,20,20]]]
```

次に、画像1と画像2の類似度を計算します。以下の順序です。

* 画像1 - 画像2の差を取ると以下のようになります

```ruby
[[[-10,-10,-10],[10,10,10]],[[0,0,0],[0,0,0]]]
```

* 次に絶対値をとります。

```ruby
[[[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'](https://github.com/ruby-numo/numo-narray)を使います。

画像データをnumo-narrayの行列に変換するには以下の通りです。


```ruby
# 画像 はファイルパスが入ります
image = MiniMagick::Image.open(画像)
array = Numo::DFloat.cast(image.pixels)
```

行列演算を使って、類似度を計算するには以下の通りです。

```ruby

  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) # これが類似度
```

# 類似画像を見つけるスクリプト
ここまでで、画像を行列とした取得し、取得した行列を元に類似度を計算する方法を定義しました。
最後に、これらをまとめて類似画像を見つけるスクリプトを書きます。


まず、画像から行列を取得するクラス

```analysed_image.rb
# 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）を表示します。

```nearest_image.rb
# 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
```

実際にモジュールを動かすスクリプトは以下の通り。

```test.rb
# 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とした画像自身も取得できていますし、単純なアルゴリズムですが、まあまあの精度・・・ですかね。

```irb_output

> 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}
```

![image.png](https://qiita-image-store.s3.amazonaws.com/0/124445/9354bfca-3da8-a9a0-4378-44da193006b2.png)


以上。

# 所感
* ピクセルの行列にすると、画像がかなり身近になる感覚がえられる。これならなんでもできそう
* 行列演算とても楽ちんで速い