AIY Voice Kit + Web カメラで顔認識(誰かそこにいる?)

 前回は Voice Kit + Web カメラで画像を表示しつつ、音声で写真を撮るということをやってみました。今回は Web カメラで撮っている画像の中に人がいるかどうかを顔認識によって判定してみたいと思います。

実装内容

 今回の実装内容としては、スクリプトを実行すると Web カメラ画像の表示を開始し、 "OK Google, is anyone there?"(そこに誰かいる?)と発話すると、画像内に人の顔があるかを検出し、顔があった場合は "Yes, someone is there." と言って画像内で検出された顔を枠で囲み、緑色のLEDを点灯させます。顔がない場合は "No one is there." と言って黄色のLEDを点灯させます。

f:id:akanuma-hiroaki:20180211091346j:plain:w450

OpenCV で顔検出

 今回顔検出には OpenCV を使用します。下記チュートリアルを参考にしました。

Haar Cascadesを使った顔検出 — OpenCV-Python Tutorials 1 documentation

 OpenCV のインストールは前回までで実施済みで使えるようになっていますが、 検出器を使うために github からリポジトリを clone します。検出アルゴリズムについては上記チュートリアルで詳しく紹介されていますので、ここでは割愛します。

pi@raspberrypi:~ $ git clone https://github.com/opencv/opencv.git
Cloning into 'opencv'...
remote: Counting objects: 221908, done.
remote: Compressing objects: 100% (5/5), done.
remote: Total 221908 (delta 0), reused 0 (delta 0), pack-reused 221903
Receiving objects: 100% (221908/221908), 438.92 MiB | 1.57 MiB/s, done.
Resolving deltas: 100% (154066/154066), done.
Checking out files: 100% (5734/5734), done.

 検出器は opencv/data/haarcascades に格納されています。

pi@raspberrypi:~ $ ls -l opencv/data/haarcascades
合計 9568
-rw-r--r-- 1 pi pi  341406  26 07:59 haarcascade_eye.xml
-rw-r--r-- 1 pi pi  601661  26 07:59 haarcascade_eye_tree_eyeglasses.xml
-rwxr-xr-x 1 pi pi  411388  26 07:59 haarcascade_frontalcatface.xml
-rwxr-xr-x 1 pi pi  382918  26 07:59 haarcascade_frontalcatface_extended.xml
-rw-r--r-- 1 pi pi  676709  26 07:59 haarcascade_frontalface_alt.xml
-rw-r--r-- 1 pi pi  540616  26 07:59 haarcascade_frontalface_alt2.xml
-rw-r--r-- 1 pi pi 2689040  26 07:59 haarcascade_frontalface_alt_tree.xml
-rw-r--r-- 1 pi pi  930127  26 07:59 haarcascade_frontalface_default.xml
-rw-r--r-- 1 pi pi  476825  26 07:59 haarcascade_fullbody.xml
-rw-r--r-- 1 pi pi  195369  26 07:59 haarcascade_lefteye_2splits.xml
-rw-r--r-- 1 pi pi   47775  26 07:59 haarcascade_licence_plate_rus_16stages.xml
-rw-r--r-- 1 pi pi  395320  26 07:59 haarcascade_lowerbody.xml
-rw-r--r-- 1 pi pi  828514  26 07:59 haarcascade_profileface.xml
-rw-r--r-- 1 pi pi  196170  26 07:59 haarcascade_righteye_2splits.xml
-rw-r--r-- 1 pi pi   75482  26 07:59 haarcascade_russian_plate_number.xml
-rw-r--r-- 1 pi pi  188506  26 07:59 haarcascade_smile.xml
-rw-r--r-- 1 pi pi  785817  26 07:59 haarcascade_upperbody.xml

 今回はこの中から一番ベーシックな、 haarcascade_frontalface_default.xml を使用しますので、任意のディレクトリにコピーしておきます。

 それではコードを実装します。今回の主な変更としては、カメラ画像の表示を更新している無限ループの中に、下記の処理を追加しています。 "OK Google, is anyone there?" という発話を検知したら self.face_detection フラグを True にすることでこの処理が実行されます。

