Flutter: 画像から文字を認識する方法

プログラミング
スポンサーリンク




こんにちは、おみです。

今回は、Flutter + Firebase MLKitで、画像からテキストデータを読み込む処理を実装したいと思います。

完成イメージは、下記動画になります。

 

スポンサーリンク

開発環境

Android Studio: 4.0.1

flutter: 47.1.2

dart: 193.7361

 

 

下準備

Firebaseとの連携

今回は、Firebaseを使用しますので、下記の記事を参考にFlutterとの連携を行ってください。

Flutter: firebaseと連携しよう
こんにちは、おみです。 最近は、Flutterを使用してアプリ開発の勉強をしているのですが、そんな時に、firebaseというサービスを発見しました。 →firebaseとは... Googleが運営している、mBaaS/...

 

ライブラリのダウンロード

ライブラリは、以下のものを使用します。

少々数は多いですが、リンクを参考にダウンロードしてください。

※ 執筆時点のバージョンです。(2020/09/07)

path_provider: ^1.6.14
barcode_scan: ^3.0.1
image_picker: ^0.6.7+7
mlkit: ^0.15.1
image: ^2.1.14
intl: ^0.16.1

path_provider: 公式

barcode_scan: 公式

image_picker: 公式

mlkit: 公式

image: 公式

intl: 公式

 

 

ソースコード

ファイル構成は、こんな感じです。

lib
 |-common
 |   |-mlkit
 |   |   |-MLKitModule.dart
 |   |   |-TextDetectDecoration.dart
 |   |   |-BarcodeDetectDecoration.dart
 |   |   |-ScannerType.dart
 |   |
 |   |-CameraController.dart
 |
 |-page
 |   |-ReaderMenuPage.dart
 |   |-ReaderDetailPage.dart
 |
 |
 |-main.dart
 |
 |

 

 

main.dart

import 'package:flutter/material.dart';
import 'package:flutter_demo/page/ReaderMenuPage.dart';

// -----------------------------------
// メソッド名 : main
// 処理概要  : メインメソッド
// -----------------------------------
void main() {
  runApp(MyApp());
}

// -----------------------------------
// クラス名  : MyApp
// クラス概要 : アプリケーションルートクラス
// -----------------------------------
class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Link Scanner',
      theme: new ThemeData.dark(),
      home: ReaderMenuPage()
    );
  }
}

 

ReaderMenuPage.dart

import 'dart:io';

import 'package:flutter/material.dart';
import 'package:flutter_demo/common/CameraController.dart';
import 'package:flutter_demo/common/mlkit/ScannerType.dart';
import 'package:image_picker/image_picker.dart';

import 'ReaderDetailPage.dart';

// -----------------------------------
// クラス名  : ReaderMenuPage
// クラス概要 : 読み込みメニューページ
// -----------------------------------
class ReaderMenuPage extends StatefulWidget {
  ReaderMenuPage({Key key}) : super(key: key);

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

// -----------------------------------
// クラス名  : _ReaderMenuState
// クラス概要 : 読み込みメニューページステート
// -----------------------------------
class _ReaderMenuState extends State<ReaderMenuPage> {
  // 変数宣言
  /* 読み込み対象 */ ScannerType _scannerType = ScannerType.text;
  /* 読み込み方法 */ ImageSource _imageSource = ImageSource.camera;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("画像認識"),
      ),
      body: Column(
        children: <Widget>[
          Text("読み込み対象を選択してください。",
            style: TextStyle(
              fontWeight: FontWeight.bold,
              fontSize: 20,
            ),
          ),
          RadioListTile<ScannerType>(
            title: Text(
              "テキストを読み込む",
            ),
            groupValue: _scannerType,
            value: ScannerType.text,
            onChanged: (value) {
              _scannerType = value;
              setState(() {});
            },
          ),
          RadioListTile<ScannerType>(
            title: Text("バーコードを読み込む"),
            groupValue: _scannerType,
            value: ScannerType.barcode,
            onChanged: (value) {
              _scannerType = value;
              setState(() {});
            },
          ),
          Text("読み込み方法を選択してください。",
            style: TextStyle(
              fontWeight: FontWeight.bold,
              fontSize: 20,
            ),
          ),
          RadioListTile<ImageSource>(
            title: Text("カメラ"),
            groupValue: _imageSource,
            value: ImageSource.camera,
            onChanged: (value) {
              _imageSource = value;
              setState(() {});
            },
          ),
          RadioListTile<ImageSource>(
            title: Text("ギャラリー"),
            groupValue: _imageSource,
            value: ImageSource.gallery,
            onChanged: (value) {
              _imageSource = value;
              setState(() {});
            },
          ),
          RaisedButton(
              child: Text("スキャン開始"),
              onPressed: () async {
                // 指定された読み込み方法で画像を取得
                File file = await CameraController.getAndSaveImageFromDevice(_imageSource);

                // 画像が取得できた場合に明細ページに遷移
                if (file != null) {
                Navigator.push(
                  context,
                  new MaterialPageRoute(builder: (context) => new ReaderDetailPage(file, _scannerType)),
                );
                }
              }),
        ],
      ),
    );
  }
}

 

