TensorFlowの画像認識をモバイルで動かす&その仕組み

  • 51
    いいね
  • 0
    コメント

TensorFlowの画像認識をモバイルで動かす&その仕組み

TensorFlowがモバイルで動くという話がちょっと前から気になっていて、Androidで動かしてみました。サンプルの動かし方と、どのような仕組みになっているかまとめました。

動かし方

主にTensorFlowのReadmeに書いてある内容です。Androidを知っている人なら特に問題無いと思いますが以下日本語で書いてます。

事前準備

Android SDK以外にも以下のツールが必要になります。

1.Bazelの導入

TensorFlowのBuildにGoogleの新しいBuild toolであるBazelが必須になります。
MacであればHomebrewでかんたんに導入できます。
Bazelの挙動についてやファイル構成についてはチュートリアルがあるので、そこも目を通しておくとよいです。
初めて触ったのですが、ワークスペースを作ってそこでビルドするところとかGoっぽいなと思いました。こちらの記事も参考になるかと思います。

$ brew info bazel
bazel: stable 0.3.0 (bottled), HEAD
Google's own build tool
http://www.bazel.io/
/usr/local/Cellar/bazel/0.3.0 (8 files, 92.4M) *
  Poured from bottle on 2016-08-30 at 17:14:10
From: https://github.com/Homebrew/homebrew-core/blob/master/Formula/bazel.rb
==> Caveats

2.Android NDKの導入

Android NDKの導入はこちらを参照しました。Android Studio 2.2からはAndroid StudioからC++/Cのコンパイルができると書いてあります。なんでこの際ASも2.2にあげておくべきでしょう。

Using Android Studio 2.2 and higher, you can use the NDK to compile C and C++ code into a native library and package it into your APK using Gradle, the IDE's integrated build system.

Build TensorFlow for Android

まずはディレクトリを作る所から始めます。
自分はとりあえずBazel用のディレクトリを作り、そこに git clone で TensorFlowのレポジトリをとってきました。

$ mkdir bazel  //Bazel用のディレクトリ作っておく
$ cd bazel
$ git clone git@github.com:tensorflow/tensorflow.git

次に、/tensorflow/WORKSPACE のファイルを自分のローカルのNDKやSDKに向くようにパス設定等を書きます。

 # Uncomment and update the paths in these entries to build the Android demo.
-#android_sdk_repository(
-#    name = "androidsdk",
-#    api_level = 23,
-#    build_tools_version = "23.0.1",
-#    # Replace with path to Android SDK on your system
-#    path = "<PATH_TO_SDK>",
-#)
-#
-#android_ndk_repository(
-#    name="androidndk",
-#    path="<PATH_TO_NDK>",
-#    api_level=21)

+android_sdk_repository(
+    name = "androidsdk",
+    api_level = 23,
+    build_tools_version = "23.0.3",
+    path = "/Applications/sdk/",
+)
+
+android_ndk_repository(
+    name="androidndk",
+    path="/Applications/sdk/ndk-bundle/",
+    api_level=21)

サンプルで使うモデルデータはサイズが大きく、別梱になっています。zipをDLしてきて tensorflow/examples/android/assets/ に配置します。

temp $ unzip inception5h.zip -d ../tensorflow/tensorflow/examples/android/assets/
temp $ ls -ltr ../tensorflow/tensorflow/examples/android/assets/
total 105296
-rw-r-----  1 tomoaki  staff  53884595 Nov 18  2015 tensorflow_inception_graph.pb
-r--r-----  1 tomoaki  staff     11416 Nov 18  2015 LICENSE
-r--r-----  1 tomoaki  staff     10492 Nov 18  2015 imagenet_comp_graph_label_strings.txt
temp $ 

ファイルサイズは50MBくらいでした。

あとはapkをbuildします。WORKSPACEがある /tensorflow配下で以下コマンド実行

$ bazel build //tensorflow/examples/android:tensorflow_demo
...
Target //tensorflow/examples/android:tensorflow_demo up-to-date:
  bazel-bin/tensorflow/examples/android/tensorflow_demo_deploy.jar
  bazel-bin/tensorflow/examples/android/tensorflow_demo_unsigned.apk
  bazel-bin/tensorflow/examples/android/tensorflow_demo.apk
