ロボット作成の実験で行ったことをまとめていきます。
初回の実験。Raspberry Piのセットアップを行っていたらあっという間に時間が過ぎてしまった。班分けが勝手に決められてしまったのは残念だったけど仕方がない。
-
Raspberry Piのセットアップ
- ヒートシンクがRaspberry Piに既に装着されていた。また、MicroSDカードには既にUbuntu Mate 16.04.3 LTSがインストールされていたので、OSのインストール作業(イメージファイルのダウンロード、SDカードへのイメージファイルの書き込み、OSのセットアップ)は省略することができた。去年実験を行った人がそのままにしていったのだと思う。
- Raspberry PiのIPアドレスが、DHCPによって動的に割り振られないように、静的アドレスに設定した。
- Raspberry PiのI2CバスとSPIバスが有効になっていることを確認した。
-
サウンドの設定
- Raspberry Piから音声を出力させるのにかなり手間取った。
aplay
コマンドを起動するとエラーが出力されて異常終了してしまったが、これは設定ファイルに謎のゴミが含まれていたためで、設定ファイルを修正する必要があった。説明書通りに設定を行っても、スピーカーから出る音が非常に小さい。スピーカーの設定は一旦取りやめることにした。マイクの接続とマイク音声の録音については問題はなかった。
- Raspberry Piから音声を出力させるのにかなり手間取った。
-
各種ソフトウェアのインストールとセットアップ
- OpenCV、WiringPi、Julius(音声認識ソフトウェア)は既にインストールされていた。
- 音声認識のテストは非常に大変だった。説明書通りのコマンドを実行しても、上手く日本語を認識してくれない。それ以前に、説明書の日本語がよく理解できない。何をしようとしているのかもよく分からない。例えば、シェルスクリプトを書き換えろと言われているが、そのシェルスクリプトの在り処が分からない。音声認識を実行するために入力しなければならないコマンドが誤っている。シンボリックリンクを張れといわれているが、そもそもシンボリックリンクを張る必要性が無い。結局仕様書を頼るのは諦めて、インターネットで使用法を調査する必要に迫られる。「仕様書なるものは過信してはいけない」ということを学生の間に教え込もうとしているに違いない。
- 超音波センサのテストも手こずったが、こちらは仕様書をじっくり読めば何とか理解できた。
- Webカメラのテストは非常に簡単であった。WebカメラをRaspberry Piに接続したあと、C++とOpenCVで書かれたテストプログラムをコンパイルして実行するだけであった。
初回の実験では、誤植の多い実験書に翻弄された感じだった。自分が想定していないところでエラーが出て、時間が取られる。本当に世話が焼ける。昨年実験を行ったときの設定などがそのまま残されていて、何か悪さをしているに違いないと思われる。ここまで手こずるならOSのインストールからやり直せばよかったと後悔した。すっかり体力を奪われた。
2回目の実験。Pythonのpip
コマンドが動作しなくて本当に困った。動作しない原因が本当によく分からない。
-
各種ソフトウェアのインストールとセットアップ
- pthreadのテストは直ぐに終わった。C++で書かれたpthreadのテストプログラムをコンパイルして実行するだけであった。C++はよく分かりません。
-
アルミフレームの組み立て
- アルミフレームの組み立ては同じ班の人にお願いした。折角苦労して取り付けたフレームを取り外してしまって、班の人を不機嫌にさせてしまったので申し訳なかった。ハードはやらないと決めた。
-
ステッピングモータの動作
- 自分はステッピングモータを回転させるための準備を行った。とはいっても、ロボット本体にモータを取り付けるためのアルミ板(アルミブラケット)に、モータをネジで固定するだけであった。モータ電源の接続は少し大変であった。モータドライバと電源装置とを接続するための配線は自作する必要があったが、そのことは説明書には特に書かれていなかった。やはり説明書を信用してはいけなかった。配線を自作するといっても、導線を適当な長さに切って、先端の被膜を剥がすだけである(不器用なのでこれも時間が掛かる)。
- 電源装置とモータドライバとの間には緊急停止ボタンを接続するように指示されていた。緊急停止ボタンに配線を繋ぐときに、グランドを一箇所にまとめる必要があるということに気付かなかった(説明書が良くない)。このせいで、モータを回転させるまでにかなり時間が掛かった。断線していないかどうか、電圧値は正しいかどうかなどをテスターを用いて細かく調べた。電源装置の使い方もマニュアルで確認した。
- テストプログラムをコンパイルして実行すると、モータは回転してくれた。実行ファイルに与えるパラメータの意味は、ソースコードを解読すれば理解できると思う(まだソースコードを読んでいないのでよく分からない)。WiringPiなるライブラリの使用法についても調べる必要がある。
-
pip
コマンドが動作しないpip
コマンドが動かないのでライブラリをインストールできない。Could not find a version that satisfies the requirement
で始まるエラーが出て止まる。pip
をバージョン8.1.1からバージョン18.0にアップデートすると(pip install --upgrade pip
)、pip
コマンドそのものが使えなくなってしまう(python3 -m pip
と入力しなければならない)。またアップデートしてもライブラリがインストールできないという状況に変わりはなかった。- Pythonのバージョンを3.5.2から3.6.1に引き上げる、Python関連のパッケージを再インストールする、
pip
のバージョンを色々と変える(8.1.1、9.0.2、10.0.1、18.0など)、get-pip.py
経由でpip
を導入する、仮想環境を作成してその内部でパッケージのインストールを実行するなど、思い付く限りの方法を片っ端から試してみたが、どれも上手く行かない。パッケージ(Tensorflow、OpenCV-Pythonの2つ)をインストールしようとして3時間以上格闘したが、結局疲れ果てて心が折れてしまった。
昼ご飯を忘れて作業をしていた。昼ご飯を取ったのは午後2時を過ぎてからのことだった。この実験には底知れぬ闇が広がっている。
pip
よ、動いてくれ。
-
pip
コマンドが復活- このままPythonのライブラリがインストールできないと非常に困るので金曜日なのに作業をした。最初に取り掛かったのは、Python 3.5.2を再インストールする(
apt-get install --reinstall python3-dev
)ことだった。依存関係が壊れるなどの細かいトラブルはあったが、何とかなったので一安心した。pip
コマンドはバージョンが8.1.1だとどうやら古過ぎるみたいなので、バージョン9.0.2を導入した(python3 ./get-pip.py pip==9.0.2
)。
- このままPythonのライブラリがインストールできないと非常に困るので金曜日なのに作業をした。最初に取り掛かったのは、Python 3.5.2を再インストールする(
-
TensorflowとKerasのインストール
- Tensorflowは、
python3 -m pip install tensorflow
のコマンドを叩くだけでインストールが完了した。数分経っても端末の画面が更新されないことがあり不安になったが、ps
コマンドで確認してみると、どうやらインストール時に必要なライブラリをコンパイルしていて、そのコンパイルに物凄く時間が掛かっているためだった。python3 -c "import tensorflow as tf; print(tf.__version__)
のコードが正常に動作したときの安心感は忘れられない。Kerasも全く問題なくインストールできた(pip install keras
)。これで友人がプログラムを作れる。 - Tensorflowのインストールでは以下のページを大いに参考にした。
- Tensorflowは、
-
OpenCVのインストール
- OpenCV-Pythonは、
pip install opencv-python
とpip install opencv-contrib-python
のいずれも、Could not find a version that satisfies the requirement
のエラーが出て動かなかったので、仕方なくソースコードをビルドしてインストールすることにした。 Note that the wheel format does not currently support properly ARM architecture so there are no packages for ARM based platforms in PyPI
とPyPIのopencv-pythonのページに記載されているので、恐らくこれが原因と思われる。- OpenCVのインストールでは以下のページを参考にした。インストールするOpenCVのバージョンは3.4.3とした。
- OpenCV-Pythonは、
-
地味な作業
- ロボットの高さ、奥行き、横幅、車輪の間隔、車輪の半径などを定規で大まかに測定した。
- SSHでRaspberry Piに(踏み台サーバを経由して)リモートログインできることを確認した。
OpenCVをビルドしている最中に午後6時を回ってしまったので、次の実験ではビルドを途中から行うことになった。ここまで環境構築に時間が取られるとは思ってもいなかった。
-
OpenCVのインストール
- OpenCV(バージョン3.4.3)のソースコードのビルドと、インストールが無事に完了した。これで画像認識のプログラムが組めるようになったので一安心。インストールのために実行したコマンドは大体以下のようになった。
$ sudo apt-get update $ sudo apt-get install build-essential cmake pkg-config $ sudo apt-get install libjpeg-dev libtiff5-dev libjasper-dev libpng12-dev $ sudo apt-get install libavcodec-dev libavformat-dev libswscale-dev libv4l-dev $ sudo apt-get install libxvidcore-dev libx264-dev $ sudo apt-get install libgtk2.0-dev $ sudo apt-get install libatlas-base-dev gfortran $ cd ~ $ wget -o opencv-3.4.3.zip https://github.com/opencv/opencv/archive/3.4.3.zip $ wget -o opencv_contrib-3.4.3.zip https://github.com/opencv/opencv_contrib/archive/3.4.3.zip $ unzip ./opencv-3.4.3.zip $ unzip ./opencv_contrib-3.4.3.zip $ cd opencv-3.4.3/ $ mkdir build $ cd build $ cmake -D CMAKE_BUILD_TYPE=RELEASE -D CMAKE_INSTALL_PREFIX=/usr/local -D INSTALL_PYTHON_EXAMPLES=ON -D OPENCV_EXTRA_MODULES_PATH=~/opencv_contrib-3.4.3/modules -D BUILD_EXAMPLES=ON .. $ make $ sudo make install $ sudo ldconfig
OpenCVがPythonから利用できることは以下のようにして確認できた。
$ python3 >>> import cv2 >>> print(cv2.__version__) 3.4.3
ソースコードをコンパイルする際、
make -j2
やmake -j4
のように並列実行のオプションを付けると、メモリ不足でOSがハングアップしてしまうので、単純にmake
とした方が良いと思う。 -
モータの制御プログラムの作成
- WiringPiのPython向けのラッパライブラリは、
pip install wiringpi
のコマンドを実行するだけで簡単にインストールすることができた。モータ制御のサンプルプログラムはC言語で書かれていたので、Pythonに移植した。期待通りに動作させることができた。 - WiringPiの使用法は以下のサイトで確認した。
- WiringPiのPython向けのラッパライブラリは、
- 超音波センサのテスト
- I2C接続された超音波センサSRF02から測距データを取得するためのプログラムを、Pythonとsmbusライブラリで作成した。smbusライブラリで提供されている関数は以下のサイトで確認した。
- C言語のサンプルプログラムをそのままPythonに移植したところ、
IOError: [Errno 5] Input/output error
やIOError: [Errno 121] Remote I/O error
という内容の例外が発生して動かないことが多かった(動作が不安定だった)。インターネットで検索すると、Raspberry Piが超音波センサを認識できていないためにこのようなエラーが発生する、という旨の記述が多くみられたが、sudo i2cdetect -y 1
で確認すると超音波センサのアドレスが表示されたので認識はできている。エラーが発生する理由についての説明が以下のサイトに記述されていた。 - 結局以下のサイトのプログラムを利用することにした。本当に助かる。
IOError
例外は発生しなくなったので一安心。恐らく、呼び出すsmbusの関数を間違えていたのだと思う。 - SRF02センサについての情報は以下のサイトから確認した。仕様書を読むのは大切なんだなと改めて思った。
- 鉛蓄電池のテスト
- 仕様書に書かれていた通りの回路(基本回路)を実際に組み立てて、モータを鉛蓄電池で動作させることを行った。圧着という用語を覚えた。何もかもが未知なので、1つ1つの作業に物凄い体力を消費する。
- 画像認識のテスト
- 友人の作成した物体認識(ニューラルネットワークベース)のプログラムが動作した。但し1枚の画像を認識するのに13秒も掛かるので、Raspberry Pi上でリアルタイムの物体認識を行うのは明らかに不可能だと分かった。カメラから取得した映像をRaspberry Piからコンピュータへ送信し、コンピュータ上で物体認識のプログラムを実行して、実行結果(検出された物体)をRaspberry Piに送り返すという方法もあるが、画像をやり取りしようとするとネットワークのかなりの帯域を消費してしまうと思う。もう少し方法を検討する必要がある。
完成までに一体何をすればよいのか、まだ把握しきれていない。その日にできることをこなしている状態だ。ロボットの機構についてはまだ殆ど検討できていない。終わりがみえない。他の班は圧倒的な進捗を生み出している。気分だけが焦る。正直のところ非常に追い込まれている。
不毛でした。進捗を生むのがかなり大変です。
- モータを扱うクラスの作成
- ステッピングモータ(L6470)を扱うクラスをPythonで作成した。モータへのバイト列の書き込みと読み込み、初期化、状態の取得、指定速度での走行、減速と停止などの基本的な機能を持ったメソッドを用意した。
- モータとの通信プログラムの作成(失敗)
- モータに所定の命令を送って動作させるためのプログラムを作成しようと試みたが、徒労に終わった。モータへの命令を待ち受けるサーバと、モータに命令を送信するクライアントの2つのプログラムを書いて実行させてみたが、クライアントが命令を送信してからサーバが受信するまでに数秒掛かったり、送信した命令をサーバが実行してくれなかったり、また命令を受信できなかったりとトラブルが続発した。クライアントとサーバ形式での通信は諦めることにした。TCPを用いて通信していたが、恐らく
send
とrecv
の使い方が誤っていたのだと思う。
- モータに所定の命令を送って動作させるためのプログラムを作成しようと試みたが、徒労に終わった。モータへの命令を待ち受けるサーバと、モータに命令を送信するクライアントの2つのプログラムを書いて実行させてみたが、クライアントが命令を送信してからサーバが受信するまでに数秒掛かったり、送信した命令をサーバが実行してくれなかったり、また命令を受信できなかったりとトラブルが続発した。クライアントとサーバ形式での通信は諦めることにした。TCPを用いて通信していたが、恐らく
週3でロボットの実験に取り組んでいます。この日は目に見える進捗がありました。
-
モータとの通信プログラムの作成
- ソケット通信で命令をやり取りする方法が上手く行かなかったので、代わりにプロセス間通信を用いることにした。Pythonの
multiprocessing
という標準ライブラリを使用すれば簡単に実現することができる。 - Python(CPython)ではGIL(Global Interpreter Lock)の制約上、ある時点で実行できるスレッドは1つに限られるため、複数のスレッドを作成し起動した場合でも、シングルスレッドで実行するのと本質的に変わらなくなってしまう。運が悪ければシングルスレッドよりも性能が悪化することすらある。マルチプロセスであればこの制約を回避でき、CPUのコアをフル活用できるようになる(本当の並列処理が可能になる)。Raspberry Pi Model 3 Bには4コアのCPUが搭載されているが、マルチプロセスでプログラムを組めば全てのコアを使えるようになる。但しプロセス間でのデータの共有を考えなくてはいけない。プログラムを作成する上で、公式サイトのドキュメントが大いに役立った。
- モータへのコマンドをやり取りするためのキューを作成する(このキューはプロセス間で共有される)。命令を送る側のプロセスでは、このキューに命令を流し込み、命令を受け取って実行する側のプロセスでは、このキューから命令を取り出すようにする。作成したプログラムは期待通りに動作させることができた。
- ソケット通信で命令をやり取りする方法が上手く行かなかったので、代わりにプロセス間通信を用いることにした。Pythonの
-
音声認識エンジンJuliusを扱うクラスの作成
- 音声認識エンジンJuliusをモジュールモードで起動すると、ソケット通信(TCP)によってPythonのプログラムから利用できるようになる。Juliusを起動するためのシェルスクリプトを予め用意しておき、Pythonの標準ライブラリ
subprocess
を用いて実行する。これで、サーバのプロセス(音声認識を実行し、結果をクライアント側に送信するプロセス)が起動される。サーバと通信するためのTCPソケットも作成しておく。ここまでできれば、あとはPythonのライブラリmultiprocessing
を用いてクライアント側のプロセス(先程作成したソケットを用いてサーバの出力結果を取得するプロセス)を作成し、起動するだけである。 - サーバの出力結果はXML文字列に近いが、エスケープされていなかったり、XMLヘッダが先頭に無かったりと、XML文字列としては不完全であるため、出力結果をそのままPythonのライブラリ
xml.etree.ElementTree
で解析しようとしてもエラーが出て上手く行かない。ライブラリに文字列を渡す前に予め加工しておく必要がある。例えば、文章の開始を表す特殊な単語が<s>
、文章の終わりを表す単語が</s>
として、WHYPO
タグのWORD
属性に格納されるが、この<s>
と</s>
に含まれる山括弧をエスケープする、或いは別の単語(start
とend
など)で置き換えるなどの前処理が要る。 - 音声認識の高速化と精度向上のために辞書を作成した。
- 作成したクラスはモータの命令キューを保持している。認識された単語に基づいてモータに命令を追加する処理をクライアント側のプロセスに追加したことで、音声でモータを操作できるようになった。
- 音声認識エンジンJuliusをモジュールモードで起動すると、ソケット通信(TCP)によってPythonのプログラムから利用できるようになる。Juliusを起動するためのシェルスクリプトを予め用意しておき、Pythonの標準ライブラリ
-
超音波センサSrf02を扱うクラスの作成
- 超音波センサのテストプログラムを利用して簡単なクラスを作成した。超音波センサの値を一定間隔で取得し続けるプロセスを作成し起動するだけである。このクラスからもモータの命令キューを利用できる。センサの値がある閾値未満になったら、ロボットの目の前に障害物があると判断して、モータの命令キューに停止命令を追加する処理を加えたので、障害物を検知して止まれるようになった。
月曜日は何故か進捗が生まれない。
-
ウェブカメラを扱うクラスの作成
- OpenCVを利用してウェブカメラから動画像を取得するためのクラスを作成した。顔認識のプログラムも非常に容易に作成できた。
-
モータの命令処理の改良
- 先日のプログラムは、命令キューに追加された命令を順番に実行していくものだった。一度追加された命令は必ず最後まで実行されるので分かり易いが、命令が無限ループに入るとその後の命令を一切処理できなくなる。また、例えば加速の命令によってロボットが前進している最中に、突然人が前を通り掛かったとする。このとき超音波センサのプロセスがモータに停止命令を追加するが、停止命令は加速命令が終わった後に実行されるから、加速命令が終了する前に人に衝突してしまう可能性がある。従って実行中の命令をキャンセルするための処理を記述する必要がある。
- 命令を受け取って実行する側のプロセスでは、命令を受け取ったら、その命令を実行するための新たなプロセスを作成して起動する。受け取った命令がキャンセル命令であった場合は、以前に作成したプロセスを強制終了させる。かなり乱暴な方法だが、このようにプログラムを作り直すことで、実行中の命令をキャンセルできるようになった。
-
SDカードのバックアップ
- Raspberry Piに差さっているSDカードのバックアップを取った。Win32DiskImagerというフリーソフトを使用してイメージファイル(容量は30GB)を作成した。これでSDカードが突然故障しても直ぐに復活できる。
月曜日よりは進捗があった。
-
モータの命令処理の改良
- 先日の方法では問題があることが分かったのでまた新たな方法を考えなくてはならない。例えば加速と減速の2つの命令がキューに追加されたとすると、加速するためのプロセスと減速するためのプロセスが2つ作られて並列に実行されることになる。このときプロセスの切り替えによって加速と減速の処理が交互に行われてしまうので、加速と減速のコマンドは永遠に終了しなくなる。モータは加速と減速を繰り返すのでガタガタと音が鳴り、ロボットは定速で動き続ける(このバグに実際に遭遇して頭を抱えた)。
- 2つの命令が競合しないように、モータの命令を受け取って実行する側のプロセスで、作成したプロセスが終了するまで待つようにすることはできる。但しプロセスの終了を
join()
メソッドで待機すると、その時点でプログラムの実行がブロックされてしまうので、プロセスの実行途中にキャンセル命令が送信されても、プロセスが終了するまで受け取れなくなる。従って実行中の命令をキャンセルできなくなる。 - 次のような方法に修正する必要がある。モータの命令を受け取るためのプロセス(P1)では、命令を受け取ったら新たなキュー(Q1)にこの命令を追加する。プロセスP1では別の新たなプロセスP2を作成し起動しておく。プロセスP2では、キューQ1から命令を取り出して順番に実行していく。プロセスP1がキャンセル命令を受け取った場合は、プロセスP2を強制終了させてキューQ1を空にする。
-
音声合成システムOpenJTalkを操作するクラスの作成
- OpenJTalkを起動するためのシェルスクリプトを作成し、Pythonのライブラリ
subprocess
を用いてこれを実行するだけの手軽なクラスを作成した。命令を管理するためのキューを新たに用意し、このキューを監視するプロセスを1つ起動する。命令は喋らせたい文章を属性として保持している。プロセス内では、キューに命令が追加されたら、命令から文章を取り出してシェルスクリプトのコマンドライン引数に指定し、実行する。
- OpenJTalkを起動するためのシェルスクリプトを作成し、Pythonのライブラリ
-
顔を追いかける
- 画像中の顔の位置を元に、モータに命令(左に曲がる、右に曲がる、直進)を送信する処理を、ウェブカメラ用のクラスに追加した。人の顔を認識して追いかけてくるロボットができた。ロボットが自分で判断して動くのは面白い。
作成したプログラムがかなり複雑になった。モータの命令を受け取るプロセス、Juliusの結果を取得するプロセス、Juliusのサーバのプロセス、OpenJTalkへの命令を受け取るプロセス、超音波センサの値を取得するプロセス、ウェブカメラから画像を取得するプロセスなど、多くのプロセスが連携しながら協調動作している。自分の頭では追い付かなくなってきている。バグが発生したときに原因の特定が難しくなっている。あとは人工生命を作っている気分になる。ロボットの製作を開始して今日で丁度1か月になる。既に4単位分ぐらいの労力が掛かっている。