使ってみようML Kit

これからの時代はエッジコンピューティング来るでしょと思い、
まずは画像処理系のスキルを身につけるべくFirebaseのML Kitを触ってみたのでメモ。

# ML Kitとは
Googleで開発されている機械学習を用いた機能を、モバイルアプリへ簡単に組み込むことができるSDK。
AndroidとiOS向けのライブラリが公開されている。
オンデバイスで動作するAPIとクラウドベースのAPIを利用することができる。
オンデバイスAPIを使えばネットワークの状態がなくても動作させることができる。
[公式HP MLKit説明](https://developers.google.com/ml-kit/migration)

# ML Kitでできること
2021/02時点で以下の機能が公開されている。

| 機能 | オンデバイス | クラウド | その他メモ |
| :-: | :-: | :-: | :-: |
| テキスト認識 | ○ | ○ | 日本語はクラウドのみ、オンデバイスはラテン文字の認識 |
| 顔検出| ○ |  | |
| バーコードスキャン | ○ |  |  |
| 画像のラベル付 | ○ | ○ |  |
| オブジェクトの検出とトラッキング | ○ |  |  |
| ランドマーク認識 |  | ○ |  |
| 言語認識 | ○ |  |  |
| 翻訳 | ○ |  |  |
| スマートリプライ | ○ |  | 英語のみ対応、会話履歴を解析して回答案を作成する |
| AutoML推論 | ○ |  |  |
| カスタム推論 | ○ |  |  |
| Pose Detection(Beta) | ○ |  | 人体の顔や手足の動きを追跡できる |
| Entity Extraction(Beta) | ○ |  | テキストの表記から種別(住所、電話番号、日付、etc)を判定する |

# 利用料金は？
オンデバイスは無料。
クラウドは有料。ただし、毎月1000リスクエストまで無料枠が設定されている。

# 実際にやってみた
以下の5つのインタフェースを検証してみた。
1. テキスト認識(クラウド)
2. テキスト認識(オンデバイス)
3. バーコードスキャン
4. 画像のラベル付け
5. 翻訳(英語→日本語)

### 出力結果はこんな感じ
※最強の資格を持つ花子さん(免許センターのサンプル)と最近某家電量販店で価格調査したNASの写真(QRコードと1次元バーコード)を利用して検証。
<img width="300" alt="qiita-square" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1153679/57952431-ac3a-d69a-480e-76f4244df5f4.jpeg">
<img width="300"alt="qiita-square" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1153679/3b9a5053-90e4-de17-aa8f-39166f01ad12.jpeg">
<img width="300"alt="qiita-square" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1153679/1e7bd471-9813-f9e4-728b-a756c7db49eb.jpeg">


### 1. テキスト認識(クラウド)
```java:テキスト認識
FirebaseVisionImage visionImage = FirebaseVisionImage.fromBitmap(bitmap);
// Recognize Text   (Support Japanese)
FirebaseVisionDocumentTextRecognizer detector = FirebaseVision.getInstance().getCloudDocumentTextRecognizer();
Task<FirebaseVisionDocumentText> result = detector.processImage(visionImage)
    .addOnSuccessListener(new OnSuccessListener<FirebaseVisionDocumentText>() {
        @Override
        public void onSuccess(FirebaseVisionDocumentText result) {
            // Task completed successfully
            // TextBlock単位で情報取得。
            List<FirebaseVisionDocumentText.Block> blocks = result.getBlocks();
            ・・・中略・・・
            for (int i = 0; i < blocks.size(); i++) {
                // per Block
                FirebaseVisionDocumentText.Block block = blocks.get(i);
                // テキスト解析結果
                String recognizedText = block.getText();
                // blockの矩形情報を取得
                Rect rect = block.getBoundingBox();
                //corner top left
                String.format("(%d, %d)",rect.left, rect.top);
                //corner top right
                String.format("(%d, %d)",rect.right, rect.top);
                //corner bottom right
                String.format("(%d, %d)",rect.right, rect.bottom);
                //corner bottom left
                String.format("(%d, %d)",rect.left, rect.bottom);
            }

            try {
                detector.close(); //close処理
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    })
    .addOnFailureListener(new OnFailureListener() {
        @Override
        public void onFailure(@NonNull Exception e) {
            ・・・中略・・・
        }
    });
```
どういう単位で区切られているかは掴めず。

### 2. テキスト認識(オンデバイス)
ラテン語は取得してくれるそうな。日本語は取れない。

```java:テキスト認識(オンデバイス)
InputImage bundleImage = InputImage.fromBitmap(bitmap, 0);
// Recognize text https://developers.google.com/ml-kit/vision/text-recognition/android
// Non-support Japanese,Chinese,Korean. You want recognize that, you need to use CloudApi.
TextRecognizer recognizer = TextRecognition.getClient();
Task<Text> textRecognizedResult =
        recognizer.process(bundleImage)
                .addOnSuccessListener(new OnSuccessListener<Text>() {
                    @Override
                    public void onSuccess(Text visionText) {
                        // TextBlock単位で情報取得。
                        List<Text.TextBlock> blocks = visionText.getTextBlocks();
                        ・・・中略・・・
                        for (int i = 0; i < blocks.size(); i++) {
                            // per Block  Block要素はText.LineのListを持つのでList<Text.Line> lines = blocks.get(i).getLines();といった感じでLine単位での処理も可能
                            // Point配列は、0:左上、1:右上、2:右下、3:左下で構成される
                            Point[] point = blocks.get(i).getCornerPoints();
                            // pointはnullを許容するのでnullチェック
                            if (point != null) {
                              String recognizedText = blocks.get(i).getText();
                              //corner top left
                              String.format("(%d, %d)",point[0].x, point[0].y);
                              //corner top right
                              String.format("(%d, %d)",point[1].x, point[1].y);
                              //corner bottom right
                              String.format("(%d, %d)",point[2].x, point[2].y);
                              //corner bottom left
                              String.format("(%d, %d)",point[3].x, point[3].y);
                            }
                        }
                        recognizer.close();// close処理
                    }
                })
                .addOnFailureListener(
                        new OnFailureListener() {
                            @Override
                            public void onFailure(@NonNull Exception e) {
                                ・・・中略・・・
                            }
                        });
```

### 3. バーコードスキャン(オンデバイス)
ML KitのバーコードタイプがEnumで実装されていないので自作クラスを作成。
1ページに複数のバーコードがあってもとってくれる。

```java:バーコードスキャン(オンデバイス)
// Scan barcode https://developers.google.com/ml-kit/vision/barcode-scanning/android#java
// set paramater
BarcodeScannerOptions options =
        new BarcodeScannerOptions.Builder()
                /**  スキャン対象のバーコードの型を設定しておくことが性能面で推奨されている。未指定の場合は自動判定してくれる。
                .setBarcodeFormats(
                        Barcode.FORMAT_QR_CODE, //QR Code
                        Barcode.FORMAT_CODABAR, //Codabar
                        Barcode.FORMAT_CODE_128,
                        Barcode.FORMAT_CODE_93,
                        Barcode.FORMAT_CODE_39,
                        Barcode.FORMAT_EAN_8, // JAN Code
                        Barcode.FORMAT_EAN_13) // JAN Code
                 **/
                .build();
BarcodeScanner scanner = BarcodeScanning.getClient(options);

Task<List<Barcode>> barcodeScanResult = scanner.process(bundleImage)
        .addOnSuccessListener(new OnSuccessListener<List<Barcode>>() {
            @Override
            public void onSuccess(List<Barcode> barcodes) {  //複数バーコードが取得可能
                for (int i = 0; i < barcodes.size(); i++) {
                    // per Block
                    Point[] point = barcodes.get(i).getCornerPoints();
                    if (point != null) {
                       String displayValue = barcodes.get(i).getDisplayValue();
                       String barcodeType = BarcodeType.getById(barcodes.get(i).getFormat()).getLabel();
                       ・・・中略。Pointの取り方はTextと同じ・・・
                    }
                }
            }
        })
        .addOnFailureListener(new OnFailureListener() {
            @Override
            public void onFailure(@NonNull Exception e) {
                ・・・中略・・・
            }
        });

    // https://developers.google.com/android/reference/com/google/mlkit/vision/barcode/Barcode.BarcodeFormat
    public enum BarcodeType {
        FORMAT_UNKNOWN("FORMAT_UNKNOWN",-1),
        FORMAT_ALL_FORMATS("FORMAT_ALL_FORMATS",0),
        FORMAT_CODE_128("FORMAT_CODE_128",1),
        FORMAT_CODE_39("FORMAT_CODE_39",2),
        FORMAT_CODE_93("FORMAT_CODE_93",4),
        FORMAT_CODABAR("FORMAT_CODABAR",8),
        FORMAT_DATA_MATRIX("FORMAT_DATA_MATRIX",16),
        FORMAT_EAN_13("FORMAT_EAN_13",32),
        FORMAT_EAN_8("FORMAT_EAN_8",64),
        FORMAT_ITF("FORMAT_ITF",128),
        FORMAT_QR_CODE("FORMAT_QR_CODE",256),
        FORMAT_UPC_A("FORMAT_UPC_A",512),
        FORMAT_UPC_E("FORMAT_UPC_E",1024),
        FORMAT_PDF417("FORMAT_PDF417",2048),
        FORMAT_AZTEC("FORMAT_AZTEC",4096),
        TYPE_UNKNOWN("TYPE_UNKNOWN",0),
        TYPE_CONTACT_INFO("TYPE_CONTACT_INFO",1),
        TYPE_EMAIL("TYPE_EMAIL",2),
        TYPE_ISBN("TYPE_ISBN",3),
        TYPE_PHONE("TYPE_PHONE",4),
        TYPE_PRODUCT("TYPE_PRODUCT",5),
        TYPE_SMS("TYPE_SMS",6),
        TYPE_TEXT("TYPE_TEXT",7),
        TYPE_URL("TYPE_URL",8),
        TYPE_WIFI("TYPE_WIFI",9),
        TYPE_GEO("TYPE_GEO",10),
        TYPE_CALENDAR_EVENT("TYPE_CALENDAR_EVENT",11),
        TYPE_DRIVER_LICENSE("TYPE_DRIVER_LICENSE",12);

        private String label;
        private int id;

        private BarcodeType(String label, int id) {
            this.label = label;
            this.id = id;
        }

        public String getLabel() {
            return label;
        }

        public int getId() {
            return id;
        }

        public static BarcodeType getById(int id) {
            for( BarcodeType barcodeType : BarcodeType.values() ) {
                if( barcodeType.getId() == id ) {
                    return barcodeType;
                }
            }
            return null;
        }
    }

```

### 4. 画像ラベル付け(オンデバイス)
HumanとかChildとかPaperとかHandとか。色々ラベルつけてくれる。

```java:画像ラベル付け(オンデバイス)
// Image labeling
ImageLabeler labeler = ImageLabeling.getClient(ImageLabelerOptions.DEFAULT_OPTIONS);
labeler.process(bundleImage)
        .addOnSuccessListener(new OnSuccessListener<List<ImageLabel>>() {
            @Override
            public void onSuccess(List<ImageLabel> labels) { //複数のラベル取得
                // Task completed successfully
                for (int i = 0; i < labels.size(); i++) {
                    String label = labels.get(i).getText();
                    String confidence = String.format("%f",labels.get(i).getConfidence());
                }

                labeler.close(); //close処理
            }
        })
        .addOnFailureListener(new OnFailureListener() {
            @Override
            public void onFailure(@NonNull Exception e) {
                ・・・中略・・・
            }
        });
```

### 5. 翻訳(オンデバイス)
This is a pen. →　これはペンです。　の翻訳結果は確認できた。

```java:翻訳(オンデバイス)
// Create an English-JAPANESE translator:
TranslatorOptions translationOptions =
        new TranslatorOptions.Builder()
                .setSourceLanguage(TranslateLanguage.ENGLISH)
                .setTargetLanguage(TranslateLanguage.JAPANESE)
                .build();
final Translator englishJapaneseTranslator = Translation.getClient(translationOptions);

if (!hasDownlodTranslataMode) {
    DownloadConditions conditions = new DownloadConditions.Builder().requireWifi().build();
    englishJapaneseTranslator.downloadModelIfNeeded(conditions)
            .addOnSuccessListener(
                    new OnSuccessListener() {
                        @Override
                        public void onSuccess(Object o) {
                            // Model downloaded successfully. Okay to start translating. 30MBくらいの学習モデルを端末にダウンロードする処理が必要。サンプルなので変数に状態をセット。
                            hasDownlodTranslataMode = true;
                        }
                    })
            .addOnFailureListener(
                    new OnFailureListener() {
                        @Override
                        public void onFailure(@NonNull Exception e) {
                            // Model couldn’t be downloaded or other internal error.
                            // ...
                        }
                    });
}

// 学習モデルがダウンロード終わっていれば翻訳処理を実施
if (hasDownlodTranslataMode) {
    englishJapaneseTranslator.translate("This is a pen.")
            .addOnSuccessListener(
                    new OnSuccessListener() {
                        @Override
                        public void onSuccess(@NonNull Object result) {
                            // Translation successful.
                            String translatedText = (String) result; // 「これはペンです。」が出力される。
                        }
                    })
            .addOnFailureListener(
                    new OnFailureListener() {
                        @Override
                        public void onFailure(@NonNull Exception e) {
                            ・・・中略・・
                        }
                    });
}
```

# Github URL
[MLKitサンプルコード(Android)](https://github.com/koguren/mlkitsample.git)

# 感想
- 実装は数行レベル。日本語の解析単位のクセを見抜くのがポイントかも。
- 本当はやりたかった、「この矩形位置のOCR結果を返してくれる」は自前での実装が必要っぽい。
- オンデバイスのテキスト解析は日本語未対応（翻訳→翻訳で無理やりできる？？）
- テキスト解析(クラウド)は日本語であってもそこそこ解析してくれる

# 参考
環境構築は以下のサイトを参考にさせていただきました。
[ML Kit For Firebaseを使ってスマホで色々検出してみた](https://dev.classmethod.jp/articles/mlkit_detection_demo/)