前説
OpenCVで画像をハッシュ値に変換する方法 でハッシュ値を元に画像検索しようとしていて、約10万画像をテストデータとして集めたいと思っています。画像を扱う機械学習のためのデータセットまとめ
などからImageNetをみてダウンロードをしようとしたのですが、ロードが遅くて使い方も分からずすぐにフリーズしてしまった。
てことで、あるときにflickrにある画像を収集して何かしらの画像処理をしていたことを思い出したのでflickr APIを使うことに決めたとです。その当時はFlickrDownloadr使っていた気がします。
で、qiitaか調べていくうちにarticleで提示されているサンプルコードが同じことに何故かです。
みなさん考えることは一緒でDeepLearningやMachineLearningのサンプルデータとして使おうとしているっぽいです。
この記事では、かなり使い回されているコードを元にリファクタリングしてみようと思います
テンプレートコード
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の依存性が排除される
- なぜならLinux・MacとWindowsではパスを区切る形式が
- 結構多めのキーワード(クエリ)を使いたいので別ファイルにして読み込ませる
- FlickrにUPされているそれぞれの画像にはlicenceが付与されている
- copy right以外の画像を取得したい
リファクタリング
API KeyとSecret Key は configファイルに移し替える
[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 = 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
などのライブラリを使うと処理時間が短縮すると思います。
次回は並列処理を使ってみたいと思います
参考になりそうなリンク
- Djangoで機械学習アプリケーション ~データ収集・モデル構築と評価~
- 【Tensorflow・VGG16】転移学習による画像分類
- PythonでFlickr APIから画像取得
- [面倒なことはPythonで!]ネットから画像の自動収集
- 画像認識アプリ~改良版~CNN
- FlickrのAPIを使ってみよう
- FlickrAPIでの画像取得
- ディズニーのキャラクターを分類してみた
- ディープラーニングを使用して「あなたにそっくりな女優判別プログラム」を作ったおはなし
- Pythonで画像データを取得してみる
- Pythonで画像データをFlickerで取得する
- Flickr APIを使って画像ファイルをダウンロードする