adish intelligence

アディッシュ株式会社のエンジニアブログです。

PythonでBing Search APIを使って画像データを集める方法

こんにちは、エンジニアの白木(shirakiya)です。
近年、人工知能がすごく話題になっていますが、adishでも画像認識タスクなどで、機械学習を利用しています。

画像認識では、DeepLearningを使うのがデファクトスタンダードな状況になっていて、その中でどういったモデルを使うかの議論が行われていますが、どのモデルを使うにせよ必要なのは 教師データ です。
画像認識タスクでの教師データの中身は、認識対象としたい「画像」とその画像の「分類情報」が対になったもので、それが大量にあればあるほど認識対象においての汎化性能*1が高くなります。(例えば、スイカを認識させたい場合の教師データは多数の「スイカの画像」と「スイカの分類ID(整数値)」の対です。※分類IDは個人個人が設定するもの)

f:id:shirakiya:20160707112425p:plain

つまり、画像認識タスクが行える分類器を作るためには大量の画像が必要になってきます。
(弊社もDeepLearning技術の検証目的で画像が必要でした。)

画像を集める方法は色々あると思います。ざっと思いつく方法を列挙していくと、

  • 人力で
    • ググッて、色んなサイトから画像を「名前を付けて保存」で保存する
    • クラウドソーシングを使って画像を集めてきてもらう
  • プログラムで
    • 画像検索系APIを使って、一気に画像を検索・保存する
    • 特定のサイトをスクレイピングして、画像を集める
  • サービスで
    • (規約等で画像の利用に同意してもらっているという条件下で)Webサービスを通して画像を集める

といったところでしょうか。
やはり大量の画像を集めないといけないので、そこはプログラムに任せるのが得策だと思います。
スクレイピングは例えばこちらの記事のように、欲しい画像が多くある・ありそうな特定のブログ等から画像を収集する方法のことです。スクレイピングはサイトへのアクセスをプログラムで実行させるため、DDoS攻撃のように対象サイトのサーバーに負荷をかけてしまう可能性があり、その対策として間隔を空けてアクセスするようにプログラムしなければなりません。
このため、大量に画像を集めたいという今回のケースでは、取得に要する時間の観点から、より目的にマッチしている「画像検索系APIを使って、一気に画像を検索・保存する」方法を、Pythonを使って入門的にご紹介します。

画像検索API

画像検索系APIは

等他にもいくつかありますが、 欲しい画像予算 に合わせてAPIを選定するのが良いと思います。
例えば、Flickrは「風景や絵になる写真」が多いですが、一方でGoogleだとアニメ画像等も含めて結果が出てくる印象です。また、日本への対応とかも観点であったりして、例えばFlickrとGoogleで「太宰府」で画像検索してみると、イメージしている画像とその違いがわかるかなと思います。(左がFlickrで、右がGoogleです)

f:id:shirakiya:20160706172234p:plain

今回はMicrosoftの画像検索サービス「Bing」の検索API「Bing Search API」を使ってみることとします。

Bing Search API

Bing Search APIはBingの検索とその結果の取得をプログラム上で行うためのAPIです。
https://datamarket.azure.com/dataset/5BA839F1-12CE-4CCE-BF57-A49D98D29A44

アカウント作成

まずAPIを利用できるようにするために、Microsoft Azure Marketplaceにアカウントを作成するところから始めます。

1. Microsoftアカウントを作る

少しややこしいのですが、本来作りたいのはMicrosoft Azure Marketplaceのアカウントですが、そのためにはMicrosoftアカウントを作成しないといけません。
以下のページから、Microsoftアカウントを作成します。
https://signup.live.com/signup

2. Microsoft Azure Marketplaceに専用のアカウント情報を入力

1でアカウントを作成してログインしている状態で、Microsoft Azure Marketplaceにアクセスし、Marketplace専用のアカウント情報を入力します。

アカウント情報のページで「プライマリアカウントキー」の文字列がゲットできればOKです。

Bing Search API を有効にする

Bing Search API へアクセスします。
「アクティブなサブスクリプション」から任意のプランを選んでください。今回は無料の5000トランザクション/月のプランを選ぶこととします。
※ ちなみに1トランザクションにつき50枚の画像が取得可能なので、5000トランザクションだと月に25万枚取得することができる計算です。

f:id:shirakiya:20160706172421p:plain

同意画面が出てチェックしてOKすると、購入完了画面が出ます。

