眠気を判定！目のまばたき検知をDlibとOpenCVを組み合わせて数十行で作る

# モチベーション
　とあるイベントで、ドライバーの眠気をアラートする仕組みを画像処理と温度センサーで実現する、そんな展示を見つけました。画像処理には知見があったので、よし俺もやってみようと思い立ったのが始まりでした。それと、眠くないのに「よく眠そうな顔をしている」と、大きく開く目ではないことから誤解を招くことがあり、本当は眠くないことを証明してやるぞ。という気持ちも重なりました。

## 完成イメージ
眠そうになったら「Sleepy eyes. Wake Up!」とやさしい心の声で起こしてくれる。
![mogamin6.gif](https://qiita-image-store.s3.amazonaws.com/0/86213/c9d7d129-8263-7c31-12f2-cb5ec67579c0.gif)


# さぁやろう
流れは、こうです。

* まず、顔を検出し、その顔の枠から目を検出する。
* 目の大きさを図って、開いている時と閉じている時の何らかの差を見つける。
* その差を見つけたら、アラートを画面に表示させる。

差ってなんだろう。と思いながら。。。


## OpenCVのHaar-like特徴分類器で実装してみるが、まばたきが厳しい
　顔検知で最初に思いつくのがOpenCVのHaar-like特徴分類器です。これは、画像の一部分を抜き出して個々に明暗差を算出し、それを繰り返して組み合わせることによって特定の物体を判別するものです。機械学習の一つの手法です。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/86213/1b787023-4e1e-d485-d7ed-f69077fd3a15.png)

