Pipe Renderでタグ「PTAMの改良」が付けられているもの

PTAM+ARToolKit+通信機能を合わせた動画を公開しました。
最後の通信の辺りは楽しんでいただけると思います。






詳細ページ:
http://render.s73.xrea.com/pipe_render/2009/03/ptam.html
http://render.s73.xrea.com/pipe_render/2009/03/ptam-2-2.html
http://render.s73.xrea.com/pipe_render/2009/04/ptam-3-1.html

BGM:エコテロニカ(sansuiさん)

※また、何度も書きますが、ライセンス上の問題により、
このバージョンのアプリは、実行ファイル・ソースコード共に公開いたしません。
ご了承ください。


前回の改良によって、PTAMのCG座標系の中心点をユーザーが決められるようになるので、
この座標系を離れた複数のユーザーで共有することも可能になります。

ARToolKitでもこれは可能でしたが、PTAMの場合はマップを広げることができるので、マーカーが映るのは最初の1回だけでOKになります。
部屋中にマーカーを張ることなく、いろんな方向を見てもトラッキングが失敗しないようにすることが可能です。


この座標系をユーザー間で共有するには、最低限、座標系を表す変換行列(カメラ座標系とワールド座標系の)が必要になります。


----- < 座標系データの送受信 > -----

通信には、とりあえずWinSockを使用することにしました。
ここで送信されるデータは、まだ変換行列程度ですが、速度を重視してUDPを使用します。

Geekなぺーじ:winsockプログラミング


変換行列はmpTracker->GetCurrentPose()で取得できるので、
これをそのまま送ってやることになります。



----- < CGの描画 > -----

受信側でデータを受け取ったら、そのデータを使用してCGを描画します。
この時、この変換行列が”ワールド座標系からカメラ座標系”の変換を表していることに注意してください。

例えば、マップの描画時に新しいカメラを追加する場合はMapViewer::DrawMap()内に処理を追加します。
関数内のDrawCamera(se3CamFromWorld);をもう一つ追加して、その引数に受信した変換行列を設定してやれば終了です。

3Dモデル(4つの目)の描画は、ARDriver::Render()関数内の、mGame.DrawStuff()で行われています。
ただ、この関数をそのまま使うと、引数に指定したベクトル方向に視線を合わせるようになるので、
そこの処理をコメントアウトし、相手のカメラの座標系に合わせる様にします。


この時描画されるCGでは、相手のカメラの位置と方向のみを表すことができます。
なので、PTAMのサンプルに入っている目のCGのような、単純なモデルが適していると思われます。



PTAMの問題点の1つに、「マップの3D座標空間が勝手に決められてしまう」というものがあります。

3D座標空間は、初期化完了時に出るグリッド平面で表されます。
これはCGにとっては「地面」を意味しているので、地面が正しい位置にないとCG表示もおかしくなってしまいます。
(例えば、垂直な壁が地面になってしまう等)


この問題を解決するには、地面の決定に別な方法を使う必要があります。例えば、
  • ユーザーのコントロールによって、地面の位置や方向を再調整する。
  • 加速度センサーでカメラの下方向を認識、そのベクトルにほぼ垂直で、面を形作っている特徴点の集合を探す
  • 地面の位置に目印を置いて、それを認識させる。
などが考えられます。


ここで、上記の項目に何か見覚えがあるものが入っています。それは、「目印」の認識です。
これは、今まで散々使ってきたARToolKitを使えば簡単にできそうです。



----- < ライセンスの問題 > -----

ですが、ARToolKitの利用には、ライセンス面で大きな問題があります。
ARToolKitのライセンスはGPLになっています。
そして、PTAMのライセンスもGPLに近く、しかしこれとは異なるライセンスとなっています。

この2つのソースを一緒にし、そして実行ファイルやソースコードを公開した場合には、2つのライセンスが完全に重なってしまい、ライセンス違反となる危険性があります。
なので、現在のバージョンでは、実行ファイルやソースコードの公開は行えません。


では、出力結果のみを公開する場合はどうでしょうか?
GPLでは、出力結果のみの公開で(条件はありますが)ソースコードの公開義務は発生せず、問題は起きません。
PTAMの、出力結果を公開する場合では、それがPTAMから出力されたことを明記する必要があるようです。

これならば、PTAM+ARToolKitを実行し、その動画を公開した場合には問題がないように思えます。



----- < PTAMでのマーカー認識 > -----

さて、PTAMにてマーカーの認識処理を加える場合、どういった処理が考えられるでしょうか?
  1. PTAMのマップ座標系を、マーカーから得られた3Dワールド座標系に合わせる
  2. 2次元画像内で、マーカーの四角の頂点位置を計算して、特徴点のリストに追加する
1の場合は、マーカー座標系をマップ座標系にうまく変形し、合わせてやる必要があります。こちらの方が正確ですが、座標系の違いを調査するのに手間がかかります。
2の場合は、マーカーの頂点だけ、他の特徴点とは別な処理を行う必要があります。こちらは逆に簡単ですが、正確性に欠ける場合があります。

理想的には1番なのですが、とりあえず最初に2番で実装してみます。



----- < 実際の変更点 > -----

