20201021のTensorFlowに関する記事は3件です。

超解像手法/ESPCNを組んでみた

概要

今回は、超解像手法の1つであるESPCN(efficient sub-pixel convolutional neural network)を組みましたので、そのまとめとして投稿します。
元論文はこちらから→Real-Time Single Image and Video Super-Resolution Using an Efficient Sub-Pixel Convolutional Neural Network

目次

1.はじめに
2.ESPCNとは
3.PC環境
4.コード説明
5.終わりに

1.はじめに

超解像とは、解像度が低い画像や動画像に対して解像度を向上させる技術のことであり、ESPCNは2016年に提案された手法のことです。(ちなみに初のDeeplearningの手法として挙げられるSRCNNは2014年)
SRCNNはバイキュービック法など、既存の拡大手法に組み合わせて解像度の向上を図る手法でしたが、このESPCNではDeeplearningのモデルの中に拡大フェーズが導入されており、任意の倍率で拡大することができます。
今回は、この手法をpythonで組みましたので、コード紹介をしたいと思います。
コードの全容はGitHubにも投稿しているのでそちらをご確認ください。
https://github.com/nekononekomori/espcn_keras

2.ESPCNとは

ESPCNとは、DeeplearningのモデルにSubpixel Convolution(Pixel shuffle)を導入して解像度の向上を図った手法のことです。コードを載せるのがメインなので、詳細の説明は省きますが、ESPCNについて説明しているサイトを載せておきます。
https://buildersbox.corp-sansan.com/entry/2019/03/20/110000
https://qiita.com/oki_uta_aiota/items/74c056718e69627859c0
https://qiita.com/jiny2001/items/e2175b52013bf655d617

3.PC環境

cpu : intel corei7 8th Gen
gpu : NVIDIA GeForce RTX 1080ti
os : ubuntu 20.04

4.コード説明

GitHubを見ていただくと分かるのですが、主に3つのコードからなっています。
・datacreate.py → データセット生成プログラム
・model.py → ESPCNのプログラム
・main.py → 実行プログラム
datacreate.pyとmodel.pyで関数を作成し、main.pyで実行しています。

datacreate.pyの説明

datacreate.py
import cv2
import os
import random
import glob
import numpy as np
import tensorflow as tf

#任意のフレーム数を切り出すプログラム
def save_frame(path,        #データが入っているファイルのパス
               data_number, #1枚の画像から切り取る写真の数
               cut_height,  #保存サイズ(縦)(低画質)
               cut_width,   #保存サイズ(横)(低画質)
               mag,         #拡大倍率
               ext='jpg'):

    #データセットのリストを生成
    low_data_list = []
    high_data_list = []

    path = path + "/*"
    files = glob.glob(path)

    for img in files:
        img = cv2.imread(img, cv2.IMREAD_GRAYSCALE)
        H, W = img.shape

        cut_height_mag = cut_height * mag
        cut_width_mag = cut_width * mag

        if cut_height_mag > H or cut_width_mag > W:
            return

        for q in range(data_number):
            ram_h = random.randint(0, H - cut_height_mag)
            ram_w = random.randint(0, W - cut_width_mag)

            cut_img = img[ram_h : ram_h + cut_height_mag, ram_w: ram_w + cut_width_mag]

            #がウシアンフィルタでぼかしを入れた後に縮小
            img1 = cv2.GaussianBlur(img, (5, 5), 0)
            img2 = img1[ram_h : ram_h + cut_height_mag, ram_w: ram_w + cut_width_mag]
            img3 = cv2.resize(img2, (cut_height, cut_width))

            high_data_list.append(cut_img)
            low_data_list.append(img3)

    #numpy → tensor + 正規化
    low_data_list = tf.convert_to_tensor(low_data_list, np.float32)
    high_data_list = tf.convert_to_tensor(high_data_list, np.float32)
    low_data_list /= 255
    high_data_list /= 255

    return low_data_list, high_data_list

これは、データセットを生成するプログラムになります。

def save_frame(path,        #データが入っているファイルのパス
               data_number, #1枚の画像から切り取る写真の数
               cut_height,  #保存サイズ(縦)(低解像度)
               cut_width,   #保存サイズ(横)(低解像度)
               mag,         #拡大倍率
               ext='jpg'):

ここは関数の定義です。コメントアウトで書いている通りですが、
pathはフォルダのパスです。(例えば、fileという名前のフォルダに写真が入っているなら、"./file"と入力です。)
data_numberは1枚の写真を複数枚切り取ってデータのかさましをします。
cut_heightとcut_wedthは低解像度の画像サイズです。最終的な出力結果は倍率magをかけた値になります。
(cut_height = 300, cut_width = 300, mag = 300なら、
結果は900 * 900のサイズの画像となります。)

    path = path + "/*"
    files = glob.glob(path)

ここは、ファイルにある全ての写真をリストにして返しています。

for img in files:
        img = cv2.imread(img, cv2.IMREAD_GRAYSCALE)
        H, W = img.shape

        cut_height_mag = cut_height * mag
        cut_width_mag = cut_width * mag

        if cut_height_mag > H or cut_width_mag > W:
            return

        for q in range(data_number):
            ram_h = random.randint(0, H - cut_height_mag)
            ram_w = random.randint(0, W - cut_width_mag)

            cut_img = img[ram_h : ram_h + cut_height_mag, ram_w: ram_w + cut_width_mag]

            #ガウシアンフィルタでぼかしを入れた後に縮小
            img1 = cv2.GaussianBlur(img, (5, 5), 0)
            img2 = img1[ram_h : ram_h + cut_height_mag, ram_w: ram_w + cut_width_mag]
            img3 = cv2.resize(img2, (cut_height, cut_width))

            high_data_list.append(cut_img)
            low_data_list.append(img3)

ここは先ほどリストにした写真を1枚ずつ取り出して、data_numberの数だけ切り取っています。
切り取る場所をランダムにしたいのでrandom.randintを使用しています。
そして、ガウシアンフィルタでぼかして低解像度画像を生成しています。
最後にリストにappendで追加しています。

    #numpy → tensor + 正規化
    low_data_list = tf.convert_to_tensor(low_data_list, np.float32)
    high_data_list = tf.convert_to_tensor(high_data_list, np.float32)
    low_data_list /= 255
    high_data_list /= 255

    return low_data_list, high_data_list

ここは、keras, tensorflowではnumpy配列ではなくtensorに変換する必要があるため、変換を行っています。同時に正規化もここでしておきます。

最後に、低解像度の画像を格納したリストと高解像度の画像を格納したリストを返して関数は終了です。

main.pyの説明

main.py
import tensorflow as tf
from tensorflow.python.keras.models import Model
from tensorflow.python.keras.layers import Conv2D, Input, Lambda

def ESPCN(upsampling_scale):
    input_shape = Input((None, None, 1))

    conv2d_0 = Conv2D(filters = 64,
                        kernel_size = (5, 5),
                        padding = "same",
                        activation = "relu",
                        )(input_shape)
    conv2d_1 = Conv2D(filters = 32,
                        kernel_size = (3, 3),
                        padding = "same",
                        activation = "relu",
                        )(conv2d_0)
    conv2d_2 = Conv2D(filters = upsampling_scale ** 2,
                        kernel_size = (3, 3),
                        padding = "same",
                        )(conv2d_1)

    pixel_shuffle = Lambda(lambda z: tf.nn.depth_to_space(z, upsampling_scale))(conv2d_2)

    model = Model(inputs = input_shape, outputs = [pixel_shuffle])

    model.summary()

    return model

