音声認識でドローンを飛ばす

Table of Content

諸君 私はロボットが好きだ
諸君 私はロボットが大好きだ

ゲッターが好きだ
マジンガーが好きだ
ジャイアントロボが好きだ
ビッグオーが好きだ

ロジャースミスが「ビッグオーアクション!!」といって、轟音と共に街を吹き飛ばすのが好きだ
空中高く放り上げられたゲッターが 「チェンジゲッターワン!!」でばらばらとなって合体した時など心がおどる

ということで、音声でロボットを動かすというのは人類のロマンを果たすために音声認識でtelloを飛ばしました。
telloの動かし方は下記を参照してください。
https://needtec.sakura.ne.jp/wod07672/?p=9212

.NETで音声認識

.NETには標準で「System.Speech.Recognition」がサポートされており、音声認識が行えます。

サンプルコード全体は以下にあります。
https://github.com/mima3/Tello/tree/master/TelloSpeech

簡単な流れ

1.SpeechRecognitionEngineを作成する
2.SpeechRecognitionEngineに文法を与える
3.SpeechRecognizedイベントを処理する。

SpeechRecognitionEngineを作成する。

参照設定で「System.Speech」を追加する。
「using System.Speech.Recognition;」を宣言する。
SpeechRecognitionEngineオブジェクトを作成する。

SpeechRecognitionEngine recognizer;
// 略
recognizer = new SpeechRecognitionEngine(new System.Globalization.CultureInfo("ja-JP"));

コンストラクタにロケールを与えているので、使っているOSの言語によって、ここは変更する必要があります。

文法を作成する

ここで発音のフレーズとその意味を関連付けます。
たとえば、ロボットを発進させるとき兜甲児は「マジン、ゴー!」といいますし、ロジャースミスは「ビッグオーアクション!」といいます。
おなじ発進をおこなうという目的のための命令フレーズは主人公の数だけあるといっていいでしょう。
System.Speech.RecognitionではSemanticResultValueを用いることで、フレーズと意味を紐づけることができます。
以下は様々な発進フレーズをtelloの離着陸に関連付けたものになります。

        Choices CreateSingleCommandChoices()
        {
            Choices ret = new Choices();
            ret.Add(new SemanticResultValue("てーくおふ", "takeoff"));
            ret.Add(new SemanticResultValue("はっしん", "takeoff"));
            ret.Add(new SemanticResultValue("てろ、ごー", "takeoff"));
            ret.Add(new SemanticResultValue("てろ、あくしょん", "takeoff"));

            ret.Add(new SemanticResultValue("らんど", "land"));
            ret.Add(new SemanticResultValue("ちゃくりく", "land"));
            return ret;
        }

ChoicesオブジェクトをGrammarBulderオブジェクトに追加し、そこからGrammarオブジェクトを作成してそれをSppechRecognitionEngineにロードします。

            Choices singleCommands = CreateSingleCommandChoices();
            GrammarBuilder cmdGb = new GrammarBuilder();
            cmdGb.Append(new SemanticResultKey("singleCommands", singleCommands));
            Grammar cmdG = new Grammar(cmdGb);
            recognizer.LoadGrammar(cmdG);

ここでは作成した発進の種類について「singleCommands」というキー名で登録しています。
この「singleCommands」というキー名は音声認識に成功した場合に、なにを認識できたのか(takeoffを認識したのか、landを認識したのか)を取得するのに使用します。

SpeechRecognizedイベントを処理する

マイクからの入力でSpeechRecognitionEngineが与えられた文法を解析できた場合は、SpeechRecognizedが発火されます。
以下ではイベントの実装例をしめします。

イベントハンドラを登録する例

            // Add a handler for the speech recognized event.  
            recognizer.SpeechRecognized +=
              new EventHandler<SpeechRecognizedEventArgs>(recognizer_SpeechRecognized);

