すけるくんの最初のバージョンを抜本的に組み直した。春休みのおかげで、ほぼ引きこもりで作業してきた。主な変更点は次の通り。
(1)衝撃にも壊れにくくする
(2)前後左右に超音波距離センサーを動かすことができる(SPIでデータ取得)
(3)エンコーダーで4個のモーターの回転数を独立にカウントできる(SPIでデータ取得)
(4)新基盤の上に制御ボードを配置、部品を圧縮配置
(5)6電池を廃止し、12V(X4)のみにした、外部電源は24Vのまま
(6)非接触型DCDCコンバータを並列にして、5V電源の容量を増大させた
以上に伴い、その他、細かい仕様の変更を行った。音声出力は、12.5X12.5のまま。音声認識マイクもそのまま。
二つのデバイスを、二つのMCP23S17で同時に制御する
その動画をyoutubeにアップしました。
https://youtu.be/u_VU2cOFWVY
デバイスアドレスを0、1に区別することで、一つのSPIチャンネルで別々に制御できることが確かめられた。
Cで書いた距離センサモジュールをjavaから呼び出す
4つの距離センサは、Cで制御し、4つのモータのエンコーダーからの回転数取得はjavaで制御しなければならない事情を先に書いた。そこで、2つのMCP23S17を使って、それぞれを距離センサとエンコーダ処理用にする戦略をとることにした。
RaspberryPi の一つのSPI0チャンネルに、上記の二つのMCP23S17をバスでぶら下げることにする。デバイスの識別は、A0,A1,A2端子を1、0で区別すればいい(8個区別出来る)。基本的に、0番と1番のデバイスにする。
距離センサはCプログラムで制御するのだが、ロボットの方は、javaで制御システムが書かれているので、センサのCプログラムは、ライブラリ化(libdistance.so)して、jna経由で、javaから直接そのライブラリを使うようにすればいい。これはすでにやったことがあるので、簡単にできると思ったが、結局、昨日の夜から今日の夜まで、かかった(途中、大学に行って学生の修論指導はしたが)!
第一に手こずった問題は、libdistance.soを作成して、javaのjnaを使ったプログラムを書いて、lib distance.soをLD_LIBRARY_PATHの環境変数で与えられる場所において、動かしてみたのだが、どうやっても、
「java.lang.UnsatisfiedLinkError: /tmp/jna-3577/jna760224136558438898.tmp: /tmp/jna-3577/jna760224136558438898.tmp: 共有オブジェクトファイルを開けません: そのようなファイルやディレクトリはありません」
というエラーが出る。色々やって、かなり長時間格闘して、結論的には、最新のjna.jar (4.5.1)とraspberrypiの何らかの相性が悪いという判断をした。同じ状況で、linux(ubuntu16.04)では、ちゃんと動くのだから。
これは間違っているかもしれない。が、私の能力の範囲での結論はそうなのだ。だから、去年ちゃんとraspberrypi上で動いたjna.jarを持ってきて、それでセットしたらちゃんと動いた。あ〜あ。
もう一つ、こちらは大したことはないのだが、wiringPiのライブラリもちゃんとくっつけて、libdistance.soを作成しないと、wiringPiの関数が見つからないと怒られる!
プログラムをいかに掲載する。まず、Cライブラリのプログラム distance.cは以下のようになる。
/* 距離センサーライブラリ MCP23S17をデバイス0番でセットしておく 使い方: getdistance('センサー番号:整数 0-3') */ #include <stdio.h> #include <wiringPi.h> #include <mcp23s17.h> #include <sys/time.h> #include <unistd.h> // 64より大きな数字、適当 wiringpiの要請 #define BASE 123 int trigPin, echoPin; int pulseIn(int pin, int level, int timeout){ struct timeval tn, t0, t1; long micros; gettimeofday(&t0, NULL); micros = 0; while (digitalRead(pin) != level){ gettimeofday(&tn, NULL); if (tn.tv_sec > t0.tv_sec) micros = 1000000L; else micros = 0; micros += (tn.tv_usec - t0.tv_usec); if (micros > timeout) return 0; } gettimeofday(&t1, NULL); while (digitalRead(pin) == level){ gettimeofday(&tn, NULL); if (tn.tv_sec > t0.tv_sec) micros = 1000000L; else micros = 0; micros = micros + (tn.tv_usec - t0.tv_usec); if (micros > timeout) return 0; } if (tn.tv_sec > t1.tv_sec) micros = 1000000L; else micros = 0; micros = micros + (tn.tv_usec - t1.tv_usec); return micros; } // セットアップ関数の呼び出しは、最初の1回だけである // 何度も呼び出してはならない void setup(int pin){ // pin -> 0-3 trigPin = BASE+pin+8; // B0-B3 echoPin = BASE+pin; // A0-A3 wiringPiSetup () ; mcp23s17Setup (BASE, 0, 0) ; pinMode (trigPin, OUTPUT) ; pinMode (echoPin, INPUT) ; pullUpDnControl (echoPin, PUD_UP) ; } int getdistance(){ long duration, distance; // HIGHが10μs継続するパルスを出す digitalWrite(trigPin, LOW); // Added this line delayMicroseconds(2); // Added this line digitalWrite(trigPin, HIGH); delayMicroseconds(10); // Added this line digitalWrite(trigPin, LOW); // その結果出力された超音波を受け取る // その時間差から距離を測る duration = pulseIn(echoPin, HIGH, 1000000); //printf("duration=%d ",duration); distance = (duration/2) / 29.1; //printf("距離 = %d cm\n",distance); //sleep(1); return (int)distance; }
これをコンパイルするためには次のようにする。
gcc -fPIC -shared -o libdistance.so distance.c -lwiringPi
最後に、wiringPiのライブラリをくっつけているところ大事。というのも、これをつけなくても、コンパイル自身は難なく通ってしまうから、実行時に叱られる。
export LD_LIBRARY_PATH=../XXX :$LD_LIBRARY_PATH
sudo ldconfig
とかする。../XXXはlibdistance.soをおく場所である。あるいは、
/etc/ld.so.conf.d
の設定ファイルに、そのパスを書いておき、ldconfigを実行するとかする。
import com.sun.jna.Library; import com.sun.jna.Native; import java.util.logging.Level; import java.util.logging.Logger; // ここの部分がlibdistance.soと接続するおまじない interface DistanceLib extends Library { DistanceLib INSTANCE = (DistanceLib) Native.loadLibrary("distance", DistanceLib.class); // 使うべき関数を宣言する int getdistance() ; void setup(int pin); } /** * * @author washida */ public class Distance { DistanceLib dlib = DistanceLib.INSTANCE; void show(){ dlib.setup(0); for(int i=0;i<100;i++){ int dis = dlib.getdistance(); System.out.println("No."+ i +": 距離 = "+dis+"cm"); try { Thread.sleep(1000); } catch (InterruptedException ex) { Logger.getLogger(Distance.class.getName()).log(Level.SEVERE, null, ex); } } } /** * @param args the command line arguments */ public static void main(String[] args) { // TODO code application logic here Distance dis = new Distance(); dis.show(); } }
こちらは特に問題はないだろう。
MCP23S17を使って、US-015の距離センサーを動かす
ロボットに四個の距離センサーをつけたい。四方の距離をいつでも測りたいというわけだ。US-015は、音波を発信してその跳ね返り時間で距離を測る。音を出すトリガとエコーを受け取る2つのgpioを消費する。八個のgpioを直接ラズパイのポートから取るというのは勿体なさすぎる。そこで、spiモジュールを使って、ラズパイのgpioを拡張してそこから取りたい。これが目標だ。
モーターの回転数をMCP23S17経由で取るというのは成功した。しかも、JAVAのpi4jを使って。これは先に書いた。
距離センサーは、同じようにpi4jでやろうとしたのだが、うまくいかない。超音波の発信と受信は、微妙な時間のタイミングで行われるので、どうもpi4jのライブラリとの相性が悪いと思った。
そこで止むを得ず、CのWiringPiライブラリで動かすことにした。まあ、動いた。
https://youtu.be/1zhjbX62gA8
ただ、問題はある。モータ制御との整合性だ。モーターは、JAVAで、距離センサーは、Cというのは、何しろ一つのMCP23S17なので、見るからにうまくいきそうにない。
両方Cで書くしかないが、モータの方はピンの値の変化をpi4jの割り込みで取っているので、それを割り込みを使わないでやれるようなCのプログラムを書かなければならない。それが面倒だ
距離センサのプログラムを以下に掲載しておく。
http://nopnop2002.webcrow.jp/HC-SR04/HC-SR04-1.html
と、wiringPiのMCP23S17サンプルを参考にさせていただいた
わかりやすく言うと、その二つを融合しただけである
#include <stdio.h> #include <wiringPi.h> #include <mcp23s17.h> #include <sys/time.h> #include <unistd.h> #define BASE 123 // トリガピンとエコーピンは、MCP23S17のピン番号である // 8ピンは、B0 // 7ピンは、A7 である #define trigPin 8 #define echoPin 7 int pulseIn(int pin, int level, int timeout) { struct timeval tn, t0, t1; long micros; gettimeofday(&t0, NULL); micros = 0; while (digitalRead(pin) != level) { gettimeofday(&tn, NULL); if (tn.tv_sec > t0.tv_sec) micros = 1000000L; else micros = 0; micros += (tn.tv_usec - t0.tv_usec); if (micros > timeout) return 0; } gettimeofday(&t1, NULL); while (digitalRead(pin) == level) { gettimeofday(&tn, NULL); if (tn.tv_sec > t0.tv_sec) micros = 1000000L; else micros = 0; micros = micros + (tn.tv_usec - t0.tv_usec); if (micros > timeout) return 0; } if (tn.tv_sec > t1.tv_sec) micros = 1000000L; else micros = 0; micros = micros + (tn.tv_usec - t1.tv_usec); return micros; } int main (void) { int i, bit ; int j,k,l,m; long duration, distance; wiringPiSetup () ; mcp23s17Setup (BASE, 0, 0) ; printf ("距離センサーテスト with Raspberry Pi - MCP23S17\n") ; pinMode (BASE + trigPin, OUTPUT) ; pinMode (BASE + echoPin, INPUT) ; pullUpDnControl (BASE + echoPin, PUD_UP) ; for(i=0;i<100;i++){ digitalWrite(BASE + trigPin, LOW); // Added this line delayMicroseconds(2); // Added this line digitalWrite(BASE + trigPin, HIGH); delayMicroseconds(10); // Added this line digitalWrite(BASE + trigPin, LOW); duration = pulseIn(BASE+echoPin, HIGH, 1000000); //printf("duration=%d ",duration); distance = (duration/2) / 29.1; printf("距離 = %d cm\n",distance); sleep(1); } return 0 ; }
エンコーダー付きDCモーターの回転数を取得する
エンコーダー付きDCモーターから回転数を取得する。モーターは前に書いたpololuの12Vモーター。
https://www.pololu.com/product/2824
モーターの配線に関する情報は上記にある。
モーターは四個あり、回転数の分解能を大きくするためには、各モーターがgpioを2本使う。合計8本。これをRaspberrypi3の本体から全部取るというのは、できなくなはないが、他に使う余裕はなくなってしまう。
そこで、MCP23S17チップを使って、GPIOの拡張をすることにした。配線は、記載の通りやれば良い。
http://ww1.microchip.com/downloads/jp/DeviceDoc/20001952C_JP.pdf
結構配線間違いで、時間を使ってしまった。MCP23S17はSPIポートを使う。ラズパイの設定で、使えるようにしなければならないのは当然である。MCP23017という兄弟のようなチップもあるが、こちらはI2Cポートを使う。I2Cポートはサーボモーターのためにとっておきたいし、SPIの方が一つ一つのデバイスとの絡みで更新速度が早いなどのメリットもある。
これで16ピン増やせた。
ロボットのシステムはJAVAで動かすので、pi4jのライブラリと、MCP23S17のサンプルを取ってくれば、難なく動く。A0,A1,A2をLOWにしておかなければならないことと、RESETをHIGHにしておかないと正常稼働しないのには注意。
MCP23S17の拡張16GPIOポートのうち、8個はこのモーター回転数のために使うが、残りは、距離センサーのために使う予定だ。
ディープラーニングでターゲットの数字を認識
ラズパイのカメラを経由し、ターゲットに書いてある数字をディープラーニングで学習させたシステムで認識させようとしている。それによって舞台上にある目標位置に、ロボットが自動的に移動できるようにしたいわけだ。
ロボットのシステムソフトはjavaで書いてるので、ロボットのopencvもjavaで動かしたいのだが、色々やってもうまくいかないので、opencvはC++で動かして、それをネットワーククライアントにして、javaのシステムの方にネットワークサーバーを動かしてカメラデータを把握することにした。
ただ、カメラの生データをネットワークで取得すると、時間がかかりすぎりので、クライアント側で、分割して、システムの入力データに合うように加工し、画像認識の28X28ピクセルデータにしてロボットのシステムに送るようにすれば早い。システム側で、その画像に目標とする数字が書いてあるかを認識するのだ。認識システムそのものは、ここにも記載したディープラーニングで学習させたニューラルネットワークにさせる。
左上の画像が、ラズパイとカメラ、右上が数字が書いてあるターゲット、左下が、その画像を縦横4分割した場合の画像セットである。下の左から三番目の画像に、数字の4が写っている。それをニューラルネットワークに通すと、だいたい、信頼性の低い形で8とかの認識結果を出すのだが、ターゲットのあるところは、4が最も信頼性がある数字として出してくる。でも、その信頼性はとても低い。4以外にしたのはこの黒い画像も写り込んでいるからだ。
ラズパイのカメラをopencvで制御する
すけるくんに目標追跡機能を持たせる必要がある。
基本的な戦略は、カメラデータを取得して、その中にある目標対象のパターンを読み取り、見える位置などを把握して、距離センサーで、接近していくというものだ。
カメラデータの取得をOpencvにやらせる。当初は、opencvをjavaで動かしたかったが、Raspiにjava用のopencvを入れようとすると、なぜか最終局面で失敗する。それでも、やれないことはなかったが、opencvのコンパイルにむやみやたらに時間がかかるので、その路線を放棄した。
C系の言語でいいわということになったら、一番安直に
http://blue-black.ink/?page_id=2214
の物を入れた。opencvで、ラズパイのカメラが確かに動くサンプルもついていた。記載の通りインストールすると、しっかりカメラデータをライブで取ってきていた。
Netbeansでコンパイルできるように、そのプロジェクト化をする。ただし、一ヶ所だけ、Makefileの中で変えないといけないところがある。
USERLAND_ROOT
を実際にそれがある場所に変更しなければならない。
それで、NeabeansのC/C++ プロジェクト化できる。
サンプルの中の
IplImageデータの中に、カメラで取得したデータ、RGBそれぞれ1バイトずつ入っているので、それを利用すればいい。
戦略としては、画面データを様々なパターンで分割して、どこに標識があるのかを見極めよう。そのために、ディープラーニングで得られたパターン認識力を使う。
「すけるくん」のプロトタイプ
この間、インフルエンザのA型になって外出ができなくて、家に閉じこもっていた。その時間を利用して、なんども組み立て直していた「すけるくん」を、基本的に作り上げた。まだ、声かけに対して言葉を返したり、前後左右、回転などの動きをしたりする機能だけしか持たせていない。
仕様を書いておこう。
モーター(4個):pololu社製(pololu-2824)、統合型直行エンコーダー搭載、50:1、メタルギアドモーター、37mm X54mm 、12vで使用、無負荷時回転数: 200 rpm、無負荷時電流: 300 mA、ストール時電流: 5 A、ストール時トルク: 12 kg-cm (エンコーダーは、現在は使っていないが、いずれ回転数の正確な把握のために使う予定)このモーターの前に、ダイセン社ロボサイトモーターを使ってみたが、トルクが174g-cmしかなく、5kg以上あるすけるくんをしっかりと動かすことができなかった。トルクは70倍になったことになる。
ホイール(4個):メカナムホイール 60mm、6mハブ
メカナムホイール の装着
ロボットエミリーを前後左右斜めに自由に動かそうと、メカナムホイール を装着した。
ただ、最初付け方を間違えた。右2個、左2個買ってきて、その通り右と左につけたら、前進後退以外うまく動かない。よくよく調べてみると、ホイールの向きが間違っていた。前後に逆のものをつけなければならなかったのだ。笑ってしまった。
動画を見てもらえばわかると思う。
音声認識から音声合成への一連の手続きの覚書
RaspberryPi上で、Juliusを使って音声認識させ、open_jtalkで音声合成し、音声を出力するまでの一連の手続きの覚書である。
まず、juliusについては、ダウンロードしてコンパイルするだけで良い。ただし、 --enable-words-intオプションをつけたほうが良いかと思うが、現状していない。いずれ、制約が大きいと感じたら、こんんパイルし直せば良い。-mictypeも指定していなかったと思う。記憶は定かではない。
Juliusには、音響モデルと言語モデルが必要なのだが、このキットがダウンロードできる。最終的には、用途にあったモデルを作成する必要があるが、そう難しくはない。『音声認識システム第2版』などを参考にするとよくわかる。
キットを使う場合にはいくつか注意が必要だ。まず、前提として、macで同じことをやろうとすると、juliusをモジュールモードにした時にコマンドが正常に働かない。juiiusをjavaからコントロールする場合、ネットワークソケットを経由するのが最も便利だ。他に、ライブラリのlibjuliusだけを用いて、それをjavaのjnaを使って、javaから呼び出す方法も考えられるが、文字コード変換機能を自前で用意しなければならなかったり(そう難しくないようだ)、いろいろなコマンドを制御しなければならないので面倒になる。
Macでコマンドが働かないと言うのは、音声を喋らせているときは認識をPAUSEしなければ、喋っている音声を認識してしまうので、エコー化してしまう。それが終わったらRESUMEコマンドで再度認識させるようにしなければならないのだが、MACの場合、一旦PUASEしてしまうと再度、RESUMEしても、なぜだか認識するようにならないのだ。いろいろ探ったがダメだった。諦めた。本来、RaspberryPIで動けば良いので、macの問題は無視することにした。RaspberryPIの場合は、PAUSEとRESUMEコマンドは正常に機能する。
次に、文字認識キットの使用方法についてだ。当面使うのがいいと思うが、juliusのサイトには3種類置かれている。ディクテーションキットと話し言葉モデルキットが使われるべきだと思う。まず、後者は、dnn、すなわち深層ニューラルネットが使われているものであり、前者は、深層ニューラルネットとGMMの二つがある。基本、DMMモデルは認識が遅い。時間がかかる。
前者で、GMMを使うときのコマンドラインは、ディクテーションキットがあるディレクトリで、
julius -C main.jconf -C am-gmm.jconf
とやる。モジュールモードにするときは、
julius -C main.jconf -C am-gmm.jconf -module
とすれば良い。ディクテーションキットのdnnを使うときは、
julius -C main.jconf -C am-dnn.jconf -dnnconf julius.dnnconf
と-dnnconfオプションが必要となる。モジュールにする場合は、先と同様に-moduleを付け加えれば良い。
話し言葉キットの場合は、そのディレクトリで、
julius -C main.jconf -dnnconf main.dnnconf
とする。これについては、フォルダに、run.batがあるので、それを確認すれば良い。
次に、open_jtalkを使った音声合成である。
/usr/bin/open_jtalk -m /usr/share/hts-voice/Mei/mei_normal.htsvoice -x /var/lib/mecab/dic/open-jtalk/naist-jdic
の後に-owで出力のwavファイル名を指定して、その後にテキストファイルを直接指定する。上記は女性の声になる。女性の声を入れて置かなければならない。
できたwavファイルについては、aplayで発声させるのであるが、先の記事にも書いたように、
aplay -D plughw:2,0 test.wav
でオプションをつけないと音はならない。
デバイス番号については、
cat /proc/asound/cards
で調べる。私の場合は、
0 [Device ]: USB-Audio - USB PnP Audio Device
C-Media Electronics Inc. USB PnP Audio Device at usb-3f980000.usb-1.5, full spe
1 [ALSA ]: bcm2835 - bcm2835 ALSA
bcm2835 ALSA
2 [DAC ]: USB-Audio - USB Audio DAC
Burr-Brown from TI USB Audio DAC at usb-3f980000.usb-1.3, full speed
で、DACは2番なのだ。
ボリュームについては、
alsamixer -c 2
で、調整する。10Wx2では、相当小さくしないと、うるさい。