INFO: Elapsed time: 786.287s, Critical Path: 676.97s

高速と言われるBazel使っても10分以上時間かかりました。TensorFlowのCoreファイルのコンパイルに時間がかかっていたもようです。2回目以降は13秒程度でビルドできました

INFO: Elapsed time: 12.936s, Critical Path: 11.17s

アプリをデバイスにインストールします。

adb install bazel-bin/tensorflow/examples/android/tensorflow_demo.apk

アプリを起動してみました。ちゃんとカップが認識されている!!予想以上に精度高いです。
Screen Shot 2016-09-25 at 16.08.25.png

仕組み

Androidサイド

どんな風に処理されているのか見てみました。Android側のファイル構成は以下のようになっています。

├── AutoFitTextureView.java
├── CameraActivity.java
├── CameraConnectionFragment.java
├── Classifier.java
├── RecognitionScoreView.java
├── TensorFlowClassifier.java
├── TensorFlowImageListener.java
└── env
    ├── ImageUtils.java
    └── Logger.java

キモになっているのは TensorFlowImageListener.javaTensorFlowClassifier.java になります。 TensorFlowImageListener.java が CameraのPreviewを取得し(1)、Previewを受け取った TensorFlowClassifier.java でbitmapの評価を行い(2)、その結果を RecognitionScoreView.java に返す(3)ような実装になっています。

CameraConnectionFragment.java
private final TensorFlowImageListener tfPreviewListener = new TensorFlowImageListener();

/**
 * Creates a new {@link CameraCaptureSession} for camera preview.
 */
private void createCameraPreviewSession() {
  ...
  // Create the reader for the preview frames.
  previewReader =
        ImageReader.newInstance(
            previewSize.getWidth(), previewSize.getHeight(), ImageFormat.YUV_420_888, 2);

  previewReader.setOnImageAvailableListener(tfPreviewListener, backgroundHandler); // (1) Camera Previewからimageを取得する処理
  ...
  // Initialization
  tfPreviewListener.initialize(
      getActivity().getAssets(), scoreView, inferenceHandler, sensorOrientation);
}
TensorFlowImageListener.java
private final TensorFlowClassifier tensorflow = new TensorFlowClassifier();

@Override
  public void onImageAvailable(final ImageReader reader) {
    image = reader.acquireLatestImage();
  ...
    rgbBytes = new int[previewWidth * previewHeight];
    rgbFrameBitmap = Bitmap.createBitmap(previewWidth, previewHeight, Config.ARGB_8888);
    croppedBitmap = Bitmap.createBitmap(INPUT_SIZE, INPUT_SIZE, Config.ARGB_8888);
  ...
    rgbFrameBitmap.setPixels(rgbBytes, 0, previewWidth, 0, 0, previewWidth, previewHeight);
    drawResizedBitmap(rgbFrameBitmap, croppedBitmap);
  ...  
    handler.post(() ->{
            final List<Classifier.Recognition> results 
               = tensorflow.recognizeImage(croppedBitmap); // (2)色調変換したbitmapをTensorFlowClassifierに渡す
            scoreView.setResults(results);(3) 結果をViewに渡す
            computing = false;
          }
        );
  }
TensorFlowClassifier.java
private native String classifyImageBmp(Bitmap bitmap); //C++のネイティブメソッド

 @Override
 public List<Recognition> recognizeImage(final Bitmap bitmap) {
   final ArrayList<Recognition> recognitions = new ArrayList<Recognition>();
   // classifyImageBmp
   for (final String result : classifyImageBmp(bitmap).split("\n")) {
   ...
   }
 }

色調について

サンプルアプリでは YUV_420_888 で取得されたbitmapをrgbaに変換/リサイズして TensorFlowに渡しています。 AndroidのDocumentによると、ImageReaderから渡せるImageオブジェクトはこのデータを返すことが出来るものの、TensorFlow側は RGBAにしか現状対応してないために、このようになっているようです。高速化するならさらに軽量なRGB_565とかでもよいのかもしれないですね。

