画像認識の基礎 ~特徴とは~
画像認識部分の実装に入る前に、少し特徴に関する説明をします。特徴とは、画像データの中の特徴的な部分(コーナー、物体の境界線、矩形の領域、動画のフレーム間の差異など)のことで、アプリケーションに応じて処理しやすいものを特徴として扱います。
画像認識の場合、特定の種類の特徴を検出するアルゴリズムで特徴を見つけ、特徴が何を表しているかを数値化し、あらかじめ取り込まれた訓練画像データとカメラで得られた画像データとの間での差を見ることで特定の画像を検出します。ここでの画像認識に利用しているORBアルゴリズムはパテントフリーで知られるコーナー検出のアルゴリズムです。
画像認識アプリケーションの実装
画像処理の部分はJNIにより、ネイティブコード(C++)として実装します。
ヘッダファイルの作成
AndroidのコードからC++のメソッドを呼び出すためには、JavaのJNI(Java Native Interface)という機能を利用します。Javaのコード内でどのメソッドを呼び出すかを宣言し、javahコマンドを実行することでC++のヘッダファイル(.hpp)が生成されます。javahコマンドによって生成されたファイル内のメソッドを実装ファイル内(.cpp)に実装することでJavaとC++間で連携できます。
CameraActivity.java
ネイティブメソッドを呼び出すクラスに以下の宣言を追記します。
private native void setTrain(long addrTrainImage); private native boolean detectImage(long addrGray, long addrRgba);
次にターミナルを開き、プロジェクトのルートディレクトリでjavahコマンドを実行します。Javahコマンドを実行する前にクラスファイルでメソッドが宣言されていなければならないことに注意してください。Eclipseの場合、プロジェクトをクリーンかビルドすることでクラスファイルが更新されます。
$ javah -classpath "./bin/classes:<path to android-sdk>/platforms/android-18/android.jar:<path to android-sdk>/platforms/android-18/data/layoutlib.jar:<OpenCV Library home>/bin/classes/" -o orbdetect.hpp <パッケージ名>.<クラス名>
orbdetect.cpp
続いてネイティブメソッドを実装します。javahメソッドにより得られたメソッド名に合わせて実装します。OpenCVのモジュールからはcore.hpp、improc.hpp、features2d.hppをそれぞれインクルードする必要があります。Java_com_orb_CameraActivity_setTrainというメソッドとJava_com_orb_CameraActivity_detectImageというメソッドが、それぞれ訓練画像を登録するメソッドと画像の中に訓練画像が含まれているかを調べ、訓練画像が検出できた場合に画面上にテキストを表示するメソッドです。
orb.operatorメソッド(24、39行目)により画像から特徴を抽出し、matcher.matchメソッド(44行目)により、2つの画像から得られた特徴を比較して似ている特徴点を抽出します。抽出された情報には似た特徴同士のペアの一覧がcv::Pointクラスのvector配列として得られます。この中には、特徴がどれだけ似ているかという指標やその特徴点の画面内の座標情報などが含まれていて、ここではその指標の値が閾値よりも近い特徴がどれだけあるかを判断することで画像の検出を判定しています。OpenCVのorbクラスのリファレンスも併せて参照してください。
#include <jni.h> #include <android/log.h> #include <../../../sdk/native/jni/include/opencv2/core/core.hpp> #include <../../../sdk/native/jni/include/opencv2/imgproc/imgproc.hpp> #include <../../../sdk/native/jni/include/opencv2/features2d/features2d.hpp> extern "C"{ // Global変数 cv::ORB orb; std::vector<cv::KeyPoint> train_features; cv::Mat train_descriptor; JNIEXPORT void JNICALL Java_com_orb_CameraActivity_setTrain (JNIEnv *env, jobject obj, jlong addrTrainImage) { cv::Mat gray_train_img; // grayscale変換 cv::Mat& train_img = *(cv::Mat*)addrTrainImage; cv::cvtColor(train_img, gray_train_img, CV_RGB2GRAY); // ORB検出器による処理 orb = cv::ORB(); orb.operator()(train_img, cv::noArray(), train_features, train_descriptor, false); } JNIEXPORT jboolean JNICALL Java_com_orb_CameraActivity_detectImage (JNIEnv *env, jobject obj, jlong addrRgba, jlong addrGray) { // 特徴量ベクターと特徴量記述子 std::vector<cv::KeyPoint> query_features; cv::Mat query_descriptor; // 画像を取得 cv::Mat& rgba_query_img = *(cv::Mat*)addrRgba; cv::Mat& gray_query_img = *(cv::Mat*)addrGray; // 特徴量を計算 orb.operator()(gray_query_img, cv::noArray(), query_features, query_descriptor, false); // マッチング std::vector<cv::DMatch> matches; cv::BFMatcher matcher(cv::NORM_HAMMING, true); matcher.match(query_descriptor, train_descriptor, matches); // 閾値を設定 const double threshold = 45.0; std::vector<cv::DMatch> matches_good; for (int i = 0; i < (int)matches.size(); i++) { // よい特徴だけ残す if (matches[i].distance < threshold) matches_good.push_back(matches[i]); // 全体の5%以上が閾値よりも低い値を持っていた場合、検出したものとする float eval = (float)matches.size() * 0.05; if(matches_good.size() > eval){ // よい特徴の数が基準以上であれば、"image detected"と画面上部に表示 cv::putText(rgba_query_img, "image detected”, cv::Point(50,50), cv::FONT_HERSHEY_SIMPLEX, 1.5, cv::Scalar(255,255,0), 2, CV_AA); return true; }else{ return false; } } } }
Android.mk
Android.mkを以下を追記し、カメラプレビューの実装時と同様にndk-buildを実行します。これにより、orbdetect.cppがコンパイルされ、実行可能なファイルが作られます。
# Custom Library LOCAL_MODULE := imdetector LOCAL_SRC_FILES := orbdetect.cpp LOCAL_LDLIBS := -L$(SYSROOT)/usr/lib -llog include $(BUILD_SHARED_LIBRARY)
Androidからのネイティブメソッドの呼び出し
Androidのコードの修正です。アプリ起動時にライブラリをロードする処理とカメラを利用するクラス内でメソッドの呼び出しを追加する必要があります。
ORBDetect.java
新しく作成したモジュールを追加で読み込みます。
} else { // 追加で読み込むJNIライブラリがあればここでロードする System.loadLibrary("imdetector"); }
CameraActivity.java
JNIでOpenCVのオブジェクトをネイティブとの間でやりとりするには、nativeObjというアドレスを指すlong型変数を使います。訓練画像を登録するメソッドはonResume()メソッド内で呼び出しています。
// 訓練画像を登録するメソッド private void initTrain(){ // リソースから訓練画像を取得してMat型に変換 Bitmap image = BitmapFactory.decodeResource(getResources(), R.drawable.train_img); Mat mat = new Mat(); Utils.bitmapToMat(image, mat); // ネイティブメソッドの呼び出し setTrain(mat.nativeObj); } // CvCameraViewListener2のメソッドの実装 public Mat onCameraFrame(CvCameraViewFrame inputFrame) { Mat frame = inputFrame.rgba(); Mat gray = inputFrame.gray(); // ネイティブメソッドの呼び出し detectImage(frame.nativeObj, gray.nativeObj); return frame; }
画像認識実装アプリの実行結果
対象画像の検出に成功すると、Java_com_orb_CameraActivity_detectImageメソッド内のcv::putTextが呼び出されて、画面上部に検出したことを示す文字が表示されます。ただし、処理速度は2fps程度とかなり低速です。次は処理速度の改善を行います。