読者です 読者をやめる 読者になる 読者になる

俺の備忘録

個人的な備忘録です。

Raspberry Piで音声認識してカメラで写真を撮ってメールで送ってみた

python Raspberry Pi

やったこと

Raspberry Piにマイク付きUSBカメラを接続し、特定の音声(キーワード)に反応し、カメラで写真を撮ってメール送信するシステムを作ってみた。

家で待ってる子供がカメラに向かって「パパ!」って叫んだら、子供の写真を撮って 仕事中のパパに届いたら楽しいかなと思って作った。

用意したもの

全てAmazonで購入。

Raspberry Pi 3本体
https://www.amazon.co.jp/gp/product/B01CSFZ4JG/ref=oh_aui_detailpage_o01_s01?ie=UTF8&psc=1

②USBカメラ(マイク付き)
ElecomのUCAM-C0220FBNBK
https://www.amazon.co.jp/gp/product/B00UZNLIBW/ref=oh_aui_detailpage_o01_s01?ie=UTF8&psc=1

③その他 SDカードや電源等も合わせて購入したけど、割愛。

システム構成

おおまかな流れは以下の通り。
- USBマイクから音声を入力し、音声認識ソフト(Julius)で解析
- Pythonプログラムで音声認識結果を取得し、特定のキーワードが含まれているか判定。
含まれているならUSBカメラで写真を撮る。
- 写真を撮ったらgmailで送る

f:id:magayengineer:20160706231649j:plain

写真撮影ソフトの準備

写真撮影にはfswebcamを利用。
ここを参考にした => http://seesaawiki.jp/w/renkin3q/d/Linux/fswebcam

①インストール

apt-get install fswebcam

②撮ってみる

fswebcam -r 640x480 <ファイル名> -F 10

-Fオプションでや-Sオプションをいじると、写真の明るさを調整できる。

マイクの準備

音声認識ソフトを使う際に、マイクはUSBマイクの優先度を上げておく必要があるので設定する。

①今の優先度の確認

sudo cat /proc/asound/modules

USBマイクの優先度が2番目ということがわかる

0 snd_bcm2835
1 snd_usb_audio

②優先度の変更 /etc/modprobe.d/alsa-base.confを以下のようにいじる。   なお、ファイルが存在しない場合は自分で作成する。

options snd_usb_audio index=0
options snd_bcm2835 index=1

終わったらリブートする

③USBマイクのカード番号とデバイス番号を調べる

arecord -l

カード 1: UCAMC0220F [UCAM-C0220F], デバイス 0: USB Audio [USB Audio]
  サブデバイス: 1/1
  サブデバイス #0: subdevice #0

カード1番の、デバイス0がUSBマイクということがわかる なお、認識されないことがよくある。その場合は、リブートを何回かすれば認識される。

④マイクの感度を設定

amixer sset Mic 4096 -c 0

4096は感度。最大値は使っているマイクで違う模様。 0はarecord -lで調べたデバイス番号を指定

⑤録音テスト

arecord -D plughw:1,0 -f cd test.wav

1,0はarecordで調べたカード番号とデバイス番号

ちょっとよくわかってないけど必要なこと

このモジュールを読まないと、音声認識ソフトがうまくうごかないっぽい。

/etc/modulesに以下を追記してリブート

snd-pcm-oss

音声認識ソフトの準備

