トップページ -> Pythonによる視線検出について -> リアルタイムな視線の判定

リアルタイムな視線の判定

前回までで視線の検出をするためにdlibを利用して目の位置を特定することができました. 今回は右目を切り出して視線を判定してみたいと思います.

前回まで

視線の判定方法

今回は以下の方法で視線を判定します.

  1. カメラ映像から右目の位置を特定し切り出す.
  2. 切り出した目の画像を白 or 黒で二値化する.
  3. 二値化した画像の重心(=黒目の中心)の位置を求める.
  4. 黒目がx_r以下なら右向き,x_l以上なら左向き,y_t以下なら上向き,y_b以上なら下向きとして判定する.
得られた映像は鏡写しになっていることに注意します. また,y座標が画面下向きに増えることにも注意してください.
ウィンドウと座標
ウィンドウと座標

predictor から得られる顔のパーツについて

predictorから得られる顔のパーツについて少し説明をしておきます. 鏡写しなので左右が反転していることに注意すると,右目の右端から反時計回りに36, 37, 38, 39,40, 41 , 左目の右端から反時計回りに42, 43, 44, 45, 46, 47となっています. 画面上ではなく実際の位置関係で説明しています.ややこしいので画像を見てみてください. 今回は右目のみを利用するので36, 37, 38, 39,40, 41を使います.

predictor のインデックスについて
predictorのインデックスについて

目の切り出しと二値化

上で確認したインデックスを利用して,画像から右目を切り出します. show_eye で 右目の切り出し,画像のサイズ変更,画像の二値化をし重心を求め描画することができます. 二値化するための閾値を自分で調整しないといけませんが,だいたいの目の中心を求めることができました. 目元が明るいときは閾値を上げ,暗いときは閾値を下げてください.※ 普通の部屋だと40くらいが妥当かもしれません.

目の中心を求める


# 画像比率を維持しながら縦横を指定して拡大する 余った部分は白でパディング 
def resize_with_pad(image,new_shape,padding_color=[255,255,255]):
    original_shape = (image.shape[1], image.shape[0])
    ratio = float(max(new_shape))/max(original_shape)
    new_size = tuple([int(x*ratio) for x in original_shape])
    image = cv2.resize(image, new_size)
    delta_w = new_shape[0] - new_size[0]
    delta_h = new_shape[1] - new_size[1]
    top, bottom = delta_h//2 + delta_h-(delta_h//2), 0
    left, right = delta_w//2, delta_w-(delta_w//2)
    top, bottom, left, right = max(top,0),max(bottom,0),max(right,0),max(left,0)
    image = cv2.copyMakeBorder(image, top, bottom, left, right, cv2.BORDER_CONSTANT, value=padding_color)
    return image

# 右目 36:左端 37, 38:上 39:右端 40,41:下
# 左目 42:左端 43, 44:上 45:右端 46,47:下
def show_eye(img, parts):
    # 目の位置を求める
    x0_right = parts[36].x
    x1_right = parts[39].x
    y0_right = min(parts[37].y, parts[38].y)
    y1_right = max(parts[40].y, parts[41].y)
    
    # 目の長方形をスライスで切り出す
    right_eye = img[y0_right:y1_right, x0_right:x1_right]
    
    # そのままの大きさでは見づらいので拡大する
    right_eye = resize_with_pad(right_eye,(600,300),padding_color=[255,255,255])
    
    # 重心を求めるために二値化する
    img_gray = cv2.cvtColor(right_eye, cv2.COLOR_BGR2GRAY)
    ret, img_bin = cv2.threshold(img_gray, 100, 255, cv2.THRESH_BINARY) # 閾値を100にしています
    # ret2, img_bin = cv2.threshold(img_gray, 0, 255, cv2.THRESH_OTSU)
    
    # 重心を求める
    img_rev = 255 - img_bin # 白黒を反転する
    mu = cv2.moments(img_rev, False) # 反転した画像の白い部分の重心を求める
    
    try:
        x,y= int(mu["m10"]/mu["m00"]) , int(mu["m01"]/mu["m00"])
        # 重心(=目の中心)を描画する
        cv2.circle(img_bin, (x, y), 5, (122), -1)
        cv2.circle(img_bin, (x, y), 20, (122), 1)
        cv2.circle(right_eye, (x, y), 5, (255,0,0), -1)
        cv2.circle(right_eye, (x, y), 20, (255,0,0), 1)
    except:
        pass
    
    cv2.imshow("Right Eye (bin)", img_bin)
    cv2.imshow("Right Eye", right_eye)

