Deep Learningアプリケーション開発 (6) TensorFlow Lite with C++ on Raspberry Pi

# この記事について
機械学習、Deep Learningの専門家ではない人が、Deep Learningを応用したアプリケーションを作れるようになるのが目的です。MNIST数字識別する簡単なアプリケーションを、色々な方法で作ってみます。特に、組み込み向けアプリケーション(Edge AI)を意識しています。
モデルそのものには言及しません。数学的な話も出てきません。Deep Learningモデルをどうやって使うか(エッジ推論)、ということに重点を置いています。

1. [Kerasで簡単にMNIST数字識別モデルを作り、Pythonで確認](https://qiita.com/take-iwiw/items/796ec8560563625ace34)
2. [TensorFlowモデルに変換してPythonで使用してみる (Windows, Linux)](https://qiita.com/take-iwiw/items/80fc73ff23d8f51650f5)
3. [TensorFlowモデルに変換してCで使用してみる (Windows, Linux)](https://qiita.com/take-iwiw/items/d7cb2bd10a8bbb81cd8b)
4. [TensorFlow Liteモデルに変換してPythonで使用してみる (Windows, Linux)](https://qiita.com/take-iwiw/items/d63aa67e5c700fcea70a)
5. [TensorFlow Liteモデルに変換してCで使用してみる (Linux)](https://qiita.com/take-iwiw/items/d998df1981d46285df62)
6. [TensorFlow Liteモデルに変換してC++で使用してみる (Raspberry Pi)](https://qiita.com/take-iwiw/items/4d198f6203348ef7fd31)   <--- 今回の内容
7. [TensorFlow LiteモデルをEdge TPU上で動かしてみる (Raspberry Pi)](https://qiita.com/take-iwiw/items/6aeab468c326ecc21563) 



## 今回の内容
- TensorFlow Lite for C++のライブラリ(RaspberryPi用)を作る
- TensorFlow Lite用モデルを使って、入力画像から数字識別するC++アプリケーションを作る

TensorFLow Lite用モデルは[前回](https://qiita.com/take-iwiw/items/d63aa67e5c700fcea70a)作成した`conv_mnist.tflite`を使います
ソースコードとサンプル入力画像: https://github.com/take-iwiw/CNN_NumberDetector/tree/master/05_TensorflowLite_CPP

## 環境
- OS: Windows 10 (64-bt)
- CPU = Intel Core i7-6700@3.4GHz (物理コア=4、論理プロセッサ数=8)
- OS(on VirtualBox): Ubuntu 16.04
	- 開発環境: bazel 0.23.1, cmake, gcc
- Raspberry Pi 2 Model B (2018-11-13-raspbian-stretch)

## 全体の方針
まず、PC Linux(Ubuntu)上で、ラズパイ用のTensorFlow Lite for C++ ライブラリをクロスビルドします。
そして、作成したTensorFlowLiteライブラリを使用するアプリケーションを作成します。ビルドは、クロスビルドとネイティブビルドの両方を試してみます。

# TensorFlow Lite for C++のライブラリ(RaspberryPi用)を作る
## ビルドする
基本的には、https://www.tensorflow.org/lite/guide/build_rpi に書かれている通りにやればOKです。[Bazelのインストール](https://qiita.com/take-iwiw/items/d998df1981d46285df62#%E3%83%93%E3%83%AB%E3%83%89%E3%83%84%E3%83%BC%E3%83%AB%E3%81%AE%E7%94%A8%E6%84%8F)をしておく必要があるかもしれません。
PC Linux(Ubuntu)上で、以下コマンドを実行します。
が、2点注意があるので先に↓の注意事項を確認してください。

```sh:tfliteライブラリのクロスビルド(onUbuntu)
sudo apt-get update
sudo apt-get install crossbuild-essential-armhf
cd ~/
git clone https://github.com/tensorflow/tensorflow.git
cd tensorflow
git checkout 5d43d943153836f6c3bafd97d6bedbc124b99e37
./tensorflow/lite/tools/make/download_dependencies.sh
nano tensorflow/lite/tools/make/Makefile
./tensorflow/lite/tools/make/build_rpi_lib.sh
ls tensorflow/lite/tools/make/gen/rpi_armv7l/lib/libtensorflow-lite.a
```

生成物は `tensorflow/lite/tools/make/gen/rpi_armv7l/lib/libtensorflow-lite.a` になります。
※上記コマンドでは念のため、僕の方で動作確認できたバージョンを指定しています。本当はタグが打たれているv1.13.1あたりを指定すべきなのだと思いますが、後述のMakefileの問題を直すのが少し面倒になるため、中途半端ですがこのバージョンを指定することにしました。


## 注意事項1
VirtualBox上の共有フォルダで作業は行わないようにしてください。
download_dependencies.sh実行中にシンボリックリンク作成エラーが出てしまいます。
困ったことに、ライブラリ自体は作成されてしまいます。が、これを使用したアプリケーションをビルドしようとすると、以下のようなエラーが出てリンクに失敗します。先のコマンドではホームディレクトリにcdしてから作業しています。

```エラー例
./libtensorflow-lite.a(spectrogram.o): In function `tflite::internal::Spectrogram::ProcessCoreFFT()':
spectrogram.cc:(.text+0x1d2): undefined reference to `rdft'
```

## 注意事項2
これは、TensorFlow側の問題だと思うのですが、現時点(2019年3月10日)では、Android用のNNAPIを使用する設定になってしまっています。そのため、これもライブラリ自体の作成はできるのですが、アプリケーションをビルドしようとすると、以下のようなエラーが出てリンクに失敗します。

```エラー例
undefined reference to `NnApiImplementation()'
```

`tensorflow/lite/tools/make/Makefile` を以下のように編集します。これは、単純にMakefileの条件設定のif文の問題だと思うので、そのうち修正されると思います。

```txt:tensorflow/lite/tools/make/Makefile
original)
BUILD_WITH_NNAPI=true
change)
BUILD_WITH_NNAPI=false
```

参考: https://github.com/tensorflow/tensorflow/issues/25120


# TensorFlow Lite用モデルを使って、入力画像から数字識別するC++アプリケーションを作る
## PC上でクロスビルドしてみる (コマンドライン)
最初にgit cloneした、tensorflowディレクトリで作業します。
まず、先ほど作成した`libtensorflow-lite.a`を作業ディレクトリにコピーしておきます。また、TensorFlowLiteだけを使用するサンプルコード(OpenCV未使用)を用意しておきます(https://github.com/take-iwiw/CNN_NumberDetector/blob/master/05_TensorflowLite_CPP/main_without_opencv.cpp )。ソースコードは、前回作成したPC Linux用のコードと全く同じものが使用できます。

以下のようなコマンドでコンパイルが出来ます。pthreadが必要みたいでした。

```sh:アプリケーションのクロスビルド(onUbuntu)
cd ~/tensorflow
cp tensorflow/lite/tools/make/gen/rpi_armv7l/lib/libtensorflow-lite.a .

# サンプルコードをダウンロード
wget https://raw.githubusercontent.com/take-iwiw/CNN_NumberDetector/master/05_TensorflowLite_CPP/main_without_opencv.cpp

# ビルド
arm-linux-gnueabihf-g++ main_without_opencv.cpp -I.  -I./tensorflow -I./tensorflow/lite/tools/make/downloads -I./tensorflow/lite/tools/make/downloads/eigen -I./tensorflow/lite/tools/make/downloads/absl -I./tensorflow/lite/tools/make/downloads/gemmlowp -I./tensorlow/lite/tools/make/downloads/neon_2_sse -I./tensorflow/lite/tools/make/downloads/farmhash/src -I./tensorflow/lite/tools/make/downloads/flatbuffers/include -std=c++11 -lstdc++ -lm -lpthread -ltensorflow-lite -L. 

# ラズパイにアップロード (IPアドレスは適宜置き換えてください)
scp ./a.out pi@192.168.1.88:/home/pi/a_cross.out
wget https://github.com/take-iwiw/CNN_NumberDetector/raw/master/05_TensorflowLite_CPP/resource/conv_mnist.tflite
scp conv_mnist.tflite pi@192.168.1.88:/home/pi/.
```

ラズパイ上で、`./a.out` で実行できます。結果は次のネイティブビルド版と同様なので省略します。

## ラズパイ上でネイティブビルドしてみる (コマンドライン)
OpenCV等を使用したプロジェクトの場合、クロスビルドだと色々と設定が面倒なので、ラズパイ上でネイティブビルドしてみます。とはいえ、使用するコンパイラが変わっただけです。
ラズパイ上でも、tensorflowリポジトリをcloneして、依存ファイルをダウンロードしておきます。これはビルド時にヘッダファイルが必要なためです。また、先ほど作成した`libtensorflow-lite.a`をコピーしておきます。

```sh:PCからラズパイにライブラリをコピーしておく(onUbuntu)
scp tensorflow/lite/tools/make/gen/rpi_armv7l/lib/libtensorflow-lite.a pi@192.168.1.88:/home/pi/.
# ラズパイのIPアドレスは適宜置き換えてください
```

```sh:アプリケーションのネイティブビルド(onラズパイ)
# ビルドに必要なヘッダ取得
git clone https://github.com/tensorflow/tensorflow.git
cd tensorflow
git checkout 5d43d943153836f6c3bafd97d6bedbc124b99e37
./tensorflow/lite/tools/make/download_dependencies.sh

# PCからアップロードしたライブラリをコピーしておく
cp ~/libtensorflow-lite.a .

# サンプルコードとモデルをダウンロード
wget https://raw.githubusercontent.com/take-iwiw/CNN_NumberDetector/master/05_TensorflowLite_CPP/main_without_opencv.cpp
wget https://github.com/take-iwiw/CNN_NumberDetector/raw/master/05_TensorflowLite_CPP/resource/conv_mnist.tflite


# ビルド
g++ main_without_opencv.cpp -I.  -I./tensorflow -I./tensorflow/lite/tools/make/downloads -I./tensorflow/lite/tools/make/downloads/eigen -I./tensorflow/lite/tools/make/downloads/absl -I./tensorflow/lite/tools/make/downloads/gemmlowp -I./tensorlow/lite/tools/make/downloads/neon_2_sse -I./tensorflow/lite/tools/make/downloads/farmhash/src -I./tensorflow/lite/tools/make/downloads/flatbuffers/include -std=c++11 -lstdc++ -lm -lpthread -ltensorflow-lite -L. 

# 実行
./a.out
```

```txt:実行結果
pi@raspberrypi:~/tensorflow $ ./a.out 
=== Pre-invoke Interpreter State ===
~略~
=== Post-invoke Interpreter State ===
Interpreter has 16 tensors and 5 nodes
Inputs: 10
Outputs: 8

Tensor   0 conv2d_1/Conv2D_bias kTfLiteFloat32   kTfLiteMmapRo         32 bytes ( 0.0 MB)  8
Tensor   1 conv2d_1/Relu        kTfLiteFloat32  kTfLiteArenaRw      25088 bytes ( 0.0 MB)  1 28 28 8
Tensor   2 conv2d_1/kernel      kTfLiteFloat32   kTfLiteMmapRo        288 bytes ( 0.0 MB)  1 3 3 8
Tensor   3 conv2d_2/Conv2D_bias kTfLiteFloat32   kTfLiteMmapRo         16 bytes ( 0.0 MB)  4
Tensor   4 conv2d_2/Relu        kTfLiteFloat32  kTfLiteArenaRw       3136 bytes ( 0.0 MB)  1 14 14 4
Tensor   5 conv2d_2/kernel      kTfLiteFloat32   kTfLiteMmapRo       1152 bytes ( 0.0 MB)  4 3 3 8
Tensor   6 dense_1/BiasAdd      kTfLiteFloat32  kTfLiteArenaRw         40 bytes ( 0.0 MB)  1 10
Tensor   7 dense_1/MatMul_bias  kTfLiteFloat32   kTfLiteMmapRo         40 bytes ( 0.0 MB)  10
Tensor   8 dense_1/Softmax      kTfLiteFloat32  kTfLiteArenaRw         40 bytes ( 0.0 MB)  1 10
Tensor   9 dense_1/kernel/transpose kTfLiteFloat32   kTfLiteMmapRo      31360 bytes ( 0.0 MB)  10 784
Tensor  10 input_1              kTfLiteFloat32  kTfLiteArenaRw       3136 bytes ( 0.0 MB)  1 28 28 1
Tensor  11 max_pooling2d_1/MaxPool kTfLiteFloat32  kTfLiteArenaRw       6272 bytes ( 0.0 MB)  1 14 14 8
Tensor  12 (null)               kTfLiteNoType  kTfLiteMemNone          0 bytes ( 0.0 MB)  (null)
Tensor  13 (null)               kTfLiteNoType  kTfLiteMemNone          0 bytes ( 0.0 MB)  (null)
Tensor  14 (null)               kTfLiteFloat32  kTfLiteArenaRw      56448 bytes ( 0.1 MB)  1 14 14 72
Tensor  15 (null)               kTfLiteFloat32 kTfLiteArenaRwPersistent       1152 bytes ( 0.0 MB)  72 4

Node   0 Operator Builtin Code   4
  Inputs: 10 2 0
  Outputs: 1
Node   1 Operator Builtin Code  17
  Inputs: 1
  Outputs: 11
Node   2 Operator Builtin Code   3
  Inputs: 11 5 3
  Outputs: 4
Node   3 Operator Builtin Code   9
  Inputs: 4 9 7
  Outputs: 6
Node   4 Operator Builtin Code  25
  Inputs: 6
  Outputs: 8
prob of 0: 0.020
prob of 1: 0.504
prob of 2: 0.140
prob of 3: 0.020
prob of 4: 0.006
prob of 5: 0.026
prob of 6: 0.021
prob of 7: 0.220
prob of 8: 0.012
prob of 9: 0.033
```

入力がオール0の値なので、結果に意味はありません。

## クロスプラットフォーム対応なプロジェクトにする
今回作成したアプリケーション用プログラムでは、ソースコードは、PC用とラズパイ用で全く同じものが使えました。
また、ラズパイ用ビルドにおいて、クロスビルドとネイティブビルドの違いはtoolchainの違いだけです(arm-linux-gnueabihf-g++ or g++)。

これらを簡単に切り替えられるようなcmakeプロジェクトを作ってみました。PC Linux(Ubuntu)、ラズパイ用ネイティブビルド、ラズパイ用クロスビルドに対応しています。
https://github.com/take-iwiw/CNN_NumberDetector/tree/master/05_TensorflowLite_CPP
トップのCMakeLists.txtはこんな感じです。

```txt:CMakeLists.txt
cmake_minimum_required(VERSION 2.8)
project(NumberDetector)

# Switch build target
set(BUILD_TARGET PC CACHE STRING "Build target?")
set(LINK_ONLY OFF CACHE BOOL "Use pre-built library?")

# Common build settings
include(cmakes/common.cmake)

# Build settings for each target
if(${BUILD_TARGET} STREQUAL PC)
	if(WIN32)
		message(FATAL_ERROR "[BUILD] PC Windows is not supported")
	else()
		message("[BUILD] PC Linux")
		include(cmakes/PC_LINUX.cmake)
	endif()
elseif(${BUILD_TARGET} STREQUAL RASPI_NATIVE)
	message("[BUILD] Raspberry Pi Native")
	include(cmakes/RASPI_NATIVE.cmake)
elseif(${BUILD_TARGET} STREQUAL RASPI_CROSS)
	message("[BUILD] Raspberry Pi Cross")
	include(cmakes/RASPI_CROSS.cmake)
else()
	message(FATAL_ERROR "[BUILD] Invalid target")
endif()

# Create Main project
add_executable(NumberDetector
	main.cpp
	# main_without_opencv.cpp
)

# For OpenCV
find_package(OpenCV REQUIRED)
if(OpenCV_FOUND)
	target_include_directories(NumberDetector PUBLIC ${OpenCV_INCLUDE_DIRS})
	target_link_libraries(NumberDetector ${OpenCV_LIBS})
endif()

# For Tensorflow Lite
if((${BUILD_TARGET} STREQUAL PC) AND UNIX)
	# use shared library
	# target_link_libraries(NumberDetector ${CMAKE_SOURCE_DIR}/external_libs/tensorflow_prebuilt/linux-cpu-x86_64/libtensorflowlite.so)
	# file(COPY ${CMAKE_SOURCE_DIR}/external_libs/tensorflow_prebuilt/linux-cpu-x86_64/libtensorflowlite.so DESTINATION ${PROJECT_BINARY_DIR}/)
	# use static library
	target_link_libraries(NumberDetector ${CMAKE_SOURCE_DIR}/external_libs/tensorflow_prebuilt_static/linux-cpu-x86_64/libtensorflow-lite.a)
elseif((${BUILD_TARGET} STREQUAL RASPI_NATIVE) OR (${BUILD_TARGET} STREQUAL RASPI_CROSS))
	target_link_libraries(NumberDetector ${CMAKE_SOURCE_DIR}/external_libs/tensorflow_prebuilt_static/rpi_armv7l/libtensorflow-lite.a)
else()
	message(FATAL_ERROR "[BUILD] Invalid target")
endif()
target_include_directories(NumberDetector PUBLIC ${CMAKE_SOURCE_DIR}/external_libs/tensorflow/)
target_include_directories(NumberDetector PUBLIC ${CMAKE_SOURCE_DIR}/external_libs/tensorflow/tensorflow)
target_include_directories(NumberDetector PUBLIC ${CMAKE_SOURCE_DIR}/external_libs/tensorflow/tensorflow/lite/tools/make/downloads)
target_include_directories(NumberDetector PUBLIC ${CMAKE_SOURCE_DIR}/external_libs/tensorflow/tensorflow/lite/tools/make/downloads/eigen )
target_include_directories(NumberDetector PUBLIC ${CMAKE_SOURCE_DIR}/external_libs/tensorflow/tensorflow/lite/tools/make/downloads/absl)
target_include_directories(NumberDetector PUBLIC ${CMAKE_SOURCE_DIR}/external_libs/tensorflow/tensorflow/lite/tools/make/downloads/gemmlowp)
target_include_directories(NumberDetector PUBLIC ${CMAKE_SOURCE_DIR}/external_libs/tensorflow/tensorlow/lite/tools/make/downloads/neon_2_sse)
target_include_directories(NumberDetector PUBLIC ${CMAKE_SOURCE_DIR}/external_libs/tensorflow/tensorflow/lite/tools/make/downloads/farmhash/src)
target_include_directories(NumberDetector PUBLIC ${CMAKE_SOURCE_DIR}/external_libs/tensorflow/tensorflow/lite/tools/make/downloads/flatbuffers/include)

# Copy resouce
file(COPY ${CMAKE_SOURCE_DIR}/resource/ DESTINATION ${PROJECT_BINARY_DIR}/resource/)
add_definitions(-DRESOURCE_DIR="${PROJECT_BINARY_DIR}/resource/")
```

```sh:ビルド方法
cd 05_TensorflowLite_CPP/
mkdir build && cd build
cmake ..		# PC Linux native
# cmake .. -DBUILD_TARGET=RASPI_CROSS		# Cross compile for Raspberry Pi on PC Linux
# cmake .. -DBUILD_TARGET=RASPI_NATIVE	# Native compile on Raspberry Pi
make
```