Android(JAVA)でTensorFlowLiteを使って画像分類をやってみる

#初めに
今回は、Android(Java)でTensorFlowLiteを使って、画像分類をしようと思います！
もし、コード等に間違え、改善点があれば、教えてください！
#TensorFlowLiteとは
[TensorFlowLiteのガイド](https://www.tensorflow.org/lite/guide)によると、、、

TensorFlowLiteはスマートフォンやIotデバイスなどでTensorFlowモデルを使用するためのツールセットです。

**TensorFlow Liteのインタプリタ携帯電話、組み込みLinuxデバイス、およびマイクロコントローラを含む多くの異なるハードウェアの種類、に特別に最適化されたモデルを実行します。**

TensorFlowライトコンバータインタプリタによって使用するための効率的な形式にTensorFlowモデルを変換し、バイナリサイズとパフォーマンスを向上させるために最適化を導入することができます。

**⇒つまり、PCだけじゃなくて、スマートフォンやIotデバイスなどでも簡単に実行できる、TensorFlowの軽量版的なやつか！**
将来的には、スマートフォンだけで学習までできるとか！
すごい！

#TensorFlowLiteを使用した開発手順
###1.TensorFlowモデルを用意する
TensorFlowで学習済みのモデルを用意します。
今回は、ホストされたモデルを使用するので、割愛します！
###2.TensorFlowのモデルを変換する
TensorFlowLiteではTensorFlowのモデルをそのまま使用することができないので、専用の形式(tflite)に変換します。
~~変換方法等は[こちら](https://qiita.com/yohachi/items/434f0da356161e82c24)の記事がわかりやすいので、参考にしてください。~~
記事が削除されていたので、私の書いた記事と、公式の記事を張っておきます。
[モデルの変換方法](https://qiita.com/Teraterayuki/items/a1043d4bccfd2f24f1c3)
[公式記事](https://www.tensorflow.org/lite/convert?hl=ja)
###3.組み込む！
今回は、Android(Java)の組み込み方法を解説します！

#組み込む！
###新規プロジェクトの作成
プロジェクト名等は任意の名前にしてください！
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/167678/733d141b-62a3-a41e-c6b2-423b5caa30ae.png)
今回は、[AndroidX](https://developer.android.com/jetpack/androidx?hl=ja)を使用します。
「Use androidx.* artifacts」にチェックすれば、OKです。
AndroidXの使用については任意なので、使わなくても大丈夫です。
###依存関係の追加
appディレクトリ下のbuild.gradleに

```gradle:build.gradle(app)
dependencies {
    implementation 'org.tensorflow:tensorflow-lite:0.0.0-nightly'
    implementation 'org.tensorflow:tensorflow-lite-gpu:0.0.0-nightly'
}
```

を追加します。
このままだと、すべてのCPUと命令セット用の[ABI](https://developer.android.com/ndk/guides/abis)が含まれていますが、「armeab-v7a」と「arm64-v8a」が含まれていれば、ほとんどのAndroidデバイスをカバーできるので、ほかのABIは含めいないように設定します。
含まれていても問題はないですが、アプリのサイズが減るので、おすすめです。

```gradle:build.gradle(app)
android {
    defaultConfig {
        ndk {
            abiFilters 'armeabi-v7a', 'arm64-v8a'
        }
    }
}
```

ABIについては[こちら](https://qiita.com/kz_12/items/9d3a951b8b0f4e5ea1ff)の記事がわかりやすいので参考にしてください。

Androidではassetフォルダーに入れられたものを圧縮してしまうので、モデルをassetフォルダーに入れると、圧縮されて読み込むことができなくなってしまいます。そこで、tfliteファイルを圧縮しないように指定してあげます。

```gradle:build.gradle(app)
android {
    defaultConfig {
        aaptOptions {
            noCompress "tflite"
        }
    }
}
```

###モデルの設置
モデルとlabel_textをassetフォルダーに設置します。
[こちら](https://www.tensorflow.org/lite/models/image_classification/overview)よりモデルをダウンロードしてください。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/167678/f3fb2fc3-9acb-e066-e878-c9fa6140e2bb.png)

まずは、assetフォルダーフォルダーを作成します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/167678/7e646dbb-ad25-41b3-f123-efc7f21aae8c.png)
解凍したフォルダの中から、ファイルをコピーします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/167678/bb7e5edc-82bf-4e54-216a-7087bddb46af.png)
コピーしたら、名前を「model.tflite」と「labels.txt」に変更します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/167678/01b1abe1-656e-ce3c-f0b8-5e97f61263f1.png)

これでモデルの設置は完了です。


###クラスのコピー&カスタマイズ
[TensorFlowLiteのAndroidSample](https://github.com/tensorflow/examples/tree/master/lite/examples/image_classification/android/app/src/main/java/org/tensorflow/lite/examples/classification/tflite)の3つのクラスと、[こちら](https://github.com/tensorflow/examples/blob/master/lite/examples/image_classification/android/app/src/main/java/org/tensorflow/lite/examples/classification/env/Logger.java)のLogger.javaをコピーします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/167678/d32756a3-6101-15fd-b270-92b6ef031e75.png)
コピーしただけだと、エラーが発生します。
Classifier.javaでLoggerクラスのインポート先を書き換えます。

```java:Classfier.java
import org.tensorflow.lite.Interpreter;
//ここを削除するimport org.tensorflow.lite.examples.classification.env.Logger;
import org.tensorflow.lite.gpu.GpuDelegate;

/** A classifier specialized to label images using TensorFlow Lite. */
public abstract class Classifier {
    private static final Logger LOGGER = new Logger();
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/167678/a360eedb-81ac-009e-1d09-12693fee3c38.png)
削除すると、AndroidStudioがこんなことを聞いてくるので、「Alt+Enter」を押せば、自動でインポートしてくれます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/167678/a888f0fa-6919-8086-0ad5-416f4f977cbd.png)
インポートする際に、
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/167678/868bfb6d-1d32-cfed-e2c1-c206f64edddd.png)

2種類出てくると思いますので、<Android API ~ Platform>(android.jar)と書かれていないほうを選択します。

これで、エラーがすべて消えたと思います。

###モデルの読み込み

ClassifierFloatMobileNet.java
ClassifierQuantizedMobileNet.java
の2つに共通しているモデルの読み込み部分を変更


***元***

```java:ClassifierFloatMobileNet.java,ClassifierQuantizedMobileNet.java

  @Override
  protected String getModelPath() {
    // you can download this file from
    // see build.gradle for where to obtain this file. It should be auto
    // downloaded into assets.
    return "mobilenet_v1_1.0_224.tflite";
  }

```

***変更後***

```java:ClassifierFloatMobileNet.java,ClassifierQuantizedMobileNet

  @Override
  protected String getModelPath() {
    // you can download this file from
    // see build.gradle for where to obtain this file. It should be auto
    // downloaded into assets.
    return "model.tflite";
  }


```





###Viewの配置
こんな感じにTextView,Button,ImageViewを配置します。
ButtonにはonClickを設定押しておきます。
↑　onClickを設定する方法ってリスナーのほうがいいのかな。詳しい人おしえてくださいな
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/167678/cd9ccfa4-c816-6665-be6b-316959fc50d0.png)

```xml:activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical">

        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:orientation="horizontal">

            <TextView
                android:id="@+id/textView"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_weight="1"
                android:text="TextView" />
        </LinearLayout>

        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:orientation="horizontal">

            <Button
                android:id="@+id/button"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_weight="1"
                android:onClick="select"
                android:text="画像を選択" />
        </LinearLayout>

        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:orientation="horizontal">

            <ImageView
                android:id="@+id/imageView"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_weight="1"
                tools:srcCompat="@tools:sample/avatars" />
        </LinearLayout>
    </LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
```

###コードを書く
まず、使用する変数を宣言しましょう！

```Java:MainActivity.java
    ImageView imageView;
    TextView textView;
    Classifier classifier;
    private static final int RESULT_IMAGEFILE = 1001;  //画像取得時に使用するリクエストコード
```

onCreate内でtextview,ImageViewの紐づけを行います。

```Java:MainActivity.java
        imageView = findViewById(R.id.imageView);
        textView = findViewById(R.id.textView);
```
次に、Classfierの呼び出しを行います。

```java:MainActivity.java
        try {
            classifier = Classifier.create(this,QUANTIZED,CPU,2);
        } catch (IOException e) {
            e.printStackTrace();
        }
```
引数は、Acritivy、Modelの種類、演算に使用するデバイスの指定、使用するスレッド数を指定します。
基本はこの設定で動くと思いますが、臨機応変に変更しましょう。

###ボタンの動作を書く
ボタンを押したら、ギャラリーを開いて画像が選択できるようにIntentを飛ばします。

```java:MainAcritivy.java
public void image(View V){
        Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT);
        intent.addCategory(Intent.CATEGORY_OPENABLE);
        intent.setType("image/*");
        startActivityForResult(intent, RESULT_IMAGEFILE);
}
```
これについて詳しくは[こちら](https://qiita.com/yukiyamadajp/items/137d15a4e65ed2308787)

###ギャラリーから戻ってきてからの処理
ギャラリーから戻て来たら、画像を取得して、処理します。

```java:MainAcritivty.java
    @Override
    public void onActivityResult(int requestCode, int resultCode, Intent resultData) {
        super.onActivityResult(requestCode, resultCode, resultData);
        if (requestCode == RESULT_IMAGEFILE && resultCode == Activity.RESULT_OK) {
            if (resultData.getData() != null) {
                ParcelFileDescriptor pfDescriptor = null;
                try {
                    Uri uri = resultData.getData();
                    pfDescriptor = getContentResolver().openFileDescriptor(uri, "r");
                    if (pfDescriptor != null) {
                        FileDescriptor fileDescriptor = pfDescriptor.getFileDescriptor();
                        Bitmap bmp = BitmapFactory.decodeFileDescriptor(fileDescriptor);
                        pfDescriptor.close();
                        int height = bmp.getHeight();
                        int width = bmp.getWidth();
                        while (true) {
                            int i = 2;
                            if (width < 500 && height < 500) {
                                break;
                            } else {
                                if (width > 500 || height > 500) {
                                    width = width / i;
                                    height = height / i;
                                } else {
                                    break;
                                }
                                i++;
                            }
                        }

                        Bitmap croppedBitmap = Bitmap.createScaledBitmap(bmp, width, height, false);
                        imageView.setImageBitmap(croppedBitmap);
                        List<Classifier.Recognition> results = classifier.recognizeImage(croppedBitmap,classfier);
                        String text;
                        for (Classifier.Recognition result : results) {
                            RectF location = result.getLocation();
                            Float conf = result.getConfidence();
                            String title = result.getTitle();
                            text += title + "\n";
                        }
                         textView.setText(text);
                    }
                } catch (IOException e) {
                    e.printStackTrace();
                } finally {
                    try {
                        if (pfDescriptor != null) {
                            pfDescriptor.close();
                        }
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                }

            }
        }
    }
```
長いので、区切って説明します。

このコードは、アクティビティに戻ってきたときに呼ばれ、戻ってきたのが、ギャラリーからのものかを判定しています。

```java:MainAcrivity.java
   @Override
    public void onActivityResult(int requestCode, int resultCode, Intent resultData) {
        super.onActivityResult(requestCode, resultCode, resultData);
        if (requestCode == RESULT_IMAGEFILE && resultCode == Activity.RESULT_OK) {
        }
    }

```
このコードは、戻り値からURIを取得し、ParceFileDescriptorでファイルデータをとっています。
こんなURI「content://com.android.providers.media.documents/document/image%3A325268」が取得できるので、ここから画像を取得しています。

```java:MainAcrivity.java
            if (resultData.getData() != null) {
                ParcelFileDescriptor pfDescriptor = null;
                try {
                    Uri uri = resultData.getData();
                    pfDescriptor = getContentResolver().openFileDescriptor(uri, "r");
                    if (pfDescriptor != null) {
                        FileDescriptor fileDescriptor = pfDescriptor.getFileDescriptor();
```
このコードは先ほど取得した画像をbitmapに変換し、画像のサイズを300より小さくなるようにしています。
300よりでかい画像だと、正常に判定することができず、エラーで落ちてしまいます。
``Caused by: java.lang.ArrayIndexOutOfBoundsException``
そのため、縦横比を維持しつつ、縦横が300以内に収まるようにしています。

```java:MainAcrivity.java
                        Bitmap bmp = BitmapFactory.decodeFileDescriptor(fileDescriptor);
                        pfDescriptor.close();

                        if (!bmp.isMutable()) {
                            bmp = bmp.copy(Bitmap.Config.ARGB_8888, true);
                        }
                        int height = bmp.getHeight();
                        int width = bmp.getWidth();
                        while (true) {
                            int i = 2;
                            if (width < 300 && height < 300) {
                                break;
                            } else {
                                if (width > 300 || height > 300) {
                                    width = width / i;
                                    height = height / i;
                                } else {
                                    break;
                                }
                                i++;
                            }
                        }
                        Bitmap croppedBitmap = Bitmap.createScaledBitmap(bmp, width, height, false);
```

いよいよ判別です。
このコードでは、加工した画像で判別をし、独自のリストで受け取っています。
そして、リストをforで回して、結果を取得し、textViewに表示させています。
今回は、判別された品目名のみ出力していますが、品目である可能性がどれくらいかなども取得することができます。

```java:MainAcrivity.java
                        List<Classifier.Recognition> results = classifier.recognizeImage(croppedBitmap);
                        String text="";
                        for (Classifier.Recognition result : results) {
                            /*
                            RectF location = result.getLocation();
                            Float conf = result.getConfidence();
                            */
                            String title = result.getTitle();
                            text += title + "\n";
                        } 
                        textView.setText(text);
```
以上で完成です！！

#実際に動かしてみる
それでは、実際に動かしてみたいと思います！
まずは、犬の画像
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/167678/8890d5ac-0247-c309-a0fb-c6874a3c1c88.png)
公園のベンチ、、
爪、、、
アメリカンカメレオン、、、
んー
精度は微妙ですね
次は、美しい景色の画像。
デルフトの街並みです

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/167678/da8446b1-f1ad-432b-902c-0867916dfd97.png)

ウィンドウスクリーン、、、
ドアマット、、、
ブラインド、、、
んー
ダメやん！

#まとめ
精度は微妙でしたが、うまく?画像を分類することができました！
今度は、リアルタイムで分類をしてみたいと思います！
ではでは！

#追記
これでは、TensorFlowLiteのみを使用しましたが、併用して
MlKitを使うと簡単にできますので、おすすめです～
ぜひ使ってみてくださいー

[https://developers.google.com/ml-kit?hl=ja](https://developers.google.com/ml-kit?hl=ja)