Python + OpenCVで画像の類似度を求める

※ https://qiita.com/best_not_best/items/669aaa9e1b8de647d29d にこの記事の修正版を投稿しました！

皆さんこんにちは。@best_not_bestです。
現在のお仕事は、求人サイトの案件レコメンド機能の実装や、Google AnalyticsやAdobe Analyticsを使って、そのサイトの分析をしています。（あとは動物画像を社内SlackにひたすらPostしています。）

[前回のAdvent Calendar](http://qiita.com/best_not_best/items/7946f55a4978ac1a8b2e)では、Deep Learningを使った画像類似度判定を行いました。精度は良かったのですが、やはり学習データを収集することがネックとなります・・・。今回は、単純に2枚の画像を比較して画像の類似度を求めてみたいと思います。

## やること

以下のおいぬ様の画像の類似度を比較します。画像はGoogle先生から拾ってきています。
精度を高めるため、正面を向いている画像を選びました。

比較画像

| ファイル名 | 画像 | 説明 |
|:-----------:|:------------:|:------------:|
| 05.png | <img src="https://qiita-image-store.s3.amazonaws.com/0/17452/bb7eeb2f-e0fe-f3f7-0221-f9f8f89b2b31.png" alt="05" width="200px"> | 柴犬（かわいい） | 

比較対象

| ファイル名 | 画像 | 説明 |
|:-----------:|:------------:|:------------:|
| 01.png | <img src="https://qiita-image-store.s3.amazonaws.com/0/17452/18c09e7c-c514-8e06-b852-8e7d6a1fdf1d.png" alt="01" width="200px"> | ダックスフント（かわいい） | 
| 02.png | <img src="https://qiita-image-store.s3.amazonaws.com/0/17452/93e6fc81-5ab9-ddf1-74d1-c1612ae98755.png" alt="02" width="200px"> | コーギー（かわいい） | 
| 03.png | <img src="https://qiita-image-store.s3.amazonaws.com/0/17452/faf70354-01d0-1f42-b140-cb94a36c038a.png" alt="03" width="200px"> | ゴールデン・レトリバー（かわいい） | 
| 04.png | <img src="https://qiita-image-store.s3.amazonaws.com/0/17452/b27f891f-edfb-3dd9-433c-ae5d70b2b38e.png" alt="04" width="200px"> | 柴犬（かわいい） | 
| 06.png | <img src="https://qiita-image-store.s3.amazonaws.com/0/17452/4b2714cc-1ebb-a061-f428-91db85059ce7.png" alt="06" width="200px"> | ラブラドール・レトリーバー（かわいい） | 

同じ犬種である、05.pngと04.pngの類似度が高くなれば（いちおう）成功です。

## 環境

* マシン/OS
  * MacBook Pro (Retina, 15-inch, Mid 2014)
  * OS X Yosemite 10.10.5
* Python
  * Python 3.5.2 :: Anaconda 4.1.1 (x86_64)
* Pythonパッケージ
  * opencv3 3.1.0（[conda](https://anaconda.org/menpo/opencv3)でインストールします。）

## ディレクトリ構成

```shell-session
hist_matching.py
feature_detection.py
images
 ├─ 01.png
 ├─ 02.png
 ├─ 03.png
 ├─ 04.png
 ├─ 05.png
 └─ 06.png
```

## 検証1: ヒストグラム比較

ざっくり言うと、色合いで比較する手法です。詳細は以下を参照ください。

* <a href="http://aidiary.hatenablog.com/entry/20091003/1254574041" target="_blank">類似画像検索システムを作ろう - 人工知能に関する断創録</a>
* <a href="https://www.blog.umentu.work/python-opencv3%E3%81%A7%E5%BA%A6%E6%95%B0%E5%88%86%E5%B8%83%E3%82%92%E7%AE%97%E5%87%BA%E3%81%97%E3%81%A6%E3%83%92%E3%82%B9%E3%83%88%E3%82%B0%E3%83%A9%E3%83%A0%E5%87%BA%E5%8A%9B/" target="_blank">Python OpenCV3で度数分布を算出してヒストグラム出力 - from umentu import stupid</a>

色合いで比較するのでグレースケール変換は行いません。また、画像サイズは一律200px × 200pxに変換して比較しています。

```py3:hist_matching.py
#!/usr/bin/env python
# -*- coding: UTF-8 -*-

"""hist matching."""

import cv2
import os

TARGET_FILE = '05.png'
IMG_DIR = os.path.abspath(os.path.dirname(__file__)) + '/images/'
IMG_SIZE = (200, 200)

target_img_path = IMG_DIR + TARGET_FILE
target_img = cv2.imread(target_img_path)
target_img = cv2.resize(target_img, IMG_SIZE)
target_hist = cv2.calcHist([target_img], [0], None, [256], [0, 256])

print('TARGET_FILE: %s' % (TARGET_FILE))

files = os.listdir(IMG_DIR)
for file in files:
    if file == '.DS_Store' or file == TARGET_FILE:
        continue

    comparing_img_path = IMG_DIR + file
    comparing_img = cv2.imread(comparing_img_path)
    comparing_img = cv2.resize(comparing_img, IMG_SIZE)
    comparing_hist = cv2.calcHist([comparing_img], [0], None, [256], [0, 256])

    ret = cv2.compareHist(target_hist, comparing_hist, 0)
    print(file, ret)
```

実行すると、メモリを消費するのか`Segmentation fault: 11`や`python(18114,0x7fff7a45b000) malloc: *** error for object 0x102000e00: incorrect checksum for freed object - object was probably modified after being freed.`といったエラーがまれに表示されます。解決策は見つかりませんでした（すいません）が、大量の画像を扱う場合は、比較画像の数を絞って複数回処理を行ったほうが良さそうです。

```shell-session:実行結果
TARGET_FILE: 05.png
01.png 0.3064316801821619
02.png -0.09702013809004943
03.png 0.5273343981076624
04.png 0.5453261576844468
06.png 0.1256772923432995
```

全く同じ画像の場合、類似度は1となります。05.pngと04.pngの類似度が高くなっているのが分かります。01.pngとの類似度が高いのは意外でした。

## 検証2: 特徴点のマッチング

2画像の特徴点を抽出し、それらの距離を比較します。以下を参考にさせていただきました。

* <a href="http://aidiary.hatenablog.com/entry/20091102/1257167398" target="_blank">3日で作る高速特定物体認識システム (4) 特徴点のマッチング - 人工知能に関する断創録</a>
* <a href="http://qiita.com/hmichu/items/f5f1c778a155c7c414fd" target="_blank">[OpenCV] いまさら局所特徴量で物体検出！？ - Qiita</a>
* <a href="http://labs.eecs.tottori-u.ac.jp/sd/Member/oyamada/OpenCV/html/py_tutorials/py_feature2d/py_matcher/py_matcher.html" target="_blank">特徴点のマッチング - OpenCV-Python Tutorials 1 documentation</a>

抽出精度を高めるため、グレースケール変換を行っています。前項と同様に、画像サイズは一律200px × 200pxに変換して比較しています。

```py3:feature_detection.py
#!/usr/bin/env python
# -*- coding: UTF-8 -*-

"""feature detection."""

import cv2
import os

TARGET_FILE = '05.png'
IMG_DIR = os.path.abspath(os.path.dirname(__file__)) + '/images/'
IMG_SIZE = (200, 200)

target_img_path = IMG_DIR + TARGET_FILE
target_img = cv2.imread(target_img_path, cv2.IMREAD_GRAYSCALE)
target_img = cv2.resize(target_img, IMG_SIZE)

bf = cv2.BFMatcher(cv2.NORM_HAMMING)
# detector = cv2.ORB_create()
detector = cv2.AKAZE_create()
(target_kp, target_des) = detector.detectAndCompute(target_img, None)

print('TARGET_FILE: %s' % (TARGET_FILE))

files = os.listdir(IMG_DIR)
for file in files:
    if file == '.DS_Store' or file == TARGET_FILE:
        continue

    comparing_img_path = IMG_DIR + file
    try:
        comparing_img = cv2.imread(comparing_img_path, cv2.IMREAD_GRAYSCALE)
        comparing_img = cv2.resize(comparing_img, IMG_SIZE)
        (comparing_kp, comparing_des) = detector.detectAndCompute(comparing_img, None)
        matches = bf.match(target_des, comparing_des)
        dist = [m.distance for m in matches]
        ret = sum(dist) / len(dist)
    except cv2.error:
        ret = 100000

    print(file, ret)
```

まれに`cv2.error`を吐くため、`try except`を使っています。抽出手法はAKAZEとORBで試しました。

```shell-session:実行結果（AKAZE）
TARGET_FILE: 05.png
01.png 143.925
02.png 134.05
03.png 140.775
04.png 127.8
06.png 148.725
```

```shell-session:実行結果（ORB）
TARGET_FILE: 05.png
01.png 67.59139784946237
02.png 58.60931899641577
03.png 59.354838709677416
04.png 53.59498207885304
06.png 63.55913978494624
```

距離を算出しているので、全く同じ画像の場合の値は0となり、値が小さいほど類似度が高くなります。実施したどちらの手法でも、前項と同様に05.pngと04.pngの類似度が高くなっています。

## まとめ

* Deep Learningを使わなくてもそれなりの精度は出せる
* 手法に応じてグレースケール変換、サイズ変換を行う
* 特徴点抽出の場合、向きが異なる画像の比較は難しいので、同じ向きの画像を比較させる
* 例えば、必ず正面を向いている社員証の画像どうしは比較し易い気がする
* おいぬ様 is GOD.