EAFP

~ Easier to Ask for Forgiveness than Permission ~

大量の画像の中から同一画像を探す方法

この記事はRecruit Engineers Advent Calendar 2018 の3日目の記事です。

adventar.org

こんばんは。 @kazu0716 です。ノリでアドベントカレンダーなんて参加しましたが、中々記事がかけずかなり焦りました・・・。クソ記事でも書かないよりマシということでご容赦ください。

今回は、大量の画像の中から、同一画像を見つける必要があり、色々調べたのでシェアしようと思います。

なぜやったか

仕事の中で 「不正ユーザは同一グループに属しており、利用する画像は使い回されている」 という仮説を調査する必要がありました。ただ、画像の数が膨大であり、人の目で見て処理をするのは非常に大変だったため、Scriptで処理できないかなと思ったのがキッカケです。

制約事項

仮説が正しかった場合、最終的にはシステム化することも検討していたので、どうせならそこまで見据えて技術を調査しようと思い、下記の制約の元でも実現できるものを探しました。

  • 画像データが外部に流出してはいけない
  • 数分内で大量の画像(数万件程度)の中から同一画像を検索できる

どのように実現したか

要素技術調査から、同一画像の調査精度、パフォーマンス調査まで実施しました。

要素技術調査

色々調査してく中で「Perceptual Hash」(以下、単純にhashと表現する)という手法を知りました。手法の詳しい内容に関しては下記に譲るとして、本技術の強みとしては、画像のhash同士を比較し、画像の類似度(実際はhash同士のハミング距離)を調べることができるという点にあると考えます。

tech.unifa-e.com kzkohashi.hatenablog.com

今回は、この手法を用いて大量画像の中から同一画像を検索できるPythonのライブラリである「image-match」の調査検証を行なっていきます。

どんなライブラリか

主に下記の2つの機能を持っています。

  • 画像のhashを計算し、DB(ElasticSearch等)にデータを格納する(indexing)

    • 入力: 画像のパス(ファイルパス/URLのどちらか)
    • 出力: 特になし
  • DB内にあるデータを検索し、画像の類似度を表示する(search)

    • 入力: 画像のパス(ファイルパス/URL)
    • 出力: DB内部に格納されている画像との類似度、画像のパス(ファイルパス/URLのどちらか)
      • 特定の類似度以上のもののみ出力することも可能
      • 公式ドキュメントには0.40以下であれば同一画像と考えてよいとあり、今回はその値を閾値に設定

公式ドキュメントを参考に、簡単な使い方を紹介していこうと思います。今回python2系を使っているのは、image-matchの依存ライブラリであるscikit-image(0.12.x)がローカルの環境のpython3.7.0ではコンパイルでエラーになりpipでinstallできなかった(2018/11: 現在)ためです。

# pythonのVersion
$ python -V
Python 2.7.15

# 利用しているライブラリ
$ pip freeze
certifi==2018.10.15
chardet==3.0.4
dask==0.20.0
decorator==4.3.0
elasticsearch==6.3.1
idna==2.7
image-match==1.1.2
networkx==2.2
numpy==1.15.3
Pillow==5.3.0
requests==2.20.0
scikit-image==0.12.3
scipy==1.1.0
six==1.11.0
toolz==0.9.0
urllib3==1.24

$ ipython
In [1]: from elasticsearch import Elasticsearch
   ...: from image_match.elasticsearch_driver import SignatureES

In [2]: es = Elasticsearch()

# indexを指定しないとimagesというindexにデータが保存されます
# distance_cutoffで類似度が一定以上のものを結果から除外する
In [3]: ses = SignatureES(es, index="image_match_test",distance_cutoff=0.40)

In [4]: ses.add_image("./data/img/enm/65188/187284.jpg")

# 同一画像なのでdist=0.0になる
In [5]: ses.search_image("./data/img/enm/65188/187284.jpg")
Out[5]:
[{'dist': 0.0,
  'id': u'-vh4b2cBZ9Tpnt1Kqfmo',
  'metadata': None,
  'path': u'./data/img/enm/65188/187284.jpg',
  'score': 63.0}]

同一画像認識精度

次に、画像の類似度(実際は、ハッシュ同士のハミング距離)が0.40以下である場合同じ画像になるかを調べて見ました。 flaskで、画像search結果を表示する簡単なwebアプリを作り、人の目で見て同一でないものが混じってないかを調べて見ました。下記が、表示結果イメージになります。

f:id:kazu_0716:20181203004735p:plain

500サンプルくらいを見ましたが、違うものは混じってなさそうでした。ただ、0.40以上で判定され、同じだけど見逃してるものはあったかもしれませんが、今回は誤検知がないことを調べたかったので割愛しました。

パフォーマンスの調査

最後にパフォーマンスの調査を実施しました。利用したマシンのスペックは下記になります。

調査したのは、処理時間で主に下記の2点になります。

  • indexingにかかる時間
対象データ数 かかった時間(sec)
21156 862.8

CPU負荷: 70%程度でした。時間がかかった理由としては、以下のようなコードで単純にforを回してデータを投入したからであると考えます。ただ、並列処理をしてもElasticSearchへのデータのindexingに限界があるため、そこまでパフォーマンスは上がらないと思われます。

# -*- coding: utf-8 -*-
from __future__ import print_function, unicode_literals, absolute_import

import time
import os
import glob
import threading
import imghdr
from elasticsearch import Elasticsearch
from image_match.elasticsearch_driver import SignatureES

es = Elasticsearch()
ses = SignatureES(es, distance_cutoff=0.40)

def add_images():
    img_path = "./data/img/{}/*/*.jpg".format("koi")
    image_list = glob.glob(img_path)
    for img in image_list:
        if imghdr.what(img) is not None:
            ses.add_image(str(img))
        else:
            os.remove(str(img))

def main():
    start = time.time()
    add_images(service)
    print ("elapsed_time:{0}".format(time.time() - start) + "[sec]")


if __name__ == '__main__':
    main()
  • searchにかかる時間(DB内部の画像データ数: 21156)
検索回数 かかった時間(sec)
10 1.38
100 9.72
1000 101.64

CPU負荷: 70%程度でした。2万程度の画像を対象に10回検索しても1秒程度で結果が返ってくるので、数分で検索は十分可能であると考えらます。また、検索時間は検索回数に比例しているように思わました。

まとめ

今回大量の画像の中から同一画像を見つけるためにimage-matchというライブラリの調査を実施しました。結果、最低限のコーディングでやりたきことが実現でき、かつローカルでもそれなりにパフォーマンスを発揮することがわかりました。今後は、まずは仮説の立証を行い画像の使い回しを確認し、その後システム化検討の際には、image-matchを検討の1候補にしようと思いました。

最後に

時間を決めて取り組みましたが、あまり内容のある記事を書けなかったので、短時間で人並みの記事が書けるように数を積み重ねることと、outputをイメージしてinputすることで、効率的に成長できると感じたため、inputばかりでなく、こういったブログ等でのoutputの機会を増やしていこうと思いました。