Help us understand the problem. What is going on with this article?

THETAプラグインで連続フレームにTensorFlow Liteの物体認識をかける

はじめに

リコーの @KA-2 です。

弊社ではRICOH THETAという全周囲360度撮れるカメラを出しています。
RICOH THETA VRICOH THETA Z1は、OSにAndroidを採用しています。Androidアプリを作る感覚でTHETAをカスタマイズすることもでき、そのカスタマイズ機能を「プラグイン」と呼んでいます(詳細は本記事の末尾を参照)。

今回は、THETAプラグインでライブプリビューを扱いやすくするの続編です。
上記記事の末尾で予告したとおり、THETAのライブプリビューにTensorFlow LiteのObject Detectionをかけ、その結果を利用します。
利用といっても今回はひとまず、「THETAの姿勢基準(local座標系)で、特定物を横方向にトラッキングする」というところまでを説明します。

「特定物を全方位トラッキングする」こともできているのですが、こちらは私が執筆する次回記事としますね。というのも「equirectangularの回転処理」「THETAの姿勢情報利用」という要素が含まれており、他のTHETAプラグイン作成にも役立つ事項なので詳しく解説したいためです。
次回記事も含めて参照くださるとありがたいです。振る舞いだけチラ見せすると以下な感じです。

全方位トラッキング調整中2_400.gif
【2020/4/24 追記】
NDKでEquirectangularの回転処理をして物体を全方位トラッキングする
に続編をリリースしました!

それではまず、「THETAの映像にTensorFlow LiteのObject Detectionをかける」について解説します。

TensorFlow Lite のObject Detectionについて

私、遠い昔にNeural Network(「機械学習」でくくられるカテゴリの一角)の経験はありますがTensorFlowに触れるのは初めてです。もしかしたら、幾らか表現や用語がおかしいことがあるかもしれませんがご容赦を。
ざっくりと今回利用するモノゴトの概要を説明しておきます。

TensorFlow について

Google社が提供している機械学習を簡単に利用するためのライブラリーやツールなどの総合名称だと理解しています。「学習」と「推論」のどちらも行え、扱える「モデル」の形式も多め、複数のOSやコンピュータ言語に対応しています。
特に学習にについては多量のデータを扱いますので、マシンパワーがあるほど有利です。

TensorFlow Liteについて

TensorFlowをモバイル機器(iOSやAndroidなど)向けに最適化した機械学習のライブラリ&ツール群です。TensorFlowと比べると、機能が制限されている点が多くあります。

モバイル機器向けTensorFlowとしては、TensorFlow Liteより前にTensorFlow Mobileというものが存在していました(今も利用できますが・・・これから存在が薄れていきます)。
TensorFlow Mobileが、「学習」と「推論」のどちらも行えるのに対し、TensorFlow Liteは「推論」だけが行えます。「モデル」のデータ形式も変わりました(いくらか制限はあるようですが、データ形式の変換ツールもあります)。学習はフルスペックなTensorFlowを利用することになります。
学習はマシンパワーがあっても時間のかかる作業で、推論は小型かつ廉価な機器を使い、ネットワーク接続の有無によらず行いたくなります。ツールがこのように進化するのは自然な流れだと思います。
現在 Mobile から Lite への移行期間ということもあり、Liteは完全ではありません(徐々に充実しつつあるようです)。

2020年4月初旬時点、TensorFlow Liteのページには、8種類の例(学習済みモデル込み)が公開されています。

TFLのサンプル2.png

モデルの設計を自身で行えば応用範囲はもっと広がりますし、TensorFlow Liteの学習済みモデルが公開されている他の事例もあります。
(例えば、2019年夏頃に話題となったGoogleのHand Tracking(ハンドサイン認識)もTensorFlow Liteのモデルを利用しています。荒い認識 → 詳細認識と2段階行います。Media Pipeという仕組みが使えないとフレームレートは遅くなるものの、おそらくTHETAでも動くはずです。あとでトライしたいです。。。)

モデルには「浮動小数点モデル」だけでなく「量子化モデル(整数と表現されますが、固定小数点と思ってもよさそう)」が扱えるようになりました。量子化により、モデルのデータサイズを小さくでき、かつ、演算速度の向上も望めますが、計算精度は劣ることになります。
実施する内容次第ではありますが、スマートフォンと比べると遥かに演算能力が低いマイクロコンピューターの類でも、TensorFlow Liteが利用できるケースがでてきています。

TensorFlow LiteのObject Detection

詳しくはTensorFlow Lite Object Detectionのページを参照してください。
Android用のサンプルコードはこちらです。

1024×512 pixelのEquirectangular形式画像の中央部300×300pixelの範囲(緑枠)にTensorFlow LiteのObject Detectionをかけた例が以下となります。

1024x512で物体認識した例.jpg

さらっと言うと「"単一の画像内"の認識結果を"複数個"得られる」という処理です。
もう少し具体的に条件なども含めて羅列すると以下になります。

  • 80品目(80クラス)の識別が行えます。学習済みモデルと共に配布されているラベルファイル(テキストファイル)を参照すると具体的な品目名称が分かります
  • 入力画像は 300×300 pixelに制限されています(イチからモデルを設計すれば制限は変えられます)
  • 1回の処理で最大10個までの認識結果が得られます
  • 1品目の認識結果は、Title(認識した品目の名称)、confidence(信頼度:最大値が1.0、最小値が0.0)、Location(認識した品目の範囲=top,left,bottom,right座標)です