さすがと言いますか、短いですね。

さて、ESPCNの論文をみていると、このような構造をしていると書いています。
image.png
Convolution層の詳細はこちらを→kerasのドキュメント
pixel_shuffleはkerasには標準搭載されていないので、lambdaで代用しました。
lambbdaは任意の式をモデルに組み込めるので、拡大を表しています。
lambdaのドキュメント→https://keras.io/ja/layers/core/#lambda
tensorflowのドキュメント→https://www.tensorflow.org/api_docs/python/tf/nn/depth_to_space

ここのpixel shuffleに関しては色々なやり方があるみたいです。

model.pyの説明

model.py
import model
import data_create
import argparse
import os
import cv2

import numpy as np
import tensorflow as tf

if __name__ == "__main__":

    def psnr(y_true, y_pred):
        return tf.image.psnr(y_true, y_pred, 1, name=None)

    train_height = 17
    train_width = 17
    test_height = 200
    test_width = 200

    mag = 3.0
    cut_traindata_num = 10
    cut_testdata_num = 1

    train_file_path = "../photo_data/DIV2K_train_HR" #写真が入ったフォルダ
    test_file_path = "../photo_data/DIV2K_valid_HR" #写真が入ったフォルダ

    BATSH_SIZE = 256
    EPOCHS = 1000
    opt = tf.keras.optimizers.Adam(learning_rate=0.0001)

    parser = argparse.ArgumentParser()
    parser.add_argument('--mode', type=str, default='espcn', help='espcn, evaluate')

    args = parser.parse_args()

    if args.mode == "espcn":
        train_x, train_y = data_create.save_frame(train_file_path,   #切り取る画像のpath
                                                cut_traindata_num,  #データセットの生成数
                                                train_height, #保存サイズ
                                                train_width,
                                                mag)   #倍率

        model = model.ESPCN(mag) 
        model.compile(loss = "mean_squared_error",
                        optimizer = opt,
                        metrics = [psnr])
#https://keras.io/ja/getting-started/faq/
        model.fit(train_x,
                    train_y,
                    epochs = EPOCHS)

        model.save("espcn_model.h5")

    elif args.mode == "evaluate":
        path = "espcn_model"
        exp = ".h5"
        new_model = tf.keras.models.load_model(path + exp, custom_objects={'psnr':psnr})

        new_model.summary()

        test_x, test_y = data_create.save_frame(test_file_path,   #切り取る画像のpath
                                                cut_testdata_num,  #データセットの生成数
                                                test_height, #保存サイズ
                                                test_width,
                                                mag)   #倍率
        print(len(test_x))
        pred = new_model.predict(test_x)
        path = "resurt_" + path
        os.makedirs(path, exist_ok = True)
        path = path + "/"

        for i in range(10):
            ps = psnr(tf.reshape(test_y[i], [test_height, test_width, 1]), pred[i])
            print("psnr:{}".format(ps))

            before_res = tf.keras.preprocessing.image.array_to_img(tf.reshape(test_x[i], [int(test_height / mag), int(test_width / mag), 1]))
            change_res = tf.keras.preprocessing.image.array_to_img(tf.reshape(test_y[i], [test_height, test_width, 1]))
            y_pred = tf.keras.preprocessing.image.array_to_img(pred[i])

            before_res.save(path + "low_" + str(i) + ".jpg")
            change_res.save(path + "high_" + str(i) + ".jpg")
            y_pred.save(path + "pred_" + str(i) + ".jpg")

    else:
        raise Exception("Unknow --mode")

メインは結構長いのですが、短くできるならもっとできるかなぁというのが感想です。
以下で、中身の説明をしていこうと思います。

import model
import data_create
import argparse
import os
import cv2

import numpy as np
import tensorflow as tf

ここでは、関数や同じディレクトリ にある別のファイルを読み込んでいます。
datacreate.pyとmodel.pyとmain.pyは同じディレクトリにおいてください。

    def psnr(y_true, y_pred):
        return tf.image.psnr(y_true, y_pred, 1, name=None)

今回は、生成画像の良し悪しの判断基準にpsnrを使用しましたので、そこの定義です。
psnrはピーク信号対雑音比という名前で、簡単に言うと比較したい画像の画素値の差分を計算するって感じです。ここでは詳細の説明を省きますが、この記事とかは割と詳しく、複数の評価法が記載されています。

    train_height = 17
    train_width = 17
    test_height = 200
    test_width = 200

    mag = 3.0
    cut_traindata_num = 10
    cut_testdata_num = 1

    train_file_path = "../photo_data/DIV2K_train_HR" #写真が入ったフォルダ
    test_file_path = "../photo_data/DIV2K_valid_HR" #写真が入ったフォルダ

    BATSH_SIZE = 256
    EPOCHS = 1000
    opt = tf.keras.optimizers.Adam(learning_rate=0.0001)

ここは、今回使用する値を設定しています。config.pyとして別にしている方もgithubを見ていたら結構いますが、大規模プログラムではないため、まとめています。

学習データのサイズは、trainデータは論文が51*51と書いてあったのでそのmagで割った値の17*17を採用しました。testは見やすいように大きめにしているだけです。結果はこれの3倍の大きさになります。
データの数はファイルに含まれている画像の数の10倍です。(800枚であればデータ数は8,000)

今回、データに使用したのはよく超解像で使われているDIV2K Datasetです。データの質がいいので、少ないデータである程度の精度が出ると言われています。

    parser = argparse.ArgumentParser()
    parser.add_argument('--mode', type=str, default='espcn', help='espcn, evaluate')

    args = parser.parse_args()

ここは、モデルの学習と評価を分けたかったのでこのような形にして、--modeで選択できるようにしました。
詳細の説明はしないので、python公式のドキュメントを載せておきます。
https://docs.python.org/ja/3/library/argparse.html

  if args.mode == "espcn":
        train_x, train_y = data_create.save_frame(train_file_path,   #切り取る画像のpath
                                                cut_traindata_num,  #データセットの生成数
                                                train_height, #保存サイズ
                                                train_width,
                                                mag)   #倍率

        model = model.ESPCN(mag) 
        model.compile(loss = "mean_squared_error",
                        optimizer = opt,
                        metrics = [psnr])
#https://keras.io/ja/getting-started/faq/
        model.fit(train_x,
                    train_y,
                    epochs = EPOCHS)

        model.save("espcn_model.h5")

ここで、学習させています。srcnnと選択(後ほどやり方は記載)するとこのプログラムが動きます。

data_create.save_frameで、data_create.pyのsave_frameという関数を読み込んで、使えるようにしています。ここで、train_xとtrain_yにデータが入ったので、モデルを同様に読み込んで、compile, fitを行います。

compileなどの詳細の説明はkerasのドキュメントをご覧ください。割と論文と同じものを採用して行っています。