PTAMのソースにARToolKitの処理を入れます。
2番では、ARToolKitマーカーの認識処理であるarDetectMarkerをPTAM内で呼ぶことになります。

  • PTAMプロジェクトにARToolKitの必要なプロジェクトを追加する

  • PTAMのループ内で、arDetectMarkerを呼ぶようにする。

  • arDetectMarkerの実行結果から、マーカーの頂点座標の値を取得する。

  • 取得した頂点座標を、Tracker::TrackForInitialMap()内の特徴点のステレオペアを格納しているところに入れてやる。

  • さらにその中で、MapMaker::InitFromStereo()が呼ばれているので、そこでステレオ計算させる。

  • ステレオ計算され、3次元情報の点になったら、その4点から基準となる座標系を作成する。

  • 作成したマーカー座標系使って、MapMaker::CalcPlaneAligner()内でグリッド平面の位置合わせを行う。

  • 変形されたグリッド座標系に合わせるように、MapMaker::ApplyGlobalTransformationToMap()でマップ上の点が変形させられる。
といったような修正内容になります。



追記(2009/03/31 22:56)

目印は平面であることが分かれば十分なので、SIFTなどの特徴点認識が利用できるかもしれません。
さらに、初期化を行うときの特徴点を平面に固定してしまうなど、いろいろ考えられます。

まぁ目印があればとても分かりやすく、場合によってはPTAMの初期化がいらなくなるので、楽かもしれません。



PTAMは優れたプログラムですが、問題点もいくつかあります。
その1つは、実行にある程度のスペックを必要とすることです。

現在発売されている通常の価格帯のPCであれば、このスペック不足はほとんど問題ありません。
しかし、PTAMをEeePCのような低価格PCや、w-zero3のような携帯端末で実行しようと、これが大きな問題となります。


そこで、その解決方法のうちの1つとして、別スレッドで実行されている「マップの更新」をスレッドで行わずに、
ユーザーの操作によって発生するイベントとして処理する方法を試してみます。


お気づきの方もいると思いますが、このような処理を行った場合、このプログラムは最早PTAM(Parallel Tracking and Mapping)とは言えず、STAM(Serial Tracking and Mapping)(なんじゃそりゃ)になってしまいます。

なので、本末転倒な気がしますが、たとえマップの更新が自動じゃなくても、その他のPTAMの機能はとても魅力的なものです。
これを携帯端末などで動かすことができれば、楽しいことができそうだと考え、改良してみることにしました。




---- < マップの更新処理に必要なもの > ----

この改良で重要なのが、「マップの更新がどのように行われるか?」ということです。
PTAMの解析でも大まかに説明しましたが、マップに特徴点を追加するにはキーフレームが必要になります。

ここで、キーフレームの決め方には2つ方法があります。
1.マップの初期化時に、ユーザーがスペースキーを押した時のフレーム(2個)が、キーフレームとして保存される。
2.マップをトラッキングしている時に、よりキーフレームに適したフレームが、自動的にキーフレームとして選ばれる。

1で決まった2つのキーフレームでは、特徴点のステレオ計算が行われて、3D座標としてマップに追加されます。
2で決まった1つのキーフレームは行列に入れられ、マップの更新スレッドが実行される度に一つずつマップに追加されます。



---- < キーフレームの追加方法 > ----


ということで、更新を手動で行う場合のキーフレームの追加方法も、2つあるということになります。
それは、ユーザーが決める方法と、自動で決めさせる方法です。

・ユーザーが決める場合: 初期化時と同じように、ユーザーが適切なキーフレームを選べるような手助けをしてあげなくてはいけません。
・自動で決めさせる場合: 適切なキーフレームが選ばれますが、それは現在見ている画像とは違うフレームになります。

さて、どちらを選ぶのがより良いでしょうか?
ちなみにPTAMの初期化作業は結構面倒で、慣れないと失敗します。対して、自動の場合はボタン1回押しで済み、フレームの違いも大きな問題にはなりません。

結果、後者の自動キーフレーム生成を選択しました。
キーフレームが追加できたら、あとは任意のタイミングでマップの更新処理を行ってやるだけです。



---- < ソースの変更点 > ----


では実際に、PTAMを改良していきます。

  • 最初にマップ更新スレッドを切らなくてはいけません。マップ更新のスレッドはMapMakerクラスで行われています。
    このクラスはCVD::Threadを継承しているので、この継承関係をコメントアウトします。

  • 継承を切ってしまうと、そのままコンパイルした時にエラーが出るので、必要ないもの(run()など)もコメントアウトします。

  • ここで、MapMaker::Reset()を行わないようにしてしまうと、マップのリセットなども行われず、プログラムが動きません。
    この関数もどこかで呼ばれるようにしましょう。(MapMaker::RequestReset()が呼ばれる所など)

  • キーフレームをマップに追加する処理は、MapMaker::AddKeyFrameFromTopOfQueue()で行われます。
    これを任意のタイミング(スペースキー押下時など)に実行するようにします。

  • このままだと、マップの修正処理が行われないので、MapMaker::BundleAdjustAll()などのマップ調整関数も、更新後に行われるようにしましょう。
大雑把な変更点はこんな感じになります。プログラミングの知識があり、PTAMの大まかな処理を理解できていれば、さほど難しくないと思われます。