今のところ量子化済みモデルのみが公開されているようです。
量子化済みモデルは、(現在のTensorFlow Liteでは)GPUが使えません。CPUで動作させるときには、マルチコアを利用することが可能です。

転移学習により認識できるものを増やしたり認識率を上げたりもできますが、今回は公開されている学習済みモデルから推論を行うところのみを実施します。
(このモデルは転移学習の初期値として公開されているフシがあるので、上の例ではGoogleが説明しているとおり、洋梨が「Apple」と認識されたり、りんごが「Orange」と認識されたりしています。うん、ちゃんとTHETAでも動いている。。。)

THETA Plug-in SDKにTensorFlow Liteの動作環境を整える

今回はTensorFlow LiteのObject Detectionに限定します。
TensorFlow Liteの他学習モデル利用する際には、各自で手順をカスタマイズしてください。

作業のベースとなるプロジェクトファイル一式は以下記事で解説したものです。

【2020/04/24追記】
本記事の続編【NDKでEquirectangularの回転処理をして物体を全方位トラッキングする】のリポジトリは今回の要素も含んでいます。ちょっと作業が楽になるのでご参考まで。

今回は、ライブプリビューの連続フレーム(JPEG)をBitmapにしたデータに対して物体認識を行います。
CameraAPIで取得した連続フレームにも応用できますが、その場合には、必要な事項を各自で適用してください。(文章にすると長くみえますが、わりと簡単ですのでご安心ください)

build.gradle(Module:app)の設定

以下2つの定義を追加しています。

  • 「aaptOptions」で、モデルファイルを圧縮しないようにしています
  • 「implementation」の定義を2行書き加えています
build.gradle
android {

    ~省略~

    aaptOptions {
        noCompress "tflite"
    }
}

dependencies {

    ~省略~

    implementation 'org.tensorflow:tensorflow-lite:0.0.0-nightly'
    implementation 'org.tensorflow:tensorflow-lite-gpu:0.0.0-nightly'
}

学習済みモデルとラベルの配置

TensorFlow Lite:Object Detectionのページの「Download starter model and labels」のボタンをクリックしてzipファイルをダウンロードしてください。
記事執筆時点のファイル名は「coco_ssd_mobilenet_v1_1.0_quant_2018_06_29.zip」です。

モデルファイルダウンロード.png

ダウンロードしたzipファイルを解凍すると以下2ファイルが得られます。

ファイル名 説明
detect.tflite 物体認識の学習済みモデルです
labelmap.txt 学習済みモデルのラベル(識別可能な80品目の名称)定義ファイルです

その2つのファイルをどちらもassetsフォルダーに配置してください。

モデルファイルの配置.png

物体検出クラスの配置

Android用のサンプルコードから、以下の3ファイルを取得します。

ファイル名 説明
Classifier.java 物体認識をするクラスのインターフェースが定義されています
TensorFlowObjectDetectionAPIModel.java 物体認識をするクラスの実処理が記述されています
Logger.java 物体認識をする処理から呼び出されているログ出力クラスです

取得したファイルは、配置の仕方にあわせpackageの定義を書き換えてください。

以下は、ベースとしたプロジェクトの「~\app\src\main\java\com\theta360\extendedpreview」に3つのファイルを配置した場合の例です。この場合、packageの定義は3ファイル共に以下となります。

package com.theta360.extendedpreview;

今回のファイル配置の場合、TFLiteObjectDetectionAPIModel.javaの以下importは削除、または、コメントアウトしてください。ファイル配置が異なる場合は記述が変わります。

TFLiteObjectDetectionAPIModel.java
//import org.tensorflow.lite.examples.detection.env.Logger;

ここまで準備すると、MainActivity.javaから物体認識の処理を呼び出すことができます。

補足:コア数の指定やGPUデリゲート

前述のとおり、今回利用している学習済みモデルは量子化されているので、現在はGPUが利用できずCPUのみを利用することになります。その際、演算時に利用する最大コア数を「TFLiteObjectDetectionAPIModel.java」に定義されている以下の数値で指定できます。

TFLiteObjectDetectionAPIModel.java
  // Number of threads in the java app
  private static final int NUM_THREADS = 4;

こちらの記事でも紹介しているとおり、THETA V および THETA Z1のCPUはoctacoreです。
1から8まで順に数値を振って処理速度を確認してみてみましたが、4で処理速度が頭打ちしました。5以上の数値を指定しても意味がないようです。OSのサービスや撮影アプリに加え、この記事で説明しているアプリ内でも「MainActivity」「MOTION JPEGの常時読み取り」「WebUI用のWebサーバー」「物体認識を行うスレッド」と複数のプロセスが同時に動作していますので無理もありません。
処理速度が遅くなってもよいからCPUへの負荷や発熱を減らしたい場合には1~3の数値を試してみてください。
今回はサンプルファイルのまま動作させます。

少々余談になりますが、もしも物体認識の浮動小数点モデルが入手でき(たとえば他の物体認識モデルのデータ形式を変換する等)、それをGPUで動作させたい(GPUデリゲートを利用したい)場合には TensorFlowObjectDetectionAPIModel.java にちょっと手を加えなければいけないと思いますのでご注意ください。今回は説明を割愛します。

MainActivity.java の修正

本章末尾に折りたたんで全文を掲載しておきます。
大切そうなポイントについては以下で説明しますので必要に応じて参照してください。

