たつぷりの調査報告書

博士後期課程(理学)の学生が趣味でUnityやBlenderで遊ぶブログです。素人が独学で勉強した際の忘備録です。

Google画像検索から画像を収集する方法

どうもこんにちは、たつぷりです。最近少し忙しかったので久しぶりの更新です。最近、大学院で開かれていた機械学習素粒子物理への応用に関してのスクールに参加しました。その関連で機械学習に興味を持ったのでしばらくその関連で何かできないかなあと思っている今日この頃です。

今日は自分で何か学習をさせる時にトレーニングデータを収集する一つの方法をまとめることにしました。

目的

PytorchでDCGANのtutrialをやってみたので、復習のために自分でも同様のことをやってみようと思った。 tutrialでは、セレブの画像からFakeセレブを生成していた。そこで自分でオリジナルの学習用の画像を用意する方法をまとめておく。

似た記事は腐るほどある気もするが、自分の文脈で作成したコードが自分が後にフォローできるレベルで解説付きで示した。

目標

自分が好きなものでやったほうが楽しいという理由で、今回はバイオハザード風のFake画像を作ることを最終的な目標にする。 そこで、具体的な目標は以下である。

実装

モジュールと各種設定

最初にモジュールのインポートと、保存先のパスの設定などを行う。

import os 
from urllib import request
import requests 
from pathlib import Path 
from bs4 import BeautifulSoup 
from IPython.display import HTML, display,clear_output

#インポート先の設定 
output_folder = Path('data/re2') 
output_folder.mkdir(exist_ok=True) 
output_filename = "re2" 

header={'User-Agent':"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/43.0.2357.134 Safari/537.36"} 

query変数にGoogle検索の検索ワードを代入する。

query = "resident+evil+re2+NEST"

Beautiful Soupを用いたスクレイピング

今回はBeautiful Soupを用いてスクレイピングを行う。 Beautiful Soupはhtmlのパースを行うためのライブラリの一つである。 公式ドキュメントは下。

www.crummy.com

以下で行うのは、まずURLで指定されるページをHTMLでurllib.request で取得し、それをBeaufulSoup() でパースするという流れである。 パースとは、HTMLのようなテキストをXMLのような構造を持つデータに変換することと考えてよい。それを実行するプログラムがパーサである。 BeautifulSoup() の引数は、解析するHTMLと用いるパーサである。

#最終的に以下のリストに画像のURLの一覧を保存する。
linklist = []

#検索ワードから構成したURLで指定されるページをHTMLで取得し、BeautifulSoupでパースする。
url = "https://www.google.com/search?q=" + query + "&source=lnms&tbm=isch" 
html = request.urlopen(request.Request(url,headers=header))
soup = BeautifulSoup(html, 'lxml') 

画像のURL一覧を取得

これで、soup オブジェクトからHTMLのタグやその属性にアクセスすることができる。 以下ではimg タグを取得する。指定のタグを取得するには、find_all() メソッドを用いれば良い。実際に以下のimg_list を出力すると、

<img alt="hoge1" class="hoge2" src="hige3">
<img alt="hoge4" class="hoge5" src="hige6">
<img alt="hoge7" class="hoge8" src="hige9">

のようなリストが得らていることが分かる。

次に、これらのimgタグから、画像のソースURLを取得する。画像のソースURLは、data-src アトリビュートに格納されているのでこれを取得すればよい。具体的には

の手順を行えば良い。

#soupオブジェクトからHTMLのタグやその属性にアクセスすることができる。

#imgタグのリストを取得する。
img_list = soup.find_all("img")  

#imgのリストから画像のソースURLを取得する。
for tag in img_list:   
    if tag.has_attr('data-src'):  
        linklist.append(tag.attrs['data-src'])  

出力

以上で、検索によって得られた画像のURLのリストを得ることができた。 ここで一旦、出力の準備をする。ここでは、入力した検索ワード、取得できた画像の数、ダウンロードの進捗を表すプログレスバーなどを用意した。

ただしプログレスバーに関しては、以下の関数をどこかで定義しておく必要がある。

def progress_bar(count, total, message=''): 
    return HTML(""" 
        <progress  
            value='{count}' 
            max='{total}', 
            style='width: 30%' 
        > 
            {count} 
        </progress> {frac}% {message} 
    """.format(count=count,total=total,frac=int(float(count)/float(total)*100.),message=" "+message))

次に、一つの検索ワードだけでは十分な画像が取得できない可能性もあるので、複数の検索ワードで画像を収集する場合を想定して、ファイルの名前の付け方を少し工夫しておく。尚、後述するがこの方法では画像の重複を避けることができない。

このプロジェクトでは基本的にはファイルの名前は、hoge-0.jpg,hoge-1.jpg...    のように番号をつけて保存していくことにする。そこで保存するディレクトリから既に存在しているファイルの数を取得して、その番号からファイル名につける番号を始めれば、既にあるファイルを上書きすることはなくなる。

ディレクトリからファイルの総数を取得する方法は以下を参考にした。

Python - ファイル・ディレクトリの一覧を取得する | murashun.jp

尚ここではそうしなかったが、この段階でURLに対応した名前を付けておけば同じURLからくる画像の重複は防ぐことができそうである。

#アウトプット部分

#検索ワードやデータ数、プログレスバーを表示する。
print("Query is "+ query)  
print("Number of image:" + str(len(linklist)))  
progress = display(progress_bar(0,len(linklist)),display_id=True)  

#画像を保存するディレクトリのファイルの数を取得
files = os.listdir(output_folder)  
file = [f for f in files if os.path.isfile(os.path.join(output_folder, f))]  
index = len(file) 