elif self.face_detection:
    faces = None
    for i in range(0, 30):
        ret, frame = cap.read()
        cv2.imshow('frame', frame)
        gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
        faces = self.face_cascade.detectMultiScale(gray, 1.3, 5)
        if len(faces) != 0:
            break
        cv2.waitKey(1)

    if len(faces) == 0:
        GPIO.output(MyAssistant.GPIO_LED_YELLOW, GPIO.HIGH)
        self.print_and_say('No one is there.')
    else:
        for (x, y, w, h) in faces:
            frame = cv2.rectangle(frame, (x, y), (x+w, y+h), (255, 0, 0), 2)
        cv2.imshow('frame', frame)
        GPIO.output(MyAssistant.GPIO_LED_GREEN, GPIO.HIGH)
        self.print_and_say('Yes, someone is there.')
        cv2.waitKey(5000)

    self.face_detection = False
    GPIO.output(MyAssistant.GPIO_LED_GREEN, GPIO.LOW)
    GPIO.output(MyAssistant.GPIO_LED_YELLOW, GPIO.LOW)

 顔検出処理はキャプチャした画像に対して detectMultiScale() メソッドを実行することで行いますが、一回の処理だけだと発話直後の1フレームに対してのみの検出になるので、少し幅を持たせるために30回処理を行い、その間に顔が検出されればそこに人がいるものとしています。また、グレースケール画像に対して行った方が早いようなので、カメラ画像をグレースケール画像に変換し手から detectMultiScale() メソッドを実行しています。

for i in range(0, 30):
    ret, frame = cap.read()
    cv2.imshow('frame', frame)
    gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
    faces = self.face_cascade.detectMultiScale(gray, 1.3, 5)
    if len(faces) != 0:
        break
    cv2.waitKey(1)

 そして顔が検出された場合は検出された部分を青い枠で囲って表示し、緑色のLEDを点灯させます。 cv2.rectangle() メソッドでは矩形の領域の2点の座標を引数に指定しますが、 detectMultiScale() メソッドの戻り値は1点の座標と横幅、高さなので、1点の座標に横幅と高さを加えたものをもう1点の座標として指定しています。

for (x, y, w, h) in faces:
    frame = cv2.rectangle(frame, (x, y), (x+w, y+h), (255, 0, 0), 2)
cv2.imshow('frame', frame)
GPIO.output(MyAssistant.GPIO_LED_GREEN, GPIO.HIGH)

 処理が終わったらフラグを False に戻して LED も消灯しておきます。

self.face_detection = False
GPIO.output(MyAssistant.GPIO_LED_GREEN, GPIO.LOW)
GPIO.output(MyAssistant.GPIO_LED_YELLOW, GPIO.LOW)

 上記コードを含むスクリプト全体を下記に記載しておきます。

#!/usr/bin/env python3

import sys
import time
import cv2
import threading
from datetime import datetime

import aiy.assistant.auth_helpers
import aiy.audio
import aiy.voicehat
from google.assistant.library import Assistant
from google.assistant.library.event import EventType

import RPi.GPIO as GPIO

