■前置き
まだまだSpherotoon開発を地道に続けています。
開発当初から以下の問題がありました。
- Spheroの操作が難しい
- Shpero同士が衝突するとSphero自体が発行している色と塗る色が異なってしまう
2番目の問題の根本はSpheroの色とは関係なく塗る色を決めているところでした。
現在のシステム(Shperoを認識してそこに色を塗るシステム)では、Kinectの深度センサーのみでSpheroの位置を認識しているため、Spheroの色が何であるかはわかりません。
どうやっているかというと。。。非常にお粗末で。
スタート時点でSpheroを認識したときに勝手に色を決めてしまっています。最初に見つけたものを緑、次をピンクという具合です。
ゲーム開始当初は合っていても、深度センサーでSpheroを見失ったときに極端に位置が変わっているとこの2番目に挙げた問題が発生します。
何か方法がないか屋内における位置検出を軸として関連研究を探しました。
個体を認識するためにセンサーを追加するとか動きのパターンから個体を特定するとか。様々な手法はありましたがこの例には役立ちそうもありません。
ふと考えてみるとKinectには深度センサーの他にカラーのイメージセンサーがついています。これ使えばShperoの色が認識できるかな?と思ったのです。ちなみにこの辺のテーマで実験をされている方がいます。方式は違うので参考にはなりませんでしたが同じようなことを考えて実行している人がいるんだなぁ。。。と元気をもらいました。
HACKist » ボール型ロボットティックトイ「Sphero」の制御実験をしてみた
本題に戻します。
やりたいことは、Kinectの深度センサーで見つけたSpheroの位置のカラー情報(具体的にはofColor)をカラーイメージセンサーから取得し、その色が何色か(緑かピンク)を特定して、その色をそのSpheroの色として塗りつぶす色に使おうというトライです。
ここで問題がSpheroの色を特定する処理です。
最初はRGBで類似度(ピンクらしさ?緑らしさ?)を調べようと実験してみたのですが差異がわかりにくい。。。Hue(色相)を使うといけるんじゃないかと思い出し、少し試してみるといけそうでした。
で、ピンクと緑をoFでそれぞれ以下のように定義してやって、それぞれのhue値をgetHue()メソッドで取得し、カラーイメージセンサーから取得したofColorの値のhue値を同じくgetHue()メソッドで取得し、pinkとgreenのどちらに近いかを判定するコードを書こうとしました。
pink = ofColor(255, 0, 255); green = ofColor(0, 255, 0);
ここまでが前置きです。
■本題
7月くらいから空き時間を見つけて動くことを優先して作ってきたためコードは継接ぎだらけ。
数日前からリファクタリングをしながら、最後にこのhue値を使った色類似度計算に行きつきました。
とりあえずノートにアイデア書いて以下のようなプログラムを作りました。ベタですが。。。(言い訳ですがラグビーのサモア戦を見ながら飲みながら作っていたので頭はあまり回っていません。。。)
ちょっと補足が必要と思います。
ofColorのgetHue()メソッドはHue値を角度ではなく0-255の値で返してくれます。色相は上記のWikipediaの解説の通り環状になっています。0から始まり255の次は0に戻ります。なので5と250の距離(角度かな?)は10になります。類似度は2つの値の差で求まりそうですが、大小関係があるので絶対値を取ればいいはずです。でも、255から0に戻るところを跨ぐ処理が面倒ですね。。。あれこれ考えて以下で行けると考えました。
int distanceBetweenTwoHues(int hue1, int hue2){ int d = abs(hue1 - hue2); if (d > 127) { if (hue1 > hue2) { d = 255 - hue1 + hue2; } else { d = 255 - hue2 + hue1; } } return d; }
とりあえず動かしてみると答えはあってそうです。。。でもなぁ。。。もう少しスマートにできないものかいな?とつぶやいたのが以下です。夜の1時半くらい。眠気マックスでしたが。。。とりあえずここまでで就寝しました。
hueを使ってある色が他に与えた2つの色のどちらに近いかを計算させる関数を作っている。ofColorのhueは0-255だから境界がある。時計やカレンダでも出てくるなぁ。。。modではうまくいかんし。結局ifで場合分けを記述する必要がある。数学でなんとかできないのかな?
— Keiichi Takahashi (@ke_takahashi) 2015, 10月 3
朝、パソコンに向かうと以下のリプライが。。。ありがたいです。そうです。類似度といえばcosθ。忘れてました。。。
@ke_takahashi 色相値は円環状に変化する値なので、0 - 255 を 0 - TWO_PI にmapしてやれば単位ベクトルとして表現できるので、任意の2つの色相値の類似度は2つの単位ベクトルがなす角度の大きさで表現できます。
— つゆき (@T_Y_K) 2015, 10月 3
とりあえず数式をまとめるとsin, cosを数回計算すればいけそうです。計算してみると上記の関数と同じ答えがでました。でも、ちょっと遅そう。。。
#include <math.h> #define two_pi 6.28318530717959 int distanceBetweenTwoHues2(int hue1, int hue2){ // using the theorem of cosines if (hue1 == hue2) return 0; double hue1_rad = hue1 * two_pi / 255; double hue2_rad = hue2 * two_pi / 255; double rad = acos(cos(hue1_rad) * cos(hue2_rad) + sin(hue1_rad) * sin(hue2_rad)); int d = int(rad * 255 / two_pi + 0.5); return d; }
で、時間計測してみました。
2つのhue値を0から255まで変化させて与えます。それを100回繰り返します。それを10回実行したときの平均時間(ミリ秒)を計測しました。ちなみに以下が実験プログラムです。oFではなく普通(xcode)のc++ですが。
#include <chrono> int main(int argc, const char * argv[]) { double ave = 0; for (int i = 0; i < 10; i++){ auto start = std::chrono::system_clock::now(); for (int j = 0; j < 100; j++) { for (int hue1 = 0; hue1 < 256; hue1++){ for (int hue2 = 0; hue2 < 256; hue2++){ distanceBetweenTwoHues2(hue1, hue2); // distanceBetweenTwoHues2(hue1, hue2); } } } auto end = std::chrono::system_clock::now(); auto dur = end - start; auto msec = std::chrono::duration_cast<std::chrono::milliseconds>(dur).count(); ave += msec; } std::cout << (ave / 10) << " milli sec \n"; return 0; }
結果は以下の通り。
最初の方(ベタな式)は36ミリ秒で
cosθを使った方は610ミリ秒でした。
ベタな方が約20倍速いことになります。
フレーム毎にSpheroの個数分この処理が実行するだけですのでそれほど違いはないですね。大きな画像や動画に対して画素毎に類似度を判定する。。。場合には高速化に貢献するかもしれません。
とりあえずリファクタリングも終わったし、類似度計算もできるようになったし。明日、実際に動かして色識別がうまくいくか実験してみよう。
とりあえず今日も遅いので寝ます。