* 参考、引用サイト
 * [Face Detection using Haar Cascades](https://docs.opencv.org/4.0.0/d7/d8b/tutorial_py_face_detection.html)
 * [Viola Jones face detection and tracking explained
](https://www.youtube.com/watch?v=WfdYYNamHZ8)
 * [OpenCV Face Detection: Visualized](https://vimeo.com/12774628)  【Haar-like分類器による検出の過程を表現している動画は面白い】
 * [OpenCV haarcascades dataset on Github](https://github.com/opencv/opencv/tree/master/data/haarcascades)

### OpenCV版 実装と結果
　この手のサンプルコードはいくらでもあります。処理速度も早く、実行結果も悪くありません。悪くありませんが、判定は検知した目の数でしかありません。これでは眠そうかどうかの微妙な判定には使えません。もう寝ちゃっているのですから。実現したいのは「眠そうにしている」というまぶたが閉じるか閉じないかの微妙な判定です。OpenCVだけでは難しいことがわかりました。
![mogamin1.gif](https://qiita-image-store.s3.amazonaws.com/0/86213/63f291d0-71c9-db11-a25e-bf7e572918ef.gif)

```python:eye_blink_detector_opencv.py
import os,sys
import cv2

cap = cv2.VideoCapture(0)
cascade = cv2.CascadeClassifier('haarcascade_frontalface_alt2.xml')
eye_cascade = cv2.CascadeClassifier('haarcascade_eye_tree_eyeglasses.xml')

while True:
    ret, rgb = cap.read()

    gray = cv2.cvtColor(rgb, cv2.COLOR_BGR2GRAY)
    faces = cascade.detectMultiScale(
        gray, scaleFactor=1.11, minNeighbors=3, minSize=(100, 100))

    if len(faces) == 1:
        x, y, w, h = faces[0, :]
        cv2.rectangle(rgb, (x, y), (x + w, y + h), (255, 0, 0), 2)

        # 処理高速化のために顔の上半分を検出対象範囲とする
        eyes_gray = gray[y : y + int(h/2), x : x + w]
        eyes = eye_cascade.detectMultiScale(
            eyes_gray, scaleFactor=1.11, minNeighbors=3, minSize=(8, 8))

        for ex, ey, ew, eh in eyes:
            cv2.rectangle(rgb, (x + ex, y + ey), (x + ex + ew, y + ey + eh), (255, 255, 0), 1)

        if len(eyes) == 0:
            cv2.putText(rgb,"Sleepy eyes. Wake up!",
                (10,100), cv2.FONT_HERSHEY_PLAIN, 3, (0,0,255), 2, cv2.LINE_AA)

    cv2.imshow('frame', rgb)
    if cv2.waitKey(1) == 27:
        break  # esc to quit

cap.release()
cv2.destroyAllWindows()
```

## 良さげなライブラリDlibを見つけたが、実用的な速度にならない。
　目を検出するだけではなく、目の形を検出する方法を模索する中で[Dlib](http://dlib.net/)というライブラリを見つけました。これはOpenCVよりも細かく検出できます。

### Dlib版 実装と結果
　いい感じですね。顔の各パーツが点で取れるので、これで目が開いている時と閉じている時とのわずかな差を見つけることができそうです。ただ、FPSが平均で11ぐらいしか出ていません。処理速度が犠牲になるのは、まぁわかりますが。。。でも、実用性がないのであれば話になりません。
![mogamin3.gif](https://qiita-image-store.s3.amazonaws.com/0/86213/a6a7807e-3d2d-63b7-c0e2-54c317b5b124.gif)


```python:eye_blink_detector_dlib1.py
import os,sys
import cv2
import dlib
from imutils import face_utils

cap = cv2.VideoCapture(0)
face_detector = dlib.get_frontal_face_detector()
face_parts_detector = dlib.shape_predictor('shape_predictor_68_face_landmarks.dat')

while True:
    tick = cv2.getTickCount()

    ret, rgb = cap.read()
    gray = cv2.cvtColor(rgb, cv2.COLOR_RGB2GRAY)
    faces = face_detector(gray)

    if len(faces) == 1:
        face = faces[0]
        cv2.rectangle(rgb, (face.left(), face.top()), (face.right(), face.bottom()), (255, 0, 0), 2)

        face_parts = face_parts_detector(gray, face)
        face_parts = face_utils.shape_to_np(face_parts)

        for i, ((x, y)) in enumerate(face_parts[:]):
            cv2.circle(rgb, (x, y), 1, (0, 255, 0), -1)
            cv2.putText(rgb, str(i), (x + 2, y - 2), cv2.FONT_HERSHEY_SIMPLEX, 0.3, (0, 255, 0), 1)

    fps = cv2.getTickFrequency() / (cv2.getTickCount() - tick)
    cv2.putText(rgb, "FPS:{} ".format(int(fps)), 
        (10, 50), cv2.FONT_HERSHEY_PLAIN, 3, (0, 0, 255), 2, cv2.LINE_AA)

    cv2.imshow('frame', rgb)
    if cv2.waitKey(1) == 27:
        break  # esc to quit

cap.release()
cv2.destroyAllWindows()
```

## OpenCVとDlibを組み合わせて
　調査した結果、処理が遅いのは顔のパーツ検出ではなく、顔自体の検出であることがわかりました。そこで、顔の検出には処理速度の早いOpenCVを採用し、パーツの検出にはDlibのままで再度、実験です。

### OpenCV + Dlib版 実装と結果
　良くなりました。FPSは30が楽に出るようになりカクつきもなくなりました。これで多少重い処理を入れても大丈夫そうです。では次のステップにいきましょう。
![mogamin4.gif](https://qiita-image-store.s3.amazonaws.com/0/86213/a8440884-c182-ba3f-c916-c0e89680f419.gif)

```python:eye_blink_detector_dlib2.py
import os,sys
import cv2
import dlib
from imutils import face_utils

cap = cv2.VideoCapture(0)
face_cascade = cv2.CascadeClassifier('haarcascade_frontalface_alt2.xml')
face_parts_detector = dlib.shape_predictor('shape_predictor_68_face_landmarks.dat')

while True:
    tick = cv2.getTickCount()

    ret, rgb = cap.read()
    gray = cv2.cvtColor(rgb, cv2.COLOR_RGB2GRAY)
    faces = face_cascade.detectMultiScale(
        gray, scaleFactor=1.11, minNeighbors=3, minSize=(100, 100))

    if len(faces) == 1:
        x, y, w, h = faces[0, :]
        cv2.rectangle(rgb, (x, y), (x + w, y + h), (255, 0, 0), 2)

        face = dlib.rectangle(x, y, x + w, y + h)
        face_parts = face_parts_detector(gray, face)
        face_parts = face_utils.shape_to_np(face_parts)

        for i, ((x, y)) in enumerate(face_parts[:]):
            cv2.circle(rgb, (x, y), 1, (0, 255, 0), -1)
            cv2.putText(rgb, str(i), (x + 2, y - 2), cv2.FONT_HERSHEY_SIMPLEX, 0.3, (0, 255, 0), 1)

    fps = cv2.getTickFrequency() / (cv2.getTickCount() - tick)
    cv2.putText(rgb, "FPS:{} ".format(int(fps)), 
        (10, 50), cv2.FONT_HERSHEY_PLAIN, 3, (0, 0, 255), 2, cv2.LINE_AA)

    cv2.imshow('frame', rgb)
    if cv2.waitKey(1) == 27:
        break  # esc to quit

cap.release()
cv2.destroyAllWindows()
```


### 開いている時と閉じている時の何らかの差を見つけるには
　最初は目を四角で囲い、その面積の大きさで判定しようと試みましたが微妙にうまくいかず。どうしたもんかとネットを探していたときに、これだっ！という論文に出会いました。まばたきの検出には、次の計算式でEAR(eyes aspect ratio)を求めることが有効とのことです。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/86213/d73567b3-fcee-114b-85e3-c5381d4fbfca.png)![image.png](https://qiita-image-store.s3.amazonaws.com/0/86213/0e317c4a-4032-f59a-dbaf-7078e0cffda9.png)

* 引用元
 * [Real-Time Eye Blink Detection using Facial Landmarks](http://vision.fe.uni-lj.si/cvww2016/proceedings/papers/05.pdf) 
 * [Eye blink detection with OpenCV, Python, and dlib](https://www.pyimagesearch.com/2017/04/24/eye-blink-detection-opencv-python-dlib/)

### 早速検証した結果がこちらです。
　期待していた通り、目の開き具合を算出しそれを検出することで、「今にも眠そうな顔」を判定できました。論文によるとEARの閾値は0.2ぐらいがよいとされているので、(右目)0.2+(左目)0.2=0.4を下回ったら「Sleepy eyes. Wake up!」と表示させました。
![mogamin5.gif](https://qiita-image-store.s3.amazonaws.com/0/86213/77939f84-be76-70f3-7865-5a9b75fa9d1e.gif)

```python:eye_blink_detector_dlib3.py
import os,sys
import cv2
import dlib
from imutils import face_utils
from scipy.spatial import distance

cap = cv2.VideoCapture(0)
face_cascade = cv2.CascadeClassifier('haarcascade_frontalface_alt2.xml')
face_parts_detector = dlib.shape_predictor('shape_predictor_68_face_landmarks.dat')

def calc_ear(eye):
    A = distance.euclidean(eye[1], eye[5])
    B = distance.euclidean(eye[2], eye[4])
    C = distance.euclidean(eye[0], eye[3])
    eye_ear = (A + B) / (2.0 * C)
    return round(eye_ear, 3)

while True:
    tick = cv2.getTickCount()

    ret, rgb = cap.read()
    gray = cv2.cvtColor(rgb, cv2.COLOR_RGB2GRAY)
    faces = face_cascade.detectMultiScale(
        gray, scaleFactor=1.11, minNeighbors=3, minSize=(100, 100))

    if len(faces) == 1:
        x, y, w, h = faces[0, :]
        cv2.rectangle(rgb, (x, y), (x + w, y + h), (255, 0, 0), 2)

        face = dlib.rectangle(x, y, x + w, y + h)
        face_parts = face_parts_detector(gray, face)
        face_parts = face_utils.shape_to_np(face_parts)

        left_eye_ear = calc_ear(face_parts[42:48])
        cv2.putText(rgb, "left eye EAR:{} ".format(round(left_eye_ear, 3)), 
            (10, 100), cv2.FONT_HERSHEY_PLAIN, 1, (0, 0, 255), 1, cv2.LINE_AA)

        right_eye_ear = calc_ear(face_parts[36:42])
        cv2.putText(rgb, "right eye EAR:{} ".format(round(right_eye_ear, 3)), 
            (10, 120), cv2.FONT_HERSHEY_PLAIN, 1, (0, 0, 255), 1, cv2.LINE_AA)

        if (left_eye_ear + right_eye_ear) < 0.40:
            cv2.putText(rgb,"Sleepy eyes. Wake up!",
                (10,180), cv2.FONT_HERSHEY_PLAIN, 3, (0,0,255), 3, 1)

    fps = cv2.getTickFrequency() / (cv2.getTickCount() - tick)
    cv2.putText(rgb, "FPS:{} ".format(int(fps)), 
        (10, 50), cv2.FONT_HERSHEY_PLAIN, 2, (0, 0, 255), 2, cv2.LINE_AA)

    cv2.imshow('frame', rgb)

    if cv2.waitKey(1) == 27:
        break  # esc to quit

cap.release()
cv2.destroyAllWindows()
```

## EARの閾値は顔の大きさに依存する？
　目を閉じているにもかかわらずEAR値が0.2を超える場合もあり、調整は一筋縄ではいきません。aspectとは言え、画像に写っている顔のサイズに依存する場合があるのかもしれません。顔のサイズを一定の大きさにしてからパーツの検出を実施する改修を加えました。

### 顔のサイズを一定サイズでリサイズしてから、パーツ検知する
　顔のサイズが大きくても小さくても、一定のEAR値(ここでは0.55とした)で、柔軟な判定ができました。小さく画像ですいません。恥ずかしいもので。
![mogamin6.gif](https://qiita-image-store.s3.amazonaws.com/0/86213/c9d7d129-8263-7c31-12f2-cb5ec67579c0.gif)

```python:eye_blink_detector_dlib4.py
import os,sys
import cv2
import dlib
from imutils import face_utils
from scipy.spatial import distance

cap = cv2.VideoCapture(0)
face_cascade = cv2.CascadeClassifier('haarcascade_frontalface_alt2.xml')
face_parts_detector = dlib.shape_predictor('shape_predictor_68_face_landmarks.dat')

def calc_ear(eye):
    A = distance.euclidean(eye[1], eye[5])
    B = distance.euclidean(eye[2], eye[4])
    C = distance.euclidean(eye[0], eye[3])
    eye_ear = (A + B) / (2.0 * C)
    return round(eye_ear, 3)

def eye_marker(face_mat, position):
    for i, ((x, y)) in enumerate(position):
        cv2.circle(face_mat, (x, y), 1, (255, 255, 255), -1)
        cv2.putText(face_mat, str(i), (x + 2, y - 2), cv2.FONT_HERSHEY_SIMPLEX, 0.3, (255, 255, 255), 1)

while True:
    tick = cv2.getTickCount()

    ret, rgb = cap.read()
    gray = cv2.cvtColor(rgb, cv2.COLOR_RGB2GRAY)
    faces = face_cascade.detectMultiScale(
        gray, scaleFactor=1.11, minNeighbors=3, minSize=(100, 100))

    if len(faces) == 1:
        x, y, w, h = faces[0, :]
        cv2.rectangle(rgb, (x, y), (x + w, y + h), (255, 0, 0), 2)
        
        face_gray = gray[y :(y + h), x :(x + w)]
        scale = 480 / h
        face_gray_resized = cv2.resize(face_gray, dsize=None, fx=scale, fy=scale)

        face = dlib.rectangle(0, 0, face_gray_resized.shape[1], face_gray_resized.shape[0])
        face_parts = face_parts_detector(face_gray_resized, face)
        face_parts = face_utils.shape_to_np(face_parts)

        left_eye = face_parts[42:48]
        eye_marker(face_gray_resized, left_eye)

        left_eye_ear = calc_ear(left_eye)
        cv2.putText(rgb, "LEFT eye EAR:{} ".format(left_eye_ear), 
            (10, 100), cv2.FONT_HERSHEY_PLAIN, 1, (0, 0, 255), 1, cv2.LINE_AA)

        right_eye = face_parts[36:42]
        eye_marker(face_gray_resized, right_eye)

        right_eye_ear = calc_ear(right_eye)
        cv2.putText(rgb, "RIGHT eye EAR:{} ".format(round(right_eye_ear, 3)), 
            (10, 120), cv2.FONT_HERSHEY_PLAIN, 1, (0, 0, 255), 1, cv2.LINE_AA)

        if (left_eye_ear + right_eye_ear) < 0.55:
            cv2.putText(rgb,"Sleepy eyes. Wake up!",
                (10,180), cv2.FONT_HERSHEY_PLAIN, 3, (0,0,255), 3, 1)

        cv2.imshow('frame_resize', face_gray_resized)

    fps = cv2.getTickFrequency() / (cv2.getTickCount() - tick)
    cv2.putText(rgb, "FPS:{} ".format(int(fps)), 
        (10, 50), cv2.FONT_HERSHEY_PLAIN, 2, (0, 0, 255), 2, cv2.LINE_AA)

    cv2.imshow('frame', rgb)
    if cv2.waitKey(1) == 27:
        break  # esc to quit

cap.release()
cv2.destroyAllWindows()
```

# 最後に
　なかなかいい結果を出すことができました。ただ残念ながら、私のような「もともと眠そうな顔」をしている人に「本当は眠くない」ことを証明できませんでした。常に目を大きく開ける方法を誰か教えてください。

本記事のプログラムは以下のgithubで公開しております。
https://github.com/mogamin/eye_blink_detector.git




# 参考にさせていただいたサイト
* http://ensekitt.hatenablog.com/entry/2017/12/19/200000
* https://note.nkmk.me/python-opencv-face-detection-haar-cascade/
* https://note.nkmk.me/python-opencv-mosaic/
* http://workpiles.com/2015/04/opencv-detectmultiscale-scalefactor/