Voice Kit で顔認識をトリガーにして対話する

 前回は音声によるリクエストをトリガーにして、カメラ画像からの顔認識結果を返しましたが、今回は常に顔認識処理を続行し、顔が検出された場合はそれをトリガーにして簡単に対話する処理を実装してみたいと思います。

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

今回の実装内容

 今回の具体的な実装内容としては、ドリンクサービスコーナーでの簡単な受付処理を想定し、カメラで顔を検出した場合はユーザに飲みたいものを訪ね、ユーザの回答に対してメッセージを返すというものです。

 ちなみに今回は音声認識のインタフェースに Google Assistant ではなく Cloud Speech を使用しますので、下記サイトで紹介されているような事前のアカウントの準備が必要になります。内容については下記サイトを参照いただくこととしてここでの説明は割愛します。

aiyprojects.withgoogle.com

コード全体

 まずは今回実装したコードの全体を掲載しておきます。

#!/usr/bin/env python3

import sys
import cv2

import aiy.audio
import aiy.cloudspeech
import aiy.voicehat

class DrinkProvider:
    def __init__(self):
        self.face_cascade = cv2.CascadeClassifier('/home/pi/AIY-projects-python/src/examples/voice/haarcascade_frontalface_default.xml')

        self.menu_list = ['coffee', 'tea', 'milk', 'orange juice', 'beer']
        self.recognizer = aiy.cloudspeech.get_recognizer()
        for menu in self.menu_list:
            self.recognizer.expect_phrase(menu)
        aiy.audio.get_recorder().start()

        self.status_ui = aiy.voicehat.get_status_ui()

    def main(self):
        while(True):
            cap = cv2.VideoCapture(0)
            self.status_ui.status('ready')
            while(True):
                ret, frame = cap.read()
                gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
                faces = self.face_cascade.detectMultiScale(gray, 1.3, 5)
                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)
                if cv2.waitKey(1) & 0xff == ord('q'):
                    cap.release()
                    cv2.destroyAllWindows()
                    sys.exit(1)
                if len(faces) > 0:
                    break

            self.status_ui.status('listening')
            aiy.audio.say('Hi! What would you like to drink?')
            for i, menu in enumerate(self.menu_list):
                if i == len(self.menu_list) - 1:
                    aiy.audio.say('or')
                aiy.audio.say(menu)

            text = self.recognizer.recognize()
            if not text:
                aiy.audio.say('Sorry, I did not hear you.')
            elif text not in self.menu_list:
                print('You said: %s' % text)
                aiy.audio.say('Sorry, we can not provide it.')
            else:
                aiy.audio.say("Sure. We'll brought you a tasty %s. Please wait a sec!" % text)

            cv2.waitKey(3000)
            cap.release()

if __name__ == '__main__':
    provider = DrinkProvider()
    provider.main()

コンストラクタ

 コンストラクタではまず前回同様に OpenCV の検出器を初期化しています。

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

 そしてユーザからの回答を受け付けるための準備です。今回はあらかじめ用意したメニューの中から希望のドリンクを選択してもらう形なので、選択肢をリストに保持しておきます。そして Cloud Speech の API を利用するための recoginzer を取得し、ユーザからの回答内容が認識されやすくなるよう、期待するフレーズを expect_phrase() メソッドで登録しておきます。

self.menu_list = ['coffee', 'tea', 'milk', 'orange juice', 'beer']
self.recognizer = aiy.cloudspeech.get_recognizer()
for menu in self.menu_list:
    self.recognizer.expect_phrase(menu)
aiy.audio.get_recorder().start()

 そして最後に状態に応じて筐体上部のボタンのLEDの点灯状態を変更するために、 status_ui を取得しておきます。

self.status_ui = aiy.voicehat.get_status_ui()

顔認識処理

 main() メソッドは全体を無限ループで繰り返していますが、その最初でもう一つ無限ループを用意し、カメラ画像から顔が検出されるまで顔認識処理を繰り返すようにします。顔認識処理の内容は前回までと同様で、キーボードから q が入力されたらカメラのリソースを解放してプログラムを終了するようにしています。筐体のLEDの状態は顔認識の無限ループに入る前に ready の状態にしておきます。

cap = cv2.VideoCapture(0)
self.status_ui.status('ready')
while(True):
    ret, frame = cap.read()
    gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
    faces = self.face_cascade.detectMultiScale(gray, 1.3, 5)
    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)
    if cv2.waitKey(1) & 0xff == ord('q'):
        cap.release()
        cv2.destroyAllWindows()
        sys.exit(1)
    if len(faces) > 0:
        break

入力プロンプト

 カメラ画像から顔が検出されたらループを抜け、ユーザに回答を促すための発話を行います。選択肢としてメニューのリストの内容も読み上げ、リストの最後の要素とその前の要素の間では英語っぽく or を挟むようにしています。

self.status_ui.status('listening')
aiy.audio.say('Hi! What would you like to drink?')
for i, menu in enumerate(self.menu_list):
    if i == len(self.menu_list) - 1:
        aiy.audio.say('or')
    aiy.audio.say(menu)

音声認識処理

 入力を促す発話の後はユーザの回答を待ち受けます。 recognizer の recognize() メソッドで入力を受け付けてテキスト化します。回答がなかった場合や回答内容がメニューリストにない場合はエラー時の固定回答を返し、メニューリストに含まれるものが回答だった場合はそれを埋め込んだ形の回答を返して3秒待って処理を終了し、再び main() メソッド全体のループに戻ります。

text = self.recognizer.recognize()
if not text:
    aiy.audio.say('Sorry, I did not hear you.')
elif text not in self.menu_list:
    print('You said: %s' % text)
    aiy.audio.say('Sorry, we can not provide it.')
else:
    aiy.audio.say("Sure. We'll brought you a tasty %s. Please wait a sec!" % text)

cv2.waitKey(3000)
cap.release()

動作確認

 それではスクリプトを実行してみます。実行してみた様子は下記の動画の通りで、ひとまず期待した通りに動作しているようです。

まとめ

 今回は Google Assistant ではなく Cloud Speech を使ってシンプルな対話処理を実装してみました。hotword 以外をトリガーとした処理を組み合わせることで色々用途は広がりそうです。ただこれだと単語での回答に対してのシンプルな返答のみなので、次回は Dialogflow を使ってもっと対話的な処理を実装してみたいと思います。