マロングラッセ Mk.II

赤西真論のブログらしきもの

TwitterのTLから二次元画像だけ収集しよう! (3)lbpcascade_animefaceを使ってみる

実家に戻るまでに本編を書き上げたいので、1日1記事のペースで書いていきます。

前回まででTLの画像収集が出来るようになりました。ここから二次元画像だけを抽出していきます。

では、前回同様スクリプトの配布から行います。今回は2つあります。

GitHub Gist - TL_capture_lbp.py

lbpcascade_animeface.xml(直リン、右クリックで保存を推奨)

上のGistの中には2つファイルがあるので、どちらもダウンロードしてください。

このときに注意してほしいのが、lbpcascade.pyに関しては名前を変更しないでください。

TL_capture_lbp.pyのほうは自由に変更してもらって構いません。なんなら、前回のTL_capture.pyに上書きしてもらっても構いません。

というのも、今回はlbpcascade.pyをTL_capture_lbp.pyのほうから呼び出しているのでファイル名を変更すると参照出来なくなってしまいます。

lbpcascade_animeface.xmlは顔検出に使用するファイルです。こちらも名前を変更しないでください。

ダウンロード出来たら、とりあえず動かしてみましょう。

ダウンロードした3つのファイルを環境が存在するフォルダーに入れて、start.batを実行して、python TL_capture_lbp.pyと叩きます。

ここの流れはずっと同じです。

なかなかTLに画像が流れてこなくて、10分ぐらい放置してましたw

ログが前回と同様に流れていますが、表示がちょっと違いますね?Skipという文字が存在します。

顔検出をキチンと行って、判定をしている証拠です。

 

では、動作を確認出来たところで最初に本体であるTL_capture_lbp.pyを見てみます。

前回とほとんど同じなので、WinMergeによる差異部分をスクショで撮ってみました。

左が前回のTL_capture.py、右が今回のTL_capture_lbp.pyです。

まず、import文が1行増えています(9行目)。これが先ほど言ったlbpcascade.pyを使用するために呼び出している場所です。

次に init の部分でなにか変数に代入を行っていますね(22行目)。これ、実を言うと前回のTL_capture.pyから変更しなくてよかった部分ですw

ここで自分自身のユーザーIDを取得しています。前回のTL_capture.pyではstatusオブジェクトが降ってくるごとに取得していたのを止めただけです。

それに伴ってon_statusの確認部分も変更が入ってます(64行目)。

で、一番重要な変更点が79~88行目です。ここに顔検出に関する処理が入っています。

まず、lbpcascade.pyに定義してある関数 face2d_detectにメモリー上に保存した画像ファイルを投げます。

すると取得するかのフラグと顔検出を行った座標が返ってきます。この関数で行っている処理は後ほど説明します。

その後フラグを確認し、取得する場合は前回と同じ保存処理を、保存しない場合は「Skip : (ユーザーID)-(ファイル番号)」という出力をするようになっています。

まあ、分かりやすい変更ですね。

 

さて、一番重要な顔検出の説明を行っていきましょう。lbpcascade.pyを見てみます。

# lbpcascade_animefaceによる顔検出
import numpy as np
import cv2
cascade = cv2.CascadeClassifier("./lbpcascade_animeface.xml")
def face2d_detect(raw_file):
    # 顔の位置
    facex = []
    facey = []
    facew = []
    faceh = []
    # 画像をデコード
    image = cv2.imdecode(np.asarray(bytearray(raw_file), dtype=np.uint8), 1)
    gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
    gray = cv2.equalizeHist(gray)
    # 顔検出
    faces = cascade.detectMultiScale(gray, scaleFactor=1.11, minNeighbors=2, minSize=(64,64))
    # 顔が検出出来たか
    if len(faces) <= 0:
        return False, None
    else:
        for area in faces:
            facex.append(area[0])
            facey.append(area[1])
            facew.append(area[2])
            faceh.append(area[3])
        return True, [facex, facey, facew, faceh]

関係ない話なんですけど、コードを挿入する時に一番最初のスペースが自動的に消えるせいでインデントが無くなる問題があったんですよ。

preタグっていうものを使えばいいんですね。勉強になりました。

今回はそこまで長くないのでソースコードをそのまま載せました。説明していきます。

まず、最初のimport文です(3~4行目)。

今回はnumpyとopencv-pythonを使用しています。(ちなみにTL_capture.pyにもたくさんのimport文がありましたが、Tweepy以外は標準ライブラリなので説明しませんでした)

numpyは数値計算によく使われますが、今回はメモリー上の画像をデコードする際、配列に変換するために使用しています。

opencv-python機械学習で画像を扱うならほぼ必須と言っていいほど有名なライブラリです。

次にcascadeというオブジェクト名で判定用の分類器を作成しています(6行目)。

まあ、これはそこまで重要ではないのでこんな感じで分類器を読み込むんだなぐらいで結構です。

