見出し画像

[ラズパイ / 電子工作]MediaPipeで「超熟睡システム」自作

こんにちは、IoT探検家のシンジです。

今回は前回同様にMediaPipeを使って、目の開き具合を検知して、勉強やデスク作業中に眠たくなった時に、気持ちよく寝落ちできる「超熟睡システム」を作ってみました^_^。


MediaPipeとはGoogleが提供するオープンソースのメディアデータ向け機械学習用フレームワークで、顔認識や姿勢推定などのモデルをエッジで利用できる。

https://google.github.io/mediapipe/

用意したもの

・ラズパイ本体(Raspberry Pi 4)
・外付けのカメラ
・キーボード
・microSDカード
・Micro HDMI - HDMI ケーブル

作業の流れ

1)MediaPipeを使って顔のランドマークを検出
2)目の開き具合を検知
3)熟睡環境を作る
4)「超熟睡システム」を実演

実演に使ったコードはこのページの最後に置いてあります。

1)MediaPipeを使って顔のランドマークを検出

Attention Mesh: Overview of model architecture

利用するFace Meshは2019年発表の論文「BlazeFace」を元にしたモデルで、顔画像から468個のランドマークの座標を3次元で取得できます。

import cv2
import mediapipe as mp
mp_drawing = mp.solutions.drawing_utils
mp_drawing_styles = mp.solutions.drawing_styles
mp_face_mesh = mp.solutions.face_mesh

# カメラから画像を取得
drawing_spec = mp_drawing.DrawingSpec(thickness=1, circle_radius=1)
cap = cv2.VideoCapture(0)
with mp_face_mesh.FaceMesh(
    max_num_faces=1,
    refine_landmarks=True,
    min_detection_confidence=0.5,
    min_tracking_confidence=0.5) as face_mesh:
  while cap.isOpened():
    success, image = cap.read()
    if not success:
      print("Ignoring empty camera frame.")
      continue

    image.flags.writeable = False
    image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)

    # 画像から顔のランドマークを取得
    results = face_mesh.process(image)

    image.flags.writeable = True
    image = cv2.cvtColor(image, cv2.COLOR_RGB2BGR)

    # 画像にランドマークをメッシュ状に描写
    if results.multi_face_landmarks:
      for face_landmarks in results.multi_face_landmarks:
        mp_drawing.draw_landmarks(
            image=image,
            landmark_list=face_landmarks,
            connections=mp_face_mesh.FACEMESH_TESSELATION,
            landmark_drawing_spec=None,
            connection_drawing_spec=mp_drawing_styles
            .get_default_face_mesh_tesselation_style())
        mp_drawing.draw_landmarks(
            image=image,
            landmark_list=face_landmarks,
            connections=mp_face_mesh.FACEMESH_CONTOURS,
            landmark_drawing_spec=None,
            connection_drawing_spec=mp_drawing_styles
            .get_default_face_mesh_contours_style())
        mp_drawing.draw_landmarks(
            image=image,
            landmark_list=face_landmarks,
            connections=mp_face_mesh.FACEMESH_IRISES,
            landmark_drawing_spec=None,
            connection_drawing_spec=mp_drawing_styles
            .get_default_face_mesh_iris_connections_style())
    # Flip the image horizontally for a selfie-view display.
    cv2.imshow('MediaPipe Face Mesh', cv2.flip(image, 1))
    if cv2.waitKey(5) & 0xFF == 27:
      break
cap.release()

まずMediaPipeの公式で掲載されているPythonのコード(上のコード)を動かしてみると、下の画像のように顔のメッシュ状にランドマークが描かれます。

コード25行目のresults = face_mesh.process(image)で画像から顔のランドマークを取得して、33行目と40行目のdraw_landmarksで画像にメッシュ状にランドマークを描写。

2)目の開き具合を検知

ここからはFace Meshで取得した顔のランドマークを元にして目の開き具合を検知していきます。

今回の実装で必要なランドマークは目の周辺のものだけなので、まずは右目周辺のランドマークを取得するために、各ランドマークに紐づく番号を調べてみました。

https://raw.githubusercontent.com/google/mediapipe/master/mediapipe/modules/face_geometry/data/canonical_face_model_uv_visualization.png

右目周辺のランドマークに紐づく番号は33, 246, 7, 161, 163, 160, 144, 159, 145, 158, 153, 157, 154, 173, 155, 133だと分かりました。

次に左目周辺も同様に番号を取得して、左目と右目のランドマークを画像にドットで描写してみます。(参照サイト:how do I get the coordinates of face mash landmarks in mediapipe)

# 左目周辺と右目周辺のランドマークに紐づく番号を配列に入れる
eye_numbers = [33, 246, 7, 161, 163, 160, 144, 159, 145, 158, 153, 157, 154, 173, 155, 133, 362, 398, 382, 384, 381, 385, 380, 386, 374, 387, 373, 388, 390, 466, 249, 263]

