ラズパイで作れるよ

ラズパイでSiri風スマートスピーカー作ってみた

スポンサーリンク

どうも。まっきーです!ラズパイで、Siri風のスマートスピーカーを作成してみました。

とりあえずどんなものか説明するのは大変なので、完成したものをご覧ください。

こんな感じで、言った言葉に反応して何かしらのアクションをするという仕組みです。実際、Siriの足元にも及ばないですが、何かを作った!という達成感は得られるのではないでしょうか。

この記事を見るだけで実装できるように説明していきます!詳しく知りたい方は、各機能の参考リンクも貼っておくのでそちらを参考にしてください!

目的

作成する前に立てた目的は、

話しかけたことに反応して写真撮影などをしてくれる機能

の実装です。

今まで学習してきた様々な機能を使用したいと考えました。

学習してきた内容
  • Juliusを用いた音声認識
  • OpenJtalkによる音声発信
  • OpenCvによる顔認識
  • LED点灯・消灯
  • DHT11 を用いた温湿度測定

音声認識を主軸とし、これらの機能全てを盛り込みました(笑)

使用したもの

今回使用したものを以下に記載します。基本的にはAmazonで手に入ります。スピーカーは100均で買いました。

  • RaspberryPi 3B+
  • ラズパイカメラモジュール
  • USBマイク
  • USBスピーカー(100均で300円で買えます)
  • LED1つ
  • DHT11 温湿度センサー
  • ジャンパー線5本(オス・メス)
  • 抵抗(1kΩ)

全体フロー図

全体のフロー図です。

特定の言葉に反応して各機能を開始する仕組みにしました。では実際に作成していきます!

スマートスピーカー作成の手順

ディレクトリ構想

ディレクトリの階層は以下のようになっています。

$ tree

SiriPJT
│
├── FacialRecognition
│   ├── datasets
│   │   ├── User.1.1.jpg
│   │   ├── User.1.2.jpg
│   │   ├── User.1.3.jpg
│   │   :
│   │   (他の人を付け加えるならここに追加される)
│   ├── face_cap.py#写真を撮る
│   ├── train.py#訓練する
│   ├── face_detect.py#顔を認識する
│   ├── haarcascade_frontalface_default.xml#顔を取るための特徴量(OpenCvからダウンロード)
│   └── trainer
│       └── trainer.yml#訓練後のデータ
├── julius#音声認識エンジン(過去記事参照)
│   ├── julius-4.5
│   └── julius-kit
│        └── dictation-kit-4.5
├── dht11#温湿度測定プログラム
│   └── __init__.py
├── main.py#メインプログラム
├── led.py#LED点灯のクラス
├── temp_humi.py#温湿度測定
├── date.py#日付測定
├── camera.py#カメラ撮影
├── julius.py#ジュリアスを起動
├── voice.py#音声を出力
└── open_jtalk.wav#音声ファイル作成時に作られます

少し複雑ですが簡単に言うと、「SiriPJT」というディレクトリの中に

  • FacialRecognition 顔認識のためのディレクトリ
  • Julius Juliusを使用するためのディレクトリ
  • dht11 温湿度のためのディレクトリ
  • その他機能ごとのスクリプトファイル

が入っているということです。それぞれのソースコードは後述します。

回路図

LEDとDHT11で簡単な回路を作成します。

必要なモジュール・ライブラリ等のインストール

ディレクトリ移動があるのはJuliusのみです。その他はpi直下で行ってください。

音声発信・音声認識はマイクやスピーカーの設定も必要ですので、以下の2つを参照することをおすすめします。

Julius

mkdir julius
cd julius
 
#Juliusバージョン4.5をダウンロード
wget https://github.com/julius-speech/julius/archive/4.5.tar.gz
tar xvzf 4.5.tar.gz
cd julius-4.5 
#Makeファイルを作成
./configure --with-mictype=alsa 
#コンパイル
make
#インストール
sudo make install

#Juliusディレクトリに戻る
cd ..
#Julius-kitディレクトリを作成
mkdir julius-kit
#Julius-kitに移動
cd julius-kit
#ディクテーションキットダウンロード・解凍
wget https://osdn.net/dl/julius/dictation-kit-4.5.zip
unzip dictation-kit-4.5.zip

OpenJTalk

sudo apt-get update #アップデート
sudo apt-get install open-jtalk #インストール
#推奨されているパッケージ
#音声合成のエンジン、辞書、音声データ
sudo apt-get install open-jtalk open-jtalk-mecab-naist-jdic hts-voice-nitech-jp-atr503-m001

OpenCV

