Publish Date: 2021-05-25
今回はFlutterとFirebaseを使って写真から日本語を読み取るOCRアプリを作ってみます。
前まではfirebase_ml_visionを利用することができたのですが、現在は非推奨になってしまいCloud Vision APIをCloud Functions経由で呼び出すのが推奨されています。
ですのでこのFunctionsを利用する方法を採用しました。
まずは写真撮影したものを表示させるところまで作ってみます。
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 { _AppState createState() => _AppState(); } class _AppState extends State<App> { Future<FirebaseApp> _initialize() async { return Firebase.initializeApp(); } 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. 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; _MyHomePageState createState() => _MyHomePageState(); } class _MyHomePageState extends State<MyHomePage> { File _image; final _picker = ImagePicker(); String _result; 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.'); } }); } 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を呼ぶ }, ); } }
上記コードで写真を撮影して表示するところまでできました。
ここではFirebaseを利用するのでFirebase.initializeApp()
を実行、さらに今回はログインユーザだけがFunctionへアクセスできるようにするためFirebaseAuth.instance.signInAnonymously()
を呼んで匿名ログインしています(実際はGoogleやTwitterログインなども選択肢としてあると思います)。
Firebaseをプロジェクトへ追加していない場合はまだ動かないですが、次で説明していきます。
次はFirebase側の設定です。
まずはFirebaseをプロジェクトへ追加する作業が必要です。
iOS、Androidそれぞれ詳しい手順はFlutterFireのドキュメントに載っていますので、確認してみてください。
FlutterFireドキュメント
テキスト認識はML Kitを使えばオンデバイスでできますが、これはラテン語由来の文字しか認識できません。
そのため日本語の認識はCloud Vision APIを利用しなければなりません。
Cloud Vision APIはBlazeプランのみ有効なのでアップグレードが必要です。
Firebase Consoleからアップグレードできます。
続いてConsoleからCloud-based APIsを有効にします。
Firebase ML APIsページからCloud-based APIsを有効にします。
セキュリティ向上のため許可したCloud Vision APIのみAPIキーが使用されるように制限します。
GCPコンソールのAPIs & Services > Credentialsへ行き、API Keysにある全てのキーを編集します。
編集画面へ行くとAPI restrictionsという項目があるのでRestrict keyを選択し、プルダウンからCloud Vision API以外の使わないAPIを全て選択、保存します。
これで間違ってCloud Vision API以外でAPIキーが使用されることはありません。
今回のアプリでは匿名アカウントとしてログインしているためFirebase側で有効化する必要があります。
Authentication > Sign-in methodから設定可能です。
そして今回一番重要な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です。
画像を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アプリ作成手順を紹介しました。
Firebase ML Vision SDKが非推奨になってFunctionsを使わないといけなくなったので面倒かなと思っていたのですが、意外とあっさり実装することができました。
むしろこっちの方がFunction内で独自ロジックを組み合わせることができるので良さそうな気がします。
またクレジットカードやラテン語由来の文字を対象とした文字認識の場合はCloud Vision APIを使ったこの方法ではなくML Kitを利用した方法が適しています。
デバイス上でのみ動作するのでオフラインでかつ速く文字を認識させることができます(ただしより高精度なものを求める場合はCloud Vision APIが適しています)。
やはりFlutterやFirebaseの組み合わせは最強で、作れるアプリの幅が広がるのでこれからも色々作ってみます。
Twitter: @daiki7nohe
フロントエンドを中心に活動しています。