if results.multi_face_landmarks:
     for face_landmarks in results.multi_face_landmarks:
       for i in eye_numbers:
         eye_landmark = face_landmarks.landmark[i]

         # ランドマークのx座標とy座標を取得
         x = eye_landmark.x
         y = eye_landmark.y
 
         shape = image.shape

         # 画像の幅と高さをそれぞれx座標とy座標に掛けて非正規化することで、画像上の顔に合わせてランドマークを描画できるようになる
         relative_x = int(x * shape[1])
         relative_y = int(y * shape[0])
         
         # ランドマークをドットで描画
         cv2.circle(image, (relative_x, relative_y), radius=1, color=(0, 255, 0), thickness=1)

元のコードのランドマークの描画周りの部分をこのコードに置き換えて動かすと、このように描画されました。

続いて、目の開き具合を検知するために、目のアスペクト比(EAR:Eye Aspect Ratio)という指標を計算。(参照サイト:Drowsiness detection with OpenCV)

Real-Time Eye Blink Detection using Facial Landmarks
crd_160 = (face_landmarks.landmark[160].x, face_landmarks.landmark[160].y)
crd_144 = (face_landmarks.landmark[144].x, face_landmarks.landmark[144].y)
crd_158 = (face_landmarks.landmark[158].x, face_landmarks.landmark[158].y)
crd_153 = (face_landmarks.landmark[153].x, face_landmarks.landmark[153].y)
crd_33 = (face_landmarks.landmark[33].x, face_landmarks.landmark[33].y)
crd_133 = (face_landmarks.landmark[133].x, face_landmarks.landmark[133].y)

A = dist.euclidean(crd_160, crd_144)
B = dist.euclidean(crd_158, crd_153)
C = dist.euclidean(crd_33, crd_133)

# 右目のEARを計算
right_eye_aspect_ratio = (A + B) / (2.0 * C)

上のコードは図のp1,p2,p3,p4,p5,p6を右目周辺のランドマークに紐づく番号と置き換えて、右目のアスペクト比を計算したものです。

これで目の開き具合を検知できるようになりました。

3)熟睡環境を作る

そして、アスペクト比が一定以上の数値より低い値であり続けた時に、言い換えると目が閉じっぱなしになっている時に、そのまま自然と眠れるように以下の2つを実行して熟睡しやすい環境を作ります。

・睡眠用BGMを流す
・SwitchBotを使って部屋のライトを消す

睡眠用BGMを流す

import webbrowser

webbrowser.open("再生したい動画のURL")
睡眠用BGMで検索

Youtubeで"睡眠用BGM"で検索して評価が高い動画をURLで指定して再生。(参照サイト:Pythonでyoutubeを再生する)

SwitchBotを使って部屋のライトを消す

SwitchBot(スイッチボット)とはすべてのスイッチとボタンを機械的に制御するスマートな小型で自動化されたデバイスで、家庭やオフィス内のデバイスのオン/オフを切り替えることができる

https://www.switchbot.jp/

前もってSwitchBotアプリの近くのボット>>デバイス情報でMACアドレスを調べておきます。

また、SwitchBotはBluetooth Low Energy通信規格(以下BLE)に準拠したデバイスであり、bluepyと呼ばれるPython用ライブラリから接続して利用できます。(参照:bluepyで始めるBluetooth Low Energy(BLE)プログラミング)

import binascii
from bluepy.btle import Peripheral

# BLE通信でSwitchBotに接続
p = Peripheral("SwitchBotのMACアドレス", "random")

# UUID(Universally Unique Identifier)を指定して、Serviceを取得
hand_service = p.getServiceByUUID("cba20d00-224d-11e6-9fb8-0002a5d5c51b")
hand = hand_service.getCharacteristics("cba20002-224d-11e6-9fb8-0002a5d5c51b")[0]

# 押して戻す Press:binascii.a2b_hex("570100")
# 押す Turn On:binascii.a2b_hex("570103")
# 戻す Turn Off:binascii.a2b_hex("570104")
hand.write(binascii.a2b_hex("570103"))

p.disconnect()

続いて、SwitchBotをライトのスイッチに取り付け、こちらのサイトに掲載されている上のコードを追記。

これで目が閉じっぱなしになった時に、睡眠用BGMを流し、部屋の電気を消すことができるようになりました。

4)「超熟睡システム」を実演

ここまで出来たら、「超熟睡システム」を動かしてみます。

短めの実演動画GIF

最後に、アスペクト比が一定以上の数値より低い値であり続けた時に、言い換えると目が閉じっぱなしになっている時にアラーム音を出すコードを追加して実演してみました~。

