Hello Wor.log

IT系大学生4人による備忘録のようなもの

dlibとopencvを使って画像から瞳の位置を取得【python】

CPPXのXです。

dlibとopencvを使って瞳の位置を取得したいと思います。

環境

自分は、dlib導入時にpython3のboost入れ忘れてエラーが出たので、下記サイトを参考に解決しました。

https://www.haneca.net/?p=123

importと前準備

importするものは以下です。

import dlib
import cv2

顔器官の取得に学習が必要なため、学習済みモデルをダウンロードしておきます。

http://dlib.net/files/shape_predictor_68_face_landmarks.dat.bz2

何かしらで解凍しておいてください。

顔の検出器と、予測器を定義しておきます。
予測器の方には、先ほど解凍した学習済みモデルのパスを指定しています。

detector = dlib.get_frontal_face_detector()
predictor = dlib.shape_predictor("shape_predictor_68_face_landmarks.dat")

画像の用意

カメラを使ってやっていきたいと思います。
写っている顔の数は一人にしておいてください。

カメラが付いていない場合は、

frame = cv2.imread("画像のパス")

とやって、静止画像を代わりに使って進めていきます。

ではやっていきます。

まず、カメラから動画を取得します。

cap = cv2.VideoCapture(0)
while True:
    ret, frame = cap.read()
    cv2.imshow("me", frame) # 確認用
    # ここに処理を追加していく ----

    # ここまで ----

    if cv2.waitKey(1) == 27:
        break

cap.release()
cv2.destroyAllWindows()

これでカメラから動画が取得できたかと思います。

カメラが無い方は、以下のようにします。

frame = cv2.imread("画像のパス")
cv2.imshow("me", frame) # 確認用
# ここに処理を追加していく ----

# ここまで ----

cv2.waitKey(0)
cv2.destroyAllWindows()

frameの中身はnumpy配列です。
色の並びはBGRで、dtypeはuint8でやっていきます。

確認ができたら、確認用と書いてある行は消しておいてください。

顔器官座標を取得

dets = detector(frame[:, :, ::-1])

顔を検出します。
今回は、写っている顔が1つの想定なので、以下のようにして顔器官を取得します。

dets = detector(frame[:, :, ::-1])
if len(dets) > 0:
    parts = predictor(frame, dets[0]).parts()

    # 確認用 ---
    img = frame * 0
    for i in parts:
        cv2.circle(img, (i.x, i.y), 3, (255, 0, 0), -1)

    cv2.imshow("me", img)
    # 確認用 ここまで ---

実行すると、青の点で顔器官が表示されているかと思います。
こんな感じです。

f:id:cppx:20171225220921p:plain

partsの中に座標が入っており、座標の順番に関しては、以下で分かりやすく説明されていました。

http://mizutanikirin.net/unity-dlib-facelandmark-detector%E3%81%A7%E9%A1%94%E8%AA%8D%E8%AD%98

角座標へのアクアセスは、それぞれの要素の .x, .yで行えます。

# 例えば、鼻のてっぺん座標を取得したい場合
parts[33].x
parts[33].y

確認ができたら、確認用コードは消しておいてください。

次は、瞳の位置を取得していきます。

瞳座標を取得

先ほど取得した顔器官座標を使っていきます。

方針としては、目の画像から黒い部分を抜き出して、その重心を瞳とします。

瞳座標の取得を、関数にまとめたいと思います。
引数の定義は以下です。

def eye_point(img, parts, left=True):
    pass

顔画像と、顔器官座標、左目or右目(Trueで左目)
です。

関数の中身を埋めていきます。

まず、目玉の座標を取得してしまいましょう。

    if left:
        eyes = [
                parts[36],
                min(parts[37], parts[38], key=lambda x: x.y),
                max(parts[40], parts[41], key=lambda x: x.y),
                parts[39],
                ]
    else:
        eyes = [
                parts[42],
                min(parts[43], parts[44], key=lambda x: x.y),
                max(parts[46], parts[47], key=lambda x: x.y),
                parts[45],
                ]

