カメラ画像のキャプチャはCameraPreviewクラスで行っています。また、余分な画像をキャプチャしないようにOneShotPreviewCallbackで1度画像をキャプチャするたびにコールバックをセットし直しています。
//--------- 省略 --------------- public void surfaceCreated(SurfaceHolder holder) { Log.i(TAG, "surfaceCreated"); mCamera = Camera.open(); try { mCamera.setPreviewDisplay(holder); } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); } mCamera.setOneShotPreviewCallback(mCallback); } private PreviewCallback mCallback = new PreviewCallback() { public void onPreviewFrame(byte[] data, Camera camera) { Message msg = new Message(); msg.arg1 = detectImage(mFrameWidth, mFrameHeight, data); // 学習画像を検出したら、その画像のIDを返す。検出できなければ-1を返す mHandler.sendMessage(msg); // 検出結果をDetectImageActivityへ伝える } }; //--------- 省略 ---------------
「PreviewCallback.onPreviewFrame()」では、キャプチャした画像をネイティブメソッド「detectImage()」へ渡しています。detectImage()は引数で渡したキャプチャ画像中にあらかじめ登録しておいた学習画像が含まれていれば、その画像のID、含まれていなければ「-1」を返します。
Androidアプリでは他のスレッドからUIの操作を行うと例外が発生します。そのため、検出結果は、以下のように「DetectImageActivityのHandler」を使って「InfomationView」に伝えています。そして、その際に「CameraPreview」のコールバックをセットし直して、プレビューをリスタートしています。
private class MainHandler extends Handler { public void handleMessage(Message msg) { mInfoview.setDetectImageId(msg.arg1); //検出した画像IDをセット mInfoview.invalidate(); //InfomationViewの更新 mCameraPreview.restartPreviewCallback(); // Previewのリスタート } }
PreviewCallback.onPreviewFrame()内でdetectImage()が呼ばれると、「jni/jni_part.cpp」の「Java_com_example_detectimage_CameraPreview_detectImage()」が呼ばれます。
JNIEXPORT jint JNICALL Java_com_example_detectimage_CameraPreview_detectImage(JNIEnv* env, jobject thiz, jint width, jint height, jbyteArray yuv) { LOGV("detectImage"); jbyte* _yuv = env->GetByteArrayElements(yuv, 0); vector<KeyPoint> queryKeypoints; Mat queryDescriptors; Mat myuv(height + height/2, width, CV_8UC1, (unsigned char *)_yuv); Mat mgray(height, width, CV_8UC1, (unsigned char *)_yuv); detector.detect(mgray, queryKeypoints); extractor.compute(mgray, queryKeypoints, queryDescriptors); // BrustForceMatcher による画像マッチング vector<DMatch> matches; matcher.match(queryDescriptors, matches); int votes[IMAGE_NUM]; // 学習画像の投票箱 for(int i = 0; i < IMAGE_NUM; i++) votes[i] = 0; // キャプチャ画像の各特徴点に対して、ハミング距離がしきい値より小さい特徴点を持つ学習画像へ投票 for(int i = 0; i < matches.size(); i++){ if(matches[i].distance < THRESHOLD){ votes[matches[i].imgIdx]++; } } // 投票数の多い画像のIDを調査 int maxImageId = -1; int maxVotes = 0; for(int i = 0; i < IMAGE_NUM; i++){ if(votes[i] > maxVotes){ maxImageId = i; //マッチした特徴点を一番多く持つ学習画像のID maxVotes = votes[i]; //マッチした特徴点の数 } } vector<Mat> trainDescs = matcher.getTrainDescriptors(); float similarity = (float)maxVotes/trainDescs[maxImageId].rows*100; if(similarity < 5){ maxImageId = -1; // マッチした特徴点の数が全体の5%より少なければ、未検出とする } env->ReleaseByteArrayElements(yuv, _yuv, 0); return maxImageId; }
「Java_com_example_detectimage_CameraPreview_detectImage()」では、「setTrainingImages()」と同様に特徴点queryKeypoints、特徴ベクトルqueryDescriptorsを抽出します。
キャプチャ画像の特徴量を抽出したら、「matcher.match()」で学習画像とキャプチャ画像を照合します。照合結果は変数「matches」に格納されます。
matchesには、キャプチャ画像の各特徴点のインデックスと、その点に対応する最も距離の小さかった学習画像の特徴点のインデックス、対応点間のハミング距離、画像IDがキャプチャ画像の特徴点の数だけ格納されます。2つの特徴点間の距離は、それぞれが持つ特徴ベクトルが、どれだけ類似しているかを表しており、値が小さいほど似ていることになります。
つまり、「キャプチャ画像の各特徴点が、どの学習画像のどの特徴点と対応し、どのくらい似ているか」という情報を得られます。このmatchesのデータを基に画像検出を判別します。
ただし、matchesに格納されるキャプチャ画像の各特徴点と対応する学習画像の特徴点は正しいとは限らない(特徴点間の距離が大きいもの含まれる)ので、注意が必要です。
そのため、画像の判別は以下のようにしています。
このようにして、画像中の各特徴点検出した学習画像のIDをJavaへ返します。
今回は3Dモデルではなく、画面平面にあらかじめ登録しておいた文字列を表示するだけの簡単なものです。文字列の表示には「InfomationView」というViewクラスを継承したクラスで行っています。
「DetectImageActivity.MainHandler」がメッセージを受け取って、「mInfoView.invalidate()」が呼ばれると、下記の「InfomationView.onDraw()」が実行され、ビューの再描画が行われます。
@Override public void onDraw(Canvas canvas) { if (mDetectImageId != -1) { String explanation = getExplanation(mDetectImageId); mStartPosX = getDisplayStartPosX(explanation, FONT_SIZE); // 画面上部中央へ表示するときの開始位置 int charNum = calcMaxLengthOnLine(explanation, mStartPosX, FONT_SIZE); // 1行に表示する文字数 int linage = (int) Math.ceil(explanation.length() / (double) charNum); // 表示する文字列の行数 mPaint.setColor(Color.argb(180, 0, 0, 0)); mPaint.setStyle(Style.FILL); // 説明文の背景を描画 canvas.drawRect(mStartPosX - PADDING, mStartPosY - FONT_SIZE, mStartPosX + charNum * FONT_SIZE + PADDING, mStartPosY + (linage - 1) * FONT_SIZE + PADDING, mPaint); mPaint.setTextSize(FONT_SIZE); mPaint.setColor(Color.WHITE); // 画像の説明文の描画 int i = 0; for (; i < linage - 1; i++) { canvas.drawText(explanation, i * charNum, (i + 1) * charNum, mStartPosX, mStartPosY + i * FONT_SIZE, mPaint); } // 最後の行の描画 if (explanation.length() % charNum != 0) { canvas.drawText(explanation, i * charNum, i * charNum + explanation.length() % charNum, mStartPosX, mStartPosY + i * FONT_SIZE, mPaint); } else { canvas.drawText(explanation, i * charNum, i * charNum + charNum, mStartPosX, mStartPosY + i * FONT_SIZE, mPaint); } } }
InfomationView.onDraw()では、キャプチャ画像から検出した学習画像ID「mDetectImageId」が「-1」でなければ、そのIDに対応した説明文を「getExplanation(mDetectImageId)」で取得し、描画します。IDが「-1」であれば、何もしないようになっています。
今回はOSSの画像処理ライブラリであるOpenCVを使って画像認識をするAndroidアプリを作成しました。しかし作成したアプリは、画像を認識すると文字を表示する簡単なアプリで、ARというには少しもの足りないかと思います。
3Dモデルを表示するには座標変換行列を計算する必要があり、その計算を2次元の座標点の差分から計算するのは難しいことです。ですが、第2・3回で紹介したARToolkit系ライブラリのマーカー検出APIに組み込むことは可能ですので、画像マーカーで3Dモデルを表示したい場合は、そういったことを検討してみてもよいかと思います。
また、今回紹介した「ORB特徴量」を用いた画像認識手法はほんの一例で、他にもOpenCVで実装されている画像検出や画像照合の手法はありますので、いろいろ試してみてください。
これまで6回に渡ってOSSを利用したAndroidのARアプリ開発やiPhoneアプリで利用できるARライブラリを紹介してきました。これまではARを導入するのは、なかなか難しい部分がありましたが、さまざまなプラットフォーム/ライブラリの登場により、現在ではARを使ったUIの導入もハードルがだいぶ低くなってきています。
また、ARアプリのコンテストなども国内外で開催されるなど盛り上がりを見せており、今後ますます普及していくのではないでしょうか。
本連載は、今回で最後となりますが、Android/iPhoneのARアプリを開発してみようと思われている方が、この連載を通じてARアプリの技術要素や開発の勘所を少しでもつかんでいただけたら幸いです。
Copyright © ITmedia, Inc. All Rights Reserved.