コード

フォルダ構成

  • sound

    • ei.mp3

  • mediapipe_detect_drowsiness.py

mediapipe_detect_drowsiness.py

import cv2
import mediapipe as mp
from PIL import Image, ImageDraw, ImageFont
import argparse
import pygame
from threading import Thread
import time
import binascii
from bluepy.btle import Peripheral
import webbrowser 
from mediapipe.python.solutions.face_mesh_connections import FACEMESH_TESSELATION
from mediapipe.python.solutions.face_mesh_connections import FACEMESH_CONTOURS

ap = argparse.ArgumentParser()
ap.add_argument("-ei", "--ei", type=str, default="sound/ei.mp3", help="path sound file")
args = vars(ap.parse_args())

def sound(path):
   pygame.init()
   pygame.mixer.music.load('sound/ei.mp3')
   pygame.mixer.music.play()
   time.sleep(2)
   pygame.mixer.music.stop()

ALARM_ON = False   
 
EYE_AR_THRESH = 0.3
EYE_AR_CONSEC_FRAMES = 40

COUNTER = 0
TOTAL = 0

eye_numbers = [33, 246, 7, 161, 163, 160, 144, 159, 145, 158, 153, 157, 154, 173, 155, 133, 362, 398, 382, 384, 381, 385, 380, 386, 374, 387, 373, 388, 390, 466, 249, 263]
crd_list = []

drawing_spec = mp_drawing.DrawingSpec(thickness=1, circle_radius=1)
cap = cv2.VideoCapture(0)

with mp_face_mesh.FaceMesh(
   min_detection_confidence=0.5,
   min_tracking_confidence=0.5) as face_mesh:
 while cap.isOpened():
   success, image = cap.read()

   if not success:
     print("Ignoring empty camera frame.")
     continue
   
   image = cv2.cvtColor(cv2.flip(image, 1), cv2.COLOR_BGR2RGB) 
   image.flags.writeable = False

   results = face_mesh.process(image)

   image.flags.writeable = True
   image = cv2.cvtColor(image, cv2.COLOR_RGB2BGR)

   if results.multi_face_landmarks:
     for face_landmarks in results.multi_face_landmarks:
       for i in eye_numbers:
         eye_landmark = face_landmarks.landmark[i]
         x = eye_landmark.x
         y = eye_landmark.y
 
         crd_i = (x, y)
         crd_list.append(crd_i)
 
         shape = image.shape
         relative_x = int(x * shape[1])
         relative_y = int(y * shape[0])
 
         cv2.circle(image, (relative_x, relative_y), radius=1, color=(0, 255, 0), thickness=1)
         crd_i = (face_landmarks.landmark[i].x, face_landmarks.landmark[i].y)
         
         A = dist.euclidean(crd_list[5], crd_list[6])
         B = dist.euclidean(crd_list[9], crd_list[10])
         C = dist.euclidean(crd_list[0], crd_list[15])
 
         D = dist.euclidean(crd_list[21], crd_list[22])
         E = dist.euclidean(crd_list[25], crd_list[26])
         F = dist.euclidean(crd_list[16], crd_list[31])

         left_eye_aspect_ratio = (A + B) / (2.0 * C)
         right_eye_aspect_ratio = (D + E) / (2.0 * F)
         ear = (left_eye_aspect_ratio + right_eye_aspect_ratio) / 2.0

         cv2.putText(image, "EAR: {:.2f}".format(ear), (10, 20), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (255, 255, 255), 2)

         if ear < EYE_AR_THRESH:
           COUNTER += 1

           if COUNTER == 10:
             ALARM_ON = True
             t = Thread(target=sound_ei, args=(args["ei"],))
             t.daemon = True
             t.start()

           elif COUNTER == EYE_AR_CONSEC_FRAMES:
             webbrowser.open("https://www.youtube.com/watch?v=hl-6pkwtd_s")  
             p = Peripheral("E1:18:91:8B:56:E2", "random")
             hand_service = p.getServiceByUUID("cba20d00-224d-11e6-9fb8-0002a5d5c51b")
             hand = hand_service.getCharacteristics("cba20002-224d-11e6-9fb8-0002a5d5c51b")[0]
             hand.write(binascii.a2b_hex("570103"))
             p.disconnect()

           elif COUNTER >= EYE_AR_CONSEC_FRAMES:
             cv2.putText(image, "Sleep", (10, 60), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 0, 255), 2)
           
         else:
           COUNTER = 0
           ALARM_ON = False

   cv2.imshow('MediaPipe FaceMesh', image)
   key = cv2.waitKey(1) & 0xFF
 
   if key == ord("q"):
     break

cap.release()

Youtubeで見る

この記事が気に入ったらサポートをしてみませんか?