事前にTHETAプラグインでライブプリビューを扱いやすくするの内容も理解しておいてください。

物体検出クラスの呼び出し

今回のメインディッシュです。わりと簡単です。

固定値の定義

モデルファイル名、ラベルファイル名、画像1辺のサイズ(単位はpixel,正方形です)、モデルの量子化有無を定義しています。

MainActivity.java
    private static final String TF_OD_API_MODEL_FILE = "detect.tflite";
    private static final String TF_OD_API_LABELS_FILE = "file:///android_asset/labelmap.txt";
    private static final int TF_OD_API_INPUT_SIZE = 300;
    private static final boolean TF_OD_API_IS_QUANTIZED = true;

初期化

Classifierクラスのオブジェクトをdetectorという名称で生成しています。

MainActivity.java
                ///////////////////////////////////////////////////////////////////////
                // TFLite Initial detector
                ///////////////////////////////////////////////////////////////////////
                Classifier detector=null;
                try {
                    Log.d(TAG, "### TFLite Initial detector ###");
                    detector = TFLiteObjectDetectionAPIModel.create(
                            getAssets(),
                            TF_OD_API_MODEL_FILE,
                            TF_OD_API_LABELS_FILE,
                            TF_OD_API_INPUT_SIZE,
                            TF_OD_API_IS_QUANTIZED);
                } catch (final IOException e) {
                    e.printStackTrace();
                    Log.d(TAG, "IOException:" + e);
                    mFinished = true;
                }

認識処理

今回は、事前に300×300 pixelに切り出したBitmapを与え物体認識処理を行っています。
目的によっては「黒帯つきで300×300 pixelにリサイズして画像全体に認識処理をかける」とか「縦に引き伸ばした300×300 pixelの画像全体に認識処理をかける」ということも考えられますが、、、画素数が減るほどに小さなもの(遠くのもの)は認識されにくくなりますし、縦横比率を変えると元々歪んでいる映像がさらに変形します。今回利用しているモデルはそのようなデータで学習されていませんので、あまりお勧めできません。
(とはいえ、ある程度動作してしまうのが機械学習の不思議さではあります)

MainActivity.java
                        ///////////////////////////////////////////////////////////////////////
                        // TFLite Object detection
                        ///////////////////////////////////////////////////////////////////////
                        final List<Classifier.Recognition> results = detector.recognizeImage(cropBitmap);
                        Log.d(TAG, "### TFLite Object detection [result] ###");
                        for (final Classifier.Recognition result : results) {
                            drawDetectResult(result, resultCanvas, mPaint, offsetX, offsetY);
                        }

結果はClassifier.RecognitionクラスのList形式で、recognizeImageメソッドの戻り値として取得できます。Listの数だけループさせながら結果を描画するdrawDetectResultメソッドを呼び出しています。

検出結果の利用方法

drawDetectResultメソッドを参照してください。
TensorFlow LiteのObject Detectionのページで説明されている通り、confidence値でカットオフしたほうがよいです。今回は 0.54以上の結果を描画するようにしてみました。お好みに応じて値を変えてください。

MainActivity.java
        double confidence = Double.valueOf(inResult.getConfidence());
        if ( confidence >= 0.54 ) {
            Log.d(TAG, "[result] Title:" + inResult.getTitle());
            Log.d(TAG, "[result] Confidence:" + inResult.getConfidence());
            Log.d(TAG, "[result] Location:" + inResult.getLocation());

            省略

        }

Bitmapクラスで Equirectangularの横方向回転をする

折角の全天球(全方位)画像が得られているのに、物体認識できるエリアを固定してしまうのは勿体ないので、ひとまず、バナナを横方向にトラッキングする処理をしています。
横方向の回転であれば、特殊なライブラリなど使わずに行えるのを知って頂きたいためです。

図示すると以下のような処理をしています。

横方向回転.PNG

コードを記述する際のポイントは以下2点です。

  • BitmapをinMutable=trueで生成し編集可能にしておく
  • CanvasクラスとPaintクラスを使って切り貼りを行う

詳しくは、rotationYawメソッドを参照してください。
本格的なEquirectangularの回転処理については、私が執筆する次回記事で解説する予定です。

OLEDの表示

以下のような表示を行っています。

OLED表示.jpg

詳しくは、displayResultメソッドを参照してください。
THETA Vで今回のサンプルを動かす場合には無駄な処理になります。コメントアウトすると僅かながら処理負荷の低減になります。

高負荷処理のタスク優先度

「THREAD_PRIORITY_わかりやすい名称」の形式で定義されているタスク優先度についてはこちらを参照してください。一般の方のまとめですが、こちらのページの説明もわかりやすいです。
数値が小さいほど優先度が高く、数値が大きいほど優先度が低くなります。

imageProcessingThreadの先頭では、以下のようにタスク優先度を指定しています。
その他は、指定をせず初期状態のままとしています。

MainActivity.java
    public void imageProcessingThread() {
        new Thread(new Runnable() {
            @Override
            public void run() {
                省略
                android.os.Process.setThreadPriority(android.os.Process.THREAD_PRIORITY_BACKGROUND);

                省略

            }
        }).start();
    }

これは、アプリケーション全体の振る舞いを考慮して設定しています。
定常状態(WebUIは立ち上げている)で動作し続けている処理は以下の3種類です。

