技術評論社さんのご厚意により、本文を公開いたします。
「SoftwareDesign」2009年03月號のページ(ソース一式がダウンロードできます。)と解説もご覧ください。
Android + AR(拡張現実感) で広がる現実世界
みなさん、「Augmented Reality:拡張現実感(以下、AR)」をご存じでしょうか?ARとは、現実世界の情報に、バーチャルな情報をオーバーレイすることにより、現実世界に新しい可能性を切り開くものです。
本節で作成するアプリでは、カメラで撮った画像を解析して、マーカーを認識し、その上に3Dオブジェクトをオーバーレイ表示することにより、ARの一端を実現します。もちろん、オーバーレイする情報を変えるなど、さまざまな応用が可能です。とくに、Android版ARアプリケーションの大きなアドバンテージは、持ち運べることにあるといえるでしょう。例えば、GPSによるロケーション情報を組み合わせたりすることが可能です。持ち出せるARアプリケーション、面白いと思いませんか?
本アプリは、ライブラリのとしての側面が強いため、改造し独自アプリを作る上で必要な部分とAndroidプログラミングとして特殊である部分の解説に絞らせていただきます。代わりに、日本Androidの会のHP上に[[特設コーナー>執筆/雑誌/SD/2009/03]]を設けましたので、そちらをあわせてご覧いただければと思います。~
リスト1に示すように、Androidパッケージ名は「jp.android_group.artoolkit」、Activity名は「NyARToolkitAndroidActivity」です。ソースコードを読む場合は、NyARToolkitAndroidActivity#onCreate(Bundle)で初期化処理を行っていますので、ここを起点に読み進めていただければと思います。また、大まかには
-カメラによる、リアル映像の取得 -Androidにおける、デバイスの利用方法 -ARライブラリによる、マーカーの検知と三次元位置・姿勢の取得 -Androidにおける、外部ライブラリの使用方法 -3D(OpenGL/ES)による、3Dモデル・オブジェクトの描画・表示 -Android版OpenGL/ESの利用方法
のように構成されております。以後は、この順番にしたがって解説を進めていきます。
-jp.android_group.artoolkitパッケージ・・・本アプリのベースパッケージ - NyARToolkitAndroidActivityクラス・・・本アプリの起動Activityクラス - ARToolkitDrawerクラス・・・ARToolkit処理のためのクラス - ModelRenderer・・・GLSurfaceView#Rendererインタフェースの実装 -jp.android_group.artoolkit.hardwareパッケージ・・・各種カメラ - SocketCamera・・・Socketでカメラ画像を受信するカメラの実装 - G1Camera・・・G1のカメラデバイスのラッパー実装 -jp.android_group.artoolkit.modelパッケージ・・・各種3Dオブジェクト - Cube・・・AndroidのサンプルにあるCubeクラス -jp.android_group.artoolkit.viewパッケージ・・・OpenGL/ES処理のためのViewクラス - GLSurfaceView・・・OpenGL/ES処理のためのSurfaceViewクラスの実装 - GLBg・・・背景テクスチャを貼り付けるためのOpenGL/ES処理実装
最初に、カメラ画像の取得を行います。しかし、Androidエミュレータには、カメラデバイスがついていないため、カメラ画像を取得することが出来ません。かといって、執筆現在(2009/01)、国内でAndroid実機を入手するのは一般的ではありません。(※注1) そこで、Androidエミュレータ上から母艦PCに繋げたWebカメラデバイスの画像を取得する方法
を紹介します。
Android Marketより、Dev 1 Phoneの発注が可能ですが、$499とお値段が張る面もあり一般的とはいえません。
動作には、JMF(Java Media Framework)が必要です。
public class WebcamBroadcaster { // (略) public void start() { synchronized (lock) { // JMFによりWebカメラデバイスを初期化する。 worker = new Worker(); worker.run(); } } private class Worker extends Thread { public void run() { ServerSocket ss; ss = new ServerSocket(port); while(true) { Socket socket = ss.accept(); OutputStream out = socket.getOutputStream(); // JMFによりWebカメラデバイスから、画像データを取得する。 for (int i = 0; i < data.length; i++) { dout.writeInt(data[i]); } } } } }
public class SocketCamera implements CameraIF { // (略) public void setPreviewCallback(PreviewCallback callback) { this.callback = callback; } private class CaptureThread extends Thread { public void run() { while (!mDone) { Socket socket = new Socket(); socket.connect(IPAddress_and_Port, SOCKET_TIMEOUT); InputStream in = socket.getInputStream(); // (略) callback.onPreviewFrame(buf, null); } } } } public interface PreviewCallback { void onPreviewFrame(byte[] data, Camera camera); };
カメラから画像データが取得できましたので、この画像データをAndroidで処理できる形に変換します。画像データは、byte配列として、リスト3のPreviewCallback#onPreviewFrame(byte[], Camera)メソッドに渡されてきます。このPreviewCallbackインタフェースは、リスト4のNyARToolkitAndroidActivityクラス内で実装されており、ARToolkitDrawer#draw(byte[])メソッドを呼び出しているだけとなります。そのため、具体的な処理は、リスト4のARToolkitDrawer#draw(byte[])メソッド内で行われています。
ARToolkitDrawer#draw(byte[])メソッドでは、BitmapFactory#decodeByteArray(byte[], int, int)メソッドを利用して、Bitmapクラスに変換しています。このBitmapクラスが、Android内で画像を扱うためクラスとなります。しかし、Bitmapクラスは、RGBデータの持ち方が特殊であるため、他のアプリケーションで利用したい場合、RGBの変換が必要となります。今回は、ARライブラリでも利用するため、for文の内部でAGRBになっているものをRGBAに変換しています。
public class NyARToolkitAndroidActivity extends Activity implements View.OnClickListener, SurfaceHolder.Callback { private ARToolkitDrawer arToolkit; (略) private final class JpegPreviewCallback implements PreviewCallback { public void onPreviewFrame(byte [] jpegData, Camera camera) { if(jpegData != null) { arToolkit.draw(jpegData); } } }; public class ARToolkitDrawer { (略) public void draw(byte[] data) { bitmap = BitmapFactory.decodeByteArray(data, 0, data.length); int w = bitmap.getWidth(); int h = bitmap.getHeight(); int[] pixels = new int[w * h]; byte[] buf = new byte[pixels.length * 3]; bitmap.getPixels(pixels, 0, w, 0, 0, w, h); for (int i = 0; i < pixels.length; i++) { int argb = pixels[i]; byte a = (byte) (argb & 0xFF000000 >> 24); byte r = (byte) (argb & 0x00FF0000 >> 16); byte g = (byte) (argb & 0x0000FF00 >> 8); byte b = (byte) (argb & 0x000000FF); buf[i * 3] = r; buf[i * 3 + 1] = g; buf[i * 3 + 2] = b; } (略) } }
デバイスからカメラ画像を取得するためにxxxCallbackインタフェースを利用しました。実は、この方法は、Androidフレームワーク内のデバイスで共通仕様となっており、全てのデバイスに対して応用が可能なものなのです。~ ちなみに、この他に同じ役割をするものとして、xxxListenerインタフェースがあります。これらの違いは、Androidフレームワークが管理しているデバイスかどうかです。例えば、Androidフレームワークが管理しているGPSデバイスを扱うものは、android.location.LocationListenerインタフェースとして定義されております。そして、実は、カメラデバイスは、Androidフレームワーク内で管理されているデバイスの様に見えますが、実際には、Cameraクラスの内部では、全てNative関数を呼び出しており、独自デバイスの扱いになっています。それゆえに、xxxCallbackインタフェースなのです。
本アプリでは、ARを実現するためにARToolkit(コラム参照)のAndroid向け移植である「Android版NyARToolkit」を利用しています。大まかな動作としては、カメラで撮った画像を解析して、マーカー画像(図2)を認識し、その三次元位置と姿勢を返すものとなります。
ARToolkitは、ARを実現するためのライブラリです。加藤博一先生よって開発され、現在は、ワシントン大学のHIT Lab NZ, ARToolworks.Incによって開発が進められています。 ARToolkit公式サイト:http://www.hitl.washington.edu/artoolkit/NyARToolkitプロジェクトは、いろいろな言語へのARToolkitの移植を行っているプロジェクトです。本アプリでは、本プロジェクトの成果を利用しています。また、記事化にあたり、利用を快く許可していただいた、A虎@様並びに九州工業大学の和泉様には、この場を借りて御礼を申し上げます。NyARToolkitプロジェクトサイト:http://sourceforge.jp/projects/nyartoolkit/
NyARToolkitの設定や初期化は、リスト5のARToolkitDrawerクラスで行っています。インスタンスの引数として、初期化に必要なマーカー画像(camePara)とパターンファイル(patt)を受け取ります。このインスタンス化は、NyARToolkitAndroidActivity#onCreate(Bundle)メソッドで行っています。
初期化は、画像の縦横サイズが必要なため、リスト5のARToolkitDrawer#createNyARTool(int, int)メソッドで行います。このARToolkitDrawer#createNyARTool(int, int)メソッドは、実際の画像データが渡されてくるリスト6のARToolkitDrawer#draw(byte[])メソッド内で呼ばれています。これで、NyARToolkitの初期化が終了し、利用できる状態となります。
次は、ARToolkitの処理を行い、三次元位置と姿勢を取得します。第2-2節で示したように、画像データは、ARToolkitDrawer#draw(byte[])メソッドに渡されて、ARToolkitで利用できる形に変数名:bufとして変換されています。この画像データを、リスト6のNyARRaster_RGB#wrap(raster, buf, w, h)メソッドでラスター化して、GLNyARSingleDetectMarker#detectMarkerLite(raster, 100)メソッドにて、マーカーがあるか検知しています。検知された場合は、GLNyARParam#getCameraFrustumRHf()とGLNyARSingleDetectMarker#getCameraViewRH(float[])により、三次元位置と姿勢を取得します。
public class ARToolkitDrawer { private GLNyARSingleDetectMarker nya = null; private NyARRaster_RGB raster = new NyARRaster_RGB(); private GLNyARParam ar_param = new GLNyARParam(); private NyARCode ar_code = new NyARCode(16, 16); public ARToolkitDrawer(InputStream camePara, InputStream patt, ModelRenderer mRenderer) { this.camePara = camePara; this.patt = patt; this.mRenderer = mRenderer; } private void createNyARTool(int w, int h) { // NyARToolkit setting. ar_param.loadFromARFile(camePara); ar_param.changeSize(w, h); ar_code.loadFromARFile(patt); nya = new GLNyARSingleDetectMarker(ar_param, ar_code, 80.0); nya.setContinueMode(true); } (略) }
public class ARToolkitDrawer { public void draw(byte[] data) { // (略) NyARRaster_RGB.wrap(raster, buf, w, h); boolean is_marker_exist = nya.detectMarkerLite(raster, 100); if (is_marker_exist) { float[] cameraRHf = ar_param.getCameraFrustumRHf(); nya.getCameraViewRH(resultf); // (略) } } }
Androidでは、3DライブラリとしてOpenGL/ESの1.0と1.1の一部が利用可能です。特徴は
-全てJavaで実装 -API体系はJSR239相当 -OpenGL/ES の 1.0 +1.1の一部 -微妙に引数の数が違う関数がある -EGLが見える/見えない -バッファはnio扱い -SurfaceViewクラスでの実装となる
といったところです。詳細な情報は、日本Androidの会2008/12定例イベントで講演された株式会社エイチアイの高橋憲一さんの「Androidで3Dグラフィクスを極める道 〜3Dエンジンの移植を通して〜」(※注3)を参照していただければと思います。
http://www.android-group.jp/index.php?%CA%D9%B6%AF%B2%F1%2F2008%C7%AF12%B7%EE%A4%CE%A5%A4%A5%D9%A5%F3%A5%C8#y162d50b
AndroidでOpenGL/ESを使用するには、SurfaceViewクラスの利用が必要となります。これが、Android特有の部分ですが、かなり定型処理が多いため、Androidリポジトリに含まれている「development/samples/ApiDemos/src/com/example/android/apis/graphics/」ディレクトリ内のサンプルコードを元に実装を行います。ソースコードを見ていただくとわかるのですが、いろいろな処理を行っています。しかし、大抵の処理は、OpenGL/ESとして定型的に処理をするものですので、Android的な要素を多く含む初期化と描画部分のみを解説します。
まず、初期化は、リスト7のGLSurfaceView#init()メソッドで行っています。SurfaceHolder#getHolder()にて、SurfaceHolderクラスを取得し、SurfaceView#addCallback(SurfaceHolder.Callback)でCallback先を登録しています。これにより、『Androidの画面への描画』をしています。(※注4)
Callbackについては、「コラム:Androidでのデバイスの扱い」を参照してください。~
正確には、その次に呼ばれているEglHelper#swap()が契機です。
public class GLSurfaceView extends SurfaceView implements SurfaceHolder.Callback { private void init() { // Install a SurfaceHolder.Callback so we get notified when the // underlying surface is created and destroyed mHolder = getHolder(); mHolder.addCallback(this); mHolder.setType(SurfaceHolder.SURFACE_TYPE_GPU); } public void setRenderer(Renderer renderer) { mGLThread = new GLThread(renderer); mGLThread.start(); } // (略) }
public class GLSurfaceView extends SurfaceView implements SurfaceHolder.Callback { // (略) class GLThread extends Thread { public void run() { guardedRun(); } private void guardedRun() throws InterruptedException { while (true) { // (略) if ((w > 0) && (h > 0)) { mRenderer.drawFrame(gl); mEglHelper.swap(); } } } } }
これで、3Dを使う準備が出来ました。最後に、背景となる画像=カメラ画像とその上にオーバーレイする3Dオブジェクトの描画処理をします。ここで、毎度おなじみのARToolkitDrawer#draw(byte[])メソッドへ戻ります。
ここで、初登場となるリスト9のModelRendererクラスがあります。これは、先ほど説明したRendererを拡張した実装で、背景テクスチャ処理やメタセコイヤ形式ファイルの読み込みなどの拡張を行っています。そのため、Rendererにはないメソッドが追加されています。これを踏まえて、ModelRenderer#setBgBitmap(Bitmap)で、第2-2節で生成したBitmapを背景として渡しています。
つづいて、マーカーが発見された場合、ModelRenderer#objectPointChanged(float[], float[])で3Dオブジェクトの表示し、発見されなかった場合は、ModelRenderer#objectClear()で消しています。これで、カメラ画像にマーカーが写った場合、マーカーの上に3Dオブジェクトが表示されます。
public class ARToolkitDrawer { ModelRenderer mRenderer; public void draw(byte[] data) { // (略) mRenderer.setBgBitmap(bitmap); // (略) boolean is_marker_exist = nya.detectMarkerLite(raster, 100); if (is_marker_exist) { float[] cameraRHf = ar_param.getCameraFrustumRHf(); nya.getCameraViewRH(resultf); mRenderer.objectPointChanged(resultf, cameraRHf); } else { mRenderer.objectClear(); } } }
お疲れ様でした。いかがでしたでしょうか?非常に駆け足となる解説となってしまいましたが、拡張のポイントはひととおり示せたと思っております。もちろん、説明不足感は否めず、意味不明なところも多々あります。そこで、ソースコードや追記事項など、日本Androidの会のHP上の特設コーナーにて公開する予定ですので、そちらもあわせてご覧いただければと思います。AR世界を拡張して、独自のすばらしいアプリケーションを作っていただけたら、幸いです。