sudo apt update 
sudo apt upgrade
#必要なライブラリをインストール 
sudo apt-get install libatlas-base-dev
sudo apt-get intall libjasper-dev
sudo apt-get intall libqtgui4
sudo apt-get intall python3-pyqt5
sudo apt install libqt4-test
#バージョン指定でOpenCVをインストール 
sudo pip3 install opencv-python==4.1.0.25

カスケード分類器のインストールは図を使って説明していますので「ラズパイ OpenCVインストール方法」をご覧ください。

GPIO・DHT11

#GPIO制御のモジュールインストール
sudo pip3 install RPi.GPIO
#DHT11の元ソースコードをクローン(複製)
git clone https://github.com/szazo/DHT11_Python.git

ソースコード一覧

全て表示すると長くなるので、main.py以外のファイルは折りたたんであります。クリックしたら見ることができます。

julius.py

[su_accordion class=""]
[su_spoiler title="julius.py" open="no" style="default" icon="plus" anchor="" anchor_in_url="no" class=""]

import socket
import time
import subprocess
import voice

def julius_on():
    julius = ['julius']
    jconf1 = ['-C','julius/julius-kit/dictation-kit-4.5/main.jconf']#jconf設定ファイルを読み込む.ファイルの内容がこの場所に展開される.
    jconf2 = ['-C','julius/julius-kit/dictation-kit-4.5/am-gmm.jconf']
    option = ['-module','-demo']
    cmd = julius + jconf1 + jconf2 + option
    subprocess.Popen(cmd,stdin=subprocess.PIPE)
    time.sleep(3)

[/su_spoiler]

[/su_accordion]

Juliusのモジュールモードを起動するための関数です。

詳しくは「音声認識を使ってラズパイと会話をする(Julius,OpenJtalk使用)」を参考にしてください。

voice.py

[su_accordion class=""]
[su_spoiler title="voice.py" open="no" style="default" icon="plus" anchor="" anchor_in_url="no" class=""]

#coding: utf-8
import subprocess

def jtalk(t):
    open_jtalk=['open_jtalk']
    mech=['-x','/var/lib/mecab/dic/open-jtalk/naist-jdic']
    htsvoice=['-m','/usr/share/hts-voice/mei/mei_normal.htsvoice']
    speed=['-r','1.0']
    outwav=['-ow','open_jtalk.wav']
    cmd=open_jtalk+mech+htsvoice+speed+outwav
    c = subprocess.Popen(cmd,stdin=subprocess.PIPE)
    c.stdin.write(t.encode())
    c.stdin.close()
    c.wait()
    aplay = ['aplay','-q','-Dplughw:2,0','open_jtalk.wav']
    wr = subprocess.Popen(aplay)

[/su_spoiler]

[/su_accordion]

音声を出力する関数です。引数にt(テキスト)をとり、そのテキストの文字を読み上げます。

詳しくは「ラズパイで音声を出力する方法(OpenJTalkとAquesTalk)」を参考にしてください。

led.py

[su_accordion class=""]
[su_spoiler title="led.py" open="no" style="default" icon="plus" anchor="" anchor_in_url="no" class=""]

#coding: utf-8
import RPi.GPIO as GPIO#PythonでRaspberry Piの制御するためのライブラリ
 
class LedOnOff:
    def __init__(self,gpio:int):#ピン番号を指定
        self.gpio = gpio
        GPIO.setwarnings(False)
        GPIO.setmode(GPIO.BCM) #GPIOへアクセスする番号をBCMの番号で指定することを宣言。
        GPIO.setup(gpio, GPIO.OUT)

    def ledon(self):#LEDをオンにするメソッド
        print("a")
        GPIO.output(self.gpio, GPIO.HIGH)#出力 3.3V

    def ledoff(self):#LEDをオフにするメソッド
        GPIO.output(self.gpio, GPIO.LOW)#出力 0V
        
    def GpioClean(self):#GPIOをクリアするメソッド
        GPIO.cleanup(self.gpio)
        

[/su_spoiler]

[/su_accordion]

LEDを点灯・消灯させるためのクラスです。ただ光らせたり、消したりするだけなのでクラスにする必要は0ですが、自分の練習のためにクラスにしてみました。

LEDの点灯消灯については「ラズパイでLチカをやってみよう(Python)」を参考にしてください。

temp_humi.py

[su_accordion class=""]
[su_spoiler title="temp_humid.py" open="no" style="default" icon="plus" anchor="" anchor_in_url="no" class=""]

#coding: utf-8
import RPi.GPIO as GPIO
import dht11
import time
import datetime

# initialize GPIO
GPIO.setwarnings(True)
GPIO.setmode(GPIO.BCM)