ReaderDetailPage.dart

import 'dart:io';

import 'package:flutter/material.dart';
import 'package:flutter_demo/common/mlkit/MLKitModule.dart';
import 'package:flutter_demo/common/mlkit/ScannerType.dart';

// -----------------------------------
// クラス名  : ReaderDetailPage
// クラス概要 : 読み込み明細ページ
// -----------------------------------
class ReaderDetailPage extends StatefulWidget {
  // 変数宣言
  /* 処理対象画像 */File _file;
  /* 読み込み対象 */ScannerType _scannerType;

  ReaderDetailPage(this._file, this._scannerType);

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

// -----------------------------------
// クラス名  : _ReaderDetailState
// クラス概要 : 読み込み明細ページステート
// -----------------------------------
class _ReaderDetailState extends State<ReaderDetailPage> {
  @override
  Widget build(BuildContext context) {
    return SafeArea(
      child: Scaffold(
          appBar: AppBar(
            title: Text("読み込み結果"),
          ),
          body: SingleChildScrollView(
              child: FutureBuilder<bool>(
                  future: reader(widget._file, widget._scannerType),
                  builder: (context, snapshot) {
                    // 読み込みできていない場合、処理中であることを表示
                    if (!snapshot.hasData) {
                      return CircularProgressIndicator();
                    }

                    // 読み込みが失敗した場合、メッセージを表示
                    if (!snapshot.data) {
                      return Text("読み込みに失敗しました。");
                    }

                    // 読み込みが成功した場合、読み込み結果を表示
                    return Column(
                      children: <Widget>[
                        // 読み込み結果を表示
                        Text("読み込み成功"),
                        buildImage(context, widget._file, widget._scannerType),
                        widget._scannerType == ScannerType.text ? buildTextList(context) : buildBarcodeList(context)
                      ],
                    );
                  }
                  )
          )
      ),
    );
  }
}

 

MLKitModule.dart

import 'dart:async';
import 'dart:io';
import 'dart:ui';
import 'package:flutter/material.dart';
import 'package:flutter_demo/common/mlkit/BarcodeDetectDecoration.dart';
import 'package:flutter_demo/common/mlkit/ScannerType.dart';
import 'package:flutter_demo/common/mlkit/TextDetectDecoration.dart';
import 'package:mlkit/mlkit.dart';
import 'package:image/image.dart' as img;

// MLKitのインスタンスを生成
FirebaseVisionTextDetector textDetector = FirebaseVisionTextDetector.instance;
FirebaseVisionBarcodeDetector barcodeDetector = FirebaseVisionBarcodeDetector.instance;

// 認識結果を格納するリストを生成
List<VisionText> currentTextLabels = <VisionText>[];
List<VisionBarcode> currentBarcodeLabels = <VisionBarcode>[];

// -----------------------------------------
// メソッド名 : reader
// 処理概要  : 画像から文字orバーコードを認識する
// -----------------------------------------
Future<bool> reader(File file, ScannerType scannerType) async {
  // 画像がnullである場合、falseを返却する
  if (file == null) {
    return false;
  }

  // 読み込み対象によって処理を分岐
  switch (scannerType) {
    case ScannerType.text:
      currentTextLabels = await textDetector.detectFromPath(file.path);
      break;

    case ScannerType.barcode:
      currentBarcodeLabels = await barcodeDetector.detectFromPath(file.path);
      break;
  }

  return true;
}

// -----------------------------------------
// メソッド名: buildImage
// 処理概要: 画像データを生成
// -----------------------------------------
Widget buildImage(BuildContext context, File file, ScannerType type) {
  // 画像を読み込み
  img.Image i = img.decodeImage(file.readAsBytesSync());
  Size size = Size(i.width.toDouble(), i.height.toDouble());

  return Container(
      // 画像を表示
      child: Center(
          child: file == null
              ? Text("画像を撮影してください")
              : Container(
              foregroundDecoration:
              type == ScannerType.text
                  ? TextDetectDecoration(size, currentTextLabels)
                  : BarcodeDetectDecoration(size, currentBarcodeLabels),
              child: Image.file(file, fit: BoxFit.fitWidth)
          )
      )
  );
}

// -----------------------------------------
// メソッド名: buildTextList
// 処理概要: 文字リストを生成
// -----------------------------------------
Widget buildTextList(BuildContext context) {
  if (currentTextLabels.length == 0) {
    return Text("文字の認識に失敗しました。");
  }

  return Container(
    child: ListView.builder(
      scrollDirection: Axis.vertical,
      shrinkWrap: true,
      // padding: const EdgeInsets.all(1.0),
      itemCount: currentTextLabels.length,
      itemBuilder: (context, i) {
        return _buildRow(currentTextLabels[i].text);
      },
    )
  );
}

// -----------------------------------------
// メソッド名: buildBarcodeList
// 処理概要: バーコードリストを生成
// -----------------------------------------
Widget buildBarcodeList(BuildContext context) {
  if (currentBarcodeLabels.length == 0) {
    return Text("バーコードの認識に失敗しました。");
  }

  return Container(
    child: ListView.builder(
      scrollDirection: Axis.vertical,
      shrinkWrap: true,
      // padding: const EdgeInsets.all(1.0),
      itemCount: currentBarcodeLabels.length,
      itemBuilder: (context, i) {
        return _buildRow(currentBarcodeLabels[i].rawValue);
      }
      )
  );
}

// -----------------------------------------
// メソッド名: _buildRow
// 処理概要: ListViewの行を生成
// -----------------------------------------
Widget _buildRow(text) {
  return ListTile(
    title: Text("$text"),
  );
}

 

CameraController.dart

import 'dart:async';
import 'dart:io';
import 'package:intl/intl.dart';
import 'package:path_provider/path_provider.dart';
import 'package:image_picker/image_picker.dart';

class CameraController {
  // ドキュメントのパスを取得
  static Future get localPath async {
    final directory = await getApplicationDocumentsDirectory();
    return directory.path;
  }