データの保存

linklistに画像のURLがリストとして格納されているので、各URLに対して画像のダウンロードを行う。 上述のルールでファイル名を付けていき、requests でデータを取得しopen(save_path, 'wb').write(image.content)  でデータを保存していく。

for img in linklist: 
    filename = output_filename + "-" + str(index)+".jpg"   
    save_path = output_folder.joinpath(filename)   

    message= filename + ' is importing.'   
    progress.update(progress_bar(index-len(file)+1 ,len(linklist),message=message))   


    #画像ファイルのURLからデータをダウンロード  
    try:  
        #URLからデータを取得
        image = requests.get(img)

        #データを保存
        open(save_path, 'wb').write(image.content)   

    except ValueError:
        print("Error")    

    index += 1

プログラムの実行

query に、検索したいキーワードを+ で結合して代入する。 その後、実行すると該当する画像のダウンロードが始まる。

出力のプログレスバーが100%になったらダウンロード完了である。

f:id:Tatsupuri:20201123105833p:plain

各検索キーワードに応じて、ヒットする画像の数が少なかったりすることがある。この場合は検索キーワードを色々変えて画像を取得していく。 上で既に述べたように、このプログラムではディレクトリの中のファイル数をカウントして、ナンバリングをそこから始めるようにしているので、関連する検索ワードを変えていけばどんどん画像を取得することができる。

例えば、自分の場合は

resident+evil+re2+zombie
resident+evil+re3+zombie
biohazard+re2

など、で検索をかけるとすぐに500枚くらいたまった。

結果

保存先のデータを見てみると、以下のようにそれっぽい画像がインポートされている。

f:id:Tatsupuri:20201123105836p:plain

ここですぐに問題に気が付く。これは当然予想できる問題であるが、重複する画像が存在していることである。

今回の範囲では大した量ではないので、重複している画像は手で削除した。

課題

上で述べた通り、このままでは重複データがかなり存在ことがある。これは複数のキーワードで検索しているのでの当然のことである。上では手で消すということをしたが、その都度手で削除するのは大変時間の無駄である。

上で既に述べたようにURLと一対一対応になるようにファイル名を付けておけば同じURLからくる重複は防ぐことができる。しかし拾い画を張り付けたりしているケース(?)もあって違うURLでも同じ画像があったりすることもある。

そこでせっかくなので練習として、画像の重複を判定するモデルを作成したいと思う。近いうちにその記事を書く予定である。

またもう一つの問題は今の段階では画像の拡張子をあまり意識していない点である。とりあえずJPGで保存しているが、必ずしもそうでないはずである。拡張子を取得してそれに応じてファイル名を変える工夫する必要がある。

参考にしたもの

  1. Pythonのスクレイピングで、いらすとやの画像を一気にダウンロード | ハシカケ-実現したいことから学べるプログラミングサイト
  2. Googleの画像検索APIを使って画像を大量に収集する - Qiita
  3. BeautifulSoup のエラー "Couldn't find a tree builder" の原因と対処法 - Qiita

スクリプト全文

今回作成したスクリプトの全文を以下に書いておく。これらは各ブロックをJupyterNotebookに貼り付けて動かすことを想定して作成した。

import os 
from urllib import request
#import time 
#import re 
#import requests 
from pathlib import Path 
from bs4 import BeautifulSoup 
from IPython.display import HTML, display,clear_output
#インポート先の設定 
output_folder = Path('data/re2') 
output_folder.mkdir(exist_ok=True) 
output_filename = "re2" 
header={'User-Agent':"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/43.0.2357.134 Safari/537.36"}
def progress_bar(count, total, message=''):
    return HTML("""
        <progress 
            value='{count}'
            max='{total}',
            style='width: 30%'
        >
            {count}
        </progress> {frac}% {message}
    """.format(count=count,total=total,frac=int(float(count)/float(total)*100.),message=" "+message))
query = "resident+evil+re2+NEST"
#最終的に以下のリストに画像のURLの一覧を保存する。
linklist = []
#検索ワードから構成したURLで指定されるページをHTMLで取得し、BeautifulSoupでパースする。
url = "https://www.google.com/search?q=" + query + "&source=lnms&tbm=isch" 
html = request.urlopen(request.Request(url,headers=header))
soup = BeautifulSoup(html, 'lxml')

#soupオブジェクトからHTMLのタグやその属性にアクセスすることができる。
#imgタグのリストを取得する。
img_list = soup.find_all("img")  
#imgのリストから画像のソースURLを取得する。
for tag in img_list:   
    if tag.has_attr('data-src'):  
        linklist.append(tag.attrs['data-src'])

#アウトプット部分
#検索ワードやデータ数、プログレスバーを表示する。
print("Query is "+ query)  
print("Number of image:" + str(len(linklist)))  
progress = display(progress_bar(0,len(linklist)),display_id=True)  
#画像を保存するディレクトリのファイルの数を取得
files = os.listdir(output_folder)  
file = [f for f in files if os.path.isfile(os.path.join(output_folder, f))]  
index = len(file)

for img in linklist:
    filename = output_filename + "-" + str(index)+".jpg"  
    save_path = output_folder.joinpath(filename)  

    message= filename + ' is importing.'  
    progress.update(progress_bar(index-len(file)+1 ,len(linklist),message=message))  


    #画像ファイルのURLからデータをダウンロード 
    try: 
        #URLからデータを取得
        image = requests.get(img)

        #データを保存
        open(save_path, 'wb').write(image.content)  

    except ValueError:
        print("Error")   

    index += 1