一番重要なのが次の関数 face2d_detectです(8~30行目)。というかこのファイルのほとんどがこれですよねw

まず、顔の検出座標を保存するためのリストを作成しています(10~13行目)。ちょっと汚い書き方ですけど、まあ許して...

リストはX座標、Y座標、Width(幅)、Height(高さ)の4つを用意しています。

次にメモリー上の画像を読み込んでいます(16~18行目)。

読み込む際にグレースケールに変換し、ヒストグラムの均一化を行っています。これによって、速く正確に検出出来るようにしています。

その後、分類器に画像を投げ、顔の座標を取得しています(20行目)。1行しかありませんが、このスクリプトの一番重要な部分です。

顔の座標情報が返ってきたら、検出されたかの判定を行います(22~30行目)。

座標情報はリストとして返ってくるので、個数で判定しています(22行目)。

0以下だったら、取得フラグをFalseにし、座標情報は空っぽで返します(23行目)。

それ以外なら、最初に生成したリストそれぞれに情報を追加していきます(25~29行目)。for文によって顔の数だけ追加します。

最後に取得フラグをTrue、それぞれの情報リストをまたリストにして返却しています(30行目)。

  スクリプトの説明は以上です。ここで1つ疑問に思うことがありますね?

そうです、座標情報をどこに使っているかです。

今回、画像をそのまま欲しい人がいると思って画像自体に検出した場所の枠を描いていません。では、どうして座標情報が必要なのでしょう?

これは次回のデータベースに情報を保存していく際に使用します。ついでにそれを確認するための専用ツールも配布します。

 

ここからは顔検出に関する説明です。といっても、僕はほとんど説明しませんw

参考になるリンクをその都度貼っていくので、詳しくはそちらを参考にしてください。

まず、今回使用したのはlbpcascade_animeface.xmlと呼ばれるモデルです。

ultraist.hatenablog.com

github.com

おそらく二次元画像の顔検出において一番有名なモデルだと思います。ちなみに作者の方は画像を超解像度で拡大出来るwaifu2xを作った人でもあったりします。

余談なのですが、GitHubのREADMEにサンプル画像が入ってますよね?これ、最初使った時(2016/11)の頃は「アイマスの画像をサンプルに使ってるんだなー」ぐらいだったのですが、今見たらミリマスの画像で「おぉー」ってなりました。どうでもいい話です。

で、このモデルはLBP特徴とカスケード型識別器というもので構成されています。

OpenCVで物体検出器を作成③ LBP特徴【開発会社プロフェッサ】

algorithm.joho.info

それぞれの説明はリンク先を読んでもらえるとよく分かると思います。特にLBP特徴を説明してあるページは実際に検出器を作成する過程が載っているので、参考になると思います。

顔検出に関するベースはこんなものですかね。次は実際に動かしたときの関数の説明です。

faces = cascade.detectMultiScale(gray, scaleFactor=1.11, minNeighbors=2, minSize=(64,64))

検出部分のところだけ持ってきました。grayは画像本体ってことが分かりますが、後ろの3つの引数の意味が分かりませんね?

minSizeだけはそのままなので分かりやすいです。検出時の最小サイズを指定しています。このサイズ以下だった場合は検出されません。

で、問題がscaleFactorとminNeighborsです。こちらに関してはいい感じの記事がありました。

workpiles.com

workpiles.com

参考とした記事の方ではJavaのメソッドとして紹介されていますが、ほぼ変わりません。

scaleFactorの説明を少し引用させてもらいます。

ある特定の大きさの画像で学習した分類器を使って、様々なサイズの画像に対し検出を行うために、画像を異なるスケールで複数回の探索処理を行う必要があり、そのスケール変更のパラメータとしてscaleFactorが使用されるようです。

画像のスケール変化に使うパラメータのようです。パラメータを変化させたときの検出精度についても比較があります。

今回は割りといい値として紹介されている1.11を指定しています。

では、次にminNeighborsです。こちらも引用させてもらいます。

minNeighborsは信頼性のパラメータで、値が大きくなるに連れて検出の信頼性が上がるようですが、見逃してしまう率も高くなるようです。

信頼性のパラメータらしいです。よく分かりませんね。ですが、次の文章を読むと分かります。

detectMultiScaleでは画像のスケールを変えて何度も探索を行います。その結果、分類器がTrueとなる箇所が何度も重複して検出されますが、より重複(近傍矩形を含む)が多い部分を信頼性が高いと考えます。そのしきい値がminNeighborsになります。

つまり、検出を行った際に重複が多いと信頼出来るからそのしきい値を指定していると言った感じですね。

今回は僕がやってみた感じで一番最適かなと思った2を指定しています。

これらのパラメータは自分に合った値を探すといいかもしれません。

  これで重要な部分は完成しました。ですが、このままだと画像しか無いのでよく分かりませんね。

次回はツイートの情報を元にデータベースを作成する部分を作ります。