f:id:shirakiya:20160706172614p:plain

購入すると(実際は無料ですが)、マイアカウントのマイデータのページにBing Search APIが登録されています。

f:id:shirakiya:20160706172604p:plain

これでBing Search APIを利用する準備が整いました。

検索条件の品定め

https://datamarket.azure.com/dataset/explore/bing/search
から、簡単に検索に必要な条件を入力してAPIのレスポンスを表示することができます。
また、その条件をクエリパラメータとしたリクエストURLを自動的に作成してくれるため、これを見れば作成すべきクエリの形がわかり、最初は重宝します。
ここで、適切な検索条件を見つけてからスクリプト書くと、コードが書きやすいかなと思われます。

※1. 1回の検索(画面内では"適用"ボタン)でクエリを1個消費するので注意。
※2. https://datamarket.azure.com/dataset/bing/search#schema に簡単なAPIの仕様が掲載されています。
※3. より詳細なドキュメントはこちらのリンクからwordファイルで確認できます。

スクリプトを書く

仕様として、

  • Python3系
  • コマンドラインからスクリプトを実行すれば imagesディレクトリに画像を格納する
  • 引数で「検索ワード」と「取得画像枚数」を指定することができる
  • 収集するのはjpegかpngの画像に限る

というスクリプトを作ることを考えます。
以下、サンプルコードです。

# -*- coding: utf-8 -*-
# Usage:
#    $ python collection/scripts/collect.py <検索文字列> <取得画像数>

import argparse
import urllib
import requests
import json
import io
import imghdr
import uuid
import os

# Bing Search APIの仕様で決まっている1リクエストあたりの画像取得最大枚数
ONE_SEARCH_LIMIT = 50
# Bing Search API のURL
ROOT_URL = 'https://api.datamarket.azure.com/Bing/Search/v1/Image?'
# APIキー
API_KEY = '<プライマリアカウントキー>'
# 画像のダウンロードのタイムアウト(秒)
TIMEOUT = 5
# ダウンロードした画像を格納するディレクトリ名
SAVE_DIR = 'images'