C++サイド

jni側のファイル構成は以下になっています。

ni $ tree
.
├── __init__.py
├── imageutils_jni.cc
├── jni_utils.cc
├── jni_utils.h
├── limiting_file_input_stream.h
├── rgb2yuv.cc
├── rgb2yuv.h
├── tensorflow_jni.cc
├── tensorflow_jni.h
├── yuv2rgb.cc
└── yuv2rgb.h

C++に関して学生時代のうすーい知識しかないため、ぼんやりとしか読めてないのですが、tensorflow_jni.cc がキモになっています。

TensorFlowの計算方法

TensorFlowがどのように動いているのかはこちらの記事に大変分かりやすくまとめられています。サンプルアプリも
(1)Tensor/Sessionの構築
(2)計算
の構成になっています。

(1)Tensor(多次元配列)/Session(分類器)の構築
initializeTensorFlow で行われています。
モデルデータ(tensorflow_inception_graph.pb)をProtocolBufferで読み取り、Graphを生成します。

Graphは計算処理に当たる部分です。そのGraphを元にSessionを作ります。

tensorflow_jni.cc


static std::unique_ptr<tensorflow::Session> session;

tensorflow::GraphDef tensorflow_graph;

AAssetManager* const asset_manager =
    AAssetManager_fromJava(env, java_asset_manager);

//モデルデータからTensorを処理するGraphを生成する
ReadFileToProto(asset_manager, model_cstr, &tensorflow_graph);

//Session生成
tensorflow::Status s = session->Create(tensorflow_graph);

なお、g_image_mean,g_image_stdといったグローバル変数には以下のような固定値が設定されています。

  private static final int NUM_CLASSES = 1001;
  private static final int INPUT_SIZE = 224;
  private static final int IMAGE_MEAN = 117;
  private static final float IMAGE_STD = 1;

これはなにかというと画像認識の計算モデルInception-v1と呼ばれる、Googleの論文を元に設定されたものが採用されています。 詳しくはTensorFlowのModelのコードにあります。

(2)計算
計算はClassifyImage内で実行されています。
画像のbitmapを行列配列(input_tensor)に変換したものをsessionに代入して行います。

tensorflow_jni.cc
//BitmapのRGB値を多項式行列に変換
auto input_tensor_mapped = input_tensor.tensor<float, 4>();
for (int i = 0; i < g_tensorflow_input_size; ++i) {
  const RGBA* src = bitmap_src + i * g_tensorflow_input_size;
  for (int j = 0; j < g_tensorflow_input_size; ++j) {
    // Copy 3 values
    input_tensor_mapped(0, i, j, 0) =
        (static_cast<float>(src->red) - g_image_mean) / g_image_std;
    input_tensor_mapped(0, i, j, 1) =
        (static_cast<float>(src->green) - g_image_mean) / g_image_std;
    input_tensor_mapped(0, i, j, 2) =
        (static_cast<float>(src->blue) - g_image_mean) / g_image_std;
    ++src;
  }
}

//Vectorに格納
std::vector<std::pair<std::string, tensorflow::Tensor> > input_tensors(
    {{*g_input_name, input_tensor}});

std::vector<tensorflow::Tensor> output_tensors;
std::vector<std::string> output_names({*g_output_name});

//計算
s = session->Run(input_tensors, output_names, {}, &output_tensors);

道中は若干複雑ですが最後のRunの部分ではTensorFlowがよしなに計算をCPUに振り分けて結果を返してくれるというわけです。
このプログラムではさらに受け取った結果のTop5をソートして返すようになっていました。

まとめ

ひととおりざっと動きを見てみましたが、C++に若干抵抗感はあるものの、本質的にやっていることは意外とシンプルな印象でした。アプリサイズは学習データがデカイこともあり50MB以上あったので、プロダクションでTensorFlowを入れることは現時点ではないとは思うのですが、気軽にTensorFlowの動きを理解できた気がします。