Help us understand the problem. What is going on with this article?

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

モチベーション

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

完成イメージ

眠そうになったら「Sleepy eyes. Wake Up!」とやさしい心の声で起こしてくれる。
mogamin6.gif

さぁやろう

流れは、こうです。

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

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

OpenCVのHaar-like特徴分類器で実装してみるが、まばたきが厳しい

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

image.png

OpenCV版 実装と結果

 この手のサンプルコードはいくらでもあります。処理速度も早く、実行結果も悪くありません。悪くありませんが、判定は検知した目の数でしかありません。これでは眠そうかどうかの微妙な判定には使えません。もう寝ちゃっているのですから。実現したいのは「眠そうにしている」というまぶたが閉じるか閉じないかの微妙な判定です。OpenCVだけでは難しいことがわかりました。
mogamin1.gif

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というライブラリを見つけました。これはOpenCVよりも細かく検出できます。

Dlib版 実装と結果

 いい感じですね。顔の各パーツが点で取れるので、これで目が開いている時と閉じている時とのわずかな差を見つけることができそうです。ただ、FPSが平均で11ぐらいしか出ていません。処理速度が犠牲になるのは、まぁわかりますが。。。でも、実用性がないのであれば話になりません。
mogamin3.gif

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

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.pngimage.png

早速検証した結果がこちらです。

 期待していた通り、目の開き具合を算出しそれを検出することで、「今にも眠そうな顔」を判定できました。論文によるとEARの閾値は0.2ぐらいがよいとされているので、(右目)0.2+(左目)0.2=0.4を下回ったら「Sleepy eyes. Wake up!」と表示させました。
mogamin5.gif

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

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

参考にさせていただいたサイト

Why do not you register as a user and use Qiita more conveniently?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away