最後にモデルを保存してお終いです。

    elif args.mode == "evaluate":
        path = "espcn_model"
        exp = ".h5"
        new_model = tf.keras.models.load_model(path + exp, custom_objects={'psnr':psnr})

        new_model.summary()

        test_x, test_y = data_create.save_frame(test_file_path,   #切り取る画像のpath
                                                cut_testdata_num,  #データセットの生成数
                                                test_height, #保存サイズ
                                                test_width,
                                                mag)   #倍率
        print(len(test_x))
        pred = new_model.predict(test_x)
        path = "resurt_" + path
        os.makedirs(path, exist_ok = True)
        path = path + "/"

        for i in range(10):
            ps = psnr(tf.reshape(test_y[i], [test_height, test_width, 1]), pred[i])
            print("psnr:{}".format(ps))

            before_res = tf.keras.preprocessing.image.array_to_img(tf.reshape(test_x[i], [int(test_height / mag), int(test_width / mag), 1]))
            change_res = tf.keras.preprocessing.image.array_to_img(tf.reshape(test_y[i], [test_height, test_width, 1]))
            y_pred = tf.keras.preprocessing.image.array_to_img(pred[i])

            before_res.save(path + "low_" + str(i) + ".jpg")
            change_res.save(path + "high_" + str(i) + ".jpg")
            y_pred.save(path + "pred_" + str(i) + ".jpg")

    else:
        raise Exception("Unknow --mode")

いよいよラストの説明です。
まずは、psnrが使えるように先ほど保存したモデルを読み込みます。
次に、test用のデータセットを生成し、predictで画像を生成します。

psnr値をその場で知りたかったので、計算してます。
画像を保存したかったので、tensorからnumpy配列に変換して、保存してついに終わりです!

こんな感じでしっかり高解像度化できています。

low_6.jpg
pred_6.jpg

5.終わりに

今回はESPCNを組んでみました。次はどの論文を実装してみようか悩みどころですね。
要望・質問などいつでもお待ちしております。読んでいただきありがとうございました。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Nerves/rpiで TensorFlow liteを使ってみる

1.本丸攻略へ: Nerves/rpi + Tensorflow lite

前回、前々回と2回に渡って、Windows10上で Elixir+TensorFlow liteの簡単なアプリPlugMnistを作成した。ソースコードから Tensorflow liteライブラリをメイクするところから、Elixir/Erlangの拡張機能として Tensorflow lite interpreterを利用するところまで見てきた訳だが[*1]、実はそれらの修行(?)は全て「Nerves/rpiで AIしてみよう」と言う密かな目的を達成する為であった:yum:

[*1]前哨戦として、Ubuntu/WSL2にも PlugMnistをポーティングしてみたが、思いのほか作業がスムーズに進み、特筆すべき事柄がなかった。orzガックシ

さあ、経験値はそれなりに溜まった。今や時は満ちた、いざ本丸(Nerves/rpi)の攻略に着手しよう。
尚、本記事は多分に個人的な備忘録となっている。それ故に、PlugMnistのプログラムには全く触れず、専らポーティング作業にフォーカスしている。悪しからず。

PlugMnistの全ファイルセットは、https://github.com/shoz-f/plug_mnist_nerves で閲覧できる。

2.作戦上の落とし穴

組み込みシステムの開発では、大概の場合ターゲット・ボードのリソースが貧弱なため、ターゲット上でのセルフ開発を行うことが難しい。そこでクロス開発環境の下で開発を行うことになるのだが、ソフト動作確認はターゲットで行わざるを得ずココが難関となることが多い。そんな訳で玄人は、ターゲットでの動作確認がスルッとPASSするように、できるだけ実績がある枯れたコードを利用したり、ホスト・コンピュータ上で可能な限りエミュレーションしたりと作戦を廻らすのだ。

このプロジェクトも、その例に漏れず、

  1. MinGW64/Windowsや Ubuntu/WSL2上で PlugMnistアプリを動作させ、事前に Bug出しを行う
  2. 可能な限り、巷でデファクトスタンダードで品質の安定したライブラリを使用する
  3. 他への依存関係が少ない C++テンプレート・ベースのライブラリなどを採用する

と言った作戦を展開して来た。しかしである。いざ、本丸の Nerves/rpiを攻略しようとしたところ、ポピュラーな筈のライブラリ libjpeg.*が、Nervesツールチェイン内に見つからないではないか。まさかの落とし穴である。

libjpegは PlugMnistアプリのビルドに不可欠なので、どうやって追加するかを急遽検討することになった。
作戦の立て直しだ。勘弁してよ~~:sweat:

3.Nerves開発環境の地形図

作戦を立て直すに当たって、まず「Nerves開発環境」の全体像を復習しておこう。「与えられているものは何か? 条件は何か」である。

Nervesのツールチェイン&ライブラリ群は、ホーム・ディレクトリの下の隠しディレクトリ "~/.nerves/artifacts"に置かれている。その中は、ターゲットの種類や Nervesのバージョンでネーミングされた下記のようなディレクトリで管理されている。

【ツールチェイン】
 Raspberry Pi A+/B/B+/Zero/ZeroW用
  ・nerves_toolchain_armv6_rpi_linux_gnueabi-linux_x86_64-1.3.2
 Raspberry Pi 2/3用ほか
  ・nerves_toolchain_arm_unknown_linux_gnueabihf-linux_x86_64-1.3.2

【Nervesシステム】
 Raspberry Pi A+/B/B+用
  ・nerves_system_rpi-portable-1.12.2
  ・nerves_system_rpi-portable-1.12.1
      :
 Raspberry Pi 3用
  ・nerves_system_rpi3-portable-1.12.2
      :

例えば、ターゲットを"MIX_TARGET=rpi"として Nervesを mix firmware(ビルド)すると、
(※以下断らない限り最も非力な Raspberry Pi無印をターゲットとする)

C言語で記述されたモジュールは、コンパイラ・ドライバ
 nerves_toolchain_armv6_rpi_linux_gnueabi-linux_x86_64-1.3.2/bin/armv6-rpi-linux-gnueabi-gcc
でコンパイルされ、デフォルト動作では
 nerves_toolchain_armv6_rpi_linux_gnueabi-linux_x86_64-1.3.2/lib/gcc/armv6-rpi-linux-gnueabi/9.2.0
 または nerves_toolchain_armv6_rpi_linux_gnueabi-linux_x86_64-1.3.2/lib/gcc
 または nerves_toolchain_armv6_rpi_linux_gnueabi-linux_x86_64-1.3.2/armv6-rpi-linux-gnueabi/lib
 または nerves_system_rpi-portable-1.12.2/staging/lib
 または nerves_system_rpi-portable-1.12.2/staging/usr/lib
に置かれているライブラリとリンクされる

と言った処理が行われる。よぅ mix & elixir_make & gcc (+裏方のシェルスクリプト: nerves-env-helper.sh)、おめぇさん達、いい仕事するじゃねえか:sunglasses:

"MIX_TARGET=xxx"に応じて適切なファイルセットが選択される訳だが、その仕組みはとてもシンプルに出来ている。裏方の nerves-env-helper.shがターゲット情報を環境変数にセットし、それをツールチェインの各コマンドが見ているのだ(下記)。
何かしら自前の Makefileを書く場合には、どんな情報を受け取ることができるのか、nerves-env-helper.hの中身を確認ておいた方がよいだろう。

CROSSCOMPILE=~/.nerves/artifacts/nerves_toolchain_armv6_rpi_linux_gnueabi-linux_x86_64-1.3.2/bin/armv6-rpi-linux-gnueabi
NERVES_SDK_SYSROOT=~/.nerves/artifacts/nerves_system_rpi-portable-1.12.2/staging
CC=$CROSSCOMPILE-gcc
CXX=$CROSSCOMPILE-g++
CFLAGS="-D_LARGEFILE_SOURCE -D_LARGEFILE64_SOURCE -D_FILE_OFFSET_BITS=64 -pipe -O2 -I $NERVES_SDK_SYSROOT/usr/include"
CXXFLAGS="-D_LARGEFILE_SOURCE -D_LARGEFILE64_SOURCE -D_FILE_OFFSET_BITS=64 -pipe -O2 -I $NERVES_SDK_SYSROOT/usr/include"
LDFLAGS="--sysroot=$NERVES_SDK_SYSROOT"

