オフィスでの居眠りを監視し、居眠りを検出すると空調の温度が下がって強制的に起こす、という空調管理システムが話題になっていました。
自分はよくなにかやりながら寝てしまう体質で、家でゲームやってたり本読んでるときでも寝てしまいます。眠い状態を経由せずに覚醒状態からいつのまにか寝てるので、我慢は無力です。なのでAI等の第三者が冷気とかでやさしく起こしてくれるならそれに越したことはないです。なので作ることにしました。
慶洋エンジニアリング(KEIYO) 居眠り防止 わき見運転防止 居眠りウォッチャー ひとみちゃん AN-S018
- 出版社/メーカー: ケイヨウ(Keiyo)
- メディア: Automotive
- この商品を含むブログを見る
居眠りを検出する
居眠りはどうやって検出したらよいのでしょうか。日経の記事には「まぶたを持ち上げようとするゆっくりとした動きを分析する」とありますが、なんか難しそうなので、シンプルに「一定時間以上目を閉じていたら寝ている」と判断することにしました。
目つむりの判定をネットで検索してもあまり情報がありませんでしたが、「まばたきの検出」にキーワードを変えると先行事例が見つかりました。こちらを真似してみます。
www.pyimagesearch.com
リンク先ではOpenCVという画像処理ライブラリを使用し、顔のパーツを検出しています。
Eye blink detection with OpenCV, Python, and dlib より引用
検出したパーツのうち目の形状に注目し、そのサイズの縦横比によってまばたきを検出しているようです。
Eye blink detection with OpenCV, Python, and dlib より引用
まばたきは一瞬ですが、判定時間を引き延ばせば、そのまま居眠りの検出もできそうです。
同じものを動かす
というわけでまずは元記事と同じものを動かしてみます。ちなみにOSはWindows10です。
このページを参考にAnacondaでPythonとOpenCVをインストールしました。
それから追加で必要なライブラリを入れます。
conda install scipy conda install --channel,https://conda.anaconda.org/pjamesjoyce,imutils conda install -c conda-forge dlib
環境構築は以上です。元記事にはソースコードが載っていませんが、メールアドレスを登録するとDLすることができます。それを適当な場所において実行するのですが、その前に…
vs = VideoStream(src=0).start() #<---これ # vs = VideoStream(usePiCamera=True).start() fileStream = False #<---これ
66行目と68行目のコメントアウトを外します。こうしておくとWebカメラが使用できるようになります。
起動してみます。
python detect_blinks.py --shape-predictor shape_predictor_68_face_landmarks.dat
いとも簡単にまばたきの検出ができました。すごい!
(オリジナルのコードだと目の枠は常に緑ですが、わかりやすいようにまばたき検出時は青くしています)
動画の後半で試していますが、縦横比で測定しているため、俯いたりメガネのフレームに目が重なったりすると、目を開けていても閉じていると判定されたりします。しかし俯いているときはだいたい居眠りしているときなので、今回のケースでは問題ないでしょう。
ちなみにうまく検出できない場合は、44行目のEYE_AR_THRESHの値を変えると、まばたきと判定される縦横比の閾値を変えられます。デフォルトは0.3ですが、僕の場合は2.2くらいでちょうどいい感じでした。
Arduinoとつなぐ
さて次に、この検出結果をパソコンの外に出すことを考えます。最終的にはエアコンを制御したいのですが、前段階としてとりあえずLEDをつけてみることにします。
ハードウェアはArduinoを使い、シリアル通信で制御します。
- 出版社/メーカー: スイッチサイエンス
- メディア: Personal Computers
- 購入: 2人 クリック: 15回
- この商品を含むブログを見る
僕の環境ではpyserialのインストールが必要でした。
conda install pyserial
自分は普段Pythonを使わないので、Arduinoと通信させるのも初めてです。とりあえずテスト用に適当に書いたコードを動かしてみます。
python側はこう
# -*- coding: utf-8 -*- import serial import time com_num = 'COM3' # Arduinoを繋いでるCOMポート番号 def main(): ser = serial.Serial(com_num, 9600, timeout=0) time.sleep(10) ser.write(b'1') time.sleep(1000) ser.write(b'0') ser.close() if __name__ == '__main__': main()
Arduino側はこうしました。
void setup(){ pinMode(10, OUTPUT); Serial.begin(9600); } void loop(){ int input; input = Serial.read(); if(input != -1 ){ switch(input){ case '0': digitalWrite(10, LOW); break; case '1': digitalWrite(10, HIGH); break; } } }
回路は、デジタルピン10番-LED-抵抗47Ω-GND、の順に接続します。
動かしてみると、LEDが一旦ついて、しばらくすると消えます。これでテストは成功です!
はまりポイントとしては、serial.Serial()したタイミングでArduinoが一度リセットされます。さいしょ誤動作かと思って修正を試みたのですが、どうも「そういうもの」らしいです。
続いて、さっきの顔検出のコードを改造していきましょう。一定時間目をつぶっているとLEDが点くように。つまり居眠り中はLEDが点くようにしました。
いい感じですね!
Arduinoから赤外線リモコンを発信
さいごに、Arduino側でエアコンのリモコンを実装します。このページにすべてが書いてあるので、やってみたい方は参考にしてください。
ちなみに設定温度を変更するには単に「上げ」「下げ」の信号ではなく、現在の設定温度なども加味して信号を送る必要があるみたいです(ちゃんと調べてないけど雰囲気的に)。
それはちょっと大変なので、今回は「起きているときは除湿運転」「寝たら冷房に切り替え」で気温を変えることにします。
#include <IRremote.h> IRsend irsend; #define ledpin 4 //highten unsigned int signalHighten[327] = {4436, 4448, 608, 1552, 580, 1580, 580, 1576, 584, 1576, 580, 500, 580, 504, 580, 1576, 580, 512, 580, 504, 580, 500, 580, 500, 580, 500, 580, 1576, 584, 1576, 580, 504, 576, 1592, 580, 500, 580, 504, 576, 504, 580, 500, 580, 500, 580, 1580, 580, 500, 580, 512, 580, 1580, 576, 1580, 580, 1580, 580, 1576, 580, 1580, 580, 500, 580, 1580, 580, 1592, 576, 504, 576, 504, 580, 500, 580, 500, 580, 1580, 580, 500, 580, 500, 580, 1592, 576, 1584, 576, 504, 552, 1604, 580, 504, 552, 528, 552, 528, 552, 528, 552, 544, 552, 528, 552, 528, 552, 528, 552, 528, 556, 524, 552, 532, 552, 532, 548, 1616, 552, 532, 552, 528, 552, 528, 552, 528, 552, 528, 552, 532, 552, 528, 552, 540, 552, 528, 552, 528, 552, 1608, 552, 1604, 556, 528, 528, 552, 528, 552, 528, 564, 528, 1632, 528, 552, 528, 556, 548, 1608, 528, 1628, 528, 556, 504, 576, 504, 580, 524, 8044, 4360, 4528, 504, 1656, 504, 1656, 504, 1652, 504, 1652, 508, 576, 504, 576, 504, 1652, 508, 588, 504, 576, 504, 576, 504, 576, 508, 572, 508, 1652, 508, 1652, 504, 576, 508, 1660, 508, 576, 504, 576, 504, 576, 508, 572, 508, 576, 504, 1652, 508, 572, 508, 584, 508, 1652, 508, 1652, 504, 1652, 508, 1652, 508, 1652, 504, 576, 504, 1652, 508, 1664, 508, 572, 508, 572, 508, 576, 504, 576, 508, 1648, 508, 576, 504, 576, 508, 1660, 508, 1652, 508, 572, 508, 1652, 528, 552, 532, 548, 532, 552, 528, 552, 528, 564, 532, 548, 532, 548, 532, 548, 532, 552, 532, 548, 532, 548, 532, 548, 536, 1636, 532, 548, 536, 544, 536, 548, 556, 524, 556, 524, 560, 520, 560, 520, 560, 532, 560, 524, 560, 520, 560, 1596, 560, 1600, 560, 520, 560, 524, 560, 520, 560, 532, 560, 1596, 564, 520, 560, 520, 560, 1596, 564, 1596, 564, 516, 564, 520, 560, 524, 564}; //lower unsigned int signalLower[327] = {4444, 4444, 588, 1568, 592, 1568, 588, 1572, 588, 1568, 588, 496, 588, 492, 588, 1568, 588, 508, 584, 496, 588, 492, 588, 492, 588, 492, 588, 1572, 588, 1568, 588, 496, 584, 1584, 588, 496, 584, 496, 584, 496, 588, 492, 588, 492, 588, 1572, 584, 496, 584, 508, 588, 1572, 584, 1572, 588, 1572, 584, 1576, 584, 1572, 588, 496, 584, 1572, 588, 1584, 584, 496, 584, 496, 584, 500, 580, 500, 580, 1576, 584, 500, 580, 500, 580, 1588, 584, 1576, 584, 496, 584, 1576, 580, 500, 584, 1572, 584, 500, 580, 500, 580, 512, 580, 500, 584, 500, 580, 500, 580, 500, 580, 500, 580, 500, 580, 1580, 580, 512, 580, 500, 580, 504, 580, 500, 580, 500, 580, 500, 580, 500, 580, 504, 576, 516, 580, 500, 580, 1576, 556, 528, 552, 1604, 556, 528, 552, 528, 552, 528, 552, 540, 556, 1604, 552, 1604, 556, 1604, 556, 1600, 556, 528, 552, 528, 556, 1600, 556, 1608, 556, 8012, 4408, 4480, 552, 1604, 556, 1604, 556, 1604, 528, 1628, 532, 552, 528, 552, 528, 1628, 528, 568, 504, 576, 508, 572, 528, 552, 528, 556, 504, 1652, 508, 1652, 504, 576, 508, 1660, 508, 576, 504, 576, 508, 572, 508, 572, 508, 576, 504, 1652, 508, 572, 508, 584, 508, 1652, 508, 1652, 504, 1652, 508, 1652, 508, 1648, 508, 576, 504, 1652, 508, 1664, 508, 572, 508, 572, 508, 576, 504, 576, 508, 1648, 508, 576, 504, 576, 508, 1660, 508, 1652, 508, 572, 508, 1652, 508, 572, 508, 1652, 504, 576, 508, 572, 508, 584, 508, 576, 504, 576, 528, 552, 528, 552, 532, 548, 532, 548, 532, 1628, 532, 560, 532, 548, 532, 552, 528, 552, 532, 548, 532, 548, 532, 548, 532, 552, 528, 564, 532, 548, 532, 1628, 532, 548, 532, 1624, 532, 552, 532, 548, 556, 524, 556, 536, 556, 1604, 556, 1600, 560, 1600, 560, 1600, 556, 524, 556, 524, 560, 1600, 556, 1604, 560}; void setup(){ pinMode(ledpin, OUTPUT); Serial.begin(9600); } void loop(){ int input; input = Serial.read(); if(input != -1 ){ switch(input){ case '0': digitalWrite(ledpin, LOW); irsend.sendRaw(signalLower, sizeof(signalLower) / sizeof(signalLower[0]), 38); delay(1000); break; case '1': digitalWrite(ledpin, HIGH); irsend.sendRaw(signalHighten, sizeof(signalHighten) / sizeof(signalHighten[0]), 38); delay(1000); break; } } }
Python側のコードですが、登録者だけがDLできるコードを基にしているため全部載せるとまずそうです。改造箇所だけ載せます。
最初のimportとその後の初期化
# import the necessary packages from scipy.spatial import distance as dist from imutils.video import FileVideoStream from imutils.video import VideoStream from imutils import face_utils import numpy as np import argparse import imutils import time import dlib import cv2 import serial import time PORT = 'COM3' ser = serial.Serial(PORT, 9600, timeout=1) time.sleep(10)
元ソースでいうと41行目から。
# define two constants, one for the eye aspect ratio to indicate # blink and then a second constant for the number of consecutive # frames the eye must be below the threshold EYE_AR_THRESH = 0.22 EYE_AR_CONSEC_FRAMES = 15 # initialize the frame counters and the total number of blinks COUNTER = 0 TOTAL = 0 IS_OPEN = False
元ソースでいうと103行目から。
# average the eye aspect ratio together for both eyes ear = (leftEAR + rightEAR) / 2.0 # compute the convex hull for the left and right eye, then # visualize each of the eyes leftEyeHull = cv2.convexHull(leftEye) rightEyeHull = cv2.convexHull(rightEye) if ear < EYE_AR_THRESH: cv2.drawContours(frame, [leftEyeHull], -1, (255, 0, 0), 1) cv2.drawContours(frame, [rightEyeHull], -1, (255, 0, 0), 1) else: cv2.drawContours(frame, [leftEyeHull], -1, (0, 255, 0), 1) cv2.drawContours(frame, [rightEyeHull], -1, (0, 255, 0), 1) COUNTER += 1 # check to see if the eye aspect ratio is below the blink # threshold, and if so, increment the blink frame counter if ear < EYE_AR_THRESH: if IS_OPEN == False: COUNTER = 0 elif COUNTER == EYE_AR_CONSEC_FRAMES: IS_OPEN = False ser.write(b'1') # otherwise, the eye aspect ratio is not below the blink # threshold else: # if the eyes were closed for a sufficient number of # then increment the total number of blinks if IS_OPEN == True: COUNTER = 0 elif COUNTER == EYE_AR_CONSEC_FRAMES: IS_OPEN = True ser.write(b'0') # draw the total number of blinks on the frame along with # the computed eye aspect ratio for the frame cv2.putText(frame, "Count: {}".format(COUNTER), (10, 30), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 0, 255), 2) cv2.putText(frame, "Eyes opened: {}".format(IS_OPEN), (10, 80), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 0, 255), 2) cv2.putText(frame, "EAR: {:.2f}".format(ear), (300, 30), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 0, 255), 2)
COUNTERまわりの処理を大きく変えているのは、誤検出で即座に切り替えてしまわないためです。
15フレームつづけて変更(瞼を閉じていた→開いた、またはその逆)を検出しないと、エアコンの信号は出しません。
回路的には、デジタルピン3番-赤外線LED-抵抗47Ω-GND、でいいのではないかと思います。
写真の回路は信号を強くしようとしてトランジスタを入れていますが、効果があったのかどうかよくわかりませんでした。
実際の動作風景はこんな感じです。
せっかく2カメで撮影したのにエアコンに見た目の変化がなくてガッカリですが、「ピッ」という音がするので切り替わっていることだけはわかると思います。
かくして、うちにも「居眠りをするとAI(?)が冷気で起こす」システムが配備されました!
使用感
数時間だけ運用してみたのですが、このレベルだと単に「起きた時に部屋が涼しい」だけであって、居眠り防止にはなりませんでした。
やはり目を閉じる以前、居眠りをしそうな段階で、空気を冷やさないとダメですね。
今回は全く使用していませんが不労所得が欲しいのでエアコンを操作できるIoTデバイスをいくつか貼っておきます。
スマートリモコン Nature Remo mini【Amazon Echo/Google Home対応】
- 出版社/メーカー: Nature, Inc.
- メディア: エレクトロニクス
- この商品を含むブログを見る
LS Mini【Amazon Echo/Google Home対応製品】
- 出版社/メーカー: Live Smart
- メディア: エレクトロニクス
- この商品を含むブログを見る
Ambi Climate2(アンビクライメイト2) IoTエアコン リモコン
- 出版社/メーカー: Ambi Labs
- メディア: エレクトロニクス
- この商品を含むブログを見る