Pythonの顔認識モデルをHerokuにデプロイしてFlutterから利用②

前回の記事では、Pythonの顔認識モデルをHerokuにデプロイするところまでを記載しました。

[Pythonの顔認識モデルをHerokuにデプロイしてFlutterから利用①](https://qiita.com/THERE2/items/6862ecae5fd3e8c6f133)

今回は、そのモデルをモバイルアプリから呼び出して実際に顔の比較を実現するところまでを紹介したいと思います。
モバイルアプリのサンプル作成はFlutterを利用しました。

# この記事の投稿者について
Twitterで顔認識を活用したアプリ開発についてつぶやいています。
https://twitter.com/studiothere2

noteにアプリ開発の日記を連載しています。
https://note.com/there2

# アプリの概要

![Screenshot_resize.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/508567/3bc40270-5d75-0937-bd4c-2ec6d8b9af75.png)

二つ画像をギャラリーから選択して、右下の比較ボタンを押すと二つの画像が同一人物かどうかを判定するシンプルなアプリです。
それぞれの画像のEmbeddingを前回の記事で作成したWEBサービスから取得し、そのEmbedding間のL2ノルムを計算します。
L2ノルムが0.6以下なら同一人物、それ以上なら別の人だと判定します。

例えば次のように違う人を選択すると、L2ノルムが0.6以上となり別人と判定します。

![Screenshot_1590077756.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/508567/aa7a316d-4234-ca0f-aad8-f302e034392d.png)

L2ノルムが0.5以下だと同一人物である可能性はかなり高く、約99%の精度で同一人物かどうかの判断ができる閾値が0.6という事でした。

#コード解説

### 利用パッケージ

```pubsec.yaml
dependencies:
  flutter:
    sdk: flutter
  image_picker: ^0.6.6+1
  ml_linalg: ^12.7.1
  http: ^0.12.1
  http_parser: ^3.1.4
```

- `image_picker`でギャラリーから画像を取得します。こちらはギャラリーからでもカメラからでも画像を取得できる優れものです。
- `ml_linalg`でL2ノルムを計算しています。Vector系の演算ができるDartのライブラリです。
- `http`, `http_parser`を使ってWEBサービスを呼び出しています。

### Flutterのソースコード部

まずはimport部です。必要なライブラリをインポートしています。


```main.dart
import 'dart:convert';
import 'dart:typed_data';
import 'package:flutter/material.dart';
import 'package:image_picker/image_picker.dart';
import 'package:ml_linalg/linalg.dart';
import 'package:http/http.dart' as http;
import 'package:http_parser/http_parser.dart';
import './secret.dart';
```

`secret.dart`にはWEBサービスにアクセスするための情報を保持させています。これは`git`に入っていませんので、適宜読み替えください。

```main.dart
void main() => runApp(MyApp());`

class MyApp extends StatelessWidget {
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: MyHomePage(title: 'Flutter Demo Home Page'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  MyHomePage({Key key, this.title}) : super(key: key);

  final String title;

  @override
  _MyHomePageState createState() => _MyHomePageState();
}
```

ここまでは新規にプロジェクト作ったデフォルトのまま何も変えていません。
以下からがメインのクラスです。

```main.dart
class _MyHomePageState extends State<MyHomePage> {
  ///比較用の画像１
  Uint8List _cmpImage1;

  ///比較用の画像2
  Uint8List _cmpImage2;

  //二つの顔の間のユークリッド距離。
  double _distance = 0;

  void initState() {
    super.initState();
  }
```

メンバー変数として比較する二つの画像のバイトデータ（`Uint8List`）を保持するメンバーと、二つの画像のEmbeddingのL2ノルム（ユークリッド距離）を保持する`_distance`変数を宣言しています。

### 画像の読み込み

```main.dart
  Future<Uint8List> _readImage() async {
    var imageFile = await ImagePicker.pickImage(source: ImageSource.gallery);
    return imageFile.readAsBytesSync();
  }
```

`ImagePicker`のライブラリを使って、ギャラリーから画像を取得します。
戻り値が`File`型となりますので、これを`readAsBytesSync()`メソッドを使って`Uint8List`形式に変換しておきます。
`Uint8List`は`int`型の配列でバイトデータとして扱えます。

### L2ノルムの距離から比較結果を返す関数


```main.dart
  ///二つの画像のユークリッド距離に応じて類似度を返す
  String _getCompareResultString() {
    if (_distance == 0) {
      return "";
    } else if (_distance < 0) {
      return "処理中....";
    } else if (_distance < 0.6) {
      return "同じ人です";
    } else {
      return "別の人です";
    }
  }
```

この関数で、L2ノルムの`_distance`の値に応じて画像の比較結果をテキストで返します。
-1は処理中、0.6以下なら同じ人、それ以上なら別の人としてテキストを返します。これをWidgetから呼び出して画面に表示します。

### WEBサービスの呼び出し部

ちょっと長いので分割して順番に見ていきます。

```main.dart
  void uploadFile() async {
    setState(() {
      _distance = -1;
    });
    var response;
    var postUri = Uri.parse(yourRemoteUrl);
    var request1 = http.MultipartRequest("POST", postUri);
```

まずは`_distance`に-1をセットし、画面に処理中と表示されるようにします。
`postUri`は別途読み替えてください。
ここでhttpリクエストの準備をしています。

```main.dart
    // １つ目のファイル
    debugPrint("start: " + DateTime.now().toIso8601String());
    request1.files.add(http.MultipartFile.fromBytes('file', _cmpImage1.toList(),
        filename: "upload.jpeg", contentType: MediaType('image', 'jpeg')));
    response = await request1.send();
    if (response.statusCode == 200) print("Uploaded1!");
```

httpリクエストにギャラリーから取得した画像をセットし、リクエストを送信しています。戻り値が`200`なら成功しています。

```main.dart
    var featureString1 = await response.stream.bytesToString();
    List<double> embeddings1 =
        (jsonDecode(featureString1) as List<dynamic>).cast<double>();
    debugPrint("end: " + DateTime.now().toIso8601String());
```

webサービスの戻り値をバイト配列から文字列に変換して取得（`featureString1`）し、`jsonDecode`して`double`にキャストして、結果的に`double`型の配列として取得しています。
これが画像のEmbeddingとなり、これを比較することで同一人物かどうかを判定できます。

```main.dart
    // ２つ目のファイル
    var request2 = http.MultipartRequest("POST", postUri);
    request2.files.add(http.MultipartFile.fromBytes('file', _cmpImage2.toList(),
        filename: "upload.jpeg", contentType: MediaType('image', 'jpeg')));
    response = await request2.send();
    if (response.statusCode == 200) print("Uploaded2!");
    var featureString2 = await response.stream.bytesToString();
    List<double> embeddings2 =
        (jsonDecode(featureString2) as List<dynamic>).cast<double>();
```

ここまで二つ目の画像に対しても同じ事をしました。
続いてL2ノルムの算出部です。

```main.dart
    var distance = Vector.fromList(embeddings1)
        .distanceTo(Vector.fromList(embeddings2), distance: Distance.euclidean);

    setState(() {
      _distance = distance;
    });
  }
```

`ml_linalg`のライブラリを使っているので非常に簡単です。
それぞれの`double`型の配列のEmbeddingを`Vector.fromList`で`Vector`に変換し、`distanceTo`で距離を求めればよいだけです。
`distance`の計算方法として、ユークリッド距離（L2ノルム）を指定しています。

最後にその距離をメンバー変数の`_distance`にセットして完了です。

### 画面描画部

```main.dart
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Column(
        mainAxisAlignment: MainAxisAlignment.start,
        children: <Widget>[
          Row(
            crossAxisAlignment: CrossAxisAlignment.start,
            children: <Widget>[
              Expanded(
                flex: 1,
                child: Column(
                  mainAxisAlignment: MainAxisAlignment.start,
                  children: <Widget>[
                    RaisedButton(
                      onPressed: () async {
                        var cmpImage = await _readImage();
                        setState(() {
                          _cmpImage1 = cmpImage;
                        });
                      },
                      child: Text("１つ目の画像の読み込み"),
                    ),
                    Text("１つ目の画像"),
                    Container(
                      child:
                          _cmpImage1 == null ? null : Image.memory(_cmpImage1),
                    ),
                  ],
                ),
              ),
              Expanded(
                flex: 1,
                child: Column(
                  children: <Widget>[
                    RaisedButton(
                      onPressed: () async {
                        var cmpImage = await _readImage();
                        setState(() {
                          _cmpImage2 = cmpImage;
                        });
                      },
                      child: Text("2つ目の画像の読み込み"),
                    ),
                    Text("2つ目の画像"),
                    Container(
                      child:
                          _cmpImage2 == null ? null : Image.memory(_cmpImage2),
                    ),
                  ],
                ),
              )
            ],
          ),
          SizedBox(height: 10),
          Text("顔の類似度比較結果"),
          Text(_getCompareResultString()),
          Text("L2ノルムは $_distance です"),
        ],
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: uploadFile,
        tooltip: '画像の比較',
        child: Icon(Icons.compare),
      ),
    );
  }

```

ちょっと長いですがシンプルです。
二つそれぞれの画像の読み込みボタンで画像を読み込みます。
読み込んだ画像の`Uint8List`のデータは、`Image.memory`で画面に表示できます。
`_getCompareResultString()`で画像の比較結果を日本語で表示しています。
`FloatingActionButton`の`onPressed`でWEBサービスを呼び出して画像感の距離を算出する処理を呼び出しています。

# 最後に

ちゃんと写っている顔画像ならしっかり同一人物かどうか判断してくれますのでなかなか感動します。
最近ではマスクしてても顔認識できるモデルが出てきているようです。
顔認識を利用するのはプライバシー侵害の問題もあり、使い方を気を付ける必要がありますが、うまく活用したサービスを開発できると楽しいですね。