左目が、36~39番目に入っているので、それを4点抜き出します。
目の端と(36, 39)、上瞼からより上にある方を(37 or 38)、下瞼からより下にある方を(40 or 41)。
四角で囲った時に、目玉全体が入るように座標を取得しておきます。

右目は左目の座標に+6して同様に取得します。

原点を目玉に合わせるために、座標を保存しておきます。

    org_x = eyes[0].x
    org_y = eyes[1].y

目が閉じていたら瞳を検知できないので、目が閉じているかを判別します。

    if is_close(org_y, eyes[2].y):
        return None

is_closeは後で定義します(定義はこちら)。
とりあえずコメントアウトしておいても構いません。

目玉をトリミングして二値化します。

    eye = img[org_y:eyes[2].y, org_x:eyes[-1].x] # トリミング
    _, eye = cv2.threshold(cv2.cvtColor(eye, cv2.COLOR_RGB2GRAY), 30, 255, cv2.THRESH_BINARY_INV) # 二値化

重心を求めます。

    center = get_center(eye)
    if center:
        return center[0] + org_x, center[1] + org_y
    return center

get_centerは後で定義します(定義はこちら)。
重心を求めないと瞳の座標が分からないので、次に進みます。

eye_pointの中身は以上です。
eye_pointの全体像を貼っておきます。

def eye_point(img, parts, left=True):
    if left:
        eyes = [
                parts[36],
                min(parts[37], parts[38], key=lambda x: x.y),
                max(parts[40], parts[41], key=lambda x: x.y),
                parts[39],
                ]
    else:
        eyes = [
                parts[42],
                min(parts[43], parts[44], key=lambda x: x.y),
                max(parts[46], parts[47], key=lambda x: x.y),
                parts[45],
                ]
    org_x = eyes[0].x
    org_y = eyes[1].y
    if is_close(org_y, eyes[2].y):
        return None

    eye = img[org_y:eyes[2].y, org_x:eyes[-1].x]
    _, eye = cv2.threshold(cv2.cvtColor(eye, cv2.COLOR_RGB2GRAY), 30, 255, cv2.THRESH_BINARY_INV)

    center = get_center(eye)
    if center:
        return center[0] + org_x, center[1] + org_y
    return center

画像の重心を求める

opencvがやってくれます。

def get_center(gray_img):
    moments = cv2.moments(gray_img, False)
    try:
        return int(moments['m10'] / moments['m00']), int(moments['m01'] / moments['m00'])
    except:
        return None

引数は、グレースケール画像です。

たまに0除算が発生してしまうので、雑に例外処理しています。
重心に関しては、詳しく説明しているサイトが色々あるので、説明を省きます。

これで取得した座標を瞳の位置とします。
ここまで実装したらとりあえず動くので、早く動かしたい方は確認用コードを追加して実行してみましょう。

目が閉じているか

def is_close(y0, y1):
    if abs(y0 - y1) < 10:
        return True
    return False

引数は、上瞼のy座標と、下瞼のy座標です。

上瞼と、下瞼のy座標の差で判別しています。

差が10未満なら とやっているので、画像の画素数によってうまく動きません。
うまくいかなかったら、ここの数値を色々と変えて試して見てください。

最後の確認用コード

def p(img, parts, eye):
    if eye[0]:
        cv2.circle(img, eye[0], 3, (255, 255, 0), -1)
    if eye[1]:
        cv2.circle(img, eye[1], 3, (255, 255, 0), -1)

    for i in parts:
        cv2.circle(img, (i.x, i.y), 3, (255, 0, 0), -1)

    cv2.imshow("me", img)

引数は、顔画像と、顔器官座標、瞳の座標(左, 右)です。

瞳座標を緑の点、顔器官座標を青の点で顔画像に重ねて表示しています。

メイン処理に下記を追加して実行すると、瞳の顔器官に加えて、瞳も表示されるかと思います。

    left_eye = eye_point(frame, parts)
    right_eye = eye_point(frame, parts, False)

    p(frame * 0, parts, (left_eye, right_eye))

frame * 0のところで、0倍しなければ元の顔画像に重ねて点が表示されます。

実行結果を載せておきます。

f:id:cppx:20171225225657p:plain f:id:cppx:20171225225731p:plain

おわり

コード載せておきます。

github.com