執筆/雑誌/SD/2009/03

まず、動かすには。

  1. JMFをインストールする
    1. JMF 2.1.1e Softwareより、入手してください。
  2. Webカメラを接続する
    1. JMFに対応しているものである必要がありますが、たいていの場合は、大丈夫です。
  3. カメラサーバを起動する
    1. ソースコードのWebcamBroadcasterディレクトリのWebcamBroadcasterを実行します。
    2. コンパイル済みのバイナリがおいてありますので、下記のように実行できます。
      1. java WebcamBroadcaster
  4. 母艦PCのIPを調べる
    1. Androidエミュレータは、qemu上で動いているため、「localhost」によるアクセスは出来ません。そのため、母艦PCに何かしらのIPアドレスが必要になります。
      1. プライベートIPでも構いません。
    2. 調査法は、Windows系であれば「ipconfig」、Mac/Linux系であれば「ifconfig」で調べることが出来ます。
  5. ソースコード一式をEclipseに読み込む
    1. 技術評論社さんの特設ページからダウンロードしてください。
    2. File⇒Import⇒Existing Projects into Workspace を選択してください。
    3. 「Select root directory」に、解凍先のディレクトリを指定してください。
    4. 「Projects」欄に「NyARtoolkitAndroid」が表示されますので、選択して 「Finish」を押してください。
  6. ソースコードのIPアドレスの設定を変更する
    1. res/values/strings.xml
      <string name="server_addr">192.168.86.1</string>
      の 部分を上記で調べたものに変更します。
  7. マーカーを印刷する
    1. res/raw/marker.jpg
      1. を、印刷してください。
  8. 実行する
    1. Eclipseから、実行してください。エミュレータが起動して、カメラ画像が表示されるはずです。
    2. 上記で印刷したマーカーを、カメラで写すと、3Dオブジェクト(デフォルトでは、椅子)が表示されます。

表示させる3Dオブジェクトを変更する

  1. assetsディレクトリに、読み込ませたい3Dオブジェクトのデータをコピーしてください。
    1. メタセコイヤ形式ファイルに対応しています。
    2. Androidの制限により、1MBまでのファイルしか読み出せません。
  2. ソースコード「NyARToolkitAndroidActivity#onCreate()」
    mRenderer = new ModelRenderer(false, getAssets(), "chair01.mqo", 0.04f);
    1. の行をコピーしたファイル名に変更してください。
      1. 最後のfloat値は、倍率です。

G1やDev Phone 1で動かす

  1. res/values/strings.xml
    <string name="camera_name">jp.android_group.artoolkit.hardware.SocketCamera</string> 
    を、
    <string name="camera_name">jp.android_group.artoolkit.hardware.Dev1Camera</string>
    に変更する。

ソースコードを追いかける(詳細編)

細かい点について、コードを順番に追っていきます。

NyARToolkitAndroidActivity#onCreate(Bundle)を見てみる

これが、main()メソッドに当たる部分です。ここを読むと、何をするのか?が、だいたい把握できます。

ModelRendererのインスタンス化

ModelRendererクラスは、各種3Dモデルを読み出して、描画するためのクラスです。基本的には、Android搭載の3Dチップ向けの調整や設定を入れ込んでいます。ですので、今後、Android機が増えてくれば、これを拡張したり、違うものを用意したりすることになります。

// Renderer
mRenderer = new ModelRenderer(false, getAssets(), "chair01.mqo", 0.04f);

最初の引数は、こういうものだと思ってください。エミュレータとDev Phone 1の3Dアクセラレータチップの初期化パラメータをいろいろと調整した結果、付加されたものです。

2つ目のパラメータ「getAssets()」は、「assets」ディレクトリ内のデータを読み込むために、Androidに用意されたユーティリティーです。zipファイルなど、外部のファイルから読み込むデータは、ここにおいておくのが、Android仕様のようです。

3つ目以降は、表示させる3Dオブジェクトを変更するを参照してください。

メッセージハンドラ

続いて、次の行に

mRenderer.setMainHandler(mHandler);

というものがあります。これぞ、Androidフレームワーク!というべきものの一つです。
setするところは良いとして、このハンドラを利用している箇所を見てみると、ModelRenderer#initModel(GL11)内にあります。

if (mainHandler != null) {
    mainHandler.sendMessage
        (mainHandler.obtainMessage
         (NyARToolkitAndroidActivity.SHOW_LOADING));
}

