Raspberry PiでTensorFlow Liteを使ってみる

前回、TensorFlow Liteのモデルを使ってもTensorFlowのモデルと比べて全く速くならなかったのは、ひょっとしてTensorFlowが内部でuint8をfloat32に変換して処理してるとか、TensorFlowモデルと同じくらいのメモリを使うのでキャッシュのヒット率が上がらないとかが原因で、ニューラルネットワークの実行環境としてTensorFlowでなくTensorFlow Liteを使えば速くなるのではないかと思って、やってみたら、Inception_V3_quantのtfliteモデルの1フレーム当たりの処理時間が、シングルコア(1スレッド実行)で約4.2秒(Raspberry Pi 2 v1.2(Cortex A53)、前回のTensorFlowだと約4.7秒)だった。使用したコードは後述のコードとほとんど同じなので省略する。少し速くなったが、少なくとも、TensorFlow Liteでもfloat32をuint8にすれば2倍とかのレベルで計算が速くなるという訳では無いようだ。
また、マルチコア(4スレッド実行)にすると約1.7秒だった。4コアの並列実行でCPU負荷が3.5倍になっても、2.4倍しか速くなっていない。単純なモデルの変換や実行環境の変更による高速化はこれくらいが限界なのだろうか。

とりあえず、前回と同じく、TensorFlow LiteのガイドのObject Detectionのstarter modelを動かしてみたので、やったことを記録する。

●TensorFlow Liteのビルド
TensorFlow公式のBuild TensorFlow Lite for Raspberry Piのページに従って、C++ static libraryをビルドした。
(今日見ると"install just the Python interpreter API"というリンクがあるが、筆者が先月見た時は無かったか、気付かなかったので、C++用のlibtensorflow-lite.aを作るしかないと思った。)

git clone https://github.com/tensorflow/tensorflow
cd tensorflow
git checkout v1.13.1
sudo apt-get install crossbuild-essential-armhf
./tensorflow/lite/tools/make/download_dependencies.sh
./tensorflow/lite/tools/make/build_rpi_lib.sh
なお、スワップ領域が100MBだとメモリ不足で失敗したので、1GB追加した。
sudo su -
dd if=/dev/zero of=/swapfile bs=1024 count=1048576
mkswap /swapfile 
chmod 0600 /swapfile 
swapon /swapfile

●ヘッダファイルとライブラリのインストール
/home/pi/tensorflow-lite/にインストールすることにした。

mkdir -p /home/pi/tensorflow-lite/include/tensorflow/lite
cd tensorflow/lite/
cp --parents $(find . -name "*.h*") /home/pi/tensorflow-lite/include/tensorflow/lite
cd ../..
cp -r tensorflow/lite/tools/make/downloads/flatbuffers/include/flatbuffers /home/pi/tensorflow-lite/include/
mkdir /home/pi/tensorflow-lite/lib
cp tensorflow/lite/tools/make/gen/rpi_armv7l/lib/libtensorflow-lite.a /home/pi/tensorflow-lite/lib/

●今回作ったC++のプログラム(main.cc)

#include <iostream>
#include <fstream>
#include <sstream>
#include <vector>
#include <opencv2/opencv.hpp>
#include <cstdint>

#include <tensorflow/lite/model.h>
#include <tensorflow/lite/interpreter.h>
#include <tensorflow/lite/kernels/register.h>

const char* WINNAME = "Capture";
const int CAPTURE_WIDTH = 1280;
const int CAPTURE_HEIGHT = 720;
//const int CAPTURE_WIDTH = 640;
//const int CAPTURE_HEIGHT = 480;
const int DISPLAY_WIDTH = 480;
const int DISPLAY_HEIGHT = 480;
const int colors[10][3] = {
  {255, 255, 0}, {0, 255, 255}, {128, 256, 128}, {64, 192, 255}, {128, 128, 255},
  {255, 255, 0}, {0, 255, 255}, {128, 256, 128}, {64, 192, 255}, {128, 128, 255}};