さて、先に進む前に ~/.nerves下のディレクトリ・ツリーをざっと見ておこう。
ほとんどのアイテムはクロス・コンパイルに係わるファイルだが、nerves_system_rpi-portable-1.12.2/images下のモノはそうではない。これらは、ターゲットの起動SDを作成する際に必要となるファイルで、簡単に言うと Linux OSそのものだ。それぞれのファイルの意味は次の通り。そうそう、rootfs.squashfsは後ほど再登場する予定である。

  • zImage ‥‥‥‥‥‥‥ gz圧縮された Kernelイメージ
  • rootfs.squashfs ‥‥‥ 圧縮された読み込み専用の rootファイルシステム
  • bcm2708-rpi-*.dtb ‥‥ デバイスツリー
[~/.nerves下のディレクトリ・ツリー(抜粋)]

.nerves
├── artifacts
│   ├── nerves_system_rpi-portable-1.12.1
│   ├── nerves_system_rpi-portable-1.12.2
│   │   ├── images
│   │   │   ├── bcm2708-rpi-b.dtb
│   │   │   ├── bcm2708-rpi-zero-w.dtb
│   │   │   ├── rootfs.squashfs
│   │   │   └── zImage
│   │   ├── nerves-env.sh
│   │   ├── scripts
│   │   │   └── nerves-env-helper.sh
│   │   └── staging
│   │       ├── bin
│   │       ├── dev
│   │       ├── etc
│   │       ├── lib
│   │       │   ├── libc-2.30.so
│   │       │   ├── libc.so.6 -> libc-2.30.so
│   │       │   ├── libdl-2.30.so
│   │       │   ├── libdl.so.2 -> libdl-2.30.so
│   │       │   ├── libgcc_s.so
│   │       │   ├── libgcc_s.so.1
│   │       │   ├── libm-2.30.so
│   │       │   ├── libm.so.6 -> libm-2.30.so
│   │       │   ├── libpthread-2.30.so
│   │       │   └── libpthread.so.0 -> libpthread-2.30.so
│   │       ├── media
│   │       ├── mnt
│   │       ├── proc
│   │       ├── root
│   │       ├── run
│   │       ├── sbin
│   │       ├── sys
│   │       ├── usr
│   │       │   ├── bin
│   │       │   │   ├── erl -> ../lib/erlang/bin/erl
│   │       │   │   ├── erlc -> ../lib/erlang/bin/erlc
│   │       │   │   ├── escript -> ../lib/erlang/bin/escript
│   │       │   │   ├── raspistill
│   │       │   │   └── vcgencmd
│   │       │   ├── include
│   │       │   │   ├── EGL
│   │       │   │   │   ├── egl.h
│   │       │   │   │   ├── eglext.h
│   │       │   │   │   ├── eglext_android.h
│   │       │   │   │   ├── eglext_brcm.h
│   │       │   │   │   ├── eglext_nvidia.h
│   │       │   │   │   └── eglplatform.h
│   │       │   │   ├── GLES2
│   │       │   │   │   ├── gl2.h
│   │       │   │   │   ├── gl2ext.h
│   │       │   │   │   └── gl2platform.h
│   │       │   │   ├── assert.h
│   │       │   │   ├── byteswap.h
│   │       │   │   ├── errno.h
│   │       │   │   ├── limits.h
│   │       │   │   ├── math.h
│   │       │   │   ├── memory.h
│   │       │   │   ├── pigpio.h
│   │       │   │   ├── poll.h
│   │       │   │   ├── pthread.h
│   │       │   │   ├── regex.h
│   │       │   │   ├── stdint.h
│   │       │   │   ├── stdio.h
│   │       │   │   ├── stdlib.h
│   │       │   │   ├── string.h
│   │       │   │   ├── sys
│   │       │   │   │   ├── ioctl.h
│   │       │   │   │   ├── mman.h
│   │       │   │   │   ├── random.h
│   │       │   │   │   ├── select.h
│   │       │   │   │   ├── socket.h
│   │       │   │   │   ├── stat.h
│   │       │   │   │   └── types.h
│   │       │   │   ├── time.h
│   │       │   │   └── zlib.h
│   │       │   ├── lib
│   │       │   │   ├── libGLESv2.so
│   │       │   │   ├── libGLESv2_static.a
│   │       │   │   ├── libmmal.so
│   │       │   │   ├── libmmal_components.so
│   │       │   │   ├── libmmal_core.so
│   │       │   │   ├── libmmal_util.so
│   │       │   │   ├── libmmal_vc_client.so
│   │       │   │   ├── libpigpio.so -> libpigpio.so.1
│   │       │   │   └── libpigpio.so.1
│   │       │   ├── man
│   │       │   ├── sbin
│   │       │   ├── share
│   │       │   └── src
│   │       └── var
│   ├── nerves_system_rpi3-portable-1.12.1
│   ├── nerves_system_rpi3-portable-1.12.2
│   ├── nerves_toolchain_arm_unknown_linux_gnueabihf-linux_x86_64-1.3.2
│   └── nerves_toolchain_armv6_rpi_linux_gnueabi-linux_x86_64-1.3.2
│       ├── armv6-rpi-linux-gnueabi
│       │   ├── bin
│       │   ├── include
│       │   │   └── c++
│       │   │       └── 9.2.0
│       │   │           ├── algorithm
│       │   │           ├── array
│       │   │           ├── cerrno
│       │   │           ├── deque
│       │   │           ├── iostream
│       │   │           ├── map
│       │   │           ├── regex
│       │   │           ├── string
│       │   │           └── vector
│       │   ├── lib
│       │   │   ├── libatomic.a -> ../../armv6-rpi-linux-gnueabi/sysroot/lib/libatomic.a
│       │   │   ├── libatomic.so -> ../../armv6-rpi-linux-gnueabi/sysroot/lib/libatomic.so
│       │   │   ├── libatomic.so.1 -> ../../armv6-rpi-linux-gnueabi/sysroot/lib/libatomic.so.1
│       │   │   ├── libatomic.so.1.2.0 -> ../../armv6-rpi-linux-gnueabi/sysroot/lib/libatomic.so.1.2.0
│       │   │   ├── libgcc_s.so -> ../../armv6-rpi-linux-gnueabi/sysroot/lib/libgcc_s.so
│       │   │   ├── libgcc_s.so.1 -> ../../armv6-rpi-linux-gnueabi/sysroot/lib/libgcc_s.so.1
│       │   │   ├── libstdc++.a -> ../../armv6-rpi-linux-gnueabi/sysroot/lib/libstdc++.a
│       │   │   ├── libstdc++.so -> ../../armv6-rpi-linux-gnueabi/sysroot/lib/libstdc++.so
│       │   │   ├── libstdc++.so.6 -> ../../armv6-rpi-linux-gnueabi/sysroot/lib/libstdc++.so.6
│       │   │   ├── libstdc++.so.6.0.27 -> ../../armv6-rpi-linux-gnueabi/sysroot/lib/libstdc++.so.6.0.27
│       │   │   └── libstdc++.so.6.0.27-gdb.py -> ../../armv6-rpi-linux-gnueabi/sysroot/lib/libstdc++.so.6.0.27-gdb.py
│       │   └── sysroot
│       ├── bin
│       │   ├── armv6-rpi-linux-gnueabi-addr2line
│       │   ├── armv6-rpi-linux-gnueabi-ar
│       │   ├── armv6-rpi-linux-gnueabi-as
│       │   ├── armv6-rpi-linux-gnueabi-c++
│       │   ├── armv6-rpi-linux-gnueabi-c++filt
│       │   ├── armv6-rpi-linux-gnueabi-cc -> armv6-rpi-linux-gnueabi-gcc
│       │   ├── armv6-rpi-linux-gnueabi-cpp
│       │   ├── armv6-rpi-linux-gnueabi-ct-ng.config
│       │   ├── armv6-rpi-linux-gnueabi-elfedit
│       │   ├── armv6-rpi-linux-gnueabi-g++
│       │   ├── armv6-rpi-linux-gnueabi-gcc
│       │   ├── armv6-rpi-linux-gnueabi-gcc-9.2.0
│       │   ├── armv6-rpi-linux-gnueabi-gcc-ar
│       │   ├── armv6-rpi-linux-gnueabi-gcc-nm
│       │   ├── armv6-rpi-linux-gnueabi-gcc-ranlib
│       │   ├── armv6-rpi-linux-gnueabi-gcov
│       │   ├── armv6-rpi-linux-gnueabi-gcov-dump
│       │   ├── armv6-rpi-linux-gnueabi-gcov-tool
│       │   ├── armv6-rpi-linux-gnueabi-gdb
│       │   ├── armv6-rpi-linux-gnueabi-gdb-add-index
│       │   ├── armv6-rpi-linux-gnueabi-gprof
│       │   ├── armv6-rpi-linux-gnueabi-ld
│       │   ├── armv6-rpi-linux-gnueabi-ld.bfd
│       │   ├── armv6-rpi-linux-gnueabi-ldd
│       │   ├── armv6-rpi-linux-gnueabi-nm
│       │   ├── armv6-rpi-linux-gnueabi-objcopy
│       │   ├── armv6-rpi-linux-gnueabi-objdump
│       │   ├── armv6-rpi-linux-gnueabi-populate
│       │   ├── armv6-rpi-linux-gnueabi-ranlib
│       │   ├── armv6-rpi-linux-gnueabi-readelf
│       │   ├── armv6-rpi-linux-gnueabi-size
│       │   ├── armv6-rpi-linux-gnueabi-strings
│       │   └── armv6-rpi-linux-gnueabi-strip
│       ├── include
│       ├── lib
│       │   ├── gcc
│       │   │   └── armv6-rpi-linux-gnueabi
│       │   │       └── 9.2.0
│       │   │           <<抜粋>>
│       │   │           ├── crtbegin.o
│       │   │           ├── include
│       │   │           │   ├── stdarg.h
│       │   │           │   └── stddef.h
│       │   │           ├── libgcc.a
│       │   │           └── libgcov.a
│       │   └── ldscripts
│       └── share
└── dl