ハンドラのHandler#sendMessage()メソッドを使って、「NyARToolkitAndroidActivity.SHOW_LOADING」を送っています。
これが、どこに届くのかというと、NyARToolkitAndroidActivity内にあります。

private class MainHandler extends Handler {
    @Override
    public void handleMessage(Message msg) {
            (略)
            case SHOW_LOADING: {・・・・・・・・・・・
                showDialog(DIALOG_LOADING);
                break;
            }
            case HIDE_LOADING: {
                try {
                    dismissDialog(DIALOG_LOADING);
                    removeDialog(DIALOG_LOADING);
                } catch (IllegalArgumentException e) {
                }
                break;
            }
        }
    }
}

この中で、,caseにより、「showDialog(DIALOG_LOADING)」が呼ばれて、Android上にはLoadingダイアログが表示されるという動作になります。


なぜ、こんなめんどくさいことをするのかというのは、Androidフレームワークのライフサイクルで!

Cameraクラスの取得

続いて、if文が並んでいます・・・・・・・・・・・・・・・・・・・・・・・・・・・・・
すみません、本当は、リフレクションにより取得するようにしようかと思ったのですが、ifで並べちゃいました。
Androidにおけるリフレクションについては、こちらにサンプルコードがありますので、参照していただければと思います。普通のJavaと全く一緒なので、問題はないと思いますが。

Androidフレームワークのライフサイクル

Dev1Cameraクラスのインスタンス化時に、NyARToolkitAndroidActivity自身を与えています。なぜ、こんなことをやるのでしょうか?

mCameraDevice = new Dev1Camera(this, mSurfaceView);

これこそが、Androidフレームワーク!なのです。Androidフレームワークでは、アプリケーションのライフサイクルを定義しています。そのため、Androidフレームワークのライフサイクル外で作られたインスタンスからは、Viewなどへアクセスできなくなります。
これにより、電池寿命が重要なファクターであるモバイル機器においても、厳密な電源管理をすることが出来る反面、上記や下記のように、onCreate()内やNyARToolkitAndroidActivityのインスタンス化時に引き渡しておく必要があるのです。(getterやsetterによる方法やメッセージハンドラによる方法がありますが、それを使わない理由は、最長不倒関数を参照してください。)
また、Androidのサンプルによく出てくる、下記のようなソースコードは、上記の理由に由来していると思われます。

mOrientationListener = new OrientationListener(this) {
    public void onOrientationChanged(int orientation) {
        mLastOrientation = orientation;
    }
};

そして、正攻法な方法としてメッセージハンドラが、このAndroidフレームワークのライフサイクル外で作られたインスタンスからアクセスするために用意された方法なのです。

VoicePlayerクラス。

