「PythonでFlickr APIから画像取得」に掲載されているテンプレートコードをリファクタリングしてみた（前編）

# 前説
[OpenCVで画像をハッシュ値に変換する方法](https://qiita.com/satsukiya/items/de260a7d69f0af6d4305) でハッシュ値を元に画像検索しようとしていて、約10万画像をテストデータとして集めたいと思っています。[画像を扱う機械学習のためのデータセットまとめ](https://qiita.com/leetmikeal/items/7c0d23e39bf38ab8be23)
などから[ImageNet](http://www.image-net.org/)をみてダウンロードをしようとしたのですが、ロードが遅くて使い方も分からずすぐにフリーズしてしまった。:fearful:
てことで、あるときにflickrにある画像を収集して何かしらの画像処理をしていたことを思い出したのでflickr APIを使うことに決めたとです。その当時は[FlickrDownloadr](https://archive.codeplex.com/?p=flickrdownloadr)使っていた気がします。

で、qiitaか調べていくうちにarticleで提示されているサンプルコードが同じことに何故か:cactus:です。
みなさん考えることは一緒でDeepLearningやMachineLearningのサンプルデータとして使おうとしているっぽいです。

この記事では、かなり使い回されているコードを元にリファクタリングしてみようと思います:thumbsup:

# テンプレートコード

```python
from flickrapi import FlickrAPI
from urllib.request import urlretrieve
import os, time, sys

# 「事前準備」で取得したAPI KeyとSecret Keyを設定
key = "XXXXXXXXXX"
secret = "XXXXXXXXXX"

# 1秒間隔でデータを取得(サーバー側が逼迫するため)
wait_time = 1

# 検索キーワード(実行時にファイル名の後に指定)
keyword = sys.argv[1]
# 保存フォルダ
savedir = "./" + keyword

# 接続クライアントの作成とサーチの実行
flickr = FlickrAPI(key, secret, format='parsed-json')
result = flickr.photos.search(
    text = keyword,           # 検索キーワード
    per_page = 100,           # 取得データ数
    media = 'photos',         # 写真を集める
    sort = 'relevance',       # 最新のものから取得
    safe_search = 1,          # 暴力的な画像を避ける
    extras = 'url_q, license' # 余分に取得する情報(ダウンロード用のURL、ライセンス)
)

# 結果の取り出しと格納
photos = result['photos']

for i, photo in enumerate(photos['photo']):
    url_q = photo['url_q']
    filepath = savedir + '/' + photo['id'] + '.jpg'
    if os.path.exists(filepath): continue
    urlretrieve(url_q, filepath)
    time.sleep(wait_time)
```

### 修正したい部分
- API KeyとSecret Key は configファイルに移し替える
- パスの連結は `os.path.join` を使いたい
    - なぜならLinux・MacとWindowsではパスを区切る形式が`\`と`¥`で異なるため。OSの依存性が排除される
- 結構多めのキーワード(クエリ)を使いたいので別ファイルにして読み込ませる
- FlickrにUPされているそれぞれの画像にはlicenceが付与されている
    - copy right以外の画像を取得したい

# リファクタリング

### API KeyとSecret Key は configファイルに移し替える

```config:secret.ini
[private]
key = XXXXXXXXXXXXXXXXXXXXX
secret = XXXXXXXXXXXXXXXXXX
```

```python
import configparser

if __name__ == "__main__":
	config = configparser.ConfigParser()
	config.read('secret.ini')
	confstr = "key:{0}, secret:{1}".format(config["private"]["key"], config["private"]["secret"])
	print(confstr)
```

### パスの連結は `os.path.join` を使いたい

画像を保存するフォルダは `./images/` 配下にします。なぜならkeywordを多く使いたいのと収集と削除を繰り返しするためカレントフォルダからkeywordフォルダが生成されると削除しにくいためです。 `./images`配下にフォルダが生成されることで `rm -rf ./images` とコマンド一つで削除が可能なので何かと便利です。

```Python
#画像フォルダパス
imgdir = os.path.join(os.getcwd(), "images")

#keywordフォルダ
savedir = os.path.join(imgdir, keyword)

#ファイルパス
filepath = os.path.join(savedir, src['id'] + '.jpg')
```

### 結構多めのキーワード(クエリ)を使いたいので別ファイルにして読み込ませる

テンプレートコードではコマンドラインからキーワードをひとつ入力してflickrを検索していましたが、ここでは複数のキーワードを使う予定があるので別ファイルでリスト化します。

```text:query.txt
浅草
上野
銀座
...
```

```python
    query = None
    with open("query.txt") as fin:
        query = fin.readlines()
    #\nを除去
    query = [ q.strip() for q in query]
```

### FlickrにUPされているそれぞれの画像にはlicenceが付与されている  -> copy right以外の画像を取得したい

responseで取得したそれぞれの画像データにはライセンスパラメータがあって、下記の通りに番号で管理されていて、0が著作権ありで1~3は非商用可、4~6は商用可でそれぞれ振られている。
([flickr.photos.licenses.getInfo](https://www.flickr.com/services/api/flickr.photos.licenses.getInfo.html)を参照)

```xml
  <license id="0" name="All Rights Reserved" url="" />
  <license id="1" name="Attribution-NonCommercial-ShareAlike License" url="https://creativecommons.org/licenses/by-nc-sa/2.0/" />
  <license id="2" name="Attribution-NonCommercial License" url="https://creativecommons.org/licenses/by-nc/2.0/" />
  <license id="3" name="Attribution-NonCommercial-NoDerivs License" url="https://creativecommons.org/licenses/by-nc-nd/2.0/" />
  <license id="4" name="Attribution License" url="https://creativecommons.org/licenses/by/2.0/" />
  <license id="5" name="Attribution-ShareAlike License" url="https://creativecommons.org/licenses/by-sa/2.0/" />
  <license id="6" name="Attribution-NoDerivs License" url="https://creativecommons.org/licenses/by-nd/2.0/" />
  <license id="7" name="No known copyright restrictions" url="https://www.flickr.com/commons/usage/" />
  <license id="8" name="United States Government Work" url="http://www.usa.gov/copyright.shtml" />
  <license id="9" name="Public Domain Dedication (CC0)" url="https://creativecommons.org/publicdomain/zero/1.0/" />
  <license id="10" name="Public Domain Mark" url="https://creativecommons.org/publicdomain/mark/1.0/" />
```

それで、FlickrAPIをリクエストするメソッドを作成し、データに付与されているライセンスパラメータと引数にマッチしたもののみを取得するロジックを書いてみた。

```Python
def request_flickr(keyword, count=100, license=None):
    # 接続クライアントの作成とサーチの実行
    config = configparser.ConfigParser()
    config.read('secret.ini')

    flickr = FlickrAPI(config["private"]["key"], config["private"]["secret"], format='parsed-json')
    result = flickr.photos.search(
        text = keyword,           # 検索キーワード
        per_page = count,           # 取得データ数
        media = 'photos',         # 写真を集める
        sort = 'relevance',       # 最新のものから取得
        safe_search = 1,          # 暴力的な画像を避ける
        extras = 'url_l, license' # 余分に取得する情報(ダウンロード用のURL、ライセンス)
    )

    condition = lambda p : 0 < int(p["license"])

    if license == "All_Rights_Reserved": #コピーライト
        condition = lambda p : 0 == int(p["license"])
    elif license == "NonCommercial": #非商用化
        condition = lambda p : 1 <= int(p["license"]) and int(p["license"]) <= 3
    elif license == "Commercial": #商用化
        condition = lambda p : 4 <= int(p["license"]) and int(p["license"]) <= 6
    elif license == "UnKnown": #商用化
        condition = lambda p : int(p["license"]) == 7
    elif license == "US_Government_Work": #商用化
        condition = lambda p : int(p["license"]) == 8
    elif license == "PublicDomain": #商用化
        condition = lambda p : 9<= int(p["license"]) and int(p["license"]) <= 10

    return list(filter(condition, result["photos"]["photo"]))

```

ただこれだと単一の範囲のみしか条件に追加することができない。openCVでパラメータを付与するときに `ret2,th2 = cv2.threshold(img,0,255,cv2.THRESH_BINARY+cv2.THRESH_OTSU)`のように複数追加したい場合がある。この場合では非商用1~3と商用4~6の両方を取得したい場合である。そのため、ここでは必要な条件文を配列に渡してまるごと`sum`を使って判定させている。どれか一つの条件に当てはまれば1(True)、そうでなければ0(False)を返すメソッドを作成した。

```Python
def condition(src, license=None):

	dst = []
	if license is None:
		dst.append(lambda x : 0 <= x)
	else :
		license_types = license.split("|")
		for t in license_types:
			if t == "All_Rights_Reserved": #コピーライト
				dst.append(lambda x : x == 0)
			elif t == "NonCommercial": #非商用化
				dst.append(lambda x : 1 <= x and x <= 3)
			elif t == "Commercial": #商用化
				dst.append(lambda x : 4 <= x and x <= 6)
			elif t == "UnKnown": #商用化
				dst.append(lambda x : x == 7)
			elif t == "US_Government_Work": #商用化
				dst.append(lambda x : x == 8)
			elif t == "PublicDomain": #商用化
				dst.append(lambda x : 9<= x and x <= 10)

	return 0 < sum([item(src) for item in dst]) 


if __name__ == "__main__":
	
	for i in range(11):
		val = condition(i, license="NonCommercial|PublicDomain")
		print("{}:{}".format(i,val))

```

```
0:False
1:True
2:True
3:True
4:False
5:False
6:False
7:False
8:False
9:True
10:True
```

それで、`request_flickr`メソッドに当てはめると以下の通りです。

```Python
#Flickr APIを使う
#Flickr APIを使う
def request_flickr(keyword, count=100, license=None):
    # 接続クライアントの作成とサーチの実行
    config = configparser.ConfigParser()
    config.read('secret.ini')

    flickr = FlickrAPI(config["private"]["key"], config["private"]["secret"], format='parsed-json')
    result = flickr.photos.search(
        text = keyword,           # 検索キーワード
        per_page = count,           # 取得データ数
        media = 'photos',         # 写真を集める
        sort = 'relevance',       # 最新のものから取得
        safe_search = 1,          # 暴力的な画像を避ける
        extras = 'url_l, license' # 余分に取得する情報(ダウンロード用のURL、ライセンス)
    )

    return list(filter(lambda x : multiConditionLicenses(int(x["license"]), license), result["photos"]["photo"]))


def multiConditionLicenses(src, license=None):

    dst = []
    if license is None:
        dst.append(lambda x : 0 <= x)
    else :
        license_types = license.split("|")
        for t in license_types:
            if t == "All_Rights_Reserved": #コピーライト
                dst.append(lambda x : x == 0)
            elif t == "NonCommercial": #非商用化
                dst.append(lambda x : 1 <= x and x <= 3)
            elif t == "Commercial": #商用化
                dst.append(lambda x : 4 <= x and x <= 6)
            elif t == "UnKnown": #商用化
                dst.append(lambda x : x == 7)
            elif t == "US_Government_Work": #商用化
                dst.append(lambda x : x == 8)
            elif t == "PublicDomain": #商用化
                dst.append(lambda x : 9<= x and x <= 10)

    return 0 < sum([item(src) for item in dst])

if __name__ == "__main__":
    request_flickr("ikebukuro", count=100, license="NonCommercial|Commercial")
```

# 修正したコード(全体)

説明が足りない部分を付け加えつつ、コード全体を表示します。

`count=500`はダウンロードした画像の枚数ではなく、flickrAPIでresponseで取得した画像データの数です。ここからコピーライトがついている画像などを省いているのでこの数字よりも少ないです。
テンプレートコードではURLから画像を取得するときに`urlretrieve`メソッドを使っているのですが、なぜか失敗するので新規でメソッドを作成しています。またAPIから取得したレスポンスの画像データの中に`url_l`キーが無い場合があるので `filter(lambda p : "url_l" in p.keys(),  photos)` で判別しています。

```Python
from flickrapi import FlickrAPI
import requests
import os, time, sys
import configparser
import time

#画像フォルダパス
imgdir = os.path.join(os.getcwd(), "images")

#Flickr APIを使う
def request_flickr(keyword, count=100, license=None):
    # 接続クライアントの作成とサーチの実行
    config = configparser.ConfigParser()
    config.read('secret.ini')

    flickr = FlickrAPI(config["private"]["key"], config["private"]["secret"], format='parsed-json')
    result = flickr.photos.search(
        text = keyword,           # 検索キーワード
        per_page = count,           # 取得データ数
        media = 'photos',         # 写真を集める
        sort = 'relevance',       # 最新のものから取得
        safe_search = 1,          # 暴力的な画像を避ける
        extras = 'url_l, license' # 余分に取得する情報(ダウンロード用のURL、ライセンス)
    )

    return list(filter(lambda x : multiConditionLicenses(int(x["license"]), license), result["photos"]["photo"]))


def multiConditionLicenses(src, license=None):

    dst = []
    if license is None:
        dst.append(lambda x : 0 <= x)
    else :
        license_types = license.split("|")
        for t in license_types:
            if t == "All_Rights_Reserved": #コピーライト
                dst.append(lambda x : x == 0)
            elif t == "NonCommercial": #非商用化
                dst.append(lambda x : 1 <= x and x <= 3)
            elif t == "Commercial": #商用化
                dst.append(lambda x : 4 <= x and x <= 6)
            elif t == "UnKnown": #商用化
                dst.append(lambda x : x == 7)
            elif t == "US_Government_Work": #商用化
                dst.append(lambda x : x == 8)
            elif t == "PublicDomain": #商用化
                dst.append(lambda x : 9<= x and x <= 10)

    return 0 < sum([item(src) for item in dst])


# 画像リンクからダウンロード
def download_img(url, file_name):
    r = requests.get(url, stream=True)
    if r.status_code == 200:
        with open(file_name, 'wb') as f:
            f.write(r.content)

if __name__ == "__main__":

    # 処理時間計測開始
    start = time.time()

    #クエリを取得
    query = None
    with open("query.txt") as fin:
        query = fin.readlines()
    query = [ q.strip() for q in query]

    # 保存フォルダ
    for keyword in query:
        savedir = os.path.join(imgdir, keyword)
        #なければフォルダ作成
        if not os.path.isdir(savedir):
            os.mkdir(savedir)

        photos = request_flickr(keyword, count=500, license="NonCommercial|Commercial")

        for photo in filter(lambda p : "url_l" in p.keys(),  photos):
            url = photo['url_l']
            filepath = os.path.join(os.path.join(imgdir, keyword), photo['id'] + '.jpg')
            download_img(url, filepath)
            time.sleep(1)

    print('処理時間', (time.time() - start), "秒")
```

### 処理時間
` 468.9457371234894 秒`
かかりました。
一つのフォルダに対して約1~2分はかかる計算です。
取得したい画像データを増やす、またはキーワードを増やそうとするとさらに時間がかかります。

# おわりに
まとまりのないかもしれないですが、ひとつのサンプルコードからもくもくとリファクタリングをしてみました。
今のままだと逐次的に単一で処理をしているので `Future` `asyncio` などのライブラリを使うと処理時間が短縮すると思います。

次回は並列処理を使ってみたいと思います:smirk_cat:


# 参考になりそうなリンク
- [Djangoで機械学習アプリケーション ~データ収集・モデル構築と評価~](https://qiita.com/T_gdmuy/items/1187130a8153f515af18)
- [【Tensorflow・VGG16】転移学習による画像分類](https://qiita.com/hiraku00/items/66a3606af3b2eed57778)
- [PythonでFlickr APIから画像取得](https://qiita.com/hiraku00/items/dbdaad45ea54a35e51a4)
- [[面倒なことはPythonで！]ネットから画像の自動収集](https://qiita.com/cr-fun/items/7841a114c4fe67a51dbd)
- [画像認識アプリ～改良版～CNN](https://qiita.com/T_gdmuy/items/71656dbb996b300f40ce)
- [FlickrのAPIを使ってみよう](https://qiita.com/soup01/items/738bef70e5fba378a18f)
- [FlickrAPIでの画像取得](https://qiita.com/keimoriyama/items/6ebe7e71ec0e0d8ac8bc)
- [ディズニーのキャラクターを分類してみた](https://qiita.com/kazama0119/items/ede4732d21fe00085eb6)
- [ディープラーニングを使用して「あなたにそっくりな女優判別プログラム」を作ったおはなし](https://qiita.com/k_eita/items/a50a4cae0aa2598422e4)
- [Pythonで画像データを取得してみる](https://qiita.com/__init__/items/d18dde8c8d186fdc0e3a)
- [Pythonで画像データをFlickerで取得する](http://nprogram.hatenablog.com/entry/2019/04/09/053946)
- [Flickr APIを使って画像ファイルをダウンロードする](http://ykubot.com/2017/11/05/flickr-api/)

-----
- [画像を扱う機械学習のためのデータセットまとめ](https://qiita.com/leetmikeal/items/7c0d23e39bf38ab8be23)
- [ImageNet](http://www.image-net.org/)
- [FlickrDownloadr](https://archive.codeplex.com/?p=flickrdownloadr)