ここまで、クロス開発のビルド・フェーズの視点に立って「Nerves開発環境」を復習してきた。だが、もう一つ押さえておくべき事柄がある。それは、ターゲット上の実行環境すなわち rootファイルシステムがどんな構成になっているのか、そしてそれをどうやって作成しているのかを知っておく必要がある。

と言うのも、C言語等で作成したプログラムは、その実行形式ファイルの他に共有ライブラリをいくつか必要とすることがあるからだ。その場合、プログラムが参照している共有ライブラリもターゲットにインストールしてやらないと、プログラムを実行することができないのだ。つまり、クロス開発では、ビルドに必要なファイル類を開発環境に追加するだけではダメなのだ。本プロジェクトも御多分に漏れず、Tensorflow liteを実装した tfl_interpが共有ライブラリlibjpeg.soをリンクしている。そう、他人事ではない。

ではまず、ターゲットrootファイルシステムの中身を覗いて、どこにどんな変更を加える必要があるのか見当をつけよう。覗く手段は、ざっと考えただけでも下記の4つの方法がある。たぶん、これらの他にも方法があるだろう。お薦めは、一番お手軽な 1.の手段だ。

  1. "mix firmware.unpack"で _build/rpi_dev/nerves/images/plug_mnist.fw をアンパックする
  2. "unzip"で plug_mnist.fw をアンパックしたのち、rootファイルシステムのイメージを "sudo mount -t squashfs -o loop data/rootfs.img ./xxx" で適当なマウントポイントにマウントする
  3. "unzip"で plug_mnist.fw をアンパックしたのち、rootファイルシステムのイメージを "unsquashfs data/rootfs.img" でアンパックする
  4. ターゲット・ボードを起動して sshでリモートログインし、"use Toolshed"でコマンド拡張したのちに "tree ほにゃらら"でディレクトリツリーを表示する

早速ターゲットrootファイルシステムを覗いてみる。
最初に、libjpeg.soが同梱されていないことを確認しておこう。findコマンドでごにょごにょすれば簡単に確認できる。確かに libjpeg.soは見つからなかった。次に、ディレクトリ・ツリーをざっと眺めてみたところ、共有ライブラリの置き場所はスタンダードな /libまたは /usr/lib のようだと分かった。ふむ、libjpeg.soを追加する場所は /usr/libで決まりだな。

おやっ、Elixirで記述したアプリは /srv/erlang/libの下に置かれるのか。privに置いた trf_interpなどのファイルはここに格納されるってことだな。

$ mix firmware

$ mix firmware.unpack
==> nerves
/usr/bin/make -C src all
make[1]: Entering directory '/home/shoz/plug_mnist_nerves/deps/nerves/src'
cc /home/shoz/plug_mnist_nerves/_build/rpi_dev/lib/nerves/obj/port.o  -o /home/shoz/plug_mnist_nerves/_build/rpi_dev/lib/nerves/priv/port
make[1]: Leaving directory '/home/shoz/plug_mnist_nerves/deps/nerves/src'
if [ -f test/fixtures/port/Makefile ]; then /usr/bin/make -C test/fixtures/port; fi
==> plug_mnist

Nerves environment
  MIX_TARGET:   rpi
  MIX_ENV:      dev

Unpacking to plug_mnist-rpi...
Archive:  /home/shoz/plug_mnist_nerves/_build/rpi_dev/nerves/images/plug_mnist.fw
  inflating: /home/shoz/plug_mnist_nerves/plug_mnist-rpi/meta.conf
  inflating: /home/shoz/plug_mnist_nerves/plug_mnist-rpi/data/bootcode.bin
  inflating: /home/shoz/plug_mnist_nerves/plug_mnist-rpi/data/fixup.dat
  inflating: /home/shoz/plug_mnist_nerves/plug_mnist-rpi/data/start.elf
  inflating: /home/shoz/plug_mnist_nerves/plug_mnist-rpi/data/config.txt
  inflating: /home/shoz/plug_mnist_nerves/plug_mnist-rpi/data/cmdline.txt
  inflating: /home/shoz/plug_mnist_nerves/plug_mnist-rpi/data/zImage
  inflating: /home/shoz/plug_mnist_nerves/plug_mnist-rpi/data/bcm2708-rpi-zero-w.dtb
  inflating: /home/shoz/plug_mnist_nerves/plug_mnist-rpi/data/bcm2708-rpi-b.dtb
  inflating: /home/shoz/plug_mnist_nerves/plug_mnist-rpi/data/bcm2708-rpi-b-plus.dtb
  inflating: /home/shoz/plug_mnist_nerves/plug_mnist-rpi/data/w1-gpio-pullup.dtbo
  inflating: /home/shoz/plug_mnist_nerves/plug_mnist-rpi/data/pi3-miniuart-bt.dtbo
  inflating: /home/shoz/plug_mnist_nerves/plug_mnist-rpi/data/ramoops.dtbo
  inflating: /home/shoz/plug_mnist_nerves/plug_mnist-rpi/data/rootfs.img
