Flutter MLKit TensorFlowLite で顔認識

本記事のやること
- Google ML Kit を使ってみる。
- TensorFlow Lite でモデルを自作してみる。

**結果**
<img src='https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/253995/2b47d5f4-f650-2390-3951-d66a89d0cb67.jpeg' width='70%'>

違ってますがこれが限界。ご結婚おめでとうございます。
余談ですが、過去にiOSで同じことを試したことがあります。
[iPhoneで顔認識-Qiita](https://qiita.com/koji4104/items/7afc8c8fba6a942f4145)

# つかうもの

- Flutter  
Google 社のフレームワークです。プログラム言語は Dart で、主に Android Studio で開発します。Android の他に iOS も作成できるのがポイントです。iOS の場合は Xcode のプロジェクトが作成されます。頑張れば Windows アプリもできるそうです。VisualStudioのプロジェクトファイルが作成されるようです。
Win11でアンドロイドアプリが動くようですがどうなるのでしょうかね。
- ML Kit  
Google 社の機械学習ライブラリです。SDKです。Flutter/Dart で実行します。
- TensorFlow Lite  
機械学習モデルを構築します。PC上で Python で実行します。できたモデルファイルをAndroidアプリに同梱します。

# Google ML Kit でできること

具体的には以下のことができます。今回は顔検出とカスタムモデルを使います。他にもいくつか試してみました。[github](https://github.com/koji4104/FlutterVision)

- 画像 (Vision APIs)
  - QRコード
  - 顔検出　:arrow_left:今回使用
  - 画像分類 
  - 物体検出
  - 文字認識 
  - 手書き認識 
  - ポーズ検出 
  - 自撮り抜出 
- 言語 (Natural Language APIs)
  - 言語識別
  - 翻訳
  - 応答の提案
  - 住所・電話番号検出
- カスタムモデル (TensorFlow Lite)
  - 画像分類　:arrow_left:今回使用
  - テキスト分類
  - 質問の回答

# Firebase のアプリ登録

:memo: ML Kit を使えるようにします。ローカル環境だと無料で使えますがアプリ登録は必要です。クラウド経由だと有料になります。

## (1) Firebase にプロジェクトを作成
https://console.firebase.google.com/u/0/

<img src='https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/253995/982a504b-7812-3508-3a85-2b0b29a64b2e.png' width='50%'>

## (2) アプリを追加して構成ファイルをダウンロード

以下のファイルを取得するのがゴールです。
google-services.json (androidの場合)
GoogleService-Info.plist (iosの場合)

## (3) Flutterプロジェクトに構成ファイルを追加 
Flutter プロジェクト
:file_folder:android
&emsp;:file_folder:gradle
&emsp;:open_file_folder:app
&emsp;&emsp;:file_folder:build
&emsp;&emsp;:file_folder:src
&emsp;&emsp;:notebook:build.gradle
&emsp;&emsp;:notebook:google-service.json :arrow_left:
:file_folder:ios
&emsp;:file_folder:Flutter
&emsp;:file_folder:Runner
&emsp;&emsp;:file_folder:Assets.xcaseets
&emsp;&emsp;:file_folder:Base.lpoj
&emsp;&emsp;:notebook:AppDelegate.swift
&emsp;&emsp;:notebook:GoogleService-Info.plist :arrow_left:
&emsp;&emsp;:notebook:Info.plist

# Tensorflow Lite でモデルの作成

学習モデルを作成します。PCでPython環境を用意してください。

## (1) 画像の収集

train フォルダーにサブフォルダーを作り、顔の画像をいっぱい入れます。
<img src='https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/253995/97648269-7a9a-635c-7757-5d96c3a382da.png' width='70%'>

## (2) モデルの作成

以下のコードをpythonで実行します。成功すると model.tflite と label.txt が生成されます。

```python:main.py
# pip install tensorflow
# pip install tflite-model-maker

import os
import numpy as np
import tensorflow as tf
 
from tflite_model_maker import model_spec
from tflite_model_maker import image_classifier
from tflite_model_maker.config import ExportFormat
from tflite_model_maker.config import QuantizationConfig
from tflite_model_maker.image_classifier import DataLoader
import matplotlib.pyplot as plt

data = DataLoader.from_folder('train/')
train_data, test_data = data.split(0.9)
model = image_classifier.create(train_data)
loss, accuracy = model.evaluate(test_data)

model.export(export_dir='.')
model.export(export_dir='.', export_format=ExportFormat.LABEL)
```

## (3) カスタムモデルをアプリに組み込む

Flutter プロジェクト直下の assets フォルダに model.tflite をコピーします。同梱するため pubspec.yaml に追加します。

```yaml:pubspec.yaml
flutter:
  assets:
    - assets/model.tflite  ←Add
    - assets/labels.txt  ←Add
```
:memo: 注意
プロジェクト直下の assets フォルダでないと読み込んでくれませんでした。

# 顔検出＋顔分類

二段階で行うのがポイントです。
1. MLKitのFaceを使って顔の領域を取得します。
1. 顔の数だけ領域を切り取ってTensorflowで推論します。

<img src='https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/253995/f1bcc219-78d2-ba56-2097-3d267134a046.jpeg'>
<img src='https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/253995/8481e643-4a77-da2e-dba3-cb8468e578c2.jpeg'> <img src='https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/253995/41111dc7-cf19-9028-90a7-2085f79ff691.jpeg'> <img src='https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/253995/a6404df1-1bcb-02b2-57a6-9cea9218be9c.jpeg'> <img src='https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/253995/a13cc870-e9cb-6bf1-0476-d2d4d74c1e84.jpeg'>

# ソースの一部を解説

ソースは全部 [github](https://github.com/koji4104/FlutterVision) にあります。

```dart:アプリ部分
Future<void> detect(File imagefile) async { 
  final inputImage = InputImage.fromFile(imagefile);

  // MLKitのFaceDetectorで顔の数だけ矩形を取得
  List<Rect> rects = [];
  faces = await _faceDetector!.processImage(inputImage);
  for (Face f in faces) {
    rects.add(f.boundingBox);
  }

  results.clear();
  final File cropfile = File('${(await getTemporaryDirectory()).path}/crop.jpg');
  final byteData = imagefile.readAsBytesSync();
  imglib.Image? srcimg = imglib.decodeImage(byteData);
  
  // 顔の切り抜き
  for (Rect r1 in rects) {
    r1 = r1.inflate(4.0);
    imglib.Image crop = imglib.copyCrop(srcimg!, r1.left.toInt(), r1.top.toInt(), r1.width.toInt(), r1.height.toInt());
    await cropfile.writeAsBytes(imglib.encodeJpg(crop));
    TfResult res = await _tflite!.detect(cropfile); // TfLiteで推論
    res.rect = r1;
    if(res.outputs.length>0)
      results.add(res);
  }
}
```

Google ML Kit の FaceDetector で顔の矩形を取得します。
顔の数だけを画像を切り取って Tensorflow に渡します。
苦労した点。TensorflowのSDKにある画像の切り抜きが、できそうでできませんでした
引数がImageじゃなくファイルなので、切り抜き画像を一旦ファイル保存しています。

```dart:Tensor部分
Future initModel() async {
  try {
    // 自作モデルの読み込み
    // outputTensors.length=1
    _interpreter = await Interpreter.fromAsset('model.tflite');
    final outputTensors = _interpreter!.getOutputTensors();
    outputTensors.asMap().forEach((i, tensor) {
      TensorBuffer output = TensorBuffer.createFixedSize(tensor.shape, tensor.type);
      _outputTensorBuffers[i] = output;
      _outputBuffers[i] = output.buffer;
      _outputTensorNames[i] = tensor.name;
    });

    // ラベルファイルの読み込み
    final labelData = await rootBundle.loadString('assets/labels.txt');
    final labelList = labelData.split('\n');
    _labels = labelList;

  } on Exception catch (e) {
    print('initModel '+e.toString());
  }
}

// 検出
// 引数のimagefileが顔だけを切り抜いた画像
Future<TfResult> detect(File imagefile) async {
  TfResult res = TfResult();
  try {
    TensorImage inputImage = TensorImage.fromFile(imagefile);
    inputImage = getProcessedImage(inputImage);
    res = await run(inputImage);
  } on Exception catch (e) {
    print('-- detect catch='+e.toString());
  }
  return res;
}

Future<TfResult> run(TensorImage inputImage) async {
  TfResult res = TfResult();
  try {
    final inputs = [inputImage.buffer];
    _interpreter!.runForMultipleInputs(inputs, _outputBuffers); // 推論

    // _outputTensorBuffers.length=1
    // getDoubleList() にはモデルのラベルの数だけ値が入っています
    for (int i = 0; i < _outputTensorBuffers.length; i++) {
      TensorBuffer buffer = _outputTensorBuffers[i]!;
      for (int j = 0; j < buffer.getDoubleList().length; j++) {
        TfOutput r = TfOutput();
        r.label = _labels.length>j ? _labels[j] : '-';
        r.score = buffer.getDoubleList()[j]; // スコア
        res.outputs.add(r);
      }
    }
  } on Exception catch (e) {
    print('Exception='+e.toString());
  }

  // スコア順にソート
  res.outputs.sort((b,a) => a.score.compareTo(b.score));
  return res;
}
```

# ハマりポイント

- 構成ファイル (google-services.json) を同梱しないとML Kitでエラーになります。
- Tensorflow liteの推論画像で 224x224 以外のサイズはエラーになります。
- Tensorflow SDKのImageProcessorで切り出し(crop)ができそうでできませんでした。
- TensorflowのInterpreterの getOutputTensors で出力が１つしかありませんでした。Githubで他のソースを見ると２つや４つあるような書き方です。
私があまり理解せず使っているのが原因でしょう。
- カメラアプリと描画キャンバス
カメラが16:9でタブレットが16:10のとき上下に黒が入ります。黒を消すためにカメラを拡大してはみ出るようにしました。描画するキャンバスの座標を合わせるのに苦労しました。さらにスマホでは17:9など縦に長かったりするケースもあります。

# まとめ
Flutter、MLKit、TensorFlow Lite を使ってアプリを作ってみました。モデルを自作するのがポイントです。顔認識や車の認識などは大手企業がやるでしょう。モデルを自作することでマイナー業界を狙えば、中小ITでもチャンスがあると思います。
ML Kitはいろんなことができます。やってみたいことが思いついたのでまた１ヶ月くらい頑張ろうと思います。