Flutter : Cloud Vision API を利用して OCRを行う
アプリを作るパターンを、
- モバイル : Xamarin –> Flutter
- サービス : GAE(Spring Boot)
- バックエンド : Firebase
と考えてきていたのだけれど、余暇の作業という時間的制約が大きく、上記のすべてのパーツについて学習やらしていると年が明けてしまう。。。
本当は、中間にサービス層を置いたほうが柔軟に対応できそうとは思いつつも、Flutter + Firebase でなんとか行けそうな気がしてきた。
すべてGoogleというのが若干気になるが。
という流れで、
Flutterアプリから、データの読み書きを直接行う疎通ができた
ので、以前はサービスにやらせていたOCRの機能(GAE + Spring Boot からためした Cloud Vision API) のテキスト認識を Flutterから直接動作させてみる。
ML Kit for firebase を利用すると、Firebaseから機械学習のAPIを簡易に利用できる。
ML Kit は、Google Cloud Vision API、TensorFlow Lite、Android Neural Networks API などの Google の ML テクノロジーを 1 つにまとめた SDK で、アプリで ML テクノロジーを簡単に利用できます。
Androidから利用する概要は、以下の動画で把握。
FlutterFire ライブラリを使用することで、Flutterから簡単にCloud Vision他を利用することが出来る。
その他にも、Flutter.dev 公式 パッケージ がいろいろ。
今回は、
- 画像の選択に、image_picker
- テキスト解析に、firebase_ml_vision
を利用する。
pubspec.yaml に追記
dependencies: flutter: sdk: flutter cupertino_icons: ^0.1.2 cloud_firestore: 0.13.4+2 firebase_ml_vision: 0.9.3+8 image_picker: 0.6.5
完成した画面。
この動画のように動作する。
ソース
import 'dart:io'; import 'package:firebase_ml_vision/firebase_ml_vision.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:cloud_firestore/cloud_firestore.dart'; import 'package:image_picker/image_picker.dart'; void main() => runApp(MyApp()); class MyApp extends StatelessWidget { final message = "Initial Message."; @override Widget build(BuildContext context) { return new MaterialApp( title: 'Flutter Sample', home: MyPage(message:this.message), ); } } class MyPageState extends State<MyPage>{ String _time; File _image; final _stateController = TextEditingController(); final _visionTextController = TextEditingController(); //final TextRecognizer textRecognizer = FirebaseVision.instance.textRecognizer(); final TextRecognizer textRecognizer = FirebaseVision.instance.cloudTextRecognizer(); @override void initState() { super.initState(); this._time = "Tap Floating Action Button"; } @override void dispose() { this._stateController.dispose(); super.dispose(); } Future getImage() async { var image = await ImagePicker.pickImage(source: ImageSource.camera); setState(() { this._image = image; }); } void vision() async { if (this._image != null) { FirebaseVisionImage visionImage = FirebaseVisionImage.fromFile(this._image); VisionText visionText = await textRecognizer.processImage(visionImage); String text = visionText.text; print(text); var buf = new StringBuffer(); for (TextBlock block in visionText.blocks) { final Rect boundingBox = block.boundingBox; final List<Offset> cornerPoints = block.cornerPoints; final String text = block.text; final List<RecognizedLanguage> languages = block.recognizedLanguages; print(languages); buf.write("=====================\n"); for (TextLine line in block.lines) { // Same getters as TextBlock buf.write("${line.text}\n"); for (TextElement element in line.elements) { // Same getters as TextBlock } } } setState(() { this._visionTextController.text = buf.toString(); }); } } void showTime(){ setState(() { this._time = DateTime.now().toString(); }); } void loadOnPressed() { Firestore.instance.document("sample/sandwichData") .get().then((DocumentSnapshot ds){ setState(() { this._stateController.text = ds["hotDogStatus"]; }); print("status=$this.status"); }); } void saveOnPressed() { Firestore.instance.document("sample/sandwichData") .updateData({"hotDogStatus":_stateController.text}) .then((value) => print("success")) .catchError((value) => print("error $value")); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text('Flutter Firebase Sample'), ), body: LayoutBuilder( builder: (BuildContext context, BoxConstraints viewportConstraints) { return SingleChildScrollView( child: ConstrainedBox( constraints: BoxConstraints( minHeight: viewportConstraints.maxHeight, ), child: IntrinsicHeight( child: Column( mainAxisAlignment: MainAxisAlignment.start, mainAxisSize: MainAxisSize.max, crossAxisAlignment: CrossAxisAlignment.start, children: <Widget>[ Text( this._time, style: TextStyle(fontSize: 16.0), ), Row( mainAxisAlignment: MainAxisAlignment.start, mainAxisSize: MainAxisSize.max, crossAxisAlignment: CrossAxisAlignment.start, children: <Widget>[ Flexible( child: TextField( controller: _stateController, ), ), Padding( padding: EdgeInsets.all(2.0), child: RaisedButton( onPressed: saveOnPressed, child: Text("Save")), ), Padding( padding: EdgeInsets.all(2.0), child: RaisedButton( onPressed: loadOnPressed, child: Text("Load")) ) ], ), Column( children: <Widget>[ Row( children: <Widget>[ Padding( padding: EdgeInsets.all(2.0), child: RaisedButton( onPressed: getImage, child: Text("Pick Image"), ), ), Padding( padding: EdgeInsets.all(2.0), child: RaisedButton( onPressed: vision, child: Text("Vision Api"), ), ), ], ), TextField( controller: _visionTextController, minLines: 6, maxLines: 15, decoration: InputDecoration( border: OutlineInputBorder(), ), ), Container( //width: MediaQuery.of(context).size.width, //height: 300, child: FittedBox( fit: BoxFit.fitHeight, child: _image == null ? Text('No image selected.') : Image.file(_image), ), ), ], ), ], ), ), ), ); }, ), floatingActionButton: FloatingActionButton( onPressed: showTime, child: Icon(Icons.timer), ), ); } } class MyPage extends StatefulWidget { final String message; MyPage({this.message}):super() {} @override State<StatefulWidget> createState() => new MyPageState(); }