Parallel unsquashfs: Using 8 processors
2067 inodes (2290 blocks) to write

[=============================================================|] 2290/2290 100%

created 1898 files
created 249 directories
created 169 symlinks
created 0 devices
created 0 fifos


$ find plug_mnist-rpi/rootfs/ -name "libjpeg*"
$


$ ls plug_mnist-rpi/rootfs/srv/erlang/lib/
asn1-5.0.13              logger-1.10.4          plug_mnist-0.1.0              stdlib-3.13
compiler-7.6.2           mdns_lite-0.6.5        plug_static_index_html-1.0.0  system_registry-0.8.2
cowboy-2.8.0             mime-1.4.0             public_key-1.8                telemetry-0.4.2
cowlib-2.9.1             muontrap-0.6.0         ranch-1.7.1                   toolshed-0.2.13
crypto-4.7               nerves_pack-0.4.0      ring_logger-0.8.1             uboot_env-0.3.0
dns-2.1.2                nerves_runtime-0.11.3  runtime_tools-1.15            vintage_net-0.9.1
eex-1.10.4               nerves_ssh-0.2.1       sasl-4.0                      vintage_net_direct-0.9.0
elixir-1.10.4            nerves_time-0.4.2      shoehorn-0.6.0                vintage_net_ethernet-0.9.0
gen_state_machine-2.1.0  one_dhcpd-0.2.4        socket-0.3.13                 vintage_net_wifi-0.9.0
iex-1.10.4               plug-1.10.4            ssh-4.10
jason-1.2.2              plug_cowboy-2.3.0      ssh_subsystem_fwup-0.5.1
kernel-7.0               plug_crypto-1.1.2      ssl-10.0

次に、ターゲットrootファイルシステムがどのようにして作成されるのかを調べてみよう。
これは、"mix firmware"で起動される Mixタスクを丹念に追いかけて行けば、その概略を知ることができる。キー・パーツは ~/.nerves/artifacts/nerves_system_rpi-portable-1.12.2/scriptsに置かれている "rel2fw.sh"とその下請け "merge-squashfs"の2つのシェルスクリプトだ。これらのシェルスクリプトが連携して、プロジェクト・ディレクト下の _build/rpi_dev/rel/plug_mnist/libおよび releasesと、ユーザー定義の rootfs_overlayと、そして~/.nerves/artifacts/nerves_system_rpi-portable-1.12.2/image/rootfs.squashfs を入力としてターゲットrootファイルシステムを作成している。処理フローの概略は以下の通り。

  1. _build/rpi_dev/rel/plug_mnist/libおよびreleasesからターゲットに必要なファイルだけをピックアップし、ワーク・ディレクトリに集める。この時、併せてターゲットのディレクトリ階層(/svr)をワーク・ディレクトリ内に構築する
  2. ユーザー定義の rootfs_overlayをワーク・ディレクトリにマージする。
  3. ~/.nerves/artifacts/nerves_system_rpi-portable-1.12.2/image/rootfs.squashfsをアンパックして、種rootファイルシステムとする
  4. 先のワーク・ディレクトリを種rootファイルシステムにマージする
  5. "mksquashfs"で種rootファイルシステムをパックしてターゲットrootファイルシステムとする

と言うことは、libjpeg.soをターゲットに追加するには、rootfs.squashfsに組み入れるか、あるいは rootfs_overlayに置くかだな。

4. libjpegを調達する

一通り「Nerves開発環境」を復習できたので、そろそろ "libjpeg"の調達方法を考えようか。

まず、決めるべきことが 2つある。libjpeg.soを (A)rootfs.squashfsと (B)rootfs_overlayのどちらに追加しようか? そして libjpeg.soを(C)ソースコードからメイクしようか、それとも(D)バイナリ・パッケージを利用しようか?

前者の選択については、特別な理由が無い限り (B)rootfs_overlayを選ぶで良いと思う。
仮に (A)rootfs.squashfsを選ぶと、Nervesシステムのリコンフィグ(Buildroot)に戻ってrootfs.squashfsを作り直すか[*2]、あるいは手作業で rootfs.squashfsをアンパック⇒ファイルを追加⇒再びパックすることになりそうだ。そんなに手間をかけても、本家 Nerves Projectが、Nervesシステムのアップデートすれば、それに追従するために再び作業をやり直さざるを得ない。今回のプロジェクトのように、アプリ開発が目的の場合は敢えて茨の道を進むメリットは無いんじゃないかな。

後者の選択については、とりあえず(D)バイナリ・パッケージを選ぶだな。
少々バージョンが古かったりするが、Debianリポジトリには正式な arm向けバイナリ・パッケージがあれこれと用意されている。これを利用しない手はないだろう(枯れたコード)。(C)ソースコードからのメイクは、コンパイル・オプションを間違えて嵌ってしまうという嫌なパターンのリスクがある。私としては次善の策としたい。

[*2]libjpegの追加は Buildrootのコンフィグ・メニューに含まれている。今回はPASSするが、Nervesシステムのリコンフィグは意外と簡単なので、「ものは試しに」の精神で体験してみるのも一興だろう。参考:「備忘録: WSL2 Ubuntu上で Nervesをカスタマイズしてみる」


それでは、具体的な作業に入ろう。
libjpegのバイナリ・パッケージは、下記の Debianリポジトリから入手する:

arm向けには arm64, armel, armhfの3種類のパッケージが用意されているが、Raspberry Piでは通常 armelか armhfを利用する。

Debian での名称 アーキテクチャ CPU Raspberry Pi
armel ARM Armv6(32bit) A, B, A+, B+, Zero, Zero W
armhf ハードウェア FPU がある ARM Armv7-A(32bit) 2, 3 ほか

作業手順は次の通り: プロジェクト・ディレクトリ直下にパッケージ展開用の extraディレクトリを作成する。"wget"でパッケージをダウンロードし、"dpkg -x"で展開&インストールする。併せて、rootfs_overlayに共有ライブラリをコピー。
定型作業なのでシェルスクリプトにしてしまおう:grin:

下記のシェルスクリプト(抜粋)では、"Cimg"ライブラリと "nlohomann/json"ライブラリのダウンロード&インストールも行っている。こちらは、共有ライブラリを持たない C++テンプレート・ライブラリなので、rootfs.overlayへのコピーは不要だ。

setup_nerves_extra.sh(抜粋/前半部)
proj_top=`pwd`/..

# setup deb packages
deb_repo=http://ftp.jp.debian.org/debian/pool/main