def dht(pin):   
    instance = dht11.DHT11(pin)
    
    while True:
        result = instance.read()
        if result.is_valid():
            text = "温度は%d度、湿度は%d%%です" % (result.temperature,result.humidity)
            return text

[/su_spoiler]

[/su_accordion]

温度と湿度をリアルタイムで取得する関数です。

戻り値は温度と湿度の入ったテキストです。

温湿度センサーについて詳しく知りたい方は「ラズパイとDHT11で温湿度測定 with Python」を参考にしてください。

date.py

[su_accordion class=""]
[su_spoiler title="date.py" open="no" style="default" icon="plus" anchor="" anchor_in_url="no" class=""]

#coding: utf-8
import datetime

def rdate():#日付を返す
    d = datetime.datetime.now()
    text = '%s月%s日%s時%s分です' % (d.month, d.day, d.hour, d.minute)
    return text

[/su_spoiler]

[/su_accordion]

今日の日付と時間を返す関数です。

戻り値は月、日付、時間、分の書いてあるテキストです。

camera.py

[su_accordion class=""]
[su_spoiler title="camera.py" open="no" style="default" icon="plus" anchor="" anchor_in_url="no" class=""]

#coding: utf-8
import picamera
import datetime
import voice
import cv2
import time

def shoot(): # カメラ初期化 
    with picamera.PiCamera() as camera:
        # 解像度の設定 
        camera.resolution = (1024, 768)
        # 撮影の準備 camera.start_preview() 
        # 準備している間、少し待機する
        voice.jtalk("撮影します 3,2,1")
        time.sleep(3)
        # ファイル名用の日付を取得
        date = datetime.datetime.now()
        # 撮影して指定したファイル名で保存する
        filename = 'pi'+str(date)+'.jpg'
        camera.capture(filename)
        print('complete')
 

[/su_spoiler]

[/su_accordion]

カメラを撮影する関数です。撮影するまでに3,2,1という音声を発信するためにvoice.pyをインポートして使用しています。

撮影された画像はSiriPJTの下に「pi日付.jpg」という形で保存されます。

facedetect.py

[su_accordion class=""]
[su_spoiler title="face_detect.py" open="no" style="default" icon="plus" anchor="" anchor_in_url="no" class=""]

import cv2
import numpy as np
import os
def recog():        
    recognizer = cv2.face.LBPHFaceRecognizer_create()
    recognizer.read('FacialRecognition/trainer/trainer.yml')#watchout for dir
    cascadePath = "FacialRecognition/haarcascade_frontalface_default.xml"
    faceCascade = cv2.CascadeClassifier(cascadePath);
    font = cv2.FONT_HERSHEY_SIMPLEX
    #iniciate id counter
    id = 0
    # names related to ids: example ==> Marcelo: id=1,  etc
    names = ['ちどりののぶさんです', 'まっきーさんです','小池鉄平さんです'] 
    # Initialize and start realtime video capture
    cam = cv2.VideoCapture(0)
    cam.set(3, 640) # set video widht
    cam.set(4, 480) # set video height
    # Define min window size to be recognized as a face
    minW = 0.1*cam.get(3)
    minH = 0.1*cam.get(4)
    while True:
        ret, img =cam.read()
        img = cv2.flip(img, -1) # Flip vertically
        gray = cv2.cvtColor(img,cv2.COLOR_BGR2GRAY)
        
        faces = faceCascade.detectMultiScale( 
            gray,
            scaleFactor = 1.2,
            minNeighbors = 5,
            minSize = (int(minW), int(minH)),
           )
        cv2.imshow('camera',img) 
        for(x,y,w,h) in faces:
            #cv2.rectangle(img, (x,y), (x+w,y+h), (0,255,0), 2)
            id, confidence = recognizer.predict(gray[y:y+h,x:x+w])
            if len(faces) > 0:
                
                # Check if confidence is less them 100 ==> "0" is perfect match 
                if (confidence < 100):
                    id = names[id]
                    confidence = "  {0}%".format(round(100 - confidence))
                    print(confidence)
                    cam.release()
                    cv2.destroyAllWindows()
                    return id,confidence
                else:
                    id = "わからないです"
                    confidence = "  {0}%".format(round(100 - confidence))
                    cam.release()
                    cv2.destroyAllWindows()
                    return id,confidence
                #cv2.putText(img, str(id), (x+5,y-5), font, 1, (255,255,255), 2)
                #cv2.putText(img, str(confidence), (x+5,y+h-5), font, 1, (255,255,0), 1)  
            
        #cv2.imshow('camera',img) 
        k = cv2.waitKey(10) & 0xff # Press 'ESC' for exiting video
        if k == 27:
            break
    # Do a bit of cleanup
    print("\n [INFO] Exiting Program and cleanup stuff")
    cam.release()
    cv2.destroyAllWindows()

