寝顔でスマホの顔認証ロックを解除 50万円の不正送金被害も どう防ぐ?専門家に聞いたというニュースがあり驚きました。悪い人はいろんなこと考えるんですね。
あなたの個人情報や資産に関する情報などが詰まったスマートフォン。普段はロックをかけているが、もし寝ている間にロックが解除されてしまったら? そんな恐ろしい事件が実際に起きた。警視庁は、飲食店で知り合った男性が寝た後に、男性の寝顔でスマホやネットバンキングの顔認証ロックを解除し、不正に50万円を送金した疑いで、チュニジア人の男を逮捕したのだ。9/14(木) 15:54配信
このニュースで出てきた「画面注視認識機能」。要は、スマホの画面を見ているかどうかを認識する機能です。これを簡易的に実装してみました。
カメラを注視している、ということは、虹彩が目の中心か、あるいは寄り目ぎみになっているということです。
虹彩の位置を確認するにはmediapipe
を使えばできそうですね。
目の左端から右端にかけて1本の線を引き、その線の中心に虹彩があるかどうかを確認します。
Kazuhito00様のmediapipe-python-sampleを参考にさせていただきました。 それ以外の参考にしたサイトを以下に記載します。
- mediapipe/iris.md
- mediapipe/facemesh.md
- Real-time Pupil Tracking from Monocular Video for Digital Puppetry
- MediaPipe Iris: Real-time Iris Tracking & Depth Estimation
- Face landmark detection guide
- mediapipe/canonical_face_model_uv_visualization.png
screen_gaze_function/screen_gaze_function.py
Lines 1 to 237 in 751b175
import sys | |
sys.path.append('/usr/lib/python3/dist-packages') | |
import copy | |
import cv2 as cv | |
import mediapipe as mp | |
import numpy as np | |
# ビデオファイルの初期化 | |
cap = cv.VideoCapture('assets/iris.mp4') | |
# FPSを取得 | |
fps = int(cap.get(cv.CAP_PROP_FPS)) | |
# 矢印の描画パラメータ | |
arrow_length = 50 | |
arrow_color = (0, 255, 0) # 矢印の色を設定(BGR形式) | |
# モデルロード | |
mp_face_mesh = mp.solutions.face_mesh | |
face_mesh = mp_face_mesh.FaceMesh( | |
max_num_faces=1, | |
refine_landmarks=True, | |
min_detection_confidence=0.7, | |
min_tracking_confidence=0.5, | |
) | |
def calc_min_enc_losingCircle(landmark_list): | |
""" | |
与えられたランドマークのリストに基づいて、最小の外接円を計算します。 | |
Parameters: | |
- landmark_list (list of tuple): ランドマークの座標を含むリスト。各ランドマークは(x, y)の形式のタプルです。 | |
Returns: | |
- tuple: 外接円の中心と半径を表すタプル。中心は(x, y)の形式のタプル、半径は整数です。 | |
""" | |
center, radius = cv.minEnclosingCircle(np.array(landmark_list)) | |
center = (int(center[0]), int(center[1])) | |
radius = int(radius) | |
return center, radius | |
def calc_iris_min_enc_losingCircle(image, landmarks): | |
""" | |
画像上の左目と右目の虹彩の外接円の中心と半径を計算する関数。 | |
Parameters: | |
- image (numpy.ndarray): 虹彩の外接円を計算する対象の画像。 | |
- landmarks (mediapipe.framework.formats.landmark_pb2.NormalizedLandmarkList): 顔のランドマーク情報。 | |
Returns: | |
- left_eye_info (tuple): 左目の虹彩の外接円の中心座標と半径を含むタプル。 | |
- right_eye_info (tuple): 右目の虹彩の外接円の中心座標と半径を含むタプル。 | |
""" | |
image_width, image_height = image.shape[1], image.shape[0] | |
landmark_point = [] | |
for index, landmark in enumerate(landmarks.landmark): | |
landmark_x = min(int(landmark.x * image_width), image_width - 1) | |
landmark_y = min(int(landmark.y * image_height), image_height - 1) | |
landmark_point.append((landmark_x, landmark_y)) | |
left_eye_points = [ | |
landmark_point[468], | |
landmark_point[469], | |
landmark_point[470], | |
landmark_point[471], | |
landmark_point[472], | |
] | |
right_eye_points = [ | |
landmark_point[473], | |
landmark_point[474], | |
landmark_point[475], | |
landmark_point[476], | |
landmark_point[477], | |
] | |
left_eye_info = calc_min_enc_losingCircle(left_eye_points) | |
right_eye_info = calc_min_enc_losingCircle(right_eye_points) | |
return left_eye_info, right_eye_info | |
def draw_landmarks(image, landmarks, refine_landmarks, left_eye, right_eye): | |
""" | |
画像上に顔のランドマークを描画する関数。 | |
Parameters: | |
- image (numpy.ndarray): 描画対象の画像。 | |
- landmarks (mediapipe.framework.formats.landmark_pb2.NormalizedLandmarkList): 顔のランドマーク情報。 | |
- refine_landmarks (bool): 虹彩の外接円と目の輪郭のランドマークを描画するかどうかを指定するフラグ。 | |
- left_eye (tuple): 左目の虹彩の中心座標と半径を含むタプル。 | |
- right_eye (tuple): 右目の虹彩の中心座標と半径を含むタプル。 | |
Returns: | |
- image (numpy.ndarray): ランドマークが描画された画像。 | |
- landmark_point (list): 顔のランドマークの座標リスト。 | |
""" | |
image_width, image_height = image.shape[1], image.shape[0] | |
landmark_point = [] | |
for index, landmark in enumerate(landmarks.landmark): | |
landmark_x = min(int(landmark.x * image_width), image_width - 1) | |
landmark_y = min(int(landmark.y * image_height), image_height - 1) | |
landmark_point.append((landmark_x, landmark_y)) | |
if refine_landmarks: | |
# 虹彩の外接円の描画 | |
cv.circle(image, left_eye[0], left_eye[1], (0, 255, 0), 2) | |
cv.circle(image, right_eye[0], right_eye[1], (0, 255, 0), 2) | |
# 目の輪郭のランドマークを描画 | |
left_eye_indices = [468, 469, 470, 471, 472] | |
right_eye_indices = [473, 474, 475, 476, 477] | |
for idx in left_eye_indices + right_eye_indices: | |
cv.circle(image, landmark_point[idx], 1, (0, 255, 0), 1) | |
# 左目の左端から右端を結ぶ直線を赤で描画 | |
cv.line(image, landmark_point[468], landmark_point[472], (0, 0, 255), 2) | |
# 右目の左端から右端を結ぶ直線を赤で描画 | |
cv.line(image, landmark_point[473], landmark_point[477], (0, 0, 255), 2) | |
return image, landmark_point | |
def draw_eye_lines(image, landmarks): | |
""" | |
左目と右目の直線を赤で描画する関数 | |
Parameters: | |
- image: 画像 | |
- landmarks: 顔のランドマーク | |
Returns: | |
- image: 直線が描画された画像 | |
""" | |
image_width, image_height = image.shape[1], image.shape[0] | |
landmark_point = [] | |
for index, landmark in enumerate(landmarks.landmark): | |
landmark_x = min(int(landmark.x * image_width), image_width - 1) | |
landmark_y = min(int(landmark.y * image_height), image_height - 1) | |
landmark_point.append((landmark_x, landmark_y)) | |
# 左目の直線を描画 | |
cv.line(image, landmark_point[33], landmark_point[133], (0, 0, 255), 2) | |
# 右目の直線を描画 | |
cv.line(image, landmark_point[362], landmark_point[359], (0, 0, 255), 2) | |
return image | |
def get_eye_direction(eye_start, eye_end, iris_center): | |
""" | |
目がどちらを向いているかを判断する関数 | |
Parameters: | |
- eye_start: 目の左端の座標 | |
- eye_end: 目の右端の座標 | |
- iris_center: 虹彩の中心の座標 | |
Returns: | |
- direction: 'left', 'right', or 'center' | |
""" | |
# 目の水平方向の長さを計算 | |
eye_width = np.abs(eye_end[0] - eye_start[0]) | |
# 虹彩の中心が目の水平方向のどの位置にあるかを計算 | |
relative_position = (iris_center[0] - eye_start[0]) / eye_width | |
# 虹彩の位置に基づいて方向を判断 | |
if relative_position < 0.4: | |
return 'left' | |
elif relative_position > 0.6: | |
return 'right' | |
else: | |
return 'center' | |
def draw_gaze_arrow(image, eye_center, iris_center, direction, arrow_length): | |
""" | |
目がどちらを向いているかを示す矢印を描画する関数 | |
Parameters: | |
- image: 画像 | |
- eye_center: 目の中心の座標 | |
- iris_center: 虹彩の中心の座標 | |
- direction: 'left', 'right', or 'center' | |
- length: 矢印の長さ | |
Returns: | |
- image: 矢印が描画された画像 | |
""" | |
if direction == 'left': | |
end_point = (eye_center[0] - arrow_length, eye_center[1]) | |
elif direction == 'right': | |
end_point = (eye_center[0] + arrow_length, eye_center[1]) | |
else: # center | |
end_point = iris_center | |
cv.arrowedLine(image, eye_center, end_point, (255, 0, 0), 2, tipLength=0.3) | |
return image | |
if __name__ == '__main__': | |
while True: | |
ret, image = cap.read() | |
if not ret: | |
break | |
debug_image = copy.deepcopy(image) | |
image_width, image_height = image.shape[1], image.shape[0] | |
# 検出実施 | |
image_rgb = cv.cvtColor(image, cv.COLOR_BGR2RGB) | |
results = face_mesh.process(image_rgb) | |
# 描画 | |
if results.multi_face_landmarks is not None: | |
for face_landmarks in results.multi_face_landmarks: | |
# 直線を描画 | |
debug_image = draw_eye_lines(debug_image, face_landmarks) | |
# 虹彩の外接円の計算 | |
left_eye, right_eye = None, None | |
left_eye, right_eye = calc_iris_min_enc_losingCircle( | |
debug_image, | |
face_landmarks, | |
) | |
# 描画 | |
debug_image, landmark_point = draw_landmarks( # landmark_point を受け取る | |
debug_image, | |
face_landmarks, | |
True, | |
left_eye, | |
right_eye, | |
) | |
# 虹彩の中心と目の中心を使用して、見ている方向の矢印を描画 | |
left_eye_center = (int(face_landmarks.landmark[468].x * image_width), int(face_landmarks.landmark[468].y * image_height)) | |
right_eye_center = (int(face_landmarks.landmark[473].x * image_width), int(face_landmarks.landmark[473].y * image_height)) | |
# 虹彩の中心と目の中心を使用して、見ている方向を取得 | |
left_eye_direction = get_eye_direction(landmark_point[130], landmark_point[244], left_eye[0]) | |
right_eye_direction = get_eye_direction(landmark_point[463], landmark_point[359], right_eye[0]) | |
# 虹彩の中心と目の中心を使用して、見ている方向の矢印を描画 | |
debug_image = draw_gaze_arrow(debug_image, left_eye_center, left_eye[0], left_eye_direction, arrow_length) | |
debug_image = draw_gaze_arrow(debug_image, right_eye_center, right_eye[0], right_eye_direction, arrow_length) | |
print(f"Left Eye is looking: {left_eye_direction}") | |
print(f"Right Eye is looking: {right_eye_direction}") | |
cv.imshow('MediaPipe Face Mesh Demo', debug_image) | |
# キー処理(ESC:終了) | |
wait_time = int(1000 / fps) | |
key = cv.waitKey(wait_time) | |
if key == 27: # ESC | |
break | |
cap.release() | |
cv.destroyAllWindows() |
以下の6つの関数を用意しました。
screen_gaze_function/screen_gaze_function.py
Lines 29 to 38 in 751b175
def calc_min_enc_losingCircle(landmark_list): | |
""" | |
与えられたランドマークのリストに基づいて、最小の外接円を計算します。 | |
Parameters: | |
- landmark_list (list of tuple): ランドマークの座標を含むリスト。各ランドマークは(x, y)の形式のタプルです。 | |
Returns: | |
- tuple: 外接円の中心と半径を表すタプル。中心は(x, y)の形式のタプル、半径は整数です。 | |
""" |
screen_gaze_function/screen_gaze_function.py
Lines 44 to 55 in 751b175
def calc_iris_min_enc_losingCircle(image, landmarks): | |
""" | |
画像上の左目と右目の虹彩の外接円の中心と半径を計算する関数。 | |
Parameters: | |
- image (numpy.ndarray): 虹彩の外接円を計算する対象の画像。 | |
- landmarks (mediapipe.framework.formats.landmark_pb2.NormalizedLandmarkList): 顔のランドマーク情報。 | |
Returns: | |
- left_eye_info (tuple): 左目の虹彩の外接円の中心座標と半径を含むタプル。 | |
- right_eye_info (tuple): 右目の虹彩の外接円の中心座標と半径を含むタプル。 | |
""" |
screen_gaze_function/screen_gaze_function.py
Lines 80 to 94 in 751b175
def draw_landmarks(image, landmarks, refine_landmarks, left_eye, right_eye): | |
""" | |
画像上に顔のランドマークを描画する関数。 | |
Parameters: | |
- image (numpy.ndarray): 描画対象の画像。 | |
- landmarks (mediapipe.framework.formats.landmark_pb2.NormalizedLandmarkList): 顔のランドマーク情報。 | |
- refine_landmarks (bool): 虹彩の外接円と目の輪郭のランドマークを描画するかどうかを指定するフラグ。 | |
- left_eye (tuple): 左目の虹彩の中心座標と半径を含むタプル。 | |
- right_eye (tuple): 右目の虹彩の中心座標と半径を含むタプル。 | |
Returns: | |
- image (numpy.ndarray): ランドマークが描画された画像。 | |
- landmark_point (list): 顔のランドマークの座標リスト。 | |
""" |
screen_gaze_function/screen_gaze_function.py
Lines 116 to 126 in 751b175
def draw_eye_lines(image, landmarks): | |
""" | |
左目と右目の直線を赤で描画する関数 | |
Parameters: | |
- image: 画像 | |
- landmarks: 顔のランドマーク | |
Returns: | |
- image: 直線が描画された画像 | |
""" |
screen_gaze_function/screen_gaze_function.py
Lines 139 to 150 in 751b175
def get_eye_direction(eye_start, eye_end, iris_center): | |
""" | |
目がどちらを向いているかを判断する関数 | |
Parameters: | |
- eye_start: 目の左端の座標 | |
- eye_end: 目の右端の座標 | |
- iris_center: 虹彩の中心の座標 | |
Returns: | |
- direction: 'left', 'right', or 'center' | |
""" |
screen_gaze_function/screen_gaze_function.py
Lines 165 to 178 in 751b175
def draw_gaze_arrow(image, eye_center, iris_center, direction, arrow_length): | |
""" | |
目がどちらを向いているかを示す矢印を描画する関数 | |
Parameters: | |
- image: 画像 | |
- eye_center: 目の中心の座標 | |
- iris_center: 虹彩の中心の座標 | |
- direction: 'left', 'right', or 'center' | |
- length: 矢印の長さ | |
Returns: | |
- image: 矢印が描画された画像 | |
""" |
Right Eye is looking: center
Left Eye is looking: center
Right Eye is looking: center
Left Eye is looking: left
Right Eye is looking: left
Left Eye is looking: left
Right Eye is looking: left
Left Eye is looking: left
(前後省略)
この機能を使えば、画面の指示に従い虹彩を動かすことによって、マルチモーダルな認証を実現できるかもしれませんね。