FlutterとFirebaseで日本語のOCRアプリを作る

May 24, 2021

はじめに

今回はFlutterとFirebaseを使って写真から日本語を読み取るOCRアプリを作ってみます。
前まではfirebase_ml_visionを利用することができたのですが、現在は非推奨になってしまいCloud Vision APIをCloud Functions経由で呼び出すのが推奨されています。
ですのでこのFunctionsを利用する方法を採用しました。

Flutterで画像取得処理

まずは写真撮影したものを表示させるところまで作ってみます。

import 'dart:io';
import 'package:flutter/material.dart';
import 'package:image_picker/image_picker.dart';
import 'package:firebase_core/firebase_core.dart';
import 'package:firebase_auth/firebase_auth.dart';

void main() {
  WidgetsFlutterBinding.ensureInitialized();
  runApp(App());
}

class App extends StatefulWidget {
  @override
  _AppState createState() => _AppState();
}

class _AppState extends State<App> {
  Future<FirebaseApp> _initialize() async {
    return Firebase.initializeApp();
  }

  @override
  Widget build(BuildContext context) {
    return FutureBuilder(
      // Initialize FlutterFire:
      future: _initialize(),
      builder: (context, snapshot) {
        if (snapshot.hasError) {
          return Center(child: Text('読み込みエラー'));
        }

        if (snapshot.connectionState == ConnectionState.done) {
          return MyApp();
        }

        return Center(child: Text('読み込み中...'));
      },
    );
  }
}

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

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

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

class _MyHomePageState extends State<MyHomePage> {
  File _image;
  final _picker = ImagePicker();
  String _result;

  @override
  void initState() {
    super.initState();
    _signIn();
  }

  void _signIn() async {
    await FirebaseAuth.instance.signInAnonymously();
  }

  Future _getImage() async {
    final _pickedFile = await _picker.getImage(source: ImageSource.camera);

    setState(() {
      if (_pickedFile != null) {
        _image = File(_pickedFile.path);
      } else {
        print('No image selected.');
      }
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('OCRアプリ'),
      ),
      body: Center(
        child: Padding(
          padding: EdgeInsets.all(16.0),
          child: Column(children: [
            if (_image != null) Image.file(_image, height: 400),
            if (_image != null) _analysisButton(),
            Container(
                height: 240,
                child: SingleChildScrollView(
                    scrollDirection: Axis.vertical,
                    child: Text((() {
                      if (_result != null) {
                        return _result;
                      } else if (_image != null) {
                        return 'ボタンを押すと解析が始まります';
                      } else {
                        return '文字おこししたいものを撮影してください';
                      }
                    }())))),
          ]),
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _getImage,
        tooltip: 'Pick Image',
        child: Icon(Icons.add_a_photo),
      ),
    );
  }

  Widget _analysisButton() {
    return ElevatedButton(
      child: Text('解析'),
      onPressed: () async {
        // ここで画像解析用Cloud Functionを呼ぶ
      },
    );
  }
}

上記コードで写真を撮影して表示するところまでできました。

Flutter OCR App Home

Flutter OCR App Camera

Flutter OCR App Preview

ここではFirebaseを利用するのでFirebase.initializeApp()を実行、さらに今回はログインユーザだけがFunctionへアクセスできるようにするためFirebaseAuth.instance.signInAnonymously()を呼んで匿名ログインしています(実際はGoogleやTwitterログインなども選択肢としてあると思います)。

Firebaseをプロジェクトへ追加していない場合はまだ動かないですが、次で説明していきます。

Firebase側の設定

次はFirebase側の設定です。

Firebaseをプロジェクトへ追加

まずはFirebaseをプロジェクトへ追加する作業が必要です。
iOS、Androidそれぞれ詳しい手順はFlutterFireのドキュメントに載っていますので、確認してみてください。
FlutterFireドキュメント

Blazeプランへアップグレードする

テキスト認識はML Kitを使えばオンデバイスでできますが、これはラテン語由来の文字しか認識できません。
そのため日本語の認識はCloud Vision APIを利用しなければなりません。
Cloud Vision APIはBlazeプランのみ有効なのでアップグレードが必要です。
Firebase Consoleからアップグレードできます。

Cloud-based APIsを有効化する

続いてConsoleからCloud-based APIsを有効にします。

Firebase ML APIsページからCloud-based APIsを有効にします。

firebase enable cloud based apis

APIキーを制限する

セキュリティ向上のため許可したCloud Vision APIのみAPIキーが使用されるように制限します。 GCPコンソールのAPIs & Services > Credentialsへ行き、API Keysにある全てのキーを編集します。
編集画面へ行くとAPI restrictionsという項目があるのでRestrict keyを選択し、プルダウンからCloud Vision API以外の使わないAPIを全て選択、保存します。
これで間違ってCloud Vision API以外でAPIキーが使用されることはありません。

Authenticationで匿名アカウントを許可する

今回のアプリでは匿名アカウントとしてログインしているためFirebase側で有効化する必要があります。
Authentication > Sign-in methodから設定可能です。

firebase authentication enable anonymous user

Cloud Functionのデプロイ

そして今回一番重要なCloud Vistion API使って文字認識を行うFunctionを用意します。
すでにFirebaseからサンプルが提供されていますので、これをそのまま使います。

まず、サンプルをcloneします。

$ git clone https://github.com/firebase/functions-samples
$ cd vision-annotate-image

次にdependenciesをインストールします。

$ cd functions
$ npm install
$ cd ..

Firebaseプロジェクトをイニシャライズします。Firebase CLIをインストールしていない場合は先にしておきます。

$ firebase init

このときFlutterに追加したものと同じプロジェクトを選択します。
そして最後にデプロイして完了です。

firebase deploy --only functions:annotateImage

サンプルのソースコードはTypeScriptで書かれていて、内容は以下のようになっています。

import * as functions from "firebase-functions";
import vision from "@google-cloud/vision";

const client = new vision.ImageAnnotatorClient();

// This will allow only requests with an auth token to access the Vision
// API, including anonymous ones.
// It is highly recommended to limit access only to signed-in users. This may
// be done by adding the following condition to the if statement:
//    || context.auth.token?.firebase?.sign_in_provider === 'anonymous'
// 
// For more fine-grained control, you may add additional failure checks, ie:
//    || context.auth.token?.firebase?.email_verified === false
// Also see: https://firebase.google.com/docs/auth/admin/custom-claims
export const annotateImage = functions.https.onCall(async (data, context) => {
  if (!context.auth) {
    throw new functions.https.HttpsError(
      "unauthenticated",
      "annotateImage must be called while authenticated."
    );
  }
  try {
    return await client.annotateImage(JSON.parse(data));
  } catch (e) {
    throw new functions.https.HttpsError("internal", e.message, e.details);
  }
});

かなりシンプルでログインユーザーからのリクエストかチェックしてあとは @google-cloud/vision を使ってannotateImageを呼び出しているだけです。

デプロイに成功したらあとはFlutterアプリからこのFunctionを呼び出しあげればOKです。

FlutterからCloud Functionへリクエスト

画像をbase64エンコードしたものをパラメータとしてCloud Functionへリクエストします。
以下が追記部分のコードです。

// ...
import 'dart:convert';
import 'package:cloud_functions/cloud_functions.dart';

// ...

class _MyHomePageState extends State<MyHomePage> {

