投稿日:
2018/09/18 ここ最近、金属バット – YouTube の右の人と、ニガミ17才 / ただし、BGM – YouTube のボーカルの髪型に興味津々な日々を送っています。
(ニガミ17才けっこういい)
さて、前回こんなのを書きました。
が、目の開閉判定がマシンのスペックによって左右される
という問題がございました。
※ うちの超絶古いMBA(メモリ4GM)の場合、detectMultiScale()
のパラメータをごにょごにょしないとうまく判定してくれない
そのへんどうにかならないもんかなぁと 目をつぶったら画面キャプチャを終了するやつ に関してここ数日悶々と試行錯誤を行い、まぁ前よりはマシになったやろというところでここに書き連ねておきます。
ソース
先に改善したバージョンのソースを貼ります。
blink_game2.py
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import sys
import time
from datetime import datetime
import cv2
'''
参考
@link http://ensekitt.hatenablog.com/entry/2017/12/19/200000
@link https://note.nkmk.me/python-opencv-face-detection-haar-cascade/
@link https://note.nkmk.me/python-opencv-mosaic/
@link http://workpiles.com/2015/04/opencv-detectmultiscale-scalefactor/
'''
# 顔の大きさが適切範囲か
def within_range_face_size(w):
if 180 <= w <= 240: # 調整いるかも
return True
return False
# 顔部分の情報を検出
def detect_face_parts(gray_frame):
facerect = cascade.detectMultiScale(
gray_frame,
scaleFactor=1.11,
minNeighbors=3,
minSize=(100, 100)
)
if len(facerect) != 0:
for x, y, w, h in facerect:
# 顔の部分
return {'x': x, 'y': y, 'w': w, 'h': h}
return {}
# 目を閉じているか
def is_closed_eyes(gray_frame, face_parts):
# 顔の部分
face_x = face_parts['x']
face_y = face_parts['y']
face_w = face_parts['w']
face_h = face_parts['h']
# 顔の部分から目の近傍を取る
eyes = gray_frame[face_y: face_y + int(face_h/2), face_x: face_x + face_w]
# cv2.imshow('face', eyes)
min_size = (8, 8) # 調整いるかも
''' 目の検出
眼鏡をかけている場合、精度は低くなる。
PCのスペックが良ければ、haarcascade_eye_tree_eyeglasses.xmlを使ったほうがよい。
'''
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
)
''' left_eye, right_eye
[[116 40 36 36] [34 40 40 40]] => 開いている
[[34 40 41 41]] => 閉じている
[] => 未検出
'''
# 片目だけ閉じても駄目にしたい場合(これだと結構厳しい(精度悪い?)判定になる)
# return len(left_eye) <= 1 or len(right_eye) <= 1
# どちらかの目が開いていればOK
return len(left_eye) + len(right_eye) <= 2
# 経過時間を描画
def draw_elapsed_time(frame, start_time):
now = int(datetime.now().timestamp())
put_text(frame, str(now - start_time))
# put a text in cv2
def put_text(frame, text, org=(10, 50), fontScale=3, thickness=3):
cv2.putText(
frame,
text,
org,
cv2.FONT_HERSHEY_PLAIN,
fontScale,
(0, 255, 0),
thickness,
cv2.LINE_AA
)
#
# start
#
cap = cv2.VideoCapture(0)
if cap.isOpened() is False:
print('Can not open camera')
sys.exit()
# 分類器を読み込み
# https://github.com/opencv/opencv/tree/master/data/haarcascades
cascade = cv2.CascadeClassifier(
'haarcascades/haarcascade_frontalface_alt2.xml'
)
# leftとrightは逆転する
left_eye_cascade = cv2.CascadeClassifier(
'haarcascades/haarcascade_righteye_2splits.xml'
)
right_eye_cascade = cv2.CascadeClassifier(
'haarcascades/haarcascade_lefteye_2splits.xml'
)
closed_eyes = False
is_started = False
start_time = 0
closed_time = 0
show_fps = False # FPSを表示するかどうか
while True:
# VideoCaptureから1フレーム読み込む
ret, frame = cap.read()
if show_fps is True:
tick = cv2.getTickCount()
# 処理速度を高めるために画像をグレースケールに変換したものを用意
gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
if is_started is True:
closed_eyes = is_closed_eyes(gray, face_parts)
if closed_eyes is True:
draw_elapsed_time(frame=frame, start_time=start_time)
put_text(frame, 'End', (10, 100))
cv2.imshow('frame', frame) # 目が閉じられたであろう瞬間を残す
break
else:
draw_elapsed_time(frame=frame, start_time=start_time)
if is_started is False:
face_parts = detect_face_parts(gray)
# print(face_parts)
if len(face_parts) != 0 \
and within_range_face_size(face_parts['w']) \
and is_closed_eyes(gray, face_parts) is False:
put_text(frame, 'Please press "s"')
key = cv2.waitKey(100)
if key == 115: # s が押されたら
is_started = True
start_time = int(datetime.now().timestamp())
if is_started is True and show_fps is True:
# FPSを計算する
fps = cv2.getTickFrequency() / (cv2.getTickCount() - tick)
put_text(
frame,
"FPS : " + str(int(fps)),
(frame.shape[1] - 150, 40),
2,
2
)
cv2.imshow('frame', frame)
# キー入力を1ms待って、k が27(ESC)だったらBreakする
k = cv2.waitKey(1)
if k == 27:
break
# 後処理
if closed_eyes is True:
while True:
k = cv2.waitKey(100)
if k == 27: # ESC が押されたらclose
break
cap.release()
cv2.destroyAllWindows()
View the code on Gist .
何がボトルネックだったのか
続いて何がボトルネックだったのか、どう対応したのかを書いていきます。
常に顔認識している
このゲームの特性上?、じっとしている場面が多いと思います。
であれば顔の認識が済んだら特定した座標等をそのまま使いまわせばいいんじゃね?と思った次第です。
今までは毎フレーム顔認識していました。
この部分
1
face_parts
=
detect_face_parts(gray)
このface_partsには顔部分の{'x': x座標, 'y': y座標, 'w': 幅, 'h': 高さ}
が入っているので一旦取得出来たらこの値を使いまわします。
顔のサイズを制限していない
上に通じるものがありますが、じっとしているのであれば顔のサイズも特定の範囲を相手にしていいはずです。
大きすぎても小さすぎても精度が下がるので、ある程度精度がましになるような大きさに制限します。
※ サイズによってパラメータを変えてもいいけどめんどくさかった
この部分
1
2
3
4
5
def
within_range_face_size(w):
if
180
<
=
w <
=
240
:
return
True
return
False
けっこう範囲が狭いのでアレだったら修正してみてください。
常に顔を相手にしている
今までは
顔部分の座標を特定
その座標で顔の部分を抜き出す
顔の部分を認識処理に渡す
ということを行っていましたが目の認識に関していえば、目の近傍部分だけで認識が可能なようです。
そのため、
顔部分の座標を特定
その座標で目の近傍部分 を抜き出す
目の近傍部分 を認識処理に渡す
に処理を変えました。
これだけでも結構FPSが上がります。
この部分
1
2
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回認識処理をしているはずなのに、こちらのほうが早いというのが不思議です。
中で何をしているのか全くわかりませんが両目をいっぺんに探すのと、片目だけを探すのではよっぽど計算量が違うんですかね。
この部分
01
02
03
04
05
06
07
08
09
10
11
12
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個ならその目は閉じているとみなされる
0個なら認識されていない
となるようです。
で、片目でも閉じたらアウトにする場合
1
return
len
(left_eye) <
=
1
or
len
(right_eye) <
=
1
こうなるかと思うんですけど、自分で試した結果けっこう厳しいというか精度が悪いというか、ささいなことでOUTになったりしたので
1
2
return
len
(left_eye)
+
len
(right_eye) <
=
2
おとなしくこのように実装しています。
上記が今回試行錯誤したものになります。
ふぇ〜。
その他試したこと
FPSの表示
FPSを表示させておくと、どこで/どの段階でガクッとなるかの指標が図れるかと思います。
計算、表示の仕方はググったらすぐ出ますね。
show_fps = True
とするとFPSが表示されます
コーディング規約をPEP8に準拠
せっかくなので、コーディング規約をPEP8になるべく準拠させてみるようにしました。
決まりがきちんとあるのはやっぱり楽です。
チェックツールの導入
1
$ pip
install
pep8 pytest pytest-pep8
チェック
1
$ py.
test
--pep8 blink_game2.py
Python のコーディング規約 PEP8 に準拠する を参考にさせていただきました。
遊び方
起動したら、画面frame
にフォーカスを合わせておきます
顔をカメラに向け目をクワッ
と開けます
Please press "s"
と表示されるまで顔を近づけたり遠ざけたりします
s
を押します
顔認識中かつまだスタートしていない場合、少しカクつく(キー入力を待っている)のでキー入力待ちが分かるかと思います
目が開いている状態の場合、1秒ずつカウントダウン表示します
目が閉じられたら画面キャプチャを終了します
目が閉じられたとみなされたタイミングが最後の画面キャプチャとなります
esc
を押して終了です
やってみて
やってみてどうだったかというと、うーーーん、フレームサイズをリサイズしなくてもよくなったりとか前よりは微妙に良くなったかもしれませんが、結局サイズの制限とかメガネあかんとかなってるやんという感じでまだまだ何かが足りない気がします。取りこぼしもまだたまにあって精度が確実に良くなったとは言えないですし。
こういったFPSが命みたいなものはあまり作ったことが無いので、些細なパラメータ調整とかけっこうツラみがありますね。。
(マシーンのスペックに依存したりとかあると思うんだけど他の人どうやってんだろ)
でもちょっとしたことで速度が上がったりとかそのへんは興奮しますた。
こういうのやっぱり楽しいなぁ。。
やってみて学びがあったのは確かなのでやってよかったです。
オカッパ最高!!
P.S.
あ、スペックに余裕がある人は前のやつ で十分遊べると思います。