音声ファイルを与えると、『3Dオブジェクトが表示されているときにその音声ファイルを流す』ことができる、というものです。
使用したい場合は、下記の部分をコメントアウトし、「res/raw/」ディレクトリに再生したい音声ファイルを入れてください。
ちなみに、使いどころは、、、内緒です。(;^_^A アセアセ・・・

       //TODO init VoicePlayer for ARToolkit.
//        VoicePlayer mVoiceSound = new VoicePlayer();
//        mVoiceSound.initVoice(getResources().openRawResourceFd(R.raw.xxx_voice));
//        arToolkit.setVoicePlayer(mVoiceSound);

Dev Phone 1モードでの画像の向きについて

もしかすると、画面が傾いていませんか?
本来は、それを制御するためには、OrientationListenerクラスを利用します。
NyARToolkitAndroidActivity#onCreate(Bundle)内で、

mOrientationListener = new OrientationListener(this) {
   public void onOrientationChanged(int orientation) {
      mLastOrientation = orientation;
   }
};

と、インスタンス化しています。
が、これ、思ったように動いてくれません。Dev Phone 1を、立てているにもかかわらず、『横』の判定になってしまったり、動かしていないのに、縦から横になってしまったりと、いまいち、安定しません。
そのため、今回は、入れてはおきましたが、使っていません。

有効にするには、Dev1CameraクラスのインナークラスであるImageCapture#capture()内の

//final int latchedOrientation = NyARToolkitAndroidActivity.roundOrientation(arActivity.getLastOrientation() + 90 );
(略)
parameters.set("rotation", 90);
//parameters.set("rotation", latchedOrientation);

のコメントアウトをはずしてもらえれば、OKです。
安定させられた方、是非、ご連絡をお願いします。

キャリブレーションについて

上記のOrientationListenerによる姿勢制御により、縦横が切り替わったタイミングで画面のサイズが(縦横比)が変わります。そのため、この横サイズにあったARToolkit用のキャリブレーションファイル(res/raw/camera_para.dat)が必要になります。
今回は、用意していないので、ご自分で用意してください。

ARToolkitDrawer#draw(byte[])を見てみる

ここが、すべての動作を一つに束ねる部分です。ここを読むと、何をしているのか?がだいたい把握できます。

OutOfMemoryErrorは、敵

実は、カメラから撮れる画像のサイズが、大きすぎて、「OutOfMemoryError」が発生し、アプリが落ちてしまうことがあります。しかも、「OutOfMemoryError」は、どうやらAndroidではハンドリングが不可能なようで、アプリが強制終了させられてしまいます。
そこで、サイズ調整のために入っているコードが、下記の部分です。
,如1/4サイズに調整するというオプションを指定しています。
しかし、これだと、小さくなりすぎてしまう場合があるので、△如Heightが、240より小さい場合は、元のサイズの画像を使うようにしています。

BitmapFactory.Options options = new BitmapFactory.Options();
options.inSampleSize = 4;・・・
Bitmap bitmap = BitmapFactory.decodeByteArray(data, 0, data.length, options);
if(bitmap.getHeight() < 240) {・・・
    bitmap = BitmapFactory.decodeByteArray(data, 0, data.length);
}

今回は、このような方法で逃げられましたが、実際には、メモリ制限はかなり厳しいものになる可能性があります。最悪、メモリアロケータを自作するなど、メモリの使用量は、厳密に管理する必要があるようです。(Activity#onLowMemory()をどうにかする方法もあるかもしれませんが。。。)

Threadの恐怖

基本的にこのメソッドは、カメラデータの加工やARToolkitの処理、3D描画の下処理など、非常に時間のかかる重い処理となります。
そこで、ここでいろいろとずるをするためにも、

例えば、ARToolkit処理1回につき、背景画像の書き換えだけは3回やる などすると、『一見スムーズ動いている』ように見る

描画の書き換えの細かい制御をしたいと思い、どうしようかと足掻いてみました。
結論から言うと、ひとまず、あきらめました。すみません!!!次のバージョンで考え直しますので、少々お待ちください。<(_ _)>


で、そのときの失敗談を、下記に記します。
まず、画面描画の制御ですが、GLSurfaceViewで行っています。そして、GLSurfaceViewには、

GLSurfaceView#onPause()

という、画面描画を一時停止させるメソッドが用意されていますので、これを利用しようとして、ソースコードを見ると、、、

EglHelper#finish()

を呼び出してました!EglHelperが初期化されてしまいます。
処理を軽くしたいので、これでは、本末転倒なので他の制御法を探したところ、

GLSurfaceView#queueEvent(new Runnable())

がありました。これですと、Runnable#run()で定義した処理が終了するまで、画面描画を制御できます。
ということで、これに「ARToolkitDrawer#draw(byte[])」の処理をそのまま入れてみたところ、

dead lock!orz

そして、調査を進めたのですが、時間切れとなり、妥協的に入れたのがNyARToolkitAndroidActivityクラスにある

private void draw() {
    drawFlag = true;
    try {
        // The measure against over load. 
        Thread.sleep(500);
    } catch (InterruptedException e) {
        ;
    }
    drawFlag = false;
    mGLSurfaceView.queueEvent(new Runnable() {
        @Override
        public void run() {
            while (!drawFlag) 
                try {
                    Thread.sleep(400);
                } catch (InterruptedException e) {
                    ;
                }
            }
        }
    );
}

です。だれか、カッコ良く作り直してくれるとウレシイです。<(_ _)>

訂正とお知らせ

RGBの並びについて

Camera×Callback×Bitmap節にて、RGBの持ち方に関する記述がありますが、

ビットマップとOpenGL/ESとのRGBの持ち方の違い

という方が正しいとの、指摘がありましたので、訂正させていただきます。

Android Tips

Android開発全般

パッケージの制限

EclipseでAndroidプロジェクトを作成するときに指定したパッケージ下以外に、ソースコードを入れてもコンパイルされません。
Androidプロジェクトでは、このような制限が多いので、パッケージの名前を変えるときは注意してください。

javaxなどのJ2SEライブラリ

J2SE完全互換ではないため、一部、入っていないライブラリなどがあります。とくに、javax以下が微妙に入っていないので、けっこう、つらいものがあります。
しかし、入っていないからと言って、javaxと同名のパッケージ+クラスを自作しても、動きません。Androidのコンパイル時のチェックにより、そのようなクラスは、弾かれてしまいます。
そのため、システムイメージから変えないといけないため、ハードルが高いです。

xxxManagerやxxxRegistrar、xxxProvider

「コラム:Androidでのデバイスの扱い」で、書いたようにAndroidには、xxxListener+xxxCallbackがあります。
そして、実はさらに、これらxxxListener+xxxCallbackを束ねる、xxxManagerやxxxRegistrar、xxxProviderなどがあります。
xxxListener+xxxCallbackは、これらに登録されて、一元的に扱えるようになっています。これにより、例えば、電話も3Gによる着信か?IP電話による着信か?など気にすることなく、一元的に扱えるように出来ます。

最長不倒関数

Dev1Cameraクラス

とか、ローカルな変数やら処理しまくりのメソッドやら、もっと切り分けた方がきれいなのに!とか、思いませんか?私も思います。(;^_^A アセアセ・・・
しかし、実は、意味があったりします。

それは、Dev Phone 1 や G1 などの非力なCPUですと、

メソッド呼び出しコストでさえバカに出来ないコスト

になります。
とくに、このDev1Cameraクラスのようなハードウェアと連携するようなクラスですと、致命的と言うくらいのものになります。

そのため、Androidのソースコードリポジトリにあるソースコードも、速度が必要なものは、このような構造となっており、ざらに3000行を超えるクラスなどもあります。OHAさんも、速度チューニングに苦労しているということです。

ハードウェア全般

カメラの種類

StaticCameraローカルに保存された画像をカメラ画像として読み込む
SocketCameraJpeg画像を、Socket経由で取得する
Dev1CameraG1やDev Phone 1向け実装
AttachedCamera本来的なCamera画像の取得の方法。G1やDev Phone 1では、うごかないorz⇒理由は、撮れる画像フォーマットが、Bitmapクラスで扱えないためです。バグ扱いで、すでにレビューステートに入っているので、近いうちに使えるようになるはずです。

WebcamBroadcaster

ちなみに、Armadillo-500上のAndroidでカメラプレビューする方法で使っているものと同じフォーマットなので、Armadillo-500上で本アプリは動作させることも出来ます。

マジックナンバー「1600」ってなに?

SocketCameraクラスで、カメラ画像のストリームを読み込むところにある、

if(offset+1600 > buf_size){

の「1600」ですが、Etherの1パケットサイズです。

3D全般

3D詳細

Androidのための3DやOpenGL/ESを操作するために、

GLBg
GLSurfaceView
ModelRenderer

というクラスを用意して使っています。
これらは、はっきり言って、AndroidのOpenGL/ESとの戦いの記録でしかありません。要するに、3Dチップの制限だったり、まだ、未実装としか思えなかったり、ただのバグとしか思えなかったり、などをどう回避するかというバッドノウハウの固まりです。orz
なので、基本的には、『なにも考えずにそのまま使え』ということで、解説は省略させていただきます。
このへんの詳細の話は、3DやOpenGL/ESに関しては、エイチアイの高橋さんなどをお呼びして、セミナーを開催させていただこうと思いますので、ご了承ください。

ARModelインタフェース

すみません。本当は、各種3Dフォーマットをこれでラップするはずだったのですが、間に合いませんでした。
次のバージョンでは、実装したいと思います。

NyARToolkitとメタセコの読み出しライブラリについて

現在、NyARToolkitのVer.2.0化 と Androidに付属しているfloat系のネイティブライブラリを使用するように変更を行っています。
それにあわせて公開する予定ですので、少々お待ちください。

NyARToolkitを2.2.0に対応させました。

おまけ

本節の1ページ目の写真の手

  • 手タレとして出演してもらったのは、これです。協力、ありがとうございました!
  • 原稿料の一部を手タレ料として、「第十三回 第1回チキチキjava-ja 温泉」の育英会募金に寄付させていただきました。