  // 画像をドキュメントへ保存する
  static Future saveLocalImage(File image) async{
    var now = new DateTime.now();
    var formatter = new DateFormat('yyyyMMddHHmmss');
    String formatted = formatter.format(now);

    final documentPath = await localPath;
    final imagePath = "$documentPath/$formatted-image.jpg";
    
    File imageFile = File(imagePath);
    
    // 一時フォルダに保存された画像をドキュメントへ保存し直す
    var saveFile = await imageFile.writeAsBytes(await image.readAsBytes());

    return saveFile;
    
  }
  
  // ドキュメントの画像を取得する
  static Future leadLocalImage() async {
    var now = new DateTime.now();
    var formatter = new DateFormat('yyyyMMddHHmmss');
    String formatted = formatter.format(now);

    final path = await localPath;
    final imagePath = "$path/$formatted-image.jpg";
    return File(imagePath);
  }

  static Future<File> getAndSaveImageFromDevice(ImageSource source) async{
    // 撮影した画像を取得
    var imageFile = await ImagePicker.pickImage(source: source);

    // 撮影せず閉じた場合はnullが格納される
    if (imageFile == null) {
      return null;
    }

    var saveFile = await CameraController.saveLocalImage(imageFile);

    return saveFile;
  }
}

 

TextDetectDecoration.dart

import 'dart:ui';
import 'package:flutter/material.dart';
import 'package:mlkit/mlkit.dart';

// -----------------------------------------
// クラス   : TextDetectDecoration
// クラス概要 : テキストの位置を返す
// -----------------------------------------
class TextDetectDecoration extends  Decoration {
  final Size _originalimageSize;
  final List<VisionText> _texts;

