日本大会の懇親会にて人気者のkossanの隣で爆睡していた、篠川です。

大会がひとまず一段落したので、またパラパラと記事を出していくつもりです。多分…

今回は私たちが日本大会で使った、全方位カメラでの相手ゴール周りの認識方法をものすごくざっくり説明します。

全方位ミラーを使う場合しか適応できない話が多くなりますが、ご容赦ください。

カメラの仕事は、相手ゴールの認識と、自陣ゴールの認識に大きく分けることができます。

この記事では前者についてです。

一応言っておくと、これはあくまで私たちが私たちのロボット向けに書いたプログラムの流れを、参考になるかもしれないと書き下しているだけのものです。

つまり、これは必ずしもベストとは限らないということです。

なので、ほかのチームにどれだけ当てはまるか、役に立つかは私もわかりませんし、もっといい方法も当然そのうち見つかるでしょう。

あくまで参考としてお読みください。

また、他にいいアイデアがあれば、ぜひコメントにお寄せください!

「この部分をもっと詳しく・よくわからない」といったご意見もどんどんお願いします。

内容

カメラが「相手ゴール周り」で認識するのは以下のことです。

  • 相手ゴールの方向
  • 相手ゴールの中でゴールキーパーがおらず空いている位置
  • 自機が前の角にいるか
  • ボールを蹴りだしてよいか

カメラは毎ループこれらを計算し、UART通信でメインマイコンに投げています。

大まかにこの順番に、どういうことをしているのかを説明していきます。

プログラムの書き方などはこの記事では扱いませんが、OpenMVに関してはIDEのメニューからFile→Examples以下のサンプルプログラムがとても参考になります。

そのまま貼り付けて使用するのはルール違反ですが、プログラムの書き方、ライブラリの使い方を理解する助けになると思います。

相手ゴールの方向

まずは相手ゴールの検知をするところから始まります。

全体的な方針として、角度を知りたいので、視野の中心、ミラーのてっぺんに当たる位置からぐるっと放射状に見ていきます。

close2

※画像左がロボットの正面

OpenMVで色を認識するといったら普通はimage.find_blobs(thresholds)を使うと思いますが、ここではそこまでする必要はありません

カメラをロボットに乗せた状態で、PCで視野を見ながらゴールのすぐ前→遠くまでだんだん離していくと、(当たり前ですが)ゴールが遠ざかっていくように見えますね。

close

far

ここで、下の写真の白い2円上をぐるっと見ていくと、その角度にゴールがあるなら、必ずその円上のどちらかの点がその色になっているはずです。

close with circles

far with circles

(手書きの雑な図ですみません)

ロボットが相手ゴールから背を向けるような向きになっている時はジャイロセンサーでまず正面(画像左)の向きに復帰する動きをさせるので、とりあえず前半分を回って確認していって、相手ゴールがあるかどうか調べていけばいいことになりますね。

ひとつ前のフレームとそこまでゴールの見える位置が変わることはないはずと考えると、毎ループで180度すべてを見回す必要もなく、直前にゴールがあった位置のすぐ近くを探索すれば、すぐにゴールが発見できます。

また、ある角度で黄色・青色のものを認識したとき、念のため+10度、ー10度の角度も確認し、干渉でないかどうか確認できます。周りに同じ色がなく孤立した小さなエリアはゴールではなく、ロボットのLED等の小さな部品が黄色・青色に見えているのだろうと判断できるからです。

というわけで、これによりゴールの左端の角度、右端の角度がわかりました。

相手ゴールキーパー回避

ゴールの位置が分かったところで、大抵はそこに相手ゴールキーパーがいます。

イナズマイレブンでもあるまいし、真ん中のゴールキーパーめがけてシュートしたり、ドリブルで突進していっても切ないので、ついでに相手ゴールキーパーも認識して、避けるように進ませたいところです。

というわけで、ゴールがある角度を7つくらいに分割して、それぞれの角度で「中心からどれだけ離れたところでゴールの色が出てくるか」ということを調べます。

中心から外側に向かって見ていって、最初に黄色・青色になるまでの距離を見るということです。

yellow

yellow enemy

さてここで、平均から明らかに外れて距離が遠いところがあります。上の写真で黄色く十字が打ってあるところです。

ここら辺を避けていくと、いい感じになりそうです。

というわけで、青の十字が一番長く並んでいる範囲に進めばよいですね。

補足すると、単に「平均より少しでも遠くに十字があれば、そこには相手!」とするのは良くないです。

コートの前側の角あたりにいてゴールの形が歪んでいたり、カメラの色認識に誤差が多少あったりすると、相手ロボット認識がいろんなところに出て荒ぶってしまうからです。

多少のマージンをつけましょう。

また、image.find_blobs(thresholds)はここらへんのところがごちゃごちゃにされています(よくわからん)。

相手ゴールキーパーがいるところもざっくりまとめて「ゴール」と認識されてしまったり、そもそも全方位に向いていないということで、この関数は結局一箇所も使わないことになりました。

ライブラリの関数はC言語で書かれてコンパイル済みになっており、Pythonで自力でループをまわして探して認識するより効率は良いはずなので、出来ればそちらを使いたいというのはあったのですが…

シンプルにゴールの位置を探すだけなら、きっとimage.find_blobsが一番楽なのでしょう。

ちなみに、去年の世界大会からは前向きにPSDセンサーを付けていて、正面の相手ロボットを認識してよけていくので、ドリブル時の敵よけはそちらにより頼るようになりました。