def main():
    parser = argparse.ArgumentParser(description='Collect image via Bing Search API')
    parser.add_argument('search_word', type=str, help='Search word')
    parser.add_argument('count', type=int, help='Number of collected images counts')
    options = parser.parse_args()

    if options.count % ONE_SEARCH_LIMIT != 0:
        raise Exception('number must be divisible by {0}!!'.format(ONE_SEARCH_LIMIT))

    for i in range(options.count // ONE_SEARCH_LIMIT):
        offset = i * ONE_SEARCH_LIMIT
        print(offset)
        params= {
            'Query': "'{}'".format(options.search_word),
            'Market': "'{}'".format('ja-JP'),
            '$format': 'json',
            '$top': '{0:d}'.format(ONE_SEARCH_LIMIT),
            '$skip': '{0:d}'.format(offset),
        }
        url = ROOT_URL + urllib.parse.urlencode(params)

        response_json = requests.get(url, auth=('', API_KEY))
        response = json.loads(response_json.text)

        for result in response['d']['results']:
            image_url = result['MediaUrl']
            try:
                response_image = requests.get(image_url, timeout=TIMEOUT)
                image_binary = response_image.content
            except:
                continue

            with io.BytesIO(image_binary) as fh:
                image_type = imghdr.what(fh)

            if image_type == 'jpeg':
                extension = '.jpg'
            elif image_type == 'png':
                extension = '.png'
            else:
                continue

            filename = str(uuid.uuid4()) + extension
            
            if not os.path.isdir(SAVE_DIR):
                os.mkdir(SAVE_DIR)
            with open(os.path.join(SAVE_DIR, filename), 'wb') as f:
                f.write(image_binary)


if __name__ == '__main__':
    main()

解説

import argparse

parser = argparse.ArgumentParser(description='Collect image via Bing Search API')
parser.add_argument('search_word', type=str, help='Search word')
parser.add_argument('count', type=int, help='Number of collected images counts')
options = parser.parse_args()

if options.count % ONE_SEARCH_LIMIT != 0:
    raise Exception('number must be divisible by {0}!!'.format(ONE_SEARCH_LIMIT))

ここでコマンドラインの引数を定義し、受け取っています。
Pythonでは sys.argv で引数をリストで受け取ることも可能ですが、argparseライブラリを使うとより高機能にコマンドライン引数の定義を行うことができるので、重宝します。
今回の場合では、Bing Search APIは1リクエストに付き50件まで画像を取得することができるので、取得枚数は50で割り切れる数字のみ受け付け、それ以外は例外を投げるようにしています。

import urllib
import requests
import json

# Bing Search API のURL
ROOT_URL = 'https://api.datamarket.azure.com/Bing/Search/v1/Image?'
# APIキー
API_KEY = '<プライマリアカウントキー>'

for i in range(options.count // ONE_SEARCH_LIMIT):
    offset = i * ONE_SEARCH_LIMIT
    params= {
        'Query': "'{}'".format(options.search_word),
        'Market': "'{}'".format('ja-JP'),
        '$format': 'json',
        '$top': '{0:d}'.format(ONE_SEARCH_LIMIT),
        '$skip': '{0:d}'.format(offset),
    }
    url = ROOT_URL + urllib.parse.urlencode(params)
    response_json = requests.get(url, auth=('', API_KEY))
    response = json.loads(response_json.text)

ここで指定された取得枚数に応じて、APIを数度に渡ってリクエストするようにしています。(仮に100件欲しい場合は2回ループする)
使っているパラメータの意味は以下の通りです。

  • Query: 検索ワード
  • Market: 検索対象とする国、地域の指定
  • $format: レスポンスフォーマット、デフォルトはAtom。他にjsonとxmlが選べる
  • $top: 取得画像数
  • $skip: オフセット。SQLで言うところのoffsetで、100〜149件目が得たい場合は$skip=100とする

URLエンコードをしてからGETでAPIを叩きます。引数authとAPIキーを入れるのを忘れないように。
レスポンスをjson.loadsでデコードして辞書型にしています。

# 画像のダウンロードのタイムアウト(秒)
TIMEOUT = 5

for result in response['d']['results']:
    image_url = result['MediaUrl']
   try:
       response_image = requests.get(image_url, timeout=TIMEOUT)
       image_binary = response_image.content
    except:
        continue

Bing Search APIのAPIレスポンスの中には、画像のバイナリ情報は入っていません。
なので、MediaUrlに画像のURLが文字列で入っているので、そのURLを使って再度画像をダウンロードしています。
(ここでタイムアウトは設定しておいた方が良いです。たまに無効なサイトであったりして、そのサイトのせいでプログラムの実行時間が長くなってしまいます。大量の画像を得るので、1枚2枚の画像は捨てて、プログラム全体の実行時間を短くしたほうが効率的です。)

import io
import imghdr
import uuid

with io.BytesIO(image_binary) as fh:
    image_type = imghdr.what(fh)

    if image_type == 'jpeg':
        extension = '.jpg'
    elif image_type == 'png':
        extension = '.png'
    else:
        continue

    filename = str(uuid.uuid4()) + extension

ここではダウンロードした画像のフォーマットを標準ライブラリの imghdr ライブラリを使って特定し、jpeg|png 以外の画像を捨てています。
レスポンス内のContentTypeで見る方法もあるかもですが、それだと、ContentTypeと実際の画像のフォーマットが異なっていたりしていて、整合性が取れない時があります。
そして、ファイル名にはUUIDを付けることで、ファイル名の重複による上書きを防止してます。

import os

# ダウンロードした画像を格納するディレクトリ名
SAVE_DIR = 'images'

if not os.path.isdir(SAVE_DIR):
    os.mkdir(SAVE_DIR)
with open(os.path.join(SAVE_DIR, filename), 'wb') as f:
    f.write(image_binary)

最後にimagesディレクトリに保存しています。

あとは、コマンドライン上でスクリプトを実行すれば、画像をどかっと保存することができると思います。

Python初挑戦な人やBing Search API初挑戦な人向けに少し詳しめに説明してみました。
これで画像を収集するのはプログラムに任せてしまえますね!

補足

実はMicrosoft Azure MarketplaceにあるBing Search API2016年12月15日を持って終了するとのアナウンスがあります。(書いてて気付きました…)
今はMicrosoft Azure Cognitive ServiceBing API v5というAPIが既にリリースされており、いずれこちらのAPIに切り替える必要があります。
その切替の方法は追ってこの場で報告しようと思います。

それでは!

*1: 汎化性能とは「未知データに対する分類精度」です。ざっくり言うと、まだ学習させたことがない画像を入力して出力された分類が意図通りなのかどうかの精度のことを言います。