import dlib
import cv2
import numpy as np

detector = dlib.get_frontal_face_detector()
PREDICTOR_PATH = r"C:\dlib\python_examples\shape_predictor_68_face_landmarks.dat"
predictor = dlib.shape_predictor(PREDICTOR_PATH)

cap = cv2.VideoCapture(0)
while True:
    # カメラ映像の受け取り
    ret, frame = cap.read()

    # detetorによる顔の位置の推測
    dets = detector(frame[:, :, ::-1])
    if len(dets) > 0:
        # predictorによる顔のパーツの推測
        parts = predictor(frame, dets[0]).parts()
        # 映像の描画
        show_eye(frame, parts)

    # エスケープキーを押して終了します
    if cv2.waitKey(1) == 27:
        break

cap.release()
cv2.destroyAllWindows()
実際の目の範囲からはみ出している部分に少し影響されている気がするので show_eyeに少し改良を加えたものが以下です. 境界の点を直線で繋いではみ出している部分を白くしています.
目の中心を求める ~改良版~


# 右目 36:左端 37, 38:上 39:右端 40,41:下
# 左目 42:左端 43, 44:上 45:右端 46,47:下
def show_eye(img, parts):
    # 右目の左上のカット
    delta = (parts[37].y-parts[36].y)/(parts[37].x-parts[36].x)
    y = parts[36].y
    for x in range(parts[36].x,parts[37].x+1):
        img[0:round(y),x] = 255
        y += delta
        
    # 右目の左下のカット
    delta = (parts[41].y-parts[36].y)/(parts[41].x-parts[36].x)
    y = parts[36].y
    for x in range(parts[36].x,parts[41].x+1):
        img[round(y):,x] = 255
        y += delta
        
    # 右目の上部のカット
    delta = (parts[38].y-parts[37].y)/(parts[38].x-parts[37].x)
    y = parts[37].y
    for x in range(parts[37].x,parts[38].x+1):
        # print(x,round(y))
        img[0:round(y),x] = 255
        y += delta
        
    # 右目の右上部のカット
    delta = (parts[39].y-parts[38].y)/(parts[39].x-parts[38].x)
    y = parts[38].y
    for x in range(parts[38].x,parts[39].x+1):
        # print(x,round(y))
        img[0:round(y),x] = 255
        y += delta
        
    # 右目の右下部のカット
    delta = (parts[39].y-parts[40].y)/(parts[39].x-parts[40].x)
    y = parts[40].y
    for x in range(parts[40].x,parts[39].x+1):
        # print(x,round(y))
        img[round(y):,x] = 255
        y += delta
        
    # 右目の下部のカット
    delta = (parts[41].y-parts[40].y)/(parts[41].x-parts[40].x)
    y = parts[41].y
    for x in range(parts[41].x,parts[40].x+1):
        # print(x,round(y))
        img[round(y):,x] = 255
        y += delta
    
    # 目の位置を求める
    x0_right = parts[36].x
    x1_right = parts[39].x
    y0_right = min(parts[37].y, parts[38].y)
    y1_right = max(parts[40].y, parts[41].y)
    
    # 目の長方形をスライスで切り出す
    right_eye = img[y0_right:y1_right, x0_right:x1_right]
    
    # そのままの大きさでは見づらいので拡大する
    right_eye = resize_with_pad(right_eye,(600,300),padding_color=[255,255,255])
    
    # 重心を求めるために二値化する
    img_gray = cv2.cvtColor(right_eye, cv2.COLOR_BGR2GRAY)
    ret, img_bin = cv2.threshold(img_gray, 100, 255, cv2.THRESH_BINARY)
    # ret2, img_bin = cv2.threshold(img_gray, 0, 255, cv2.THRESH_OTSU)
    
    # 重心を求める
    img_rev = 255 - img_bin # 白黒を反転する
    mu = cv2.moments(img_rev, False) # 反転した画像の白い部分の重心を求める
    
    try:
        x,y= int(mu["m10"]/mu["m00"]) , int(mu["m01"]/mu["m00"])
        # 重心(=目の中心)を描画する
        cv2.circle(img_bin, (x, y), 5, (122), -1)
        cv2.circle(img_bin, (x, y), 20, (122), 1)
        cv2.circle(right_eye, (x, y), 5, (0,0,255), -1)
        cv2.circle(right_eye, (x, y), 20, (0,0,255), 1)
    except:
        pass
    
    cv2.imshow("Right Eye (bin)", img_bin)
    cv2.imshow("Right Eye", right_eye)