ただし、斜め前などの相手ロボットを避ける、相手ロボットの背丈が低い・肉抜きがすごくてあまり距離センサーが反応しない、もともと距離が遠いなどであれば、カメラの働きは大きいです。

コート前角検知

こちらは簡単なのでさらっと。

ゴールがめちゃくちゃ右・左に見えたら、それは自機がゴールに対して左・右にいるということです。

ゴールが真横近くにあるなら、自機がコートの前の方の角にいるとわかりますね。

相手ゴールの真横まで言ってボールを追いかけてもシュートにはつながらないし、角でアウトオブバウンズしたりゴールに引っ掛かったりすると、角は嫌なことづくしです。

というわけで、深追いせずに中立点の手前くらいまで後退し、アウトオブリーチやラックオブプログレスがかかるのを待つことにします。

ゴールの端っこが相手キーパーで隠されたりすると多少ずれてしまいますが、そこはあまり深く気にしないことにしました。

メインマイコンからジャイロセンサーの数値を送ってもらって、それを使って視界に映るゴールの角度を補正すると、割といい感じになります。

ボールを蹴りだしてよいか

ここら辺はキーパー検知と絡んできます。ロボットの正面と、左右数度を見渡して、ゴールがあり、相手ロボットがいなければ、キッカーを発動していいよ、となります。

メインマイコンは、ボールをキャッチした時、この情報をもとにキックするかを決めます。

これが「キッカーNG」という判定であれば、まだシュートせずに、相手ロボットの回避をがんばりつつドリブルしていくことになります。

その他の補足・小ネタ

カメラ(OpenMV)を使っているうちに気が付いた、本記事に関連するいくつかのことを、何かのために書いておきます。

FPS

PCにつないでカメラを走らせているときは、FPS(メインループが1秒で何周するか)を見ることができます。OpenMV IDEであれば右下にFPSというものが映っているはずです。

IDEに付属しているサンプルプログラムも、FPSを表示させるものがほとんどですね。

実は過去、後先考えずに相手ロボットの認識プログラムを書いていた時、気がつくとこれが5とか6になりました。つまりループごとに間隔が0.2秒くらい空いてしまうことであり、さすがに…という話になりました。

どうやって処理をショートカットして楽にするか、またどこの精度を落として手を抜けるか考えるのも大切そうです。

また、これもOpenMVの話ですが、PCにつないでFrame Bufferを表示させるだけで、通信でなかなかの時間がかかります。

IDE右上の「Disable」というボタンを押すと、Frame Buffer用の通信をしなくなるので、よりPCにつないでいないときに近いFPSを確認できます。

ただし「Disable」するとOpenMV IDE側でFPSを表示してくれなくなるので、サンプルプログラム通りにclock.fps()をprintしてみるのがよいと思います。

ちなみに現在の最新プログラムのFPSは30弱です。実はこれでも割と頑張った。

Stack overflow / Memory allocation error

OpenMVのメモリは実をいうとけっこう貧弱なようです。listやdictを大量生産していたとき、StackOverflowや"Cannot allocate memory"系のエラーが出て途方に暮れました。

気を付けましょう。

それから衝撃的なことに、静的変数を多く使用する、400行くらいになったプログラムをOpenMVに書き込み、実行させたとき、プログラムが長すぎてメモリに載りきらずに、エラーが出たり、ビジー状態になって手に負えなくなったりしました。

どうやらPCにつないで給電された際に、書き込んだプログラムをメモリに読み込み、さらにPCから「このプログラムを実行してくれ」と流し込んだことにより、400行プログラム2個分を読み込むことになっていたようです。

そこで、長めのプログラムが書き込まれた状態でPCと接続して、何かしらプログラムを走らせる時には、Hello Worldレベルの短いプログラムを一回だけ実行させてからだと、いったんメモリの中身がリセットされるのか、その直後長いプログラムをPCから実行してもエラーを吐かないことを発見しました。

Pythonプログラム中の半角スペースをTabで置き換えるのも地味に大きいです。

でもこれ1000行とか行ったら実行できなくなりそうだな…

通信

ほとんどの場合、メインマイコンに比べるとカメラのFPSはずっと低いです。通信でデータを投げるペースが全く違うので、メインマイコンでループ毎にカメラからの情報が来るまで待ち受けていると、それにメインマイコンの処理全体がものすごく足を引っ張られることになります。

メインマイコン側では「データが来るまで待つ」ではなく、「データが来ていたらなくなるまで受け取る」とするのがよさそうです。

もう一つ重要な点として、print()をPCとつないでいないときに呼び出すと、秒単位で遅くなります。

print()を使わないようにするか、ソースコードにif not debug: def print(): passとか書いておきましょう。

干渉

相手ロボットが黄色や青の部品を積んでいたり、LEDが青色に見えたりすることがあります(私たちもあまりほかのチームのことを言えないところはあります)。

多少の小さなもので影響を受けないような仕組みにすることも大切です。なにかとびとびで小さく変な黄色・青色(・オレンジ色)のものを検知したら、ゴール(・ボール)と区別し、弾いてしまいましょう。

試合が始まる前に相手ロボットをしっかり見ておくのも大切です。後悔先に立たず。

まとめ

ここまで、実際にどうプログラムを書くのかという部分は全ておいておいて、「なにを行っているか」ということを書いてきました。

おそらくかなり雑でわかりにくいところもあると思うので、ご指摘、ご質問はどんどんお願いします。

また、かなりザーッと流れを書いたので、「この部分をもっと詳しく知りたい」というような意見は大歓迎です。

自陣ゴール周りの仕事やキーパーの動きについても、そのうち記事を書きたいと思っているので、よろしくお願いします。