julius(http://julius.osdn.jp/)を利用。 ここを見ながら設定した => http://feijoa.jp/laboratory/raspberrypi/julius/

①juliusをダウンロード&解凍&make&install

wget -O julius.tar.gz "https://osdn.jp/frs/redir.php?m=jaist&f=%2Fjulius%2F60273%2Fjulius-4.3.1.tar.gz"
tar -zxvf julius.tar.gz
cd julius
./configure
sudo make install

②ディクテーションキットと文法認識キットを取得

wget -O dictation-kit-v4.3.1-linux.tgz 'http://sourceforge.jp/frs/redir.php?m=jaist&f=%2Fjulius%2F60416%2Fdictation-kit-v4.3.1-linux.tgz'
wget -O grammar-kit-v4.1.tar.gz 'http://sourceforge.jp/frs/redir.php?m=osdn&f=%2Fjulius%2F51159%2Fgrammar-kit-v4.1.tar.gz'

解凍後、適当なフォルダに放り込んでおく

③単語帳作成 juliusに認識させたい単語を書いたファイルを作成する。 ここに書いた単語に、喋った内容が強制マッピングされる。 <= これはjuliusの動作設定次第。

dictionary.yomi

パパ   ぱぱ
大好き   だいすき
早く  はやく
ママ  まま
お土産   おみやげ
買ってきて かってきて
好き  すき
嫌い  きらい
ゴミ1 あ
ゴミ2 い

ポイントは、本当に認識させたい言葉以外の言葉をある程度登録しておくこと。 少ないと、あ => パパ, うんこ => パパみたいにマッピングされやすくなる。

④単語帳変換 juliusが認識できる形式に変換する。

iconv -f utf8 -t eucjp ~/modules/julius-kits/dictionary.yomi  | ./yomi2voca.pl > ~/modules/julius-kits/dictation-kit-v4.3.1-linux/base_dict.dic

⑤julius設定ファイル作成

custom.jconf

# 辞書ファイル指定
-w base_dict.dic
# 入力はマイクを使用
-input mic
# 解析結果をUTF8で取得
-charconv euc-jp utf8
# 以降はよくわからない!
-h model/phone_m/jnas-tri-3k16-gid.binhmm
-hlist model/phone_m/logicalTri
-output 1

⑥juliusを起動してみてテストする

custom.jconfをdictation-kit-v4.3.1-linuxディレクトリ直下に配置。 そして、カレントディレクトリをdictation-kit-v4.3.1-linuxに移動して以下のコマンドを実行

julius -C custom.jconf

試しに「パパ」って言ってみると,,,

pass1_best: パパ    
pass1_best_wordseq: パパ
pass1_best_phonemeseq: silB p a p a silE
pass1_best_score: -1628.528442
sentence1: パパ
wseq1: パパ
phseq1: silB p a p a silE
cmscore1: 0.238
score1: -1628.528442

<<< please speak >>>

たしかにパパって認識されている模様。

⑦モジュールモードでjuliusを起動する
モジュールモードで起動すると、juliusが10500ポートでTCP/IP接続を受け付けるようになる。 TCP/IPで接続をするとjuliusが音声認識を開始し、結果をTCP/IPで返してくれる。

モジュールモードで起動

julius -C custom.jconf -module

接続して結果を受け取るテストコード

#!/usr/bin/python
# -*- coding: utf-8 -*-
import socket
import cStringIO

# Raspberry PiのIPアドレス
host = '192.168.0.30'
# juliusの待ち受けポート
port = 10500

sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.connect((host, port))

xml_buff = ""
in_recoguout = False

while True:
    data = cStringIO.StringIO(sock.recv(4096))
    line = data.readline()
    # 認識結果はRECOGOUTタグで返ってくるのでそこだけ抽出
    while line:
        if line.startswith("<RECOGOUT>"):
            in_recoguout = True
            xml_buff += line
        elif line.startswith("</RECOGOUT>"):
            xml_buff += line
            print xml_buff
            in_recoguout = False
            xml_buff = ""
        else:
            if in_recoguout:
                xml_buff += line
        line = data.readline()
sock.close()

「パパ」って言うと、以下の結果が得られる。

<RECOGOUT>
  <SHYPO RANK="1" SCORE="-1970.796997" GRAM="0">
    <WHYPO WORD="パパ" CLASSID="パパ" PHONE="silB p a p a silE" CM="0.581"/>
  </SHYPO>
</RECOGOUT>

WHYPOタグのWORD属性を見れば、パパと呼ばれたか判断できる。

パパと呼ばれたら、写真を撮ってメールを送るpythonプログラム

juliusをモジュールモードで起動し、以下のプログラムを実行すると、 ”パパ”と呼ばれるたびに、写真を撮ってメールで送ることができる。

#!/usr/bin/python
# -*- coding: utf-8 -*-

import subprocess
import sys
import smtplib
import email
import traceback
import socket
import cStringIO

from xml.etree.ElementTree import fromstring
from datetime import datetime
from email.header import Header
from email.mime.multipart import MIMEMultipart
from email.mime.base import MIMEBase
from email.mime.text import MIMEText

# メールアカウント
MAIL_ADDRESS = "メールアドレス"
MAIL_PASSWARD = "パスワード"
TO_MAIL_ADDRESS = "送り先メールアドレス"

# SMTPサーバ設定
SMTP_SERVER = "smtp.gmail.com"
SMTP_PORT = 587

# Juliusサーバ
JULIUS_HOST = "localhost"
JULIUS_PORT = 10500


# メールメッセージを作成
def create_message(from_addr, to_addr, subject, body, attach_file=None):
    msg = MIMEMultipart()
    msg["From"] = from_addr
    msg["To"] = to_addr
    msg["Date"] = email.utils.formatdate()
    msg["Subject"] = Header(subject)
    body = MIMEText(body)
    msg.attach(body)

    # 添付ファイル
    if attach_file is not None:
        attachment = MIMEBase(attach_file['type'], attach_file['subtype'])
        file = open(attach_file['path'])
        attachment.set_payload(file.read())
        file.close()
        email.encoders.encode_base64(attachment)
        msg.attach(attachment)
        attachment.add_header("Content-Disposition","attachment", filename=attach_file['name'])
    return msg

# メールを送信する
def send_email(smtp_server, smtp_port, from_mail_address, from_mail_passward, to_mail_address, subject, body, attach_file=None):
    # メッセージの作成
    msg = create_message(from_mail_address, to_mail_address, subject, body, attach_file)
    # 送信
    smtpobj = smtplib.SMTP(smtp_server, smtp_port)
    smtpobj.ehlo()
    # gmailを使うのでTLSを用いる
    smtpobj.starttls()
    smtpobj.ehlo()
    smtpobj.login(from_mail_address, from_mail_passward)
    smtpobj.sendmail(from_mail_address, [to_mail_address], msg.as_string())
    smtpobj.close()


# juliusの解析結果のXMLをパース
def parse_recogout(xml_data):
    # scoreを取得(どれだけ入力音声が、認識結果と合致しているか)
    shypo = xml_data.find(".//SHYPO")
    if shypo is not None:
        score = shypo.get("SCORE")

    # 認識結果の単語を取得
    whypo = xml_data.find(".//WHYPO")
    if whypo is not None:
        word = whypo.get("WORD")
    return score, word

# fswebcamで写真を撮る
def take_picuture(picutre_dir):
    file_name = datetime.now().strftime('%Y%m%d_%H%M%S') + ".jpg"
    file_path = picture_dir + file_name
    cam_command = "fswebcam -r 640x480 {0} -F 10".format(file_path)
    p_camera = subprocess.Popen(cam_command,  shell=True)

    # 写真撮影完了までwaitする
    p_camera.wait()

    return file_name, file_path

if __name__ == "__main__":
    # 写真の保存先ディレクトリ
    picture_dir = sys.argv[1]
    if not picture_dir.endswith("/"):
        picture_dir += "/"

    try:
        # TCP/IPでjuliusに接続
        bufsize = 4096
        sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        sock.connect((JULIUS_HOST, JULIUS_PORT))
        sock_file = sock.makefile()

        xml_buff = ""
        in_recogout = False

        while True:
            # juliusから解析結果を取得
            data = cStringIO.StringIO(sock.recv(bufsize))

            # 解析結果から一行取得
            line = data.readline()

            while line:
                # 音声の解析結果を示す行だけ取り出して処理する。
                # RECOGOUTタグのみを抽出して処理する。
                if line.startswith("<RECOGOUT>"):
                    in_recogout = True
                    xml_buff += line
                elif line.startswith("</RECOGOUT>"):
                    xml_buff += line
                    xml_data = fromstring(xml_buff)
                    # XMLをパースして、解析結果を取得する
                    score, word = parse_recogout(xml_data)

                    if u'パパ' in word:
                        # パパと呼ばれたら写真を撮る
                        file_name, file_path = take_picuture(picture_dir)

                        # 添付ファイルの情報を作成
                      
                        # メール送信
                        send_email(SMTP_SERVER, SMTP_PORT, MAIL_ADDRESS, MAIL_PASSWARD, TO_MAIL_ADDRESS, "papa called", "papa called",  attach_file)

                    in_recogout = False
                    xml_buff = ""
                else:
                    if in_recogout:
                        xml_buff += line
                # 解析結果から一行取得
                line = data.readline()
    except Exception as e:
        print "error occurred", e, traceback.format_exc()
    finally:
        pass

今後の拡張について

  • juliusの設定をどうにかして文章をうまく認識できるようにしたい。今回は、単語の認識のみ扱った。文章を認識にも対応して、メール本文に音声認識結果を埋め込むとかしたい。
  • メールに返信したらRaspberry Piがメール本文に書かれたテキストを喋るようにしたい。

その他参考にしたサイト