[/su_spoiler]

[/su_accordion]

知っている顔か、知らない顔かを判別して、その人の名前を返す関数です。

戻り値は、名前の入ったテキストと、信頼度を表す数字の2つです。(id:名前,conf:信頼度)

今回は私まっきーと千鳥のノブ、小池徹平をnamesリストに格納しています。

idと対応付けて名前を設定する必要があります。

顔を識別するためには

  • 写真撮影(face_cap.py)
  • 訓練データの作成(train.py)

の操作が必要です。少しだけ他のものと比べて難易度が上がりますので「ラズパイカメラとOpenCvを使って女優の顔認識してみた」を参照することをおすすめします。

main.py

#coding: utf-8
import socket
import julius
import date
import temp_humi as temp
import camera
import voice
from led import LedOnOff
from FacialRecognition import face_detect as detect

if __name__ == "__main__":
    julius.julius_on()
    led = LedOnOff(23)#LED点灯・消灯クラスのインスタンスを作成 GPIO番号を引数に取ります
    host = '127.0.0.1'   # juliusサーバーのIPアドレス
    port = 10500         # juliusサーバーの待ち受けポート
    data_size = 1024     # 受信データバイト数
    # socket通信でjuliusサーバーに接続(接続要求)
    with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
        s.connect((host, port))

        strtemp = ""#話した言葉を格納する変数
        fin_flag = False #話終わりのフラグ
 
        
        while True:
            # juliusサーバからデータ受信
            data = s.recv(data_size).decode('utf-8')

            for line in data.split('\n'):
                # 受信データから、<WORD>の後に書かれている言葉を抽出して変数に格納する。
                # <WORD>の後に、話した言葉が記載されている。
                index = line.find('WORD="')
                if index != -1:#認識した文字列があれば
                    #strtempに話した言葉を格納
                    strtemp = strtemp + line[index+6:line.find('"',index+6)]
                # 受信データに</RECOGOUT>'があれば、話終わり ⇒ フラグをTrue    
                if '</RECOGOUT>' in line:
                    fin_flag = True
                if '<RECOGFAIL/> ' in line:
                    print("聞き取れませんでした")
                    fin_flag = False

            if fin_flag == True:
                if "つけて" in strtemp:
                    led.ledon()
                elif "消して" in strtemp:
                    led.ledoff()
                elif "温度" in strtemp:
                    voice.jtalk(temp.dht(14))#戻り値のテキストデータを使って音声出力
                elif "日付" in strtemp:
                    voice.jtalk(date.rdate())#戻り値のテキストデータを使って音声出力
                elif "写真" in strtemp:
                    camera.shoot()
                elif "誰です" in strtemp:
                    name,conf = detect.recog()##戻り値のテキストデータを使って音声出力
                    voice.jtalk(name)
                    print(conf)#信頼度
                else:
                    voice.jtalk("もう一度行ってください")
                fin_flag = False
                strtemp = ""
                

メイン関数の流れは以下の通りです

  1. 上記で作成した関数やクラスをインポートする
  2. 音声認識データを取得するため、Juliusのモジュールモードとの接続を行う
  3. 認識した音声に特定に文字が含まれているかを確認し、実行
  4. ループ

コードが複数あり、ごちゃごちゃしていますがifを使ってそれぞれ分岐させているだけです。

python3 main.py 

上記のコマンドを実行すると初めの動画のように動いてくれると思います。

動かない!という方はディレクトリが違ったりする可能性が高いです。カメラやUSBなどの物理的接続なども、エラーコードを見ながら確認していきましょう!

一番CPU占有率の高いと思われる「顔認識」は以下のような結果になりました。

結構使用していることがわかると思います。音声認識などは40%ほどの使用率でした。

反省点

それらしいものを作ることができたので、個人的には満足です!

改善点・問題点として

  • コードがぐちゃぐちゃなので、クラスなどを用いてまとめる必要がある
  • 顔認識の精度が低い(良くて40%)
  • あまり実用的ではない(連続で話しかけたパターンのテストを忘れた)

などが考えられます。

もし、「ここを直すと良い」などのご意見がありましたらコメントで教えて頂けると嬉しいです。

関連サイト

これらの記事に記載されていることを参照していただければ、コードの理解が深まると思います。

【音声認識(Julius)・音声発信(OpenJTalk)】

【顔認識(OpenCV)】

【LED、温湿度】

最後までありがとうございました!

次は実用的な監視カメラの作成をしていきたいと思っています!

スポンサーリンク

-ラズパイで作れるよ