class MyAssistant:
    GPIO_LED_GREEN = 2
    GPIO_LED_YELLOW = 3

    def __init__(self):
        self.credentials = aiy.assistant.auth_helpers.get_assistant_credentials()
        self.status_ui = aiy.voicehat.get_status_ui()

        GPIO.setmode(GPIO.BCM)
        GPIO.setup(MyAssistant.GPIO_LED_GREEN, GPIO.OUT)
        GPIO.setup(MyAssistant.GPIO_LED_YELLOW, GPIO.OUT)

        self._run_event_task = threading.Thread(target = self._run_event)
        self._show_video_task = threading.Thread(target = self._show_video)

        self.face_cascade = cv2.CascadeClassifier('/home/pi/AIY-projects-python/src/examples/voice/haarcascade_frontalface_default.xml')
        self.take_photo = False
        self.face_detection = False
        self.stop = False

    def print_and_say(self, text):
        print(text)
        aiy.audio.say(text)

    def start(self):
        self._show_video_task.start()
        self._run_event_task.start()

    def _show_video(self):
        cap = cv2.VideoCapture(0)

        while(True):
            ret, frame = cap.read()

            cv2.imshow('frame', frame)
            if self.take_photo:
                now = datetime.now()
                filename = 'capture_%s.jpg' % now.strftime('%Y%m%d_%H%M%S')

                if cv2.imwrite(filename, frame):
                    self.print_and_say('I took nice one.')
                    print('Captured Image: %s' % filename)
                    time.sleep(5)
                else:
                    self.print_and_say("I couldn't take a picture.")

                self.take_photo = False

            elif self.face_detection:
                faces = None
                for i in range(0, 30):
                    ret, frame = cap.read()
                    cv2.imshow('frame', frame)
                    gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
                    faces = self.face_cascade.detectMultiScale(gray, 1.3, 5)
                    if len(faces) != 0:
                        break
                    cv2.waitKey(1)

                if len(faces) == 0:
                    GPIO.output(MyAssistant.GPIO_LED_YELLOW, GPIO.HIGH)
                    self.print_and_say('No one is there.')
                else:
                    for (x, y, w, h) in faces:
                        frame = cv2.rectangle(frame, (x, y), (x+w, y+h), (255, 0, 0), 2)
                    cv2.imshow('frame', frame)
                    GPIO.output(MyAssistant.GPIO_LED_GREEN, GPIO.HIGH)
                    self.print_and_say('Yes, someone is there.')
                    cv2.waitKey(5000)

                self.face_detection = False
                GPIO.output(MyAssistant.GPIO_LED_GREEN, GPIO.LOW)
                GPIO.output(MyAssistant.GPIO_LED_YELLOW, GPIO.LOW)

            cv2.waitKey(1)

            if self.stop:
                break

        cap.release()
        cv2.destroyAllWindows()
        print('Stop showing video.')

    def _run_event(self):
        with Assistant(self.credentials) as assistant:
            self._assistant = assistant
            for event in assistant.start():
                self._process_event(event)

    def _process_event(self, event):
        if event.type == EventType.ON_START_FINISHED:
            self.status_ui.status('ready')
            aiy.audio.say("OK, I'm ready.")
            if sys.stdout.isatty():
                print('Say "OK, Google" then speak.')

        elif event.type == EventType.ON_RECOGNIZING_SPEECH_FINISHED and event.args:
            print('You said: %s' % event.args['text'])
            text = event.args['text'].lower()

            if text == 'take a picture':
                self._assistant.stop_conversation()
                GPIO.output(MyAssistant.GPIO_LED_GREEN, GPIO.LOW)
                GPIO.output(MyAssistant.GPIO_LED_YELLOW, GPIO.LOW)

                aiy.audio.say('OK, 3')
                GPIO.output(MyAssistant.GPIO_LED_GREEN, GPIO.HIGH)
                time.sleep(1)

                aiy.audio.say('2')
                GPIO.output(MyAssistant.GPIO_LED_GREEN, GPIO.LOW)
                GPIO.output(MyAssistant.GPIO_LED_YELLOW, GPIO.HIGH)
                time.sleep(1)

                aiy.audio.say('1')
                GPIO.output(MyAssistant.GPIO_LED_GREEN, GPIO.HIGH)
                time.sleep(1)

                self.take_photo = True

                GPIO.output(MyAssistant.GPIO_LED_GREEN, GPIO.LOW)
                GPIO.output(MyAssistant.GPIO_LED_YELLOW, GPIO.LOW)

            elif text == 'is anyone there':
                self._assistant.stop_conversation()
                self.face_detection = True

            elif text == 'goodbye':
                self.status_ui.status('stopping')
                self._assistant.stop_conversation()
                self.stop = True
                aiy.audio.say('Goodbye. See you again.')
                print('Stop processing event.')
                sys.exit()

        elif event.type == EventType.ON_CONVERSATION_TURN_STARTED:
            self.status_ui.status('listening')
        elif event.type == EventType.ON_END_OF_UTTERANCE:
            self.status_ui.status('thinking')
        elif event.type == EventType.ON_CONVERSATION_TURN_FINISHED:
            self.status_ui.status('ready')
        elif event.type == EventType.ON_ASSISTANT_ERROR and event.args and event.args['is_fatal']:
            sys.exit(1)

    def main(self):
        self.status_ui.status('starting')
        self.start()

if __name__ == '__main__':
    sample = MyAssistant()
    sample.main()

動作確認

 それでは動作確認をしてみます。下記の動画のようにだいたい想定した通りに動作してくれました。

まとめ

 今回は OpenCV を使うことで簡単に顔認識を行うことができました。調べてみると OpenCV 以外にも最近は dlib を使う方が精度が高くて良いようなのですが、 Raspberry Pi 3 Model B の環境で試した限りでは OpenCV と比べて処理が遅く、リアルタイムの顔認識には使えませんでした。ただパフォーマンスの改善についてはまだあまり調べられていないので、ビルドの仕方などによって改善する可能性はあるかと思います。顔認識では精度とパフォーマンスの両方が重要になってくると思いますので、もう少し dlib についても調べてみたいと思います。また、 OpenCV についても各検出器の処理内容や、元画像とグレースケール変換画像への処理によるパフォーマンスの違いなども気になるところではあるので、気が向いたら調べてみようと思います。