処理名称 処理概要 指定なしの優先度 変更後の優先度
GetLiveViewTask MOTION JPEG読み取り THREAD_PRIORITY_BACKGROUND(10) 変更なし
imageProcessingThread 連続フレームの物体認識 THREAD_PRIORITY_DEFAULT(0) THREAD_PRIORITY_BACKGROUND(10)
WebServer WebUIからのコマンド実行
(主に表示画像を返す)
THREAD_PRIORITY_DEFAULT(0) 変更なし

WebServerの処理は、最新の認識結果(画像)を送るだけの短い処理ですので、他の2つのタスクより優先度が高いままで問題ありません。
GetLiveViewTaskは、ひたすらMOTION JPEGを受信しながらフレーム分割をしており、滞ると困る処理になります。doInBackgroundメソッドで動作しており、優先度はTHREAD_PRIORITY_BACKGROUNDです。
このときに、imageProcessingThreadの優先度がTHREAD_PRIORITY_DEFAULTのままではMOTION JPEGの受信を邪魔していました。このためTHREAD_PRIORITY_BACKGROUNDを設定したという経緯です。
「処理時間を要する重い処理は優先度を下げる」というセオリーとおりの対応で収まりました。

ソースコード(MainActivity.java)全文

ソースコード全文を折りたたんで掲載しておきます。

