LoginSignup
2

More than 3 years have passed since last update.

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

Last updated at Posted at 2020-05-22

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

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

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

この記事の投稿者について

Twitterで顔認識を活用したアプリ開発についてつぶやいています。
https://twitter.com/studiothere2

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

アプリの概要

Screenshot_resize.png

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

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

Screenshot_1590077756.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> {
  ///比較用の画像1
  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形式に変換しておきます。
Uint8Listint型の配列でバイトデータとして扱えます。

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
    // 1つ目のファイル
    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
    // 2つ目のファイル
    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.fromListVectorに変換し、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("1つ目の画像の読み込み"),
                    ),
                    Text("1つ目の画像"),
                    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()で画像の比較結果を日本語で表示しています。
FloatingActionButtononPressedでWEBサービスを呼び出して画像感の距離を算出する処理を呼び出しています。

最後に

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

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2