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

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

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

#動かし方
主に[TensorFlowのReadme](https://github.com/tensorflow/tensorflow/blob/master/tensorflow/examples/android/README.md)に書いてある内容です。Androidを知っている人なら特に問題無いと思いますが以下日本語で書いてます。

## 事前準備
Android SDK以外にも以下のツールが必要になります。

### 1.Bazelの導入
TensorFlowのBuildにGoogleの新しいBuild toolであるBazelが必須になります。
MacであればHomebrewでかんたんに[導入できます。](https://www.bazel.io/versions/master/docs/install.html#mac-os-x)
Bazelの挙動についてやファイル構成については[チュートリアル](https://www.bazel.io/versions/master/docs/getting-started.html)があるので、そこも目を通しておくとよいです。
初めて触ったのですが、ワークスペースを作ってそこでビルドするところとかGoっぽいなと思いました。[こちらの記事](http://techlife.cookpad.com/entry/introduce-bazel-build)も参考になるかと思います。


```
$ 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の導入は[こちら](https://developer.android.com/ndk/guides/index.html)を参照しました。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ファイルのコンパイルに時間がかかっていたもようです。２回目以降は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](https://qiita-image-store.s3.amazonaws.com/0/39433/1f677043-b8e3-2996-a054-903a83633710.png "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.java` と `TensorFlowClassifier.java` になります。 `TensorFlowImageListener.java` が CameraのPreviewを取得し(1)、Previewを受け取った `TensorFlowClassifier.java` でbitmapの評価を行い(2)、その結果を `RecognitionScoreView.java` に返す(3)ような実装になっています。

```java: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);
}
```


```java: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;
          }
        );
  }
```

```java: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](
https://developer.android.com/reference/android/graphics/ImageFormat.html#YUV_420_888)によると、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がどのように動いているのかは[こちらの記事](http://qiita.com/icoxfog417/items/fb5c24e35a849f8e2c5d)に大変分かりやすくまとめられています。サンプルアプリも
(1)Tensor/Sessionの構築
(2)計算
の構成になっています。

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

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

```c++: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の論文](http://arxiv.org/pdf/1409.4842v1.pdf)を元に設定されたものが採用されています。 詳しくはTensorFlowのModelの[コード](https://github.com/tensorflow/models/blob/master/slim/nets/inception_v1.py#L36)にあります。



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

```c++: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の動きを理解できた気がします。