int main(int argc, char *argv[])
{
  const char *model_file = "coco_ssd_mobilenet_v1_1.0_quant/detect.tflite";
  const char *label_file = "coco_ssd_mobilenet_v1_1.0_quant/labelmap.txt";

  // TensorFlow Lite things
  std::unique_ptr<tflite::FlatBufferModel> model =
    tflite::FlatBufferModel::BuildFromFile(model_file);
  if (!model) {
    std::cerr << "FlatBufferModel::BuildFromFile(\"" << model_file << "\") failed." << std::endl;
    return -1;
  }

  tflite::ops::builtin::BuiltinOpResolver resolver;
  std::unique_ptr<tflite::Interpreter> interpreter;
  tflite::InterpreterBuilder(*model, resolver)(&interpreter);

  interpreter->AllocateTensors();
  interpreter->SetNumThreads(4);

  // Read class labels
  std::ifstream ifs(label_file);
  if (!ifs) {
    std::cerr << "ifstream(\"" << label_file << "\") failed." << std::endl;
    return -1;
  }
  std::vector<std::string> class_names;
  std::string line;
  while (std::getline(ifs, line)) {
    class_names.push_back(line);
  }

  // Camera settings
  cv::VideoCapture cap;
  while (!cap.isOpened()) {
    cap.open(0);
  }
  std::cout << "Camera is opened." << std::endl;
  cap.set(CV_CAP_PROP_FRAME_WIDTH, CAPTURE_WIDTH);
  cap.set(CV_CAP_PROP_FRAME_HEIGHT, CAPTURE_HEIGHT);

  // Main loop
  int key;
  do {
    cv::Mat img;
    cap >> img;

    // Crop center square
    img = img(cv::Rect(
      (CAPTURE_WIDTH - CAPTURE_HEIGHT) / 2,
      0,
      CAPTURE_HEIGHT,
      CAPTURE_HEIGHT
    ));

    // Make resized image for input
    cv::Mat X(300, 300, img.type());
    cv::resize(img, X, X.size(), cv::INTER_CUBIC);

    // Copy X to input_tensor
    uint8_t *input_tensor = interpreter->typed_tensor<uint8_t>(interpreter->inputs()[0]);
    int i = 0;
    for (int y = 0; y < 300; y++) {
      for (int x = 0; x < 300; x++) {
        input_tensor[i++] = X.data[y * X.step + x * 3 + 2]; //BGR->RGB
        input_tensor[i++] = X.data[y * X.step + x * 3 + 1];
        input_tensor[i++] = X.data[y * X.step + x * 3 + 0];
      }
    }

    // Execute inference
    interpreter->Invoke();

    // Get result
    float *result1 = interpreter->typed_output_tensor<float>(0); //Locations (Top, Left, Bottom, Right)
    float *result2 = interpreter->typed_output_tensor<float>(1); //Classes (0=Person)
    float *result3 = interpreter->typed_output_tensor<float>(2); //Scores
    float *result4 = interpreter->typed_output_tensor<float>(3); //Number of detections

    // Draw result
    cv::resize(img, img, cv::Size(DISPLAY_WIDTH, DISPLAY_HEIGHT), cv::INTER_CUBIC);
    for (int i = result4[0] - 1; i >= 0; i--) {
      int top    = result1[10*i + 0] * 300;
      int left   = result1[10*i + 1] * 300;
      int bottom = result1[10*i + 2] * 300;
      int right  = result1[10*i + 3] * 300;
      #define SWAP(X,Y) {(X)+=(Y); (Y)=(X)-(Y); (X)-=(Y);}
      if (left > right) SWAP(left, right);
      if (top > bottom) SWAP(top, bottom);
      std::string class_name = class_names[result2[i]+1];
      float score = result3[i];
      if (score < 0.5) continue;
      std::cout << "Location=(" << left << "," << top << ")-(" << right << "," << bottom << "), ";
      std::cout << "Class=" << class_name << ", ";
      std::cout << "Score=" << score << ", ";
      std::cout << std::endl;
      left = left * DISPLAY_WIDTH / 300;
      right = right * DISPLAY_WIDTH / 300;
      top = top * DISPLAY_HEIGHT / 300;
      bottom = bottom * DISPLAY_HEIGHT / 300;
      cv::rectangle(img, cv::Point(left, top), cv::Point(right, bottom), cv::Scalar(colors[i][0], colors[i][1], colors[i][2]), 1);
      cv::rectangle(img, cv::Point(left, top+20), cv::Point(left+160, top), cv::Scalar(colors[i][0], colors[i][1], colors[i][2]), CV_FILLED);
      cv::putText(img, class_name + " (" + std::to_string(score).substr(0, 5) + ")",
        cv::Point(left, top+15), cv::FONT_HERSHEY_SIMPLEX, 0.5, cv::Scalar(0, 0, 0));
    }

    cv::imshow(WINNAME, img);
    cv::moveWindow(WINNAME, 0, 0);
    key = cv::waitKey(1);
    if (key == 's')
      cv::imwrite("result.jpg", img);

  } while (key != 'q');

  cv::destroyAllWindows();
  cap.release();
  return 0;
}
参考URL
TensorFlow Lite公式ガイドのAPI解説
https://www.tensorflow.org/lite/guide/inference
TensorFlow Lite C++ APIリファレンス
https://www.tensorflow.org/lite/api_docs/cc/namespace/tflite

●Makefile

all: main

TENSORFLOW_LITE_INCLUDE_DIR = $(HOME)/tensorflow-lite/include
TENSORFLOW_LITE_LIB_DIR = $(HOME)/tensorflow-lite/lib

main: main.o
	g++ -o main  -L$(TENSORFLOW_LITE_LIB_DIR) `pkg-config --libs opencv` main.o -ltensorflow-lite -lpthread -ldl

main.o: main.cc
	g++ -I$(TENSORFLOW_LITE_INCLUDE_DIR) `pkg-config --cflags opencv` -c main.cc

clean:
	rm -f main main.o

test: main
	./main

●実行結果
前回と同じなので省略


カメラからの入力画像サイズは、より小さくすると全体のフレームレートが上がるが、筆者のRaspberry Piの画面をVNCで表示している環境では、640x480とかにするとウィンドウの表示更新がなされないことがしばしば発生するので、敢えて1280x720で動作確認している。