Google Glassで作る近未来アプリケーション(4)
Google Glassで動くARアプリケーションの実装
Google Glassの「眼鏡型」という特性を生かしたAR(Augmented Reality: 拡張現実)アプリの開発を、サンプルソースを交えながら解説。
はじめに
前回はGDKを利用した基本的なGlasswareの開発方法について解説した。今回は眼鏡型であるというGoogle Glassの特性を生かしたAR(Augmented Reality: 拡張現実)アプリケーション開発を、サンプルソースを交えながら解説する。
「Google GlassはARの用途には向いていない」とよく言われる。ARは通常、ユーザーが見ている現実世界に対してデジタルな情報や映像をオーバーレイするのに対し、Google Glassのディスプレイは、視界から斜め上に外れたところに位置しているからである。しかしカメラプレビューと合せてディスプレイに表示すれば、現実世界へのオーバーレイは可能であり、スマートフォンデバイスで同様のことを行うよりも、Google Glassの方が、相性がよいと考えている。今後、ARを活用したGlasswareも増えてくると思われるので、ぜひGlassware開発の参考にしてほしい。
サンプルプロジェクトの概要
今回は「SimpleAR」という、マーカーを認識して、その上に3Dオブジェクトを描画するシンプルなARプロジェクトを題材にする。ソースコードは全てGitHub上に公開している。
SimpleARは音声コマンド「Ok glass, augment reality」で起動できる。起動するとカメラのプレビューが画面に表示されるので、
をカメラの枠内に捉えると、以下のようにマーカー上に3Dオブジェクトが描画される。
本稿はGoogle Glassの開発についての解説だが、このアプリは通常のAndroidスマートフォンやタブレットとも互換性があり、同じように動作する(ただし、カメラパラメーターや画面サイズをGoogle Glass向けにチューニングしているため、そのまま他のデバイスで動かすと描画位置がずれる可能性が高いが)。Google Glassを持っていない読者も、ぜひ実際に動かしてみてほしい。
なお、本プロジェクトのソースコードの一部は、書籍『Mastering OpenCV』に掲載されたサンプルコード(GitHubで公開されている)を基にしている。本書はマーカー認識によるAR以外にも、顔認識から3次元再構成といった数多くの事例を解説しており、コンピュータービジョンを生かしたアプリを作りたい開発者にはお勧めの一冊である。
環境構築
サンプルプロジェクトを実行するには、Android SDK(GDK)、Android NDK、OpenCV4Android SDKが必要だ。以下それぞれの導入方法を説明する。なお、筆者の環境はMacなのでWindows環境ではそのままでは動かない可能性がある点、ご理解いただきたい。
Android SDK/GDK
前回解説した通り、Android SDK ManagerからAndroid SDKまたはGDKをインストールできる。
Android NDK
Android NDKはAndroidアプリケーションでネイティブコード(C/C++)を使用するためのツールセットだ。今回はマーカー認識処理をC++で記述しているため必要になる。
Android NDKのサイトから、使用している環境に該当するファイルをダウンロードしよう。筆者の環境では、「android-ndk-r9d-darwin-x86_64.tar.bz2」ファイルをダウンロードしている。ダウンロードしたら解凍して、適当な場所(筆者の環境では、「/Applications/android-ndk-r9d」)に配置する。
OpenCV4Android SDK
今回のプロジェクトでは、マーカー検出と座標変換の一部でOpenCVを利用している。OpenCVをAndroidアプリから使うには、下記の2通りの方法がある。
- 「Android OpenCV Manager」という.apkファイルを別途、デバイスにインストールする方法
- 静的ライブラリとしてアプリ自体に組み込む方法
OpenCV Managerをインストールした場合、アプリ自体にはOpenCVのライブラリを含めなくてよいため、アプリのサイズを小さくできる。またAndroid OSのバージョン差異やデバイスの差異をOpenCV Managerが吸収してくれるため、アプリ開発者はそのような環境の差異を意識しなくてよいというメリットがある。
一方で、アプリに静的ライブラリとして組み込めば、アプリのサイズは大きくなるが、ユーザーに別の.apkファイルをインストールさせる手間を省くことができる。
今回のサンプルでは、後者の静的ライブラリとして組み込む方法を採用する。対象デバイスをGoogle Glassに絞っているため、OSバージョンやデバイスの差異を意識しなくてよいのと、Google GlassはPlay Storeに接続できないためAndroid OpenCV Managerをユーザーに配布するのが難しいのが理由である。
Android用のOpenCVライブラリをインストールするには以下の手順を踏む。
- SourceForgeから.zipファイルをダウンロードする。筆者は「OpenCV-2.4.8-android-sdk.zip」ファイルを利用している
- 解凍して、適当な場所にフォルダーを移動する。筆者の環境では、「~/workspace/OpenCV-2.4.8-android-sdk」に配置している
- 環境変数「OPENCV_ANDROID_SDK_HOME」に、2で配置したパスの末尾に「/sdk/native/jni」を付けたものを設定する。これは今回利用するサンプルプロジェクトだけで使われる設定であり、サンプルプロジェクト内の「jni/Android.mk」ファイルから参照されている。筆者の環境では、以下のコマンドを実行した
Bashexport OPENCV_ANDROID_SDK_HOME=~/workspace/OpenCV-2.4.8-android-sdk/sdk/native/jni環境変数を設定するコマンド
プロジェクトのビルド
Android SDK(GDK)、Android NDK、OpenCV4Android SDKが導入できたら、次は実際にSimpleARプロジェクトをビルドしてみよう。前回同様、Eclipseの利用を前提とする。
1プロジェクトのチェックアウト
GitHubから最新のソースをチェックアウトする。
2Eclipseプロジェクトのインポート
Eclipseのメニューバーから[File]-[New]-[Other]-[Android Project from Existing Code]を選択する。[Root Directory]に1でチェックアウトしたパスを入力し、Eclipseのプロジェクトを作成する。
3Nativeコードのビルド
このままEclipse上で実行してもNativeコードがビルドされていないため、エラーが発生してしまう。Eclipse上でNDKのビルドを行う設定もできるが、やや面倒なので、今回はターミナルから以下のようにndk-buildコマンドを直接実行してNativeコードをビルドする。
$ cd <プロジェクトをチェックアウトしたパス>
$ <Android NDKを配置したパス>/ndk-build
|
最終的に「[armeabi-v7a] Install : libsimplear.so => libs/armeabi-v7a/libsimplear.so」という表示がされ、実際に「libs/armeabi-v7a/libsimplear.so」というファイルが作られれば成功である。
4Eclipseからアプリの実行
3で行った結果を反映させるため、Eclipse上でプロジェクトの右クリックメニューから[Refresh]を選択する。その後、Eclipseのメニューバーから[Run]-[Run As]-[Android Application]を選択するとアプリを起動できるはずだ。
プロジェクトの構成
本プロジェクトを構成するソースコードは、「src」フォルダー以下のJava部分と、「jni」フォルダー以下のNative部分(C/C++)に大別される(表1)。それぞれの役割は、Java部分がカメラプレビューの取得とOpenGL ESを利用した3Dオブジェクトの描画を、Native部分は主にマーカーの検出と3Dオブジェクトを描画するためのモデルビュー行列(後述)を計算する役割を担っている。
Java/Native | クラス名/ソース名 | 役割 |
---|---|---|
Java | MainActivity | カメラフレームを受け取り、計算されたモデルビュー行列を基に3Dオブジェクトを描画する |
NativeMarkerDetector | JNI(Java Native Interface)のメソッドを宣言し、Java/ネイティブ間の橋渡しを行う | |
JavaCameraViewEx | カメラプレビューの取得と描画を行う。OpenCVライブラリが提供しているJavaCameraViewクラスをGoogle Glassで使いやすくするために独自に修正したクラス | |
Native | simplear_jni.cpp | JNIの実体となるネイティブ関数を定義する |
MarkerDetector.cpp | マーカーの検出と、モデルビュー行列の計算を行う |
処理の概要
以下はSimpleARプロジェクトの処理を図解したものである。
- 1 JavaCameraViewEXが、カメラプレビュー画像をMainActivityにコールバックする。
- 2/3 カメラプレビュー画像を、JNIメソッドを経由してNative層に渡す。
- 4/5/6 カメラプレビュー画像の解析結果として、マーカー検出の結果から得られたモデルビュー行列(後述)をMainActivity内で受け取る。
- 7/8 モデルビュー行列を基にマーカーの位置・姿勢に合わせて3DオブジェクトをGLSurfaceViewに対して描画する。
OpenCV/OpenGL ESの初期化
まずはOpenCV/OpenGL ESの初期化部分を見てみよう(次のコード)。Activityから読み込むlayout.xmlファイルの中にJavaCameraViewExクラスとGLSurfaceViewクラスを定義しており、それぞれのクラスをActivityクラスのonCreateメソッド内で初期化している。
public class MainActivity extends Activity implements CvCameraViewListener2 {
……省略……
public void onCreate(Bundle savedInstanceState) {
……省略……
mWidth = getResources().getDimensionPixelSize(R.dimen.view_width);
mHeight = getResources().getDimensionPixelSize(R.dimen.view_height);
mCameraView = (JavaCameraViewEx) findViewById(R.id.camera_view);
mCameraView.setCameraIndex(CameraBridgeViewBase.CAMERA_ID_ANY);
mCameraView.setCvCameraViewListener(this);
mCameraView.setMaxFrameSize(mWidth, mHeight);
mCameraView.disableView();
mGLView = (GLSurfaceView) findViewById(R.id.gl_view);
mGLView.setZOrderOnTop(true);
mGLView.setEGLConfigChooser(8, 8, 8, 8, 16, 0);
mGLView.setRenderer(new GLRenderer());
mGLView.getHolder().setFormat(PixelFormat.TRANSLUCENT);
}
……省略……
}
|
3Dから2Dへの座標変換について
マーカー検出について解説する前に、3Dオブジェクトを描画する際の基本的なパイプラインと、マーカー検出で最終的に取得するモデルビュー行列が果たす役割について解説しておきたい。
カメラが捉える3D空間状の物体を、ディスプレイ上に表示するには、3Dから2Dへの座標変換を行う必要がある。その変換に必要になるのがモデルビュー行列とプロジェクション行列だ(次の図)。
モデルビュー行列は、描画対象となる物体を原点に定めたモデリング座標系を、カメラを原点に定めたカメラ座標系に変換するものである。カメラと物体の相対的な位置や姿勢を表す行列と言ってもいい。
プロジェクション行列は、カメラ座標系から、実際にカメラに写る2Dの画像に変換するための行列である。この行列はカメラ(デバイス)によって異なり、通常は「カメラキャリブレーション」という処理で、「カメラパラメーター」と呼ばれるカメラの特性を表す値を計測して、それを基に計算する。今回はGoogle Glassにフォーカスしているため、あらかじめGoogle Glass上で計測したカメラパラメーターをソースコード上に埋め込んでいる(なお、OpenCV4Android SDKには、カメラキャリブレーションを行うサンプルアプリが含まれており、Google Glassのカメラパラメーターもそのアプリを使用して計測した。興味のある読者は「OpenCV-2.4.8-android-sdk/sample/camera-calibration」フォルダー内のプロジェクトを見てほしい)。
Native層で行っているマーカー検出処理では、このパイプラインで最後に位置する2Dのディスプレイ画像上でマーカーに対応する領域を検出する。パイプラインの最後の座標が分かっており、さらに出発点となるモデリング座標と、プロジェクション行列は分かっているので、これらの情報からモデルビュー行列を逆算するのだ。
モデルビュー行列とプロジェクション行列が分かれば、3Dが最終的に2Dにどのように描画されるかが分かるので、後述するOpenGL ESの処理で、3Dオブジェクトをマーカーの位置・姿勢に合わせて描画できる。
マーカーの検出と、モデルビュー行列の取得
マーカー検出処理の起点は、カメラフレームのコールバックだ。前掲のコードで示したように、JavaCameraViewExクラス(mCameraViewオブジェクト)の初期化の際、「mCameraView.setCvCameraViewListener(this);」として、MainActivityクラス(this)をリスナーとして指定すると、カメラプレビューをそのonCameraFrameメソッドで受け取ることができる。以下がonCameraFrameメソッドの実装で、NativeMarkerDetectorクラスのインスタンスであるmMarkerDetectorを介して、Native層のマーカー検出関数を呼び出している。
@Override
public Mat onCameraFrame(CvCameraViewFrame inputFrame) {
Mat frame = inputFrame.rgba();
if (mMarkerDetector != null) {
List<Mat> transformations = new ArrayList<Mat>();
float scale = mScale == 0.0f ? 1.0f : mScale;
// NativeMarkerDetectorを介してNative関数の呼び出し
mMarkerDetector.findMarkers(frame, transformations, scale);
// 実行結果はtransformations変数に格納されている
int count = transformations.size();
if (count > 0) {
Mat mat = transformations.get(0);
mat.get(0, 0, mTransformation);
mFindMarker = true;
} else {
mFindMarker = false;
}
}
return frame;
}
|
実際に呼び出されるNativeのマーカー検出処理は以下のようになっている。以下、コードの抜粋とともに簡単に大枠の流れを説明するが、興味のある読者は各関数内部の実装ものぞいてみるとよいだろう。
bool MarkerDetector::findMarkers(const cv::Mat& bgraMat, std::vector<Marker>& detectedMarkers)
{
cv::cvtColor(bgraMat, m_grayscaleImage, CV_BGRA2GRAY); …… 1
// Make it binary
cv::threshold(m_grayscaleImage, m_thresholdImg, 127, 255, cv::THRESH_BINARY_INV); …… 2
// Detect contours
findContours(m_thresholdImg, m_contours, m_grayscaleImage.cols / 5); …… 3
// Find closed contours that can be approximated with 4 points
findCandidates(m_contours, detectedMarkers); …… 4
// Find is them are markers
recognizeMarkers(m_grayscaleImage, detectedMarkers); …… 5
// Calculate their poses
estimatePosition(detectedMarkers); …… 6
}
|
- 1カラー画像をグレイスケール画像に変換。
- 2グレイスケール画像を白黒画像に変換。
- 3白黒画像から輪郭線を検出する。
- 4輪郭線の中からマーカー形状に近い領域を絞り込む。内部的には、輪郭線から多角形へ近似させて四角形領域だけを抽出し、さらに凸凹からマーカーになり得ない領域や、小さすぎる領域などをフィルタリングして候補を絞り込んでいる。
- 5絞り込まれた候補のうち、マーカーのパターンと一致するものだけを絞り込む。内部的には前段の四角形のパターンを正方形に変換し、7×7の格子状に分割し、各格子が白か黒かをチェックしている。これで7×7の白黒格子パターンが得られるので、それをあらかじめ決められたマーカーの白黒パターンと比較して一致判定を行っている。
- 65まででカメラプレビュー画像中の四隅と、もともとのマーカー画像中の四隅との対応付けが分かったので、これを基にモデルビュー行列の計算を行う。
上記の処理を見て分かる通り、このマーカー検出処理はそれほど堅牢な処理ではないことに注意しておきたい。例えば、四角形であることを前提としているため、マーカーがカメラの画角から少しでもはみ出したら認識できないし、マーカー上に障害物があると格子パターンが一致せず認識エラーとなる。
3Dオブジェクトの描画
3Dオブジェクトの描画には、OpenGL ESを使用している。以下が描画部分のソースコードである。
OpenGL ESにはプロジェクション行列とモデルビュー行列を設定する関数があるのでそれらを使って、それぞれの行列を設定して、マーカーのモデリング座標系にオブジェクトを描画すれば、ディスプレイ画像への落とし込みはOpenGL ESが自動的にやってくれる。プロジェクション行列はオブジェクトの位置に依存しないため、GLSurfaceViewが構築されたGLSurfaceView.Renderer.onSurfaceChangedのオーバーライドメソッド内でプロジェクション行列を設定し、毎フレームを描画するGLSurfaceView.Renderer.onDrawFrameオーバーライドメソッドの処理内で、前述の処理で得られたモデルビュー行列を設定し、3Dオブジェクト(グラデーションの平面とXYZ座標軸)を描画している。
private class GLRenderer implements GLSurfaceView.Renderer {
……省略……
@Override
public void onSurfaceChanged(GL10 gl, int width, int height) {
// プロジェクション行列の設定。buildProjectionMatrixはハードコードされたGoogle Glassのカメラパラメーターからプロジェクション行列を計算するメソッドである。
projectionMatrix = buildProjectionMatrix(width, height);
gl.glMatrixMode(GL10.GL_PROJECTION);
gl.glLoadMatrixf(allocateFloatBufferDirect(projectionMatrix));
}
@Override
public void onDrawFrame(GL10 gl) {
gl.glClearColor(0.0f, 0.0f, 0.0f, 0.0f);
gl.glClear(GL10.GL_COLOR_BUFFER_BIT | GL10.GL_DEPTH_BUFFER_BIT);
// 直前のカメラフレームでマーカーが検出されている場合、ARのオーバーレイを描画
if (mFindMarker) {
drawAR(gl);
}
}
private void drawAR(GL10 gl) {
gl.glDepthMask(true);
gl.glEnable(GL10.GL_DEPTH_TEST);
gl.glPushMatrix();
gl.glLineWidth(5.0f);
// モデルビュー行列の設定
gl.glMatrixMode(GL10.GL_MODELVIEW);
gl.glLoadIdentity();
gl.glLoadMatrixf(allocateFloatBufferDirect(mTransformation));
// グラデーションの平面四角形を描画
gl.glVertexPointer(2, GL10.GL_FLOAT, 0, squareVertices);
gl.glEnableClientState(GL10.GL_VERTEX_ARRAY);
gl.glColorPointer(4, GL10.GL_UNSIGNED_BYTE, 0, squareColors);
gl.glEnableClientState(GL10.GL_COLOR_ARRAY);
gl.glDrawArrays(GL10.GL_TRIANGLE_STRIP, 0, 4);
gl.glDisableClientState(GL10.GL_COLOR_ARRAY);
float scale = 0.5f;
gl.glScalef(scale, scale, scale);
gl.glTranslatef(0f, 0f, 0.1f);
// X軸を青線で描画
gl.glColor4f(1f, 0f, 0f, 1f);
gl.glVertexPointer(3, GL10.GL_FLOAT, 0, lineX);
gl.glDrawArrays(GL10.GL_LINES, 0, 2);
// Y軸を青線で描画
gl.glColor4f(0f, 1f, 0f, 1f);
gl.glVertexPointer(3, GL10.GL_FLOAT, 0, lineY);
gl.glDrawArrays(GL10.GL_LINES, 0, 2);
// Z軸を青線で描画
gl.glColor4f(0f, 0f, 1f, 1f);
gl.glVertexPointer(3, GL10.GL_FLOAT, 0, lineZ);
gl.glDrawArrays(GL10.GL_LINES, 0, 2);
gl.glPopMatrix();
gl.glDisableClientState(GL10.GL_VERTEX_ARRAY);
}
}
|
まとめと次回
NDKやOpenCV、OpenGL ESを使用したGoogle Glass上で動くARアプリの実装を解説した。今回はマーカーを認識する単純なARアプリだったが、位置情報と組み合わせたり、マーカー以外のものを認識させたりとさまざまな可能性がある。またGoogle Glass以外にもARに特化した眼鏡型デバイスもいくつか発売され始めている。今後のアプリ開発でますます注目される分野であることは間違いないだろう。
次回は、今回同様、画像認識技術を使用し、アイアンマンやマイノリティ・レポートのような近未来を彷彿(ほうふつ)とさせるGlasswareを実装する。
※以下では、本稿の前後を合わせて5回分(第1回~第5回)のみ表示しています。
連載の全タイトルを参照するには、[この記事の連載 INDEX]を参照してください。
1. Google Glassアプリケーション開発の基礎知識
Glassware(=Glassアプリ)開発を始めるなら、日本上陸前の今がチャンス。米国シリコンバレー在住の筆者が、現地ならではの視点でGlassware開発を解説する連載スタート。
2. GDKで開発できるGoogle Glassアプリの機能とは?
GDKを使うとGoogle Glassの機能をフルに活用したアプリを開発できる。3種類のUI(Static Card/Live Card/Immersion)/タッチジェスチャー/音声認識/位置情報/センサー/カメラなど、GDKの全体像を、コードを示しながら解説する。
3. 初めてのGoogle Glassアプリ開発(GDK編)
いよいよGlass開発を実践。GDKの開発環境の構築手順と、Glassware(=Glassアプリ)の作成/実行方法を説明。また、サンプルアプリのソースコードを読み解きながら、よりGlassらしいアプリの実装方法について説明する。