PythonとOpenCVを使ったまばたき検知ゲームの(プチ)改善

ここ最近、金属バット – YouTube の右の人と、ニガミ17才 / ただし、BGM – YouTube のボーカルの髪型に興味津々な日々を送っています。
(ニガミ17才けっこういい)

さて、前回こんなのを書きました。

が、目の開閉判定がマシンのスペックによって左右されるという問題がございました。
※ うちの超絶古いMBA(メモリ4GM)の場合、detectMultiScale()のパラメータをごにょごにょしないとうまく判定してくれない

そのへんどうにかならないもんかなぁと 目をつぶったら画面キャプチャを終了するやつ に関してここ数日悶々と試行錯誤を行い、まぁ前よりはマシになったやろというところでここに書き連ねておきます。

スポンサーリンク

ソース

先に改善したバージョンのソースを貼ります。

blink_game2.py

何がボトルネックだったのか

続いて何がボトルネックだったのか、どう対応したのかを書いていきます。

常に顔認識している

このゲームの特性上?、じっとしている場面が多いと思います。
であれば顔の認識が済んだら特定した座標等をそのまま使いまわせばいいんじゃね?と思った次第です。
今までは毎フレーム顔認識していました。

この部分

face_parts = detect_face_parts(gray)

このface_partsには顔部分の{'x': x座標, 'y': y座標, 'w': 幅, 'h': 高さ}が入っているので一旦取得出来たらこの値を使いまわします。

顔のサイズを制限していない

上に通じるものがありますが、じっとしているのであれば顔のサイズも特定の範囲を相手にしていいはずです。
大きすぎても小さすぎても精度が下がるので、ある程度精度がましになるような大きさに制限します。
※ サイズによってパラメータを変えてもいいけどめんどくさかった

この部分

# 顔の大きさが適切範囲か
def within_range_face_size(w):
    if 180 <= w <= 240:  # 調整いるかも
        return True
    return False

けっこう範囲が狭いのでアレだったら修正してみてください。

常に顔を相手にしている

今までは

  1. 顔部分の座標を特定
  2. その座標で顔の部分を抜き出す
  3. 顔の部分を認識処理に渡す

ということを行っていましたが目の認識に関していえば、目の近傍部分だけで認識が可能なようです。
そのため、

  1. 顔部分の座標を特定
  2. その座標で目の近傍部分を抜き出す
  3. 目の近傍部分を認識処理に渡す

に処理を変えました。
これだけでも結構FPSが上がります。

この部分

# 顔の部分から目の近傍を取る
eyes = gray_frame[face_y: face_y + int(face_h/2), face_x: face_x + face_w]

+ int(face_h/2) で顔の上半分だけにするという安直な実装です。

認識処理が重い(分類器の変更)

認識にはhaarcascade_eye_tree_eyeglasses.xml(メガネをかけててもOKなやつ)を用いていましたが、FPSを表示してみるとこの処理が重かったことがわかりました。
FPS20いくかいかないかのレベル
haarcascade_eye.xmlも試したけどそこまで変わらなかった

なので代わりに、haarcascade_lefteye_2splits(左目用), haarcascade_righteye_2splits(右目用)の分類器を試したところ常時FPSが30後半以上でるようになったのでこちらを採用することにしました。
※ 分類器を変えた関係上、メガネをかけている場合精度が低くなります

スポンサーリンク

ソースを見て分かる通り2回認識処理をしているはずなのに、こちらのほうが早いというのが不思議です。
中で何をしているのか全くわかりませんが両目をいっぺんに探すのと、片目だけを探すのではよっぽど計算量が違うんですかね。

この部分

left_eye = left_eye_cascade.detectMultiScale(
    eyes,
    scaleFactor=1.11,
    minNeighbors=3,
    minSize=min_size
)
right_eye = right_eye_cascade.detectMultiScale(
    eyes,
    scaleFactor=1.11,
    minNeighbors=3,
    minSize=min_size
)

返り値は<class 'numpy.ndarray'>で(へぇ、numpyなんだ。。)、中の配列の数が

  • 2個ならその目は開いているとみなされる
    • こんな感じ [[116 40 36 36] [34 40 40 40]]
  • 1個ならその目は閉じているとみなされる
    • こんな感じ [[34 40 41 41]]
  • 0個なら認識されていない
    • こんな感じ []

となるようです。

で、片目でも閉じたらアウトにする場合

return len(left_eye) <= 1 or len(right_eye) <= 1

こうなるかと思うんですけど、自分で試した結果けっこう厳しいというか精度が悪いというか、ささいなことでOUTになったりしたので

# どちらかの目が開いていればOK
return len(left_eye) + len(right_eye) <= 2

おとなしくこのように実装しています。

上記が今回試行錯誤したものになります。
ふぇ〜。

その他試したこと

FPSの表示

FPSを表示させておくと、どこで/どの段階でガクッとなるかの指標が図れるかと思います。
計算、表示の仕方はググったらすぐ出ますね。

  • show_fps = True とするとFPSが表示されます

コーディング規約をPEP8に準拠

せっかくなので、コーディング規約をPEP8になるべく準拠させてみるようにしました。
決まりがきちんとあるのはやっぱり楽です。

  • チェックツールの導入
$ pip install pep8 pytest pytest-pep8
  • チェック
$ py.test --pep8 blink_game2.py

Python のコーディング規約 PEP8 に準拠する を参考にさせていただきました。

遊び方

  • 起動したら、画面frameにフォーカスを合わせておきます
  • 顔をカメラに向け目をクワッと開けます
  • Please press "s" と表示されるまで顔を近づけたり遠ざけたりします
  • sを押します
    • 顔認識中かつまだスタートしていない場合、少しカクつく(キー入力を待っている)のでキー入力待ちが分かるかと思います
  • 目が開いている状態の場合、1秒ずつカウントダウン表示します
  • 目が閉じられたら画面キャプチャを終了します
    • 目が閉じられたとみなされたタイミングが最後の画面キャプチャとなります
  • escを押して終了です

やってみて

やってみてどうだったかというと、うーーーん、フレームサイズをリサイズしなくてもよくなったりとか前よりは微妙に良くなったかもしれませんが、結局サイズの制限とかメガネあかんとかなってるやんという感じでまだまだ何かが足りない気がします。取りこぼしもまだたまにあって精度が確実に良くなったとは言えないですし。

こういったFPSが命みたいなものはあまり作ったことが無いので、些細なパラメータ調整とかけっこうツラみがありますね。。
(マシーンのスペックに依存したりとかあると思うんだけど他の人どうやってんだろ)

でもちょっとしたことで速度が上がったりとかそのへんは興奮しますた。
こういうのやっぱり楽しいなぁ。。

やってみて学びがあったのは確かなのでやってよかったです。

オカッパ最高!!

P.S.

あ、スペックに余裕がある人は前のやつで十分遊べると思います。

スポンサーリンク

シェアする

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です

コメントする

このサイトはスパムを低減するために Akismet を使っています。コメントデータの処理方法の詳細はこちらをご覧ください