  TextDetectDecoration(this._originalimageSize, this._texts);

  @override
  BoxPainter createBoxPainter([VoidCallback onChanged]) {
    return _TextDetectPainter(_originalimageSize, _texts);
  }
}

// -----------------------------------------
// クラス名  : _TextDetectPainter
// クラス概要 : テキストの位置を画像に描画する
// -----------------------------------------
class _TextDetectPainter extends BoxPainter {
  final Size _originalimageSize;
  final List<VisionText> _texts;

  _TextDetectPainter(this._originalimageSize, this._texts);

  @override
  void paint(Canvas canvas, Offset offset, ImageConfiguration configuration) {
    final paint = Paint()
      ..strokeWidth = 2.0
      ..color = Colors.red
      ..style = PaintingStyle.stroke;

    final _heightRatio = _originalimageSize.height / configuration.size.height;
    final _widthRatio = _originalimageSize.width / configuration.size.width;
    for (var text in _texts) {
      final _rect = Rect.fromLTRB(
          offset.dx + text.rect.left / _widthRatio,
          offset.dy + text.rect.top / _heightRatio,
          offset.dx + text.rect.right / _widthRatio,
          offset.dy + text.rect.bottom / _heightRatio);
      canvas.drawRect(_rect, paint);
    }
    canvas.restore();
  }
}

 

BarcodeDetectDecoration.dart

import 'package:flutter/material.dart';
import 'package:mlkit/mlkit.dart';

// -----------------------------------------
// クラス名  : BarcodeDetectDecoration
// クラス概要 : バーコードの位置を返す
// -----------------------------------------
class BarcodeDetectDecoration extends Decoration {
  final Size _originalSize;
  final List<VisionBarcode> _barcodes;

  BarcodeDetectDecoration(this._originalSize, this._barcodes);

  @override
  BoxPainter createBoxPainter([VoidCallback onChanged]) {
    return _BarcodeDetectPainter(_originalSize, _barcodes);
  }
}

// -----------------------------------------
// クラス名  : _BarcodeDetectPainter
// クラス概要 : バーコードの位置を画像に描画する
// -----------------------------------------
class _BarcodeDetectPainter extends BoxPainter {
  final Size _originalSize;
  final List<VisionBarcode> _barcodes;

  _BarcodeDetectPainter(this._originalSize, this._barcodes);

  @override
  void paint(Canvas canvas, Offset offset, ImageConfiguration configuration) {
    final paint = Paint()
      ..strokeWidth = 2.0
      ..color = Colors.red
      ..style = PaintingStyle.stroke;

    final _heightRatio = _originalSize.height / configuration.size.height;
    final _widthRatio = _originalSize.width / configuration.size.width;
    for (var barcode in _barcodes) {
      final _rect = Rect.fromLTRB(
          offset.dx + barcode.rect.left / _widthRatio,
          offset.dy + barcode.rect.top / _heightRatio,
          offset.dx + barcode.rect.right / _widthRatio,
          offset.dy + barcode.rect.bottom / _heightRatio);
      canvas.drawRect(_rect, paint);
    }
    canvas.restore();
  }
}

 

参考文献

Flutter Text & Barcode Scanner App with Firebase ML Kit | Xcoding with Alfian
ML Kit is a collection of powerful machine learning API released to the public by Google at IO 18 under the Firebase brand. In this article, we will use text re...

 

おすすめの書籍

私は、Flutterの勉強をする際、下記の書籍を使用しています。

Flutterを使用したアプリ開発の方法を、実際にアプリの開発を通してわかりやすく解説しているので、おすすめです。

 

 

 

コメント

タイトルとURLをコピーしました