case "${MIX_TARGET}" in
    "rpi"|"rpi0")
        wget -nc ${deb_repo}/libj/libjpeg-turbo/libjpeg62-turbo-dev_1.5.2-2+b1_armel.deb
        dpkg -x libjpeg62-turbo-dev_1.5.2-2+b1_armel.deb .

        wget -nc ${deb_repo}/libj/libjpeg-turbo/libjpeg62-turbo_1.5.2-2+b1_armel.deb
        dpkg -x libjpeg62-turbo_1.5.2-2+b1_armel.deb .
    ;;

    "rpi2"|"rpi3")
        wget -nc ${deb_repo}/libj/libjpeg-turbo/libjpeg62-turbo-dev_1.5.2-2+b1_armhf.deb
        dpkg -x libjpeg62-turbo-dev_1.5.2-2+b1_armhf.deb .

        wget -nc ${deb_repo}/libj/libjpeg-turbo/libjpeg62-turbo_1.5.2-2+b1_armhf.deb
        dpkg -x libjpeg62-turbo_1.5.2-2+b1_armhf.deb .
    ;;

    *) echo "Unknown target: ${MIX_TARGET}"
       exit 1
       ;;
esac

wget -nc ${deb_repo}/c/cimg/cimg-dev_2.4.5+dfsg-1_all.deb
dpkg -x cimg-dev_2.4.5+dfsg-1_all.deb .

wget -nc ${deb_repo}/n/nlohmann-json3/nlohmann-json3-dev_3.5.0-0.1_all.deb
dpkg -x nlohmann-json3-dev_3.5.0-0.1_all.deb .

# copy shared lib to target rootfs_overlay
mkdir -p ${proj_top}/rootfs_overlay/usr/lib

case "${MIX_TARGET}" in
    "rpi"|"rpi0")
        cp -au usr/lib/arm-linux-gnueabi/libjpeg.so* ${proj_top}/rootfs_overlay/usr/lib
    ;;

    "rpi2"|"rpi3")
        cp -au usr/lib/arm-linux-gnueabihf/libjpeg.so* ${proj_top}/rootfs_overlay/usr/lib
    ;;
esac

以上で、想定外の事態「libjpegが Nervesツールチェイン内に見つからない」に対処することが出来た。次節からは予定路線に戻り、Nervesへの PlugMnistのポーティングを進めよう。

5. Nerves/rpi用の Tensorflow liteをメイクする

いまのところ Nerves/rpi向けの Tensorflow liteバイナリ・パッケージを提供しているサイトはなさそうだ[*3]。 利用したければ、頑張って自前で makeするしかないだろう。

[*3]Nervesは、一部の熱いコミュニティを除き、知名度が低いので当たり前と言えば当たり前:sweat_smile:

おっと、そんなに構えなくとも大丈夫:wink:。CMakeのお手軽さには及ばないが、Tensorflow liteの Makeスクリプトは、いろいろなターゲットに簡単に対応できるように、とても巧く組織化されている。シェルスクリプトbuild_xxxx_lib.sh(ビルド環境を整えて makeコマンドを起動する)と、Makefileインクルードxxxx_makefile.inc(ターゲット設定ファイル)の2つを用意すれば、新たなターゲット向けのライブラリを makeすることができる。

tensorflow_src/tensorflow/lite/tools/make/build_xxxx_lib.sh
tensorflow_src/tensorflow/lite/tools/make/targets/xxxx_makefile.inc

おやっ、おあつらえ向きに本家Raspberry Pi用のbuild_rpi_lib.sh/rpi_makefile.incが Tensorflowファイルセットに同梱されているではないか。これらを元に Nerves用の build_nerves_lib.sh/nerves_makefile.incを作成しよう。ファイルの置き場所は、先の extraディレクトリの下で良いかな。

extra 
├── make
│   ├── targets
│   │   └── nerves_makefile.inc
│   └── build_nerves_lib.sh
└── bash:setup_nerves_extra.sh

シェルスクリプトbuild_nerves_lib.shの改造は、nervesのツールチェインに PATHを通すことと、makeコマンドの起動引数に "TARGET=nerves"を渡す箇所だけ。あとはコピペ:stuck_out_tongue_winking_eye:

build_nerves_lib.sh
set -x
set -e

case "${MIX_TARGET}" in
    "rpi"|"rpi0")
        PATH=~/.nerves/artifacts/nerves_toolchain_armv6_rpi_linux_gnueabi-linux_x86_64-1.3.2/bin:$PATH
    ;;

    "rpi2"|"rpi3")
        PATH=~/.nerves/artifacts/nerves_toolchain_arm_unknown_linux_gnueabihf-linux_x86_64-1.3.2/bin:$PATH
    ;;

    *) echo "Unknown target: ${MIX_TARGET}"
       exit 1
       ;;
esac

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
TENSORFLOW_DIR="${SCRIPT_DIR}/../../../.."

FREE_MEM="$(free -m | awk '/^Mem/ {print $2}')"
# Use "-j 4" only memory is larger than 2GB
if [[ "FREE_MEM" -gt "2000" ]]; then
  NO_JOB=4
else
  NO_JOB=1
fi

make -j ${NO_JOB} TARGET=nerves -C "${TENSORFLOW_DIR}" -f tensorflow/lite/tools/make/Makefile $@

一方、Makefileインクルードの改造は、nervesツールチェイン・コマンドのプリフィックスの定義("TARGET_TOOLCHAIN_PREFIX := armv6-rpi-linux-gnueabi-"等)を書き換えるだけ。

nerves_makefile.inc
# Settings for Raspberry Pi.
ifeq ($(TARGET),nerves)
  # Default to the architecture used on the Pi Two/Three (ArmV7), but override this
  # with TARGET_ARCH=armv6 to build for the Pi Zero or One.
  # TARGET_ARCH := armv6

  ifeq (, $(findstring "$(MIX_TARGET)","rpi" "rpi0" "rpi2" "rpi3"))
    $(error "unknown target: $(MIX_TARGET)")
  endif

  ifeq ("$(MIX_TARGET)", $(findstring "$(MIX_TARGET)","rpi" "rpi0"))
    TARGET_ARCH := armv6
    TARGET_TOOLCHAIN_PREFIX := armv6-rpi-linux-gnueabi-
    CXXFLAGS += \
      -march=armv6 \
      -mfpu=vfp \
      -funsafe-math-optimizations \
      -ftree-vectorize \
      -fPIC

    CFLAGS += \
      -march=armv6 \
      -mfpu=vfp \
      -funsafe-math-optimizations \
      -ftree-vectorize \
      -fPIC

    LDFLAGS := \
      -Wl,--no-export-dynamic \
      -Wl,--exclude-libs,ALL \
      -Wl,--gc-sections \
      -Wl,--as-needed
  endif

  ifeq ("$(MIX_TARGET)", $(findstring "$(MIX_TARGET)","rpi2" "rpi3"))
    TARGET_ARCH := armv7
    TARGET_TOOLCHAIN_PREFIX := arm-unknown-linux-gnueabihf-
    CXXFLAGS += \
      -march=armv7-a \
      -mfpu=neon-vfpv4 \
      -funsafe-math-optimizations \
      -ftree-vectorize \
      -fPIC

    CFLAGS += \
      -march=armv7-a \
      -mfpu=neon-vfpv4 \
      -funsafe-math-optimizations \
      -ftree-vectorize \
      -fPIC

    LDFLAGS := \
      -Wl,--no-export-dynamic \
      -Wl,--exclude-libs,ALL \
      -Wl,--gc-sections \
      -Wl,--as-needed
  endif

  LIBS := \
    -latomic \
    -lstdc++ \
    -lpthread \
    -lm \
    -ldl

endif

