mruby + TensorFlow Lite + AutoML Vision Edgeで画像分類する (Raspberry Pi編)

# 1. 概要

[mruby + TensorFlow Liteで画像分類する - Qiita](https://qiita.com/akuroda/items/e3d509f75713a55ae22e) の続き。さらにRaspberry Pi上のmrubyで、[Cloud AutoML Vision Edge](https://cloud.google.com/vision/automl/docs/edge-quickstart) と組み合わせて動かせるようにする。

画像を通常のAutoML Vision APIに送りつけると、遅延が大きく、月1000枚以上は費用もかかるが、AutoML Vision Edgeと組み合わせることで、Tensorflow Liteモデル作成はAutoMLに任せ、Raspberry Pi側で高速に分類し放題の環境を作ることができる。

# 2. クロスコンパイル
セルフビルドはつらいので、できるだけクロスコンパイルする方針とする。以下、ホストマシンのプロンプトは[host]、ターゲットのRaspberry Piのプロンプトは[rpi]で記載する。

|環境|OS|
|---|---|
|ホスト|WSL / Ubuntu XX|
|Raspberry Pi|Rasbian Strech|


## 2.1 準備
ホストマシンにARMのクロスコンパイル環境をインストールする。

```
[host] $ apt install -y crossbuild-essential-armhf
```

## 2.2 Tensorflow lite
tensorflow直下でbuild_rpi_lib.shを実行すると、tensorflow/lite/tools/make/gen/rpi_armv7l/lib/libtensorflow-lite.aができる。

```
[host] $ tensorflow/lite/tools/make/build_rpi_lib.sh
```

さらに、C experimental APIライブラリをビルドするために、tensorflow/lite/experimental/cに以下のMakefileを作る。

```Makefile:tensorflow/lite/experimental/c/Makefile.arm
SRCS =  c_api.cc c_api_experimental.cc
OBJS = $(subst .cc,.o,$(subst .cxx,.o,$(subst .cpp,.o,$(SRCS))))
CXXFLAGS = -DTF_COMPILE_LIBRARY -I$(TENSORFLOW_ROOT) -I$(TENSORFLOW_ROOT)/tensorflow/lite/tools/make/downloads/flatbuffers/include -fPIC
TARGET = libtensorflowlite_c
OS_ARCH = rpi_armv7l
TARGET_SHARED := $(TARGET).so
LDFLAGS += -L$(TENSORFLOW_ROOT)/tensorflow/lite/tools/make/gen/$(OS_ARCH)/lib
LIBS = -ltensorflow-lite

TARGET_TOOLCHAIN_PREFIX := arm-linux-gnueabihf-
CXX := ${TARGET_TOOLCHAIN_PREFIX}g++
CC := ${TARGET_TOOLCHAIN_PREFIX}gcc
AR := ${TARGET_TOOLCHAIN_PREFIX}ar

.SUFFIXES: .cpp .cxx .o

all : $(TARGET_SHARED)

$(TARGET_SHARED) : $(OBJS)
        mkdir -p $(OS_ARCH); \
        $(CXX) -shared -o $@ $(OBJS) $(LDFLAGS) $(LIBS)
.cxx.o :
        $(CXX) -std=c++14 -c $(CXXFLAGS) -I. $< -o $@
.cpp.o :
        $(CXX) -std=c++14 -c $(CXXFLAGS) -I. $< -o $@
clean :
        rm -f *.o $(TARGET_SHARED)
```

このディレクトリでTENSORFLOW_ROOTを指定してMakeを実行するとrpi_armv7l/libtensorflowlite_c.soができる。
(x86のバイナリと重ならないように、今回は違うディレクトリに出力した)
libtensorflowlite_c.so はmrubyのコンパイル用に/usr/arm-linux-gnueabihf/libにコピーしておく。

```
[host] tensorflow/lite/experimental/c $ export TENSORFLOW_ROOT=.../tensorflow # 適宜修正
[host] tensorflow/lite/experimental/c $ Make -f Makefile.arm
[host] tensorflow/lite/experimental/c $ cp rpi_armv7l/libtensorflowlite_c.so /usr/arm-linux-gnueabihf/lib
```

## 2.3 GD
GDは依存ライブラリが多くクロスコンパイルは大変そうなので、Raspberry Piの実機から持ってくることにする。(もしいい方法があれば教えてください)

まずは、Raspberry Pi側でGDをインストールする。

```
[rpi] $ sudo apt-get install libgd3 libgd-dev
```

lddでlibgdが依存するライブラリを調べる。

```
[rpi] $ ldd /usr/lib/arm-linux-gnueabihf/libgd.so.3
        linux-vdso.so.1 (0x7ebc3000)
        /usr/lib/arm-linux-gnueabihf/libarmmem.so (0x76ea1000)
        libm.so.6 => /lib/arm-linux-gnueabihf/libm.so.6 (0x76e12000)
        libz.so.1 => /lib/arm-linux-gnueabihf/libz.so.1 (0x76deb000)
        libpng16.so.16 => /usr/lib/arm-linux-gnueabihf/libpng16.so.16 (0x76db1000)
        libfontconfig.so.1 => /usr/lib/arm-linux-gnueabihf/libfontconfig.so.1 (0x76d6e000)
        libfreetype.so.6 => /usr/lib/arm-linux-gnueabihf/libfreetype.so.6 (0x76cd3000)
        libjpeg.so.62 => /usr/lib/arm-linux-gnueabihf/libjpeg.so.62 (0x76c8d000)
        libXpm.so.4 => /usr/lib/arm-linux-gnueabihf/libXpm.so.4 (0x76c6e000)
        libX11.so.6 => /usr/lib/arm-linux-gnueabihf/libX11.so.6 (0x76b4b000)
        libtiff.so.5 => /usr/lib/arm-linux-gnueabihf/libtiff.so.5 (0x76ad1000)
        libwebp.so.6 => /usr/lib/arm-linux-gnueabihf/libwebp.so.6 (0x76a73000)
        libc.so.6 => /lib/arm-linux-gnueabihf/libc.so.6 (0x76934000)
        /lib/ld-linux-armhf.so.3 (0x76f1e000)
        libexpat.so.1 => /lib/arm-linux-gnueabihf/libexpat.so.1 (0x76902000)
        libpthread.so.0 => /lib/arm-linux-gnueabihf/libpthread.so.0 (0x768d9000)
        libxcb.so.1 => /usr/lib/arm-linux-gnueabihf/libxcb.so.1 (0x768aa000)
        libdl.so.2 => /lib/arm-linux-gnueabihf/libdl.so.2 (0x76897000)
        liblzma.so.5 => /lib/arm-linux-gnueabihf/liblzma.so.5 (0x76866000)
        libjbig.so.0 => /usr/lib/arm-linux-gnueabihf/libjbig.so.0 (0x76849000)
        libgcc_s.so.1 => /lib/arm-linux-gnueabihf/libgcc_s.so.1 (0x7681c000)
        libXau.so.6 => /usr/lib/arm-linux-gnueabihf/libXau.so.6 (0x76f3f000)
        libXdmcp.so.6 => /usr/lib/arm-linux-gnueabihf/libXdmcp.so.6 (0x76805000)
        libbsd.so.0 => /lib/arm-linux-gnueabihf/libbsd.so.0 (0x767dc000)
```

こんな感じで依存するライブラリを適当なディレクトリに集め、libgd.so.3と手元になさそうなlibc, libm以外のライブラリをホスト環境の/usr/arm-linux-gnueabihf/libにコピーする。

```ruby:collect-lib.rb
require 'fileutils'

dest = "."
`ldd /usr/lib/arm-linux-gnueabihf/libgd.so.3`.each_line do |l|
  next if l !~ /=>/
  t1, t2, path, addr = l.split
  puts path
  FileUtils.cp(path, dest)
end
```

## 2.4 mruby

build_config.rbを修正してクロスコンパイルの設定を行う。CrossBuild.newの引数に指定した名前でbuild/以下にディレクトリが作成される。この場合は build/rpi/bin以下に mruby, mirbができる。

build_config.rbの前半にあるホスト向けのMRuby::Build.newとかはコンパイルの時間がかかるだけなので削除してしまってもよい。

mruby-tfliteはmattnさんが修正を反映してくれたので、今回はGitHub直接でいけるはず。

```build_config.rb
MRuby::CrossBuild.new('rpi') do |conf|
  toolchain :gcc
  conf.cc.command = 'arm-linux-gnueabihf-gcc'
  conf.cxx.command = 'arm-linux-gnueabihf-g++'
  conf.linker.command = 'arm-linux-gnueabihf-gcc'
  conf.linker.flags = '-L/usr/arm-linux-gnueabihf/lib'
  conf.archiver.command = 'arm-linux-gnueabihf-ar'
  conf.gembox 'default'
  conf.gem :github => 'qtkmz/mruby-gd'
  conf.gem :github => 'mattn/mruby-tflite'
 end
```

```
[host] mruby $ export TENSORFLOW_ROOT=.../tensorflow # 適宜修正
[host] mruby $ ./minirake
```

これで build/rpi/bin以下に mrubyとmirbができるので、Raspberry Piにバイナリを送る。

画像とTF liteのモデルもRaspberry Piに送って[前回](https://qiita.com/akuroda/items/e3d509f75713a55ae22e)のスクリプトを動かしてみた。Mobilenetのモデル(約4MB)で対して計測してみると、最速でモデル読み込みが10msec以内、画像読み込みが370msec、推論が300msec程度だった。ただ、速度はあまり安定せず、倍ぐらいかかる場合もあるようだ。

自前で作った約50MBのモデルでは推論は650msec程度だった。

```
[rpi ]$ time ./mruby tf_test.rb
w: 224
h 224
ch: 3
131 255 flamingo

real    0m0.930s
user    0m0.908s
sys     0m0.022s
```

Raspberry Piでmruby/mirbを実行した際に以下のようなエラーが出てしまったら、コンパイルしたマシンの/usr/arm-linux-gnueabihf/libからRaspberry Piの適当なディレクトリにlim.so.6を持ってきて、LD_LIBRARYにそのディレクトリを指定してmruby/mirbを実行するようにする。

```
mruby/mirb: /lib/arm-linux-gnueabihf/libm.so.6: version `GLIBC_2.27' not found (required by .../libtensorflowlite_c.so)
```

# 3. AutoML Vision Edge

@shinkoizumi0033 さんの [GCP Cloud AutoML Vision Edgeを使ってカスタムモデルを作ってエッジデバイスで動かす話(Firebase ML Kit) - Qiita](https://qiita.com/shinkoizumi0033/items/4ae75e0bebb2848cbd54) にある通りにtfliteのモデルを作成する。

AutoML Vison Edgeを使うと、画像サイズは224x224固定らしいので、そのサイズで学習用の画像を用意しておく。
(今回は以前使った学習用の画像を再利用したのでサイズは適当)

<img width="493" alt="automl_vision.PNG" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/171276/d0de5564-afa3-8eb5-ea8c-b78c66eace38.png">

Model TypeでEdgeを選ぶ。Optimize model forはBest trade-offにしてみた。Higher accuracyにする必要はないと思う。node hour budgetはしれっと2 node hoursを推奨されるが、無料枠を使いたいなら1に修正する。

> 毎月最初のモデル 10 個については、アカウントごとに 1 ノード時間まで無料です

学習が完了すると、dict.txt, model.tflite, tflite_metadata.jsonの3ファイルが生成される。

```json:tflite_metadata.json
{
    "batchSize": 1, 
    "imageChannels": 3, 
    "imageHeight": 224, 
    "imageWidth": 224, 
    "inferenceType": "QUANTIZED_UINT8", 
    "inputTensor": "image", 
    "inputType": "QUANTIZED_UINT8", 
    "outputTensor": "scores", 
    "supportedTfVersions": [
        "1.10", 
        "1.11", 
        "1.12", 
        "1.13"
    ]
}
```

以下のようなスクリプトで実行させる。

```tflite.rb
model = TfLite::Model.from_file 'model.tflite'
interpreter = TfLite::Interpreter.new(model)
interpreter.allocate_tensors
input = interpreter.input_tensor(0)
expected_width = input.dim(1) # 224
expected_height = input.dim(2) # 224
expected_channel = input.dim(3) # 3

img = Array.new(expected_height * expected_width * expected_channel, 0)

im = GD::Image.new_from_png_file ARGV[0]
w = im.width
h = im.height

if expected_width != w || expected_height != h
  puts "ERROR: image size mismatch"
  return
end

(0...w).each do |x|
  (0...h).each do |y|
    offset = (y * w + x ) * expected_channel
    img[offset] = im.red(im.get_pixel(x, y))
    img[offset + 1] = im.green(im.get_pixel(x, y))
    img[offset + 2] = im.blue(im.get_pixel(x, y))
  end
end
im.destroy

output = interpreter.output_tensor(0)
input = interpreter.input_tensor(0)
input.data = img
interpreter.invoke

labels = File.read('dict.txt').lines.map {|x| x.strip}

output.data.each_with_index do |v, i|
  puts "#{i} #{v} #{labels[i]}" if v > 5
end
```

こちらも時間を計測してみると、モデルサイズ約3MBで、Mobilenetの画像分類より若干程度早い程度だった。

これでAutoMLで作成したモデルでの推論環境が完成。[Edge TPU](https://cloud.google.com/edge-tpu/) を使うほどの性能は不要だが、Pythonでは重いという場合があればぜひどうぞ(あるのか？)。