  // ...

  Widget _analysisButton() {
    if (_isLoading) {
      return Center(
        child: CircularProgressIndicator(),
      );
    }

    return ElevatedButton(
      child: Text('解析'),
      onPressed: () async {
        List<int> _imageBytes = _image.readAsBytesSync();
        String _base64Image = base64Encode(_imageBytes);
        HttpsCallable _callable =
            FirebaseFunctions.instance.httpsCallable('annotateImage');
        final params = '''{
          "image": {"content": "$_base64Image"},
          "features": [{"type": "TEXT_DETECTION"}],
          "imageContext": {
            "languageHints": ["ja"]
          }
        }''';

        setState(() {
          _isLoading = true;
        });

        final _text = await _callable(params).then((v) {
          return v.data[0]["fullTextAnnotation"]["text"];
        }).catchError((e) {
          print('ERROR: $e');
          return '読み取りエラーです';
        });

        setState(() {
          _result = _text;
          _isLoading = false;
        });
      },
      },
    );
  }
}

これで撮影した画像から文字を起こすことができました。

flutter ocr result

私のヘタクソな手書き文字もなかなかの精度で認識できています(「り」が惜しい...)。

まとめ

以上でFlutterのOCRアプリ作成手順を紹介しました。
Firebase ML Vision SDKが非推奨になってFunctionsを使わないといけなくなったので面倒かなと思っていたのですが、意外とあっさり実装することができました。
むしろこっちの方がFunction内で独自ロジックを組み合わせることができるので良さそうな気がします。
またクレジットカードやラテン語由来の文字を対象とした文字認識の場合はCloud Vision APIを使ったこの方法ではなくML Kitを利用した方法が適しています。
デバイス上でのみ動作するのでオフラインでかつ速く文字を認識させることができます(ただしより高精度なものを求める場合はCloud Vision APIが適しています)。

やはりFlutterやFirebaseの組み合わせは最強で、作れるアプリの幅が広がるのでこれからも色々作ってみます。