こちらをクリックして開いてください
MainActivity.java
/**
 * Copyright 2018 Ricoh Company, Ltd.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.theta360.extendedpreview;

import android.content.Context;
import android.content.pm.ActivityInfo;
import android.os.AsyncTask;
import android.util.Log;
import android.os.Bundle;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.List;

import android.view.KeyEvent;

import com.theta360.pluginlibrary.activity.PluginActivity;
import com.theta360.pluginlibrary.callback.KeyCallback;
import com.theta360.pluginlibrary.receiver.KeyReceiver;
import com.theta360.pluginapplication.task.TakePictureTask;
import com.theta360.pluginapplication.task.TakePictureTask.Callback;
import com.theta360.pluginapplication.task.GetLiveViewTask;
import com.theta360.pluginapplication.task.MjisTimeOutTask;
import com.theta360.pluginapplication.view.MJpegInputStream;
import com.theta360.pluginapplication.oled.Oled;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;

import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Color;
import android.graphics.RectF;


public class MainActivity extends PluginActivity {
    private static final String TAG = "ExtendedPreview";

    //Button Resorce
    private boolean onKeyDownModeButton = false;
    private boolean onKeyLongPressWlan = false;
    private boolean onKeyLongPressFn = false;

    //Preview Resorce
    private int previewFormatNo;
    GetLiveViewTask mGetLiveViewTask;
    private byte[]      latestLvFrame;
    private byte[]      latestFrame_Result;

    //Preview Timeout Resorce
    private static final long FRAME_READ_TIMEOUT_MSEC  = 1000;
    MjisTimeOutTask mTimeOutTask;
    MJpegInputStream mjis;

    //WebServer Resorce
    private Context context;
    private WebServer webServer;

    //OLED Dislay Resorce
    Oled oledDisplay = null;
    private boolean mFinished;



    private TakePictureTask.Callback mTakePictureTaskCallback = new Callback() {
        @Override
        public void onTakePicture(String fileUrl) {
            startPreview(mGetLiveViewTaskCallback, previewFormatNo);
        }
    };

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        // Set enable to close by pluginlibrary, If you set false, please call close() after finishing your end processing.
        setAutoClose(true);

        setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);

        //init OLED
        oledDisplay = new Oled(getApplicationContext());
        oledDisplay.brightness(100);
        oledDisplay.clear(oledDisplay.black);
        oledDisplay.draw();

        // Set a callback when a button operation event is acquired.
        setKeyCallback(new KeyCallback() {
            @Override
            public void onKeyDown(int keyCode, KeyEvent event) {
                switch (keyCode) {
                    case KeyReceiver.KEYCODE_CAMERA :
                        stopPreview();
                        new TakePictureTask(mTakePictureTaskCallback).execute();
                        break;
                    case KeyReceiver.KEYCODE_MEDIA_RECORD :
                        // Disable onKeyUp of startup operation.
                        onKeyDownModeButton = true;
                        break;
                    default:
                        break;
                }
            }

            @Override
            public void onKeyUp(int keyCode, KeyEvent event) {

                switch (keyCode) {
                    case KeyReceiver.KEYCODE_WLAN_ON_OFF :
                        if (onKeyLongPressWlan) {
                            onKeyLongPressWlan=false;
                        } else {

                            //reset Object detection dir
                            lastDetectYaw = equiW/2; // Front
                            lastDetectPitch = equiH/2;

                        }

                        break;
                    case KeyReceiver.KEYCODE_MEDIA_RECORD :
                        if (onKeyDownModeButton) {
                            if (mGetLiveViewTask!=null) {
                                stopPreview();
                            } else {
                                startPreview(mGetLiveViewTaskCallback, previewFormatNo);
                            }
                            onKeyDownModeButton = false;
                        }
                        break;
                    case KeyEvent.KEYCODE_FUNCTION :
                        if (onKeyLongPressFn) {
                            onKeyLongPressFn=false;
                        } else {

                            //reset Object detection dir
                            lastDetectYaw = 0/*(equiW/2)*/; //Back
                            lastDetectPitch = equiH/2;

                        }

                        break;
                    default:
                        break;
                }

            }

            @Override
            public void onKeyLongPress(int keyCode, KeyEvent event) {
                switch (keyCode) {
                    case KeyReceiver.KEYCODE_WLAN_ON_OFF:
                        onKeyLongPressWlan=true;

                        //NOP : KEYCODE_WLAN_ON_OFF

                        break;
                    case KeyEvent.KEYCODE_FUNCTION :
                        onKeyLongPressFn=true;

                        //NOP : KEYCODE_FUNCTION

                        break;
                    default:
                        break;
                }

            }
        });

        this.context = getApplicationContext();
        this.webServer = new WebServer(this.context, mWebServerCallback);
        try {
            this.webServer.start();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    @Override
    protected void onResume() {
        super.onResume();

        if (isApConnected()) {

        }

        //Start LivePreview
        previewFormatNo = GetLiveViewTask.FORMAT_NO_1024_8FPS;
        startPreview(mGetLiveViewTaskCallback, previewFormatNo);

        //Start OLED thread
        mFinished = false;
        imageProcessingThread();
    }

    @Override
    protected void onPause() {
        // Do end processing
        //close();

        //Stop Web server
        this.webServer.stop();

        //Stop LivePreview
        stopPreview();

        //Stop OLED thread
        mFinished = true;

        super.onPause();
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        if (this.webServer != null) {
            this.webServer.stop();
        }
    }

    private void startPreview(GetLiveViewTask.Callback callback, int formatNo){
        if (mGetLiveViewTask!=null) {
            stopPreview();

            try {
                Thread.sleep(400);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

        mGetLiveViewTask = new GetLiveViewTask(callback, formatNo);
        mGetLiveViewTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
    }

    private void stopPreview(){
        //At the intended stop, timeout monitoring also stops.
        if (mTimeOutTask!=null) {
            mTimeOutTask.cancel(false);
            mTimeOutTask=null;
        }

        if (mGetLiveViewTask!=null) {
            mGetLiveViewTask.cancel(false);
            mGetLiveViewTask = null;
        }
    }


    /**
     * GetLiveViewTask Callback.
     */
    private GetLiveViewTask.Callback mGetLiveViewTaskCallback = new GetLiveViewTask.Callback() {

        @Override
        public void onGetResorce(MJpegInputStream inMjis) {
            mjis = inMjis;
        }

        @Override
        public void onLivePreviewFrame(byte[] previewByteArray) {
            latestLvFrame = previewByteArray;

            //Update timeout monitor
            if (mTimeOutTask!=null) {
                mTimeOutTask.cancel(false);
                mTimeOutTask=null;
            }
            mTimeOutTask = new MjisTimeOutTask(mMjisTimeOutTaskCallback, FRAME_READ_TIMEOUT_MSEC);
            mTimeOutTask.execute();
        }

        @Override
        public void onCancelled(Boolean inTimeoutOccurred) {
            mGetLiveViewTask = null;
            latestLvFrame = null;

            if (inTimeoutOccurred) {
                startPreview(mGetLiveViewTaskCallback, previewFormatNo);
            }
        }

    };


    /**
     * MjisTimeOutTask Callback.
     */
    private MjisTimeOutTask.Callback mMjisTimeOutTaskCallback = new MjisTimeOutTask.Callback() {
        @Override
        public void onTimeoutExec(){
            if (mjis!=null) {
                try {
                    // Force an IOException to `mjis.readMJpegFrame()' in GetLiveViewTask()
                    mjis.close();
                } catch (IOException e) {
                    Log.d(TAG, "[timeout] mjis.close() IOException");
                    e.printStackTrace();
                }
                mjis=null;
            }
        }
    };

    /**
     * WebServer Callback.
     */
    private WebServer.Callback mWebServerCallback = new WebServer.Callback() {
        @Override
        public void execStartPreview(int format) {
            previewFormatNo = format;
            startPreview(mGetLiveViewTaskCallback, format);
        }

        @Override
        public void execStopPreview() {
            stopPreview();
        }

        @Override
        public boolean execGetPreviewStat() {
            if (mGetLiveViewTask==null) {
                return false;
            } else {
                return true;
            }
        }

        @Override
        public byte[] getLatestFrame() {
            //return latestLvFrame;
            return latestFrame_Result;
        }
    };

    //==============================================================
    // Image processing Thread
    //==============================================================
    private static final String TF_OD_API_MODEL_FILE = "detect.tflite";
    private static final String TF_OD_API_LABELS_FILE = "file:///android_asset/labelmap.txt";
    private static final int TF_OD_API_INPUT_SIZE = 300;
    private static final boolean TF_OD_API_IS_QUANTIZED = true;

    //Object Detection dir
    private int equiW = 0;
    private int equiH = 0;

    boolean detectFlag = false;
    private int lastDetectYaw=512;
    private int lastDetectPitch=256;

    public void imageProcessingThread() {
        new Thread(new Runnable() {
            @Override
            public void run() {
                int outFps=0;
                long startTime = System.currentTimeMillis();
                android.os.Process.setThreadPriority(android.os.Process.THREAD_PRIORITY_BACKGROUND);

                ///////////////////////////////////////////////////////////////////////
                // TFLite Initial detector
                ///////////////////////////////////////////////////////////////////////
                Classifier detector=null;
                try {
                    Log.d(TAG, "### TFLite Initial detector ###");
                    detector = TFLiteObjectDetectionAPIModel.create(
                            getAssets(),
                            TF_OD_API_MODEL_FILE,
                            TF_OD_API_LABELS_FILE,
                            TF_OD_API_INPUT_SIZE,
                            TF_OD_API_IS_QUANTIZED);
                } catch (final IOException e) {
                    e.printStackTrace();
                    Log.d(TAG, "IOException:" + e);
                    mFinished = true;
                }

                //set detection area offset
                int offsetX=0;
                int offsetY=0;


                while (mFinished == false) {
                    detectFlag = false;

                    //set detection area offset
                    if ( (previewFormatNo==GetLiveViewTask.FORMAT_NO_640_8FPS) ||
                            (previewFormatNo==GetLiveViewTask.FORMAT_NO_640_30FPS) ) {
                        offsetX = 170;
                        offsetY = 10;
                        equiW = 640;
                    } else if ( (previewFormatNo==GetLiveViewTask.FORMAT_NO_1024_8FPS) ||
                            (previewFormatNo==GetLiveViewTask.FORMAT_NO_1024_30FPS) ) {
                        offsetX = 362;
                        offsetY = 106;
                        equiW = 1024;
                    } else if ( (previewFormatNo==GetLiveViewTask.FORMAT_NO_1920_8FPS) ) {
                        offsetX = 810;
                        offsetY = 330;
                        equiW = 1920;
                    } else {
                        offsetX = 170;
                        offsetY = 10;
                        equiW = 640;
                    }
                    equiH = equiW/2;


                    byte[] jpegFrame = latestLvFrame;
                    if ( jpegFrame != null ) {

                        //JPEG -> Bitmap
                        BitmapFactory.Options options = new  BitmapFactory.Options();
                        options.inMutable = true;
                        Bitmap bitmap = BitmapFactory.decodeByteArray(jpegFrame, 0, jpegFrame.length, options);

                        //rotation yaw
                        bitmap = rotationYaw(lastDetectYaw, equiW, bitmap);

                        //crop detect area
                        Bitmap cropBitmap = Bitmap.createBitmap(bitmap, offsetX, offsetY, TF_OD_API_INPUT_SIZE, TF_OD_API_INPUT_SIZE, null, true);

                        //make result canvas
                        Canvas resultCanvas = new Canvas(bitmap);
                        Paint mPaint = new Paint();
                        mPaint.setStyle(Paint.Style.STROKE);
                        mPaint.setColor( Color.GREEN );
                        resultCanvas.drawRect(offsetX, offsetY, offsetX+TF_OD_API_INPUT_SIZE, offsetY+TF_OD_API_INPUT_SIZE, mPaint);

                        ///////////////////////////////////////////////////////////////////////
                        // TFLite Object detection
                        ///////////////////////////////////////////////////////////////////////
                        final List<Classifier.Recognition> results = detector.recognizeImage(cropBitmap);
                        Log.d(TAG, "### TFLite Object detection [result] ###");
                        for (final Classifier.Recognition result : results) {
                            drawDetectResult(result, resultCanvas, mPaint, offsetX, offsetY);
                        }

                        //set result image
                        ByteArrayOutputStream baos = new ByteArrayOutputStream();
                        bitmap.compress(Bitmap.CompressFormat.JPEG, 100, baos);
                        latestFrame_Result = baos.toByteArray();

                        outFps++;
                    } else {
                        try {
                            Thread.sleep(33);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }

                    //Dislpay Detect Dir to OLED
                    double lastDetectYawDig = (lastDetectYaw-equiW/2)*360.0/equiW;
                    double lastDetectPitchDig = (equiH/2-lastDetectPitch)*180/equiH;
                    displayResult(lastDetectYawDig, lastDetectPitchDig, detectFlag);

                    long curTime = System.currentTimeMillis();
                    long diffTime = curTime - startTime;
                    if (diffTime >= 1000 ) {
                        Log.d(TAG, "[OLED]" + String.valueOf(outFps) + "[fps]" );
                        startTime = curTime;
                        outFps =0;
                    }
                }
            }
        }).start();
    }


    private Bitmap rotationYaw(int inLastDetectYaw, int equiW, Bitmap inBitmap) {
        //Yaw axis rotation [Moving the detection frame]
        Log.d(TAG, "### Yaw axis rotation START [result] ###");

        //Yaw axis rotation [Image rotation]
        Bitmap rotationBmp = Bitmap.createBitmap(equiW, (equiW/2), Bitmap.Config.ARGB_8888);
        Canvas rotationCanvas = new Canvas(rotationBmp);
        if ( (equiW/2) < inLastDetectYaw ) {
            Log.d(TAG, "Case 1 [result]");

            int leftWidth = (equiW/2) + ( equiW - inLastDetectYaw ) ;

            Bitmap leftBmp = Bitmap.createBitmap(inBitmap, (inLastDetectYaw-(equiW/2)), 0, leftWidth, (equiW/2), null, true);
            Bitmap rightBmp = Bitmap.createBitmap(inBitmap, 0, 0, (inLastDetectYaw-(equiW/2)), (equiW/2), null, true);
            Paint mPaint = new Paint();
            rotationCanvas.drawBitmap(leftBmp, 0, 0, mPaint);
            rotationCanvas.drawBitmap(rightBmp, leftWidth, 0, mPaint);

        } else if ( inLastDetectYaw<(equiW/2) ) {
            Log.d(TAG, "Case 2 [result]");

            Bitmap leftBmp = Bitmap.createBitmap(inBitmap, (inLastDetectYaw+(equiW/2)), 0, ((equiW/2)-inLastDetectYaw), (equiW/2), null, true);
            Bitmap rightBmp = Bitmap.createBitmap(inBitmap, 0, 0, (inLastDetectYaw+(equiW/2)), (equiW/2), null, true);
            Paint mPaint = new Paint();
            rotationCanvas.drawBitmap(leftBmp, 0, 0, mPaint);
            rotationCanvas.drawBitmap(rightBmp, ((equiW/2)-inLastDetectYaw), 0, mPaint);

        } else {
            Log.d(TAG, "Case 3 [result]");

            Paint mPaint = new Paint();
            rotationCanvas.drawBitmap(inBitmap, 0, 0, mPaint);
        }
        Log.d(TAG, "### Yaw axis rotation END [result] ###");

        return rotationBmp;
    }


    private void drawDetectResult(Classifier.Recognition inResult, Canvas inResultCanvas, Paint inPaint, int inOffsetX, int inOffsetY){
        double confidence = Double.valueOf(inResult.getConfidence());
        if ( confidence >= 0.54 ) {
            Log.d(TAG, "[result] Title:" + inResult.getTitle());
            Log.d(TAG, "[result] Confidence:" + inResult.getConfidence());
            Log.d(TAG, "[result] Location:" + inResult.getLocation());

            // draw result
            if (confidence >= 0.56) {
                String title = inResult.getTitle();
                if ( title.equals("apple")) {
                    inPaint.setColor( Color.RED );
                } else if ( title.equals("banana") ) {
                    inPaint.setColor( Color.YELLOW );

                    detectFlag = true;
                    updateDetectInfo(inResult, inOffsetX, inOffsetY);

                } else if ( title.equals("orange") ) {
                    inPaint.setColor(Color.CYAN );
                } else {
                    inPaint.setColor( Color.BLUE );
                }
            } else {
                inPaint.setColor( Color.DKGRAY );
            }
            RectF offsetRectF = new RectF(inResult.getLocation().left, inResult.getLocation().top, inResult.getLocation().right, inResult.getLocation().bottom);
            offsetRectF.offset( (float) inOffsetX, (float) inOffsetY );
            inResultCanvas.drawRect( offsetRectF, inPaint );
            inResultCanvas.drawText(inResult.getTitle() + " : " + inResult.getConfidence(), offsetRectF.left, offsetRectF.top, inPaint);
        }
    }

    private void updateDetectInfo(Classifier.Recognition inResult, int inOffsetX, int inOffsetY){
        int tmp = lastDetectYaw;
        int curDetectYaw = (int)( inOffsetX + inResult.getLocation().left + ((inResult.getLocation().right-inResult.getLocation().left)/2) );
        if ( curDetectYaw <= (equiW/2) ) {
            lastDetectYaw -= ((equiW/2)-curDetectYaw);
        } else {
            lastDetectYaw += (curDetectYaw-(equiW/2));
        }
        if ( equiW < lastDetectYaw ) {
            lastDetectYaw -= equiW ;
        } else if (lastDetectYaw<0) {
            lastDetectYaw = equiW + lastDetectYaw;
        }
        Log.d(TAG, "[result] lastDetectYaw=" + String.valueOf(lastDetectYaw) + ", befor=" +String.valueOf(tmp) );

        int curDetectPitch = (int)( inOffsetY + inResult.getLocation().top + ((inResult.getLocation().bottom-inResult.getLocation().top)/2) );
        lastDetectPitch = curDetectPitch;
    }

    private void displayResult(double detectYawDig, double detectPitchDig, boolean inDetectFlag) {

        double lineLength = 10.0;
        double lineEndDig = detectYawDig-90.0;
        double lineEndX = lineLength * Math.cos( Math.toRadians( lineEndDig ) );
        double lineEndY = lineLength * Math.sin( Math.toRadians( lineEndDig ) );

        double arrowLength = 6.0;
        double arrowEndX1 = arrowLength * Math.cos( Math.toRadians( lineEndDig+210.0 ) );
        double arrowEndY1 = arrowLength * Math.sin( Math.toRadians( lineEndDig+210.0 ) );
        double arrowEndX2 = arrowLength * Math.cos( Math.toRadians( lineEndDig-210.0 ) );
        double arrowEndY2 = arrowLength * Math.sin( Math.toRadians( lineEndDig-210.0 ) );

        int centerX = 15;
        int centerY = 12;

        oledDisplay.clear();

        oledDisplay.circle(centerX, centerY, 11);
        oledDisplay.line(centerX, centerY, (int)(centerX+lineEndX+0.5), (int)(centerY+lineEndY+0.5));
        oledDisplay.line((int)(centerX+lineEndX+0.5), (int)(centerY+lineEndY+0.5), (int)(centerX+lineEndX+arrowEndX1+0.5), (int)(centerY+lineEndY+arrowEndY1+0.5) );
        oledDisplay.line((int)(centerX+lineEndX+0.5), (int)(centerY+lineEndY+0.5), (int)(centerX+lineEndX+arrowEndX2+0.5), (int)(centerY+lineEndY+arrowEndY2+0.5) );

        String line1Str = "";
        if (mGetLiveViewTask!=null) {
            if (inDetectFlag) {
                line1Str = "** Lock-On! **";
            } else {
                line1Str = "- can't find -";
            }
        } else {
            line1Str = "STOP Detection";
        }
        String line2Str = "Yaw   : " + String.valueOf( (int)detectYawDig );
        String line3Str = "Pitch : " + String.valueOf( (int)detectPitchDig );

        int textLine1 = 0;
        int textLine2 = 8;
        int textLine3 = 16;
        oledDisplay.setString(35, textLine1,line1Str);
        oledDisplay.setString(35, textLine2,line2Str);
        oledDisplay.setString(35, textLine3,line3Str);

        oledDisplay.draw();
    }

}

WebUIの修正

事前にTHETAプラグインでライブプリビューを扱いやすくするの内容も理解しておいてください。

元ファイルからの修正ポイントが少ないので全文は掲載しません。
基本的には数値の書き換えだけで大丈夫です。
とはいえ、変更を忘れると意図どおりに動作しないのでご注意を。

ライブプリビュー表示の更新頻度を下げる

解説はこちらを参照してください。
元のままですとWebサーバーの処理頻度が高いため、優先度が低い物体認識処理やMOTION JPEGの読み取り処理を邪魔しすぎてしまいます。入力を8fpsとしていますので125ms周期の更新で十分です。

preview.js
function repeat() {
  const d1 = new Date();
  while (true) {
    const d2 = new Date();
    if (d2 - d1 > 125) {
      break;
    }
  }
  updatePreviwFrame();
  updatePreviewStat();
  updateEv();
}

画像サイズの指定

解説はこちらを参照してください。
1024x512 8fpsの画像に対して物体認識をかける場合、preview.jsのPREVIEW_FORMAT定義を以下としてください。

preview.js
var PREVIEW_FORMAT = 3;

WebUIの表示

ここは見た目の話なのでお好みで修正してください。
index.htmlの「titleタグの内容」「h1タグの内容」「imgタグの widthとheight」を以下としています。

index.html
<html>
  <head>
    <title>TF-Lite : Object detection</title>
    <script src="js/preview.js"></script>
  </head>
  <body onLoad="startLivePreview();updatePreviwFrame();">
    <h1>
      TF-Lite : Object detection
    </h1>
    <img id="lvimg" src="" width="1024" height="512">

    ~省略~

  </body>
</html>

物体認識に便利な小道具について

物体認識の動作確認は、一般的な画角の映像を利用するのであれば、PCのディスプレイに表示した写真で行われることが多いようです。
この手法をTHETAで行った場合、「画角が広すぎる」のと「ディスプレイと外界の輝度差」から、ディスプレイしか認識できないことが多いです。

そこで、お供え物用の食品サンプル的なフルーツを利用しました。
1個買いをするよりも、複数種類の食品サンプルが1セットになったものを選ぶとお得なようです。
物体認識全般で役立ちそうな情報なので、参考として紹介しておきます。

まとめ

WebAPIのライブプリビュー利用でも「1024x512のequirectangular形式連続フレームにTensorFlow LiteのObject Detectionをかけながら、バナナを横方向にトラッキングする」という重くてややこしい処理を、おおむね 6fps(たまに7fps)で動作させることができました。

THETAプラグイン、けっこうやるでしょ?

画像サイズを 640x320にすると、8fpsくらいまで処理速度が向上しますが、バナナのような手に持てる程度の小さな物は、レンズと物体の距離を 10cm程度まで近づけないと認識できません。
何を認識させるかによって画像サイズをいろいろと試してみてください。

ちなみに、人間の認識率は結構高かったです。全身、身体の一部どちらもよく認識します。THETAにはこれくらいの大きさのものが相性がよさそうなので、姿勢推定(人間の両腕両足なども含めたポーズがわかります)を試してみると面白いかもしれません。バンザイシャッターとか作れそうです!

ウィークポイントは発熱です。20~22℃の環境、金属ボディで放熱しやすいZ1で20~30分くらい、Z1より放熱しにくいVで10~15分くらいの連続動作が限界でした。
認識結果をトリガーに記念撮影をする程度のアプリケーションはなんとかなると思われますが、もっと長時間動作するアプリケーションをつくるには、認識処理を行う頻度を落としたり、WebUIへの表示はデバッグ時のみとしたり、認識処理のOn/Offを手軽にできるようにするなどの工夫が必要そうです。

次回は、記事先頭で説明したとおり
「ライブビューの天頂補正を行いながら、全方位のバナナトラッキングする」という事例を使って「equirectangularの回転処理」「THETAの姿勢情報利用」について詳しく説明します。

【追記】
2020/4/24に続編を投稿しました。
NDKでEquirectangularの回転処理をして物体を全方位トラッキングする
こちらもよろしくお願いします。

RICOH THETAプラグインパートナープログラムについて

THETAプラグインをご存じない方はこちらをご覧ください。
パートナープログラムへの登録方法はこちらにもまとめてあります。
QiitaのRICOH THETAプラグイン開発者コミュニティ TOPページ「About」に便利な記事リンク集もあります。
興味を持たれた方はTwitterのフォローとTHETAプラグイン開発者コミュニティ(Slack)への参加もよろしくおねがいします。

KA-2
普段は星空のある風景写真(なにかの表紙なったりもするレベル)を撮っているようです。 電子工作なんかも少しします。 ファーム屋(RTOS/制御屋/システム屋)、商品企画、星景写真撮影者(ASPJ正会員)、 肩書き色々。。。
iotlt
IoT縛りの勉強会です。 毎月イベントを実施しているので是非遊びに来てください! 登壇者を中心にQiitaでも情報発信していきます。 https://iotlt.connpass.com
https://iotlt.connpass.com/
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした