Android Things Developer Preview 2でテンソル

Android Things Developer Preview 2のリリースと同時に、テンソルによる画像分類サンプルが追加されました。

[Release Notes](https://developer.android.com/things/preview/releases.html#developer_preview_2)

Preview 2からRaspberry Pi 3、Intel Edison、NXP Picoのほかに、Intel Jouleも加わりました。Native peripheral APIのサポートも追加されましたがここでは触れません。

[Android Things TensorFlow image classifier sample](https://github.com/androidthings/sample-tensorflow-imageclassifier)

テンソルのサンプルは、[TensorFlow Android Camera Demo TF_Classify app](https://github.com/tensorflow/tensorflow/tree/master/tensorflow/examples/android)がベースになっていて、[Android TensorFlow inference library](https://github.com/tensorflow/tensorflow/tree/master/tensorflow/contrib/android)はAARにビルドされたものをスタティックにリンクしているだけなので、ざっとソースを見たぐらいではテンソル使ってますぐらいしかわかりませんが、自分のような素人にはちょうどよいとっかかりになりました。あと、Camera制御についても参考になると思います。

Android Thingsについては、[Raspberry Pi3によるAndroid Things入門](http://qiita.com/tatsu/items/dc98d600ff6c35832243)をご参照ください。

## 準備
Raspberry Pi 3を使用しています。

![rpi3_schematics_tf.png](https://qiita-image-store.s3.amazonaws.com/0/49088/faf7f327-415a-559b-b363-c03ef904b885.png)

- Android Studio 2.2+
- Raspberry Pi 3
- Raspberry Pi 3 camera module V2
- プッシュボタン（タクトスイッチ）1個
- 抵抗 1KΩ 1個
- 抵抗 470Ω 1個※自分は330Ωで代用。
- LED 1個
- ブレッドボード
- ジャンパーワイヤー オスーオス2本、オス-メス4本
- スピーカー
- ディスプレイ

スピーカー（イヤホン）とディスプレイはオプショナルですが、あったほうがよいです。あと、USBマウスも。

Android Thingsのイメージは、[ここ](https://developer.android.com/things/preview/download.html)から最新のイメージをダウンロードしてきて、[おなじみ](https://www.raspberrypi.org/documentation/installation/installing-images/)の手順でSDカードにフラッシュするだけです。

ディスプレイとLANケーブルを接続して電源を入れ起動すると、ディスプレイにIPアドレスが表示されるので、adbで接続します。

```
$ adb connect <ip-address>
connected to <ip-address>:5555
```

マルチキャストDNSに対応していれば、IPアドレスの代わりに、Android.localでもつながります。

```
$ adb connect Android.local
```

以降はWiFiで接続できます。

```
$ adb shell am startservice \
    -n com.google.wifisetup/.WifiSetupService \
    -a WifiSetupService.Connect \
    -e ssid <Network_SSID> \
    -e passphrase <Network_Passcode>
```

## 実行

ビルド手順はなく、とにかくソースを落としてきて、Android Studioで実行するだけです。

![screen-0001.png](https://qiita-image-store.s3.amazonaws.com/0/49088/85954c75-0b84-1ea4-9966-65890b1d86ee.png)


1. 最初にカメラを使用してよいか許可を求めてきます。ALLOWを2回クリックすると、[既知の不具合](https://developer.android.com/things/preview/releases.html#known_issues)のためかと思いますが、勝手にリブートしました。ディスプレイのない構成では試していません。
2. LEDが点灯するまで待ちます。
3. カメラを分類させたいものに向けます。
4. プッシュボタンを押します。
5. LEDが処理中約1秒消灯したのち、ディスプレイに撮影画像と分類項目が表示されるとともに、Text-To-Speechによって読み上げられます。
6. ディスプレイのない構成ではlogcatで結果をみれるとありましたが、それらしいログ出力は見当たりませんでした。
7. Raspberry Piのオーディオ出力はデフォルトでオート(HDMI)になっています。Android Thingsでの出力先の変更方法は不明です（知りません）。HDMIディスプレイにスピーカーかイヤホンをつければ、テキストの読み上げを聞けると思います。

![screen-0002.png](https://qiita-image-store.s3.amazonaws.com/0/49088/15732f23-c57c-6d99-e650-bb531f923713.png)


ちなみに、私の顔は、29.8%の確率でoxygen maskでした。⤵︎

## 要所（ざっくり）

まず結果から。ImageClassifierActivityでImageReader.OnImageAvailableListenerを実装し、撮影結果をもとに表示・分類・読み上げを行なっています。

```ImageClassifierActivity.java
public class ImageClassifierActivity extends Activity implements ImageReader.OnImageAvailableListener {

...snip...

    @Override
    public void onImageAvailable(ImageReader reader) {
        final Bitmap bitmap;
        try (Image image = reader.acquireNextImage()) {
            bitmap = mImagePreprocessor.preprocessImage(image);
        }

        runOnUiThread(new Runnable() {
            @Override
            public void run() {
                mImage.setImageBitmap(bitmap);
            }
        });

        final List<Classifier.Recognition> results = mTensorFlowClassifier.recognizeImage(bitmap);

        if (mTtsEngine != null) {
            // speak out loud the result of the image recognition
            if (Math.random() < 0.3) {
                mTtsEngine.setPitch(0.2f);
                mTtsEngine.speak("I see dead people...", TextToSpeech.QUEUE_ADD, null, "ID");
                mTtsEngine.setPitch(1);
                mTtsEngine.speak("just kidding...", TextToSpeech.QUEUE_ADD, null, "ID");
            } else {
                mTtsEngine.setPitch(1f);
                mTtsEngine.setVoice(mTtsEngine.getDefaultVoice());
            }
            String message;
            if (results.isEmpty()) {
                message = "I don't understand what I see. Am I using drugs?";
            } else if (results.size() == 1 || results.get(0).getConfidence() > 0.4f) {
                message = String.format(Locale.getDefault(), "I see a %s",
                        results.get(0).getTitle());
            } else {
                message = String.format(Locale.getDefault(),
                        "This is a %s or maybe a %s",
                        results.get(0).getTitle(), results.get(1).getTitle());
            }
            mTtsEngine.speak(message, TextToSpeech.QUEUE_ADD, null, "ID");
        } else {
            // if theres no TTS, we don't need to wait until the utterance is spoken, so we set
            // to ready right away.
           setReady(true);
        }

        runOnUiThread(new Runnable() {
            @Override
            public void run() {
                for (int i=0; i<mResultViews.length; i++) {
                    if (results.size() > i) {
                        Classifier.Recognition r = results.get(i);
                        mResultViews[i].setText(r.toString());
                    } else {
                        mResultViews[i].setText(null);
                    }
                }
            }
        });
    }
```

ImageClassifierActivity#mInitializeOnBackgroundでCameraHandler#initializeCameraにより自身に撮影結果をコールバックさせるように実装しています。同じ箇所で、TextToSpeechやTensorFlowの初期化も行なっています。

```ImageClassifierActivity.java
    private Runnable mInitializeOnBackground = new Runnable() {
        @Override
        public void run() {
            mImagePreprocessor = new ImagePreprocessor(CameraHandler.IMAGE_WIDTH,
                    CameraHandler.IMAGE_HEIGHT, TensorFlowImageClassifier.INPUT_SIZE);

            mTtsEngine = new TextToSpeech(ImageClassifierActivity.this,
                    new TextToSpeech.OnInitListener() {
                        @Override
                        public void onInit(int status) {
                            if (status == TextToSpeech.SUCCESS) {
                                mTtsEngine.setLanguage(Locale.US);
                                mTtsEngine.setOnUtteranceProgressListener(utteranceListener);
                            } else {
                                mTtsEngine = null;
                            }
                        }
                    });
            mCameraHandler = CameraHandler.getInstance();
            mCameraHandler.initializeCamera(
                    ImageClassifierActivity.this, mBackgroundHandler,
                    ImageClassifierActivity.this);

            mTensorFlowClassifier = new TensorFlowImageClassifier(ImageClassifierActivity.this);

            setReady(true);
        }
    };
```







onCreateから呼ばれるImageClassifierActivity#initで、ボタンとLEDの初期設定を行うとともに、前述のImageClassifierActivity#mInitializeOnBackgroundを実行しています。

```ImageClassifierActivity.java
    private void init() {
        try {
            mButtonDriver = new ButtonInputDriver(BUTTON_PIN, Button.LogicState.PRESSED_WHEN_LOW,
                    KeyEvent.KEYCODE_ENTER);
            mButtonDriver.register();
            PeripheralManagerService service = new PeripheralManagerService();
            mReadyLED = service.openGpio(LED_PIN);
            mReadyLED.setDirection(Gpio.DIRECTION_OUT_INITIALLY_LOW);
        } catch (IOException e) {
            Log.w(TAG, "Could not open GPIO", e);
        }

        mBackgroundThread = new HandlerThread("BackgroundThread");
        mBackgroundThread.start();
        mBackgroundHandler = new Handler(mBackgroundThread.getLooper());
        mBackgroundHandler.post(mInitializeOnBackground);
    }
```