イベントを処理する例

        // Handle the SpeechRecognized event.  
        void recognizer_SpeechRecognized(object sender, SpeechRecognizedEventArgs e)
        {
            Debug.WriteLine("SpeechRecognized...");
            if (e.Result.Semantics != null)
            {
                if (e.Result.Semantics.ContainsKey("singleCommands"))
                {
                    Debug.WriteLine("..." + e.Result.Semantics["singleCommands"].Value);

                    // telloにUDP経由でコマンドを送信する。
                    sendCmd((string)e.Result.Semantics["singleCommands"].Value);

                }
            }
        }

ここでは音声認識が正常に認識できた場合、「singleCommand」として登録した[takeoff」または「land」コマンドがtelloに送信されます。

数字を使う命令の音声解析

telloには「命令+数値」という形式のコマンドが存在します。たとえば「up 30」では30cm上昇しますし、「down 30」では30cm下降します。
これに対応するにはChoicesオブジェクトを2つ使って対応します。

まず、命令と数値のChoicesオブジェクトを作成します。

        Choices CreateNumberChoices()
        {
            Choices numbers = new Choices();
            for (int i = 0; i < 300; ++i)
            {
                numbers.Add(new SemanticResultValue(i.ToString(), i));
            }
            return numbers;
        }

        Choices CreateDirections()
        {
            Choices ret = new Choices();
            ret.Add(new SemanticResultValue("ふぉわーど", "forward"));
            ret.Add(new SemanticResultValue("すすめ", "forward"));

            ret.Add(new SemanticResultValue("ばっく", "back"));
            ret.Add(new SemanticResultValue("もどれ", "back"));

            ret.Add(new SemanticResultValue("あっぷ", "up"));
            ret.Add(new SemanticResultValue("あがれ", "up"));

            ret.Add(new SemanticResultValue("だうん", "down"));
            ret.Add(new SemanticResultValue("さがれ", "down"));

            ret.Add(new SemanticResultValue("らいと", "right"));
            ret.Add(new SemanticResultValue("みぎ", "right"));

            ret.Add(new SemanticResultValue("れふと", "left"));
            ret.Add(new SemanticResultValue("ひだり", "left"));

            ret.Add(new SemanticResultValue("かいてん", "cw"));
            ret.Add(new SemanticResultValue("まわれ", "cw"));
            ret.Add(new SemanticResultValue("ぎゃくかいてん", "ccw"));
            return ret;
        }

SemanticResultValueを追加すればするほど重くなるので、数字を3000とか10000まで認識できるようにするのは避けておいた方が無難でしょう。
以下は作成した命令と数のChoicesオブジェクトを利用して、「命令→数」の順番で認識する文法を登録した例です。
つまり「らいと ひゃく」と叫ぶと「right 100」と解釈します。

            //
            Choices numbers = CreateNumberChoices();
            Choices directions = CreateDirections();
            // 

            // Create a GrammarBuilder object and append the Choices object.
            GrammarBuilder moveGb = new GrammarBuilder();
            moveGb.Append(new SemanticResultKey("moveCommands", directions));
            moveGb.Append(new SemanticResultKey("moveCm" , numbers));

            // Create the Grammar instance and load it into the speech recognition engine.
            Grammar moveG = new Grammar(moveGb);

            // Create and load a dictation grammar.  
            recognizer.LoadGrammar(moveG);

あとはSpeechRecognized イベントを適切に処理します。

        void recognizer_SpeechRecognized(object sender, SpeechRecognizedEventArgs e)
        {
            Debug.WriteLine("SpeechRecognized...");
            if (e.Result.Semantics != null)
            {
                if (e.Result.Semantics.ContainsKey("singleCommands"))
                {
                    Debug.WriteLine("..." + e.Result.Semantics["singleCommands"].Value);
                    sendCmd((string)e.Result.Semantics["singleCommands"].Value);

                }
                else if (e.Result.Semantics.ContainsKey("moveCommands") &&
                         e.Result.Semantics.ContainsKey("moveCm"))
                {
                    Debug.WriteLine("..." + e.Result.Semantics["moveCommands"].Value + " " + e.Result.Semantics["moveCm"].Value);
                    sendCmd((string)e.Result.Semantics["moveCommands"].Value + " " + ((int)e.Result.Semantics["moveCm"].Value).ToString());

                }
            }
        }

全体のコード

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Net;
using System.Net.Sockets;
using System.Speech.Recognition;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;

namespace TelloSpeech
{
    /// <summary>
    /// MainWindow.xaml の相互作用ロジック
    /// </summary>
    public partial class MainWindow : Window
    {
        private UdpClient udpForCmd;     //コマンド結果受信用クライアント
        private UdpClient udpForStsRecv; //ステータスの結果受信用クライアント
        SpeechRecognitionEngine recognizer; //

        public MainWindow()
        {
            InitializeComponent();
        }

        private void btnConnect_Click(object sender, RoutedEventArgs e)
        {
            SetupTello();
            sendCmd("command");
            SetUpSpeech();
            btnConnect.IsEnabled = false;

        }

        // Telloとの通信を設定する
        private void SetupTello()
        {
            this.udpForCmd = new UdpClient(0);
            this.udpForStsRecv = new UdpClient(8890);

            // コマンド結果の受信処理
            Task.Run(() => {
                IPEndPoint remoteEP = null;//任意の送信元からのデータを受信
                while (true)
                {
                    try
                    {
                        String rcvMsg = "";
                        byte[] rcvBytes = udpForCmd.Receive(ref remoteEP);
                        Interlocked.Exchange(ref rcvMsg, Encoding.ASCII.GetString(rcvBytes));
                        txtResult.Dispatcher.BeginInvoke(
                            new Action(() =>
                            {
                                txtResult.Text = rcvMsg;
                            })
                        );
                    }
                    catch (Exception ex)
                    {
                        Debug.WriteLine(ex.Message);
                    }
                }

            });

            // ステータスの受信処理
            Task.Run(() => {
                IPEndPoint remoteEP = null;//任意の送信元からのデータを受信
                while (true)
                {
                    try
                    {
                        String rcvMsg = "";
                        byte[] rcvBytes = udpForStsRecv.Receive(ref remoteEP);
                        Interlocked.Exchange(ref rcvMsg, Encoding.ASCII.GetString(rcvBytes));
                        rcvMsg = rcvMsg.Replace(";", "\r\n");
                        txbStatus.Dispatcher.BeginInvoke(
                            new Action(() =>
                            {
                                txbStatus.Text = rcvMsg;
                            })
                        );
                    }
                    catch (Exception ex)
                    {
                        Debug.WriteLine(ex.Message);
                    }

                }

            });
        }

        // コマンド送信
        private void sendCmd(string cmd)
        {
            txtCmd.Text = cmd;
            byte[] data = Encoding.ASCII.GetBytes(cmd);
            this.udpForCmd.Send(data, data.Length, "192.168.10.1", 8889);

        }

        private void SetUpSpeech()
        {
            recognizer = new SpeechRecognitionEngine(new System.Globalization.CultureInfo("ja-JP"));

            Choices singleCommands = CreateSingleCommandChoices();
            GrammarBuilder cmdGb = new GrammarBuilder();
            cmdGb.Append(new SemanticResultKey("singleCommands", singleCommands));
            Grammar cmdG = new Grammar(cmdGb);
            recognizer.LoadGrammar(cmdG);

            //
            Choices numbers = CreateNumberChoices();
            Choices directions = CreateDirections();
            // 

            // Create a GrammarBuilder object and append the Choices object.
            GrammarBuilder moveGb = new GrammarBuilder();
            moveGb.Append(new SemanticResultKey("moveCommands", directions));
            moveGb.Append(new SemanticResultKey("moveCm" , numbers));

            // Create the Grammar instance and load it into the speech recognition engine.
            Grammar moveG = new Grammar(moveGb);

            // Create and load a dictation grammar.  
            recognizer.LoadGrammar(moveG);

            // Add a handler for the speech recognized event.  
            recognizer.SpeechRecognized +=
              new EventHandler<SpeechRecognizedEventArgs>(recognizer_SpeechRecognized);

            // Configure input to the speech recognizer.  
            recognizer.SetInputToDefaultAudioDevice();

            // Start asynchronous, continuous speech recognition.  
            recognizer.RecognizeAsync(RecognizeMode.Multiple);
        }
        Choices CreateSingleCommandChoices()
        {
            Choices ret = new Choices();
            ret.Add(new SemanticResultValue("てーくおふ", "takeoff"));
            ret.Add(new SemanticResultValue("はっしん", "takeoff"));
            ret.Add(new SemanticResultValue("てろ、ごー", "takeoff"));
            ret.Add(new SemanticResultValue("てろ、あくしょん", "takeoff"));
            ret.Add(new SemanticResultValue("らんど", "land"));
            ret.Add(new SemanticResultValue("ちゃくりく", "land"));
            ret.Add(new SemanticResultValue("きろくかいし", "streamon"));
            ret.Add(new SemanticResultValue("きろくていし", "streamoff"));

            return ret;
        }
        Choices CreateNumberChoices()
        {
            Choices numbers = new Choices();
            for (int i = 0; i < 300; ++i)
            {
                numbers.Add(new SemanticResultValue(i.ToString(), i));
            }
            return numbers;
        }

        Choices CreateDirections()
        {
            Choices ret = new Choices();
            ret.Add(new SemanticResultValue("ふぉわーど", "forward"));
            ret.Add(new SemanticResultValue("すすめ", "forward"));

            ret.Add(new SemanticResultValue("ばっく", "back"));
            ret.Add(new SemanticResultValue("もどれ", "back"));

            ret.Add(new SemanticResultValue("あっぷ", "up"));
            ret.Add(new SemanticResultValue("あがれ", "up"));

            ret.Add(new SemanticResultValue("だうん", "down"));
            ret.Add(new SemanticResultValue("さがれ", "down"));

            ret.Add(new SemanticResultValue("らいと", "right"));
            ret.Add(new SemanticResultValue("みぎ", "right"));

            ret.Add(new SemanticResultValue("れふと", "left"));
            ret.Add(new SemanticResultValue("ひだり", "left"));

            ret.Add(new SemanticResultValue("かいてん", "cw"));
            ret.Add(new SemanticResultValue("まわれ", "cw"));
            ret.Add(new SemanticResultValue("ぎゃくかいてん", "ccw"));
            return ret;
        }

        // Handle the SpeechRecognized event.  
        void recognizer_SpeechRecognized(object sender, SpeechRecognizedEventArgs e)
        {
            Debug.WriteLine("SpeechRecognized...");
            if (e.Result.Semantics != null)
            {
                if (e.Result.Semantics.ContainsKey("singleCommands"))
                {
                    Debug.WriteLine("..." + e.Result.Semantics["singleCommands"].Value);
                    sendCmd((string)e.Result.Semantics["singleCommands"].Value);

                }
                else if (e.Result.Semantics.ContainsKey("moveCommands") &&
                         e.Result.Semantics.ContainsKey("moveCm"))
                {
                    Debug.WriteLine("..." + e.Result.Semantics["moveCommands"].Value + " " + e.Result.Semantics["moveCm"].Value);
                    sendCmd((string)e.Result.Semantics["moveCommands"].Value + " " + ((int)e.Result.Semantics["moveCm"].Value).ToString());

                }
            }
        }
    }
}

まとめ

System.Speechを使用すれば簡単に音声認識プログラムがC#で実装できることを確認しました。
System.Speechについての詳細は下記を参照してください。
https://docs.microsoft.com/ja-jp/previous-versions/office/developer/speech-technologies/hh361625(v=office.14)

今回は文法をコード上に記載しましたがXMLで記載して外部ファイルとして保持することも可能のようです。
https://docs.microsoft.com/ja-jp/previous-versions/office/developer/speech-technologies/hh361594(v%3doffice.14)

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です