最後に、Tensorflow liteライブラリを作成する一連の作業を自動化するシェルスクリプトを作っておこう。
このスクリプトは、Tensorflowファイルセットを GitHubからダウンロードして、上で作成したNerves用 build_nerves_lib.sh/nerves_makefile.incを所定のディレクトリにコピーしたのち、ライブラリの作成を行う。

setup_nerves_extra.sh(抜粋/後半部)
# setup tensorflow lite
git clone https://github.com/tensorflow/tensorflow.git tensorflow_src

pushd tensorflow_src
tfl_make=./tensorflow/lite/tools/make

if [ ! -e ${tfl_make}/downloads ]
then
    ${tfl_make}/download_dependencies.sh
fi

cp -a ../make/* ${tfl_make}

${tfl_make}/build_nerves_lib.sh

popd

6. ひとまずの大団円

よお~し、PlugMnistポーティング完了まで、あと一息だ。

ここまで準備して来た内容を、"tfl_interp"の Makefileに反映しよう。主な変更点は、環境変数 MIX_TARGETをみて、C++コンパイラがヘッダーファイルやライブラリを extraディレクトリ以下で検索するように指定している点だ。

ifeq ($(MIX_APP_PATH),)
calling_from_make:
    mix compile
endif

ifeq ($(CROSSCOMPILE),)
    ifeq ($(shell uname -s),Linux)
        DEPS_HOME ?= ./extra
        LIB_EXT    = -lpthread -ldl
        TFL_GEN    = linux_x86_64
    else
        DEPS_HOME ?= C:/msys64/home/work
        LIB_EXT    = -lmman
        TFL_GEN    = windows_x86_64
        INC_EXT    = -I$(DEPS_HOME)/CImg-2.9.2
    endif
else
    ifeq (, $(findstring "$(MIX_TARGET)","rpi" "rpi0" "rpi2" "rpi3"))
      $(error "unknown target: $(MIX_TARGET)")
    endif

    ifeq ("$(MIX_TARGET)", $(findstring "$(MIX_TARGET)","rpi" "rpi0"))
      DEPS_HOME ?= ./extra
      TFL_GEN    = nerves_armv6
      INC_EXT    = -I$(DEPS_HOME)/usr/include -I$(DEPS_HOME)/usr/include/arm-linux-gnueabi
      LIB_EXT    = -L$(DEPS_HOME)/usr/lib/arm-linux-gnueabi -lpthread -ldl -latomic
    endif
    ifeq ("$(MIX_TARGET)", $(findstring "$(MIX_TARGET)","rpi2" "rpi3"))
      $(info "arm7")
      DEPS_HOME ?= ./extra
      TFL_GEN    = nerves_armv7
      INC_EXT    = -I$(DEPS_HOME)/usr/include -I$(DEPS_HOME)/usr/include/arm-linux-gnueabihf
      LIB_EXT    = -L$(DEPS_HOME)/usr/lib/arm-linux-gnueabihf -lpthread -ldl -latomic
    endif
endif

INCLUDE   = -I./src \
            -I$(DEPS_HOME)/tensorflow_src \
            -I$(DEPS_HOME)/tensorflow_src/tensorflow/lite/tools/make/downloads/flatbuffers/include \
            $(INC_EXT)
DEFINES   = #-D__LITTLE_ENDIAN__ -DTFLITE_WITHOUT_XNNPACK
CXXFLAGS += -O3 -DNDEBUG -fPIC --std=c++11 -fext-numeric-literals $(INCLUDE) $(DEFINES)
LDFLAGS  += $(LIB_EXT) -ljpeg 

LIB_TFL = $(DEPS_HOME)/tensorflow_src/tensorflow/lite/tools/make/gen/$(TFL_GEN)/lib/libtensorflow-lite.a

PREFIX = $(MIX_APP_PATH)/priv
BUILD  = $(MIX_APP_PATH)/obj

SRC=$(wildcard src/*.cc)
OBJ=$(SRC:src/%.cc=$(BUILD)/%.o)

all: $(BUILD) $(PREFIX) install

install: $(PREFIX)/tfl_interp

$(BUILD)/%.o: src/%.cc
    $(CXX) -c $(CXXFLAGS) -o $@ $<

$(PREFIX)/tfl_interp: $(OBJ)
    $(CXX) $^ $(LIB_TFL) $(LDFLAGS) -o $@

clean:
    rm -f $(PREFIX)/tfl_interp $(OBJ)

$(PREFIX) $(BUILD):
    mkdir -p $@

print-vars:
    @$(foreach v,$(.VARIABLES),$(info $v=$($v)))

.PHONY: all clean calling_from_make install print-vars

クライマックスだ:blush:
PlugMnistのビルド手順は下記リストの通り。

残念ながら extraに追加したライブラリ類は、一度だけ "setup_nerves_extra.sh"を実行してメイクしておく必要がある。そうそう、その前に MIX_TARGETの設定を忘れずに!

extraのセットアップが終わっていれば、あとはいつも通りの "mix firmware.burn"
tfl_interpをメイクし、libjpeg.soと共に、build\rpi_dev\nerves\images\plugmnist.fw にパックしてくれる。

$ export MIX_TARGET=rpi
$ pushd extra
$ ./setup_nerves_extra.sh
$ popd

$ mix firmware.burn

Raspberry Piで PlugMnistを実行してみよう。事前の MinGW/Windows10, Ubuntu/WSL2上での結果と同じだね。オッケー?
ひとまずの大団円。
plugmnist.jpg

7.ふりかえり

なんだかんだで、無事に Nerves/rpi(無印)に PlugMnistをポーティングすることが出来た。本文では触れなかったが、Nerves/rpi3での動作もOKだ。ふぅ~。と言うことで、前回掲げた「展望リスト」のひとつ目をクリア(祝):confetti_ball:

‥‥‥結局、Elixirのコードが一行も出てこない記事となってしまった:stuck_out_tongue_closed_eyes:

[展望リスト]
済み 1.今回作成した PlugMnistをそのまま Nerves RasPiにポーティングする
   2.Nerves RasPiのカメラ・モジュールで撮影した画像で推論ができるように改造する
   3.物体検出や面白そうなモデルをインストールして遊んでみる
   4.liteではなく本家のTensorFlowを利用できるようにしてみる

参考文献

[1] Window10で TensorFlow liteを使ってみる - 前編
[2] Window10で TensorFlow liteを使ってみる - 後編
[3] Can I put Debian on my Raspberry Pi?
[4] Debian:パッケージディレクトリを検索
[5] Nerves Proj Documentation: Customizing Your Own Nerves System
[6] Nerves Proj Documentation: Root Filesystem Overlays
[7] 「いかにして問題を解くか」G.Polya

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

tensorboardでWindows fatal exception: access violationが出た時

大体のtensorboardがなぜか動かん!というときの解決策は、tensorflow、tensorboardをインストールし直すことみたいですがそれでも治らなかったので備忘録です。

公式のissueより抜粋。

実行環境

Windows10
tensorflow-gpu==1.15
tensorboard==1.15

実行コマンド

同じディレクトリ内にlogsファイルが有る時
tensorboard --logdir=./logs --host=127.0.0.1
を実行
ちなみに自分の環境だとhostを指定しないと動きませんでした。(hostの指定はこのサイトを参照)

解決策

同じディレクトリ内のcheckpointファイルの名前を変更、削除

余談

これをすると直りました。おそらく参照関係でぶつかったのでしょうか。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む