【参考(外部サイト)】IdeaKing/resize_with_pad_cv2.py
resize_with_padの元となるプログラムです.

視線の向きを判定する

黒目がx_r以下なら右向き,x_l以上なら左向き,y_t以下なら上向き,y_b以上なら下向きとして判定します. 閾値の設定には議論の余地がありそうですが,一番左を向いたときの目のx座標をx_max 一番右を向いたときを目のx座標をx_minとして, 目の位置が(x_max - x_min)/3以下なら右向き,2*(x_max - x_min)/3以上なら左向き,それ以外は真ん中とします. 上下も同様に上を向いたときのy座標をy_min 下を向いたときのy座標をy_maxとしてx_l,x_r,y_t,y_bを以下のように設定します. ※ 厳密には目とカメラの距離,顔の向きを一定にしなければならないなど制約が多いです.

顔を動かさずに最大限に右を向いた状態でrキーを押して1秒間待つ,最大限に左を向いた状態でlキーを押して1秒間待つ,最大限に上を向いた状態でtキーを押して1秒間待つ,最大限に下を向いた状態でbキーを押して1秒間待つことでキャリブレーションを行います. 各方向のキャリブレーションが終了するとビープ音がなります. 全ての方向のキャリブレーションが完了するとRight Eyeウィンドウに視線判定の境界となる線,視線の方向,黒目の中心の位置が表示されるようになります.
視線判定の動画


# 画像比率を維持しながら縦横を指定して拡大する 余った部分は白でパディング 
def resize_with_pad(image,new_shape,padding_color=[255,255,255]):
    original_shape = (image.shape[1], image.shape[0])
    ratio = float(max(new_shape))/max(original_shape)
    new_size = tuple([int(x*ratio) for x in original_shape])
    image = cv2.resize(image, new_size)
    delta_w = new_shape[0] - new_size[0]
    delta_h = new_shape[1] - new_size[1]
    top, bottom = delta_h//2 + delta_h-(delta_h//2), 0
    left, right = delta_w//2, delta_w-(delta_w//2)
    top, bottom, left, right = max(top,0),max(bottom,0),max(right,0),max(left,0)
    image = cv2.copyMakeBorder(image, top, bottom, left, right, cv2.BORDER_CONSTANT, value=padding_color)
    return image

# 左目 36:左端 37, 38:上 39:右端 40,41:下
# 右目 42:左端 43, 44:上 45:右端 46,47:下
def show_eye(img, parts,xy=[None,None,None,None]):
    # 左目の左上のカット
    delta = (parts[37].y-parts[36].y)/(parts[37].x-parts[36].x)
    y = parts[36].y
    for x in range(parts[36].x,parts[37].x+1):
        img[0:round(y),x] = 255
        y += delta
        
    # 左目の左下のカット
    delta = (parts[41].y-parts[36].y)/(parts[41].x-parts[36].x)
    y = parts[36].y
    for x in range(parts[36].x,parts[41].x+1):
        img[round(y):,x] = 255
        y += delta
        
    # 左目の上部のカット
    delta = (parts[38].y-parts[37].y)/(parts[38].x-parts[37].x)
    y = parts[37].y
    for x in range(parts[37].x,parts[38].x+1):
        # print(x,round(y))
        img[0:round(y),x] = 255
        y += delta
        
    # 左目の右上部のカット
    delta = (parts[39].y-parts[38].y)/(parts[39].x-parts[38].x)
    y = parts[38].y
    for x in range(parts[38].x,parts[39].x+1):
        # print(x,round(y))
        img[0:round(y),x] = 255
        y += delta
        
    # 左目の右下部のカット
    delta = (parts[39].y-parts[40].y)/(parts[39].x-parts[40].x)
    y = parts[40].y
    for x in range(parts[40].x,parts[39].x+1):
        # print(x,round(y))
        img[round(y):,x] = 255
        y += delta
        
    # 左目の下部のカット
    delta = (parts[41].y-parts[40].y)/(parts[41].x-parts[40].x)
    y = parts[41].y
    for x in range(parts[41].x,parts[40].x+1):
        # print(x,round(y))
        img[round(y):,x] = 255
        y += delta
    
    # 目の位置を求める
    x0_right = parts[36].x
    x1_right = parts[39].x
    y0_right = min(parts[37].y, parts[38].y)
    y1_right = max(parts[40].y, parts[41].y)
    
    # 目の長方形をスライスで切り出す
    right_eye = img[y0_right:y1_right, x0_right:x1_right]
    
    # そのままの大きさでは見づらいので拡大する
    right_eye = resize_with_pad(right_eye,(600,300),padding_color=[255,255,255])
    
    # 重心を求めるために二値化する
    img_gray = cv2.cvtColor(right_eye, cv2.COLOR_BGR2GRAY)
    ret, img_bin = cv2.threshold(img_gray, 40, 255, cv2.THRESH_BINARY)
    # ret2, img_bin = cv2.threshold(img_gray, 0, 255, cv2.THRESH_OTSU)
    
    # 重心を求める
    img_rev = 255 - img_bin # 白黒を反転する
    mu = cv2.moments(img_rev, False) # 反転した画像の白い部分の重心を求める
    
    x, y = None, None
    try:
        x,y= int(mu["m10"]/mu["m00"]) , int(mu["m01"]/mu["m00"])
        # 重心(=目の中心)を描画する
        cv2.circle(img_bin, (x, y), 5, (122), -1)
        cv2.circle(img_bin, (x, y), 20, (122), 1)
        cv2.circle(right_eye, (x, y), 5, (0,0,255), -1)
        cv2.circle(right_eye, (x, y), 20, (0,0,255), 1)
    except:
        pass
    
    # 目線の向きに関する処理
    if None not in xy and x!=None and y!=None:
        # 上端の線
        cv2.line(right_eye, (0, y_t), (600, y_t), (0, 255, 0), thickness=3, lineType=cv2.LINE_4)
        # 下端の線
        cv2.line(right_eye, (0, y_b), (600, y_b), (0, 255, 0), thickness=3, lineType=cv2.LINE_4)
        # 右端の線
        cv2.line(right_eye, (x_r, 0), (x_r, 300), (0, 255, 0), thickness=3, lineType=cv2.LINE_4)
        # 左端の線
        cv2.line(right_eye, (x_l, 0), (x_l, 300), (0, 255, 0), thickness=3, lineType=cv2.LINE_4)
        
        direction = ""
        if x < x_r:
            direction += "Right"
        elif x > x_l:
            direction += "Left"
        else:
            direction += "Center"
            
        if y < y_t:
            direction += "Top"
        elif y > y_b:
            direction += "Bottom"
        else:
            pass
        
        cv2.putText(right_eye, direction, (10, 50), cv2.LINE_AA, 1, (0, 0, 0), 1)
        cv2.putText(right_eye, f"{(x,y)}", (400, 50), cv2.LINE_AA, 1, (0, 0, 0), 1)
    
    cv2.imshow("Right Eye (bin)", img_bin)
    cv2.imshow("Right Eye", right_eye)
    return (x,y)

def set_xy(x_max,x_min,y_max,y_min):
    if None not in [x_max,x_min,y_max,y_min]:
        x_r = x_min + (x_max-x_min)/3
        x_l = x_min + 2*(x_max-x_min)/3
        y_t = y_min + (y_max-y_min)/3
        y_b = y_min + 2*(y_max-y_min)/3
        print(x_r,x_l,y_t,y_b)
        return round(x_r),round(x_l),round(y_t),round(y_b)
    else:
        return None,None,None,None

import dlib
import cv2
import numpy as np
import winsound

detector = dlib.get_frontal_face_detector()
PREDICTOR_PATH = r"C:\dlib\python_examples\shape_predictor_68_face_landmarks.dat"
predictor = dlib.shape_predictor(PREDICTOR_PATH)

cap = cv2.VideoCapture(0)
print(cap.get(cv2.CAP_PROP_FPS))

x_max = None
x_min = None
y_max = None
y_min = None

x_r = None
x_l = None
y_t = None
y_b = None

while True:
    # カメラ映像の受け取り
    ret, frame = cap.read()

    # detetorによる顔の位置の推測
    dets = detector(frame[:, :, ::-1])
    if len(dets) > 0:
        # predictorによる顔のパーツの推測
        parts = predictor(frame, dets[0]).parts()
        # 映像の描画
        eye_pos = show_eye(frame, parts,xy=[x_r,x_l,y_t,y_b])
    
    key = cv2.waitKey(1)
        
    # rを押して右端のx座標を取ります
    if key == ord("r"):
        right_pos = []
        cnt = 0
        while cnt <= cap.get(cv2.CAP_PROP_FPS):
            # カメラ映像の受け取り
            ret, frame = cap.read()

            # detetorによる顔の位置の推測
            dets = detector(frame[:, :, ::-1])
            if len(dets) > 0:
                # predictorによる顔のパーツの推測
                parts = predictor(frame, dets[0]).parts()
                # 映像の描画
                eye_pos = show_eye(frame, parts)
                key = cv2.waitKey(1)
                if eye_pos[0] != None:
                    right_pos.append(eye_pos[0])
                    cnt += 1
                
        x_min = sum(right_pos)/len(right_pos)
        print(f"x_min: {x_min}")
        winsound.Beep(440, 500) # 記録が終わったらビープ音を鳴らす 
        x_r,x_l,y_t,y_b = set_xy(x_max,x_min,y_max,y_min) # 端の設定
        
    # lを押して左端のx座標を取ります
    if key == ord("l"):
        left_pos = []
        cnt = 0
        while cnt <= cap.get(cv2.CAP_PROP_FPS):
            # カメラ映像の受け取り
            ret, frame = cap.read()

            # detetorによる顔の位置の推測
            dets = detector(frame[:, :, ::-1])
            if len(dets) > 0:
                # predictorによる顔のパーツの推測
                parts = predictor(frame, dets[0]).parts()
                # 映像の描画
                eye_pos = show_eye(frame, parts)
                key = cv2.waitKey(1)
                if eye_pos[0] != None:
                    left_pos.append(eye_pos[0])
                    cnt += 1
                
        x_max = sum(left_pos)/len(left_pos)
        print(f"x_max: {x_max}")
        winsound.Beep(440, 500) # 記録が終わったらビープ音を鳴らす 
        x_r,x_l,y_t,y_b = set_xy(x_max,x_min,y_max,y_min) # 端の設定
        
    # tを押して上端のy座標を取ります
    if key == ord("t"):
        top_pos = []
        cnt = 0
        while cnt <= cap.get(cv2.CAP_PROP_FPS):
            # カメラ映像の受け取り
            ret, frame = cap.read()

            # detetorによる顔の位置の推測
            dets = detector(frame[:, :, ::-1])
            if len(dets) > 0:
                # predictorによる顔のパーツの推測
                parts = predictor(frame, dets[0]).parts()
                # 映像の描画
                eye_pos = show_eye(frame, parts)
                key = cv2.waitKey(1)
                if eye_pos[0] != None:
                    top_pos.append(eye_pos[1])
                    cnt += 1
                
        y_min = sum(top_pos)/len(top_pos)
        print(f"y_min: {y_min}")
        winsound.Beep(440, 500) # 記録が終わったらビープ音を鳴らす 
        x_r,x_l,y_t,y_b = set_xy(x_max,x_min,y_max,y_min) # 端の設定
        
    # tを押して上端のy座標を取ります
    if key == ord("b"):
        bottom_pos = []
        cnt = 0
        while cnt <= cap.get(cv2.CAP_PROP_FPS):
            # カメラ映像の受け取り
            ret, frame = cap.read()

            # detetorによる顔の位置の推測
            dets = detector(frame[:, :, ::-1])
            if len(dets) > 0:
                # predictorによる顔のパーツの推測
                parts = predictor(frame, dets[0]).parts()
                # 映像の描画
                eye_pos = show_eye(frame, parts)
                key = cv2.waitKey(1)
                if eye_pos[0] != None:
                    bottom_pos.append(eye_pos[1])
                    cnt += 1
                
        y_max = sum(bottom_pos)/len(bottom_pos)
        print(f"y_max: {y_max}")
        winsound.Beep(440, 500) # 記録が終わったらビープ音を鳴らす 
        x_r,x_l,y_t,y_b = set_xy(x_max,x_min,y_max,y_min) # 端の設定
                
            
        
    # エスケープキーを押して終了します
    if key == 27:
        break

cap.release()
cv2.destroyAllWindows()
        

キャリブレーションを行うことでリアルタイム映像から目線の方向を判定することができました. 次回は視線がスクリーンのどこを向いているのかを表示してみたいと思います.

<- 前へ戻る 【目次に戻る】 次へ進む ->