LoginSignup
2
5

More than 3 years have passed since last update.

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

Last updated at Posted at 2019-10-30

前説

OpenCVで画像をハッシュ値に変換する方法 でハッシュ値を元に画像検索しようとしていて、約10万画像をテストデータとして集めたいと思っています。画像を扱う機械学習のためのデータセットまとめ
などからImageNetをみてダウンロードをしようとしたのですが、ロードが遅くて使い方も分からずすぐにフリーズしてしまった。:fearful:
てことで、あるときにflickrにある画像を収集して何かしらの画像処理をしていたことを思い出したのでflickr APIを使うことに決めたとです。その当時はFlickrDownloadr使っていた気がします。

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

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

テンプレートコード

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ファイルに移し替える

secret.ini
[private]
key = XXXXXXXXXXXXXXXXXXXXX
secret = XXXXXXXXXXXXXXXXXX
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 とコマンド一つで削除が可能なので何かと便利です。

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

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

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

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

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

query.txt
浅草
上野
銀座
...
    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を参照)

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

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)を返すメソッドを作成した。

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メソッドに当てはめると以下の通りです。

#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) で判別しています。

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:

参考になりそうなリンク


2
5
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
5