OCRのFlutterパッケージfocused_area_ocr_flutterを自作&公開してみた

はじめに

PlayGroundアドベントカレンダー18日目を担当させていただくモバイルコースのyoです。
1年ぶりに執筆するテックブログの題材として、OCRのFlutterパッケージfocused_area_ocr_flutterを自作してpub.devに公開するまでの流れを記事にまとめました。
アドベントカレンダーなので、できるだけ多くの方に楽しんでいただけるよう、なるべく他の技術領域を学んでいる人でも理解できるような内容にしました。

成果物

こちらが今回作成した自作パッケージfocused_area_ocr_flutterになります。
google_mlkit_text_recognitionをベースに作成しました。
このパッケージを使うと、カメラ上の枠線内のテキストのみ読み取る機能をFlutter製アプリに追加することができます。

用語説明

Flutterやパッケージ、OCRなど、別の分野を学ばれている方にとっては聞き馴染みのない単語が出てきましたね。
ある程度の背景知識が無いと話が全く入ってこないと思うので、頻出用語について軽〜く解説しておきます。

用語 解説
Flutter モバイルアプリ開発で使用されるフレームワーク
パッケージ Flutterの実装を他の人が簡単に使えるようにまとめたもの(他の言語やフレームワークで言うライブラリ)
pub.dev 世界中のパッケージが公開されている"パッケージ版App Store/Google Playストア"のような場所
OCR カメラ等で読み取った手書きや印刷された文字のテキストデータを取得する技術のこと

経緯

このパッケージを自作することになった経緯についてですが、現在僕が携わっているFlutterのモバイルアプリ開発の案件で必要になったからです。

その案件内で、ユーザーがキーボードからテキストを入力する手間を省くために、OCRの技術を使ってカメラ上の枠線内のテキストを読み取り、読み取ったテキストをアプリに自動入力するという実装を任せていただきました。
単にOCRでテキストを読み取るというのであれば、google_mlkit_text_recognitionという既存のパッケージを利用すれば解決します。

しかし、今回はテキストがたくさん散りばめられた文書の中から任意のテキストを読み取る必要があったため、通常のOCRの実装だとカメラ内に写ったテキストを全て読み取ってしまいます。
そこで、カメラ上に枠線を配置し、枠線内に写ったテキストのみ読み取る実装を行うことで、ユーザーが任意のテキストのみ読み取れるようにする方針となりました。

枠線内のテキストを読み取ることができる既存のパッケージとしてflutter_scalable_ocrもありますが、カメラがフルスクリーン対応ではなく、UIのカスタマイズもできなさそうだったので、結局自前で実装することになりました。

結局、僕はその実装に30時間近くかけてしまい、とても苦労しました。
そんな苦労を他のFlutter開発者の方々にはして欲しくないと思い、誰でもその技術を利用できるよう、はじめての挑戦ではありましたがパッケージを自作することになりました。

技術調査

OCRによるテキストの読み取りは、google_mlkit_text_recognition公式サンプルコードをしっかり読んだり、書き換えたりして試行錯誤を重ねながら学びました。
カメラの枠線内のテキストのみ読み取る技術や読み取った文字を取得する技術はflutter_scalable_ocrを参考にしました。
カメラは以前別のFlutter案件で使用した経験があるcameraというパッケージを利用しました。
カメラの枠線表示などは、線や図形などを描画できるCustomPainterというFlutterの技術を使用しました。

実装

Flutter公式ドキュメントのDeveloping packages & pluginsに書いてある流れに沿ってパッケージの実装を進めました。
以下のコマンドを実行するとパッケージ用のFlutterプロジェクトを新規作成することができます。

flutter create --template=package <package_name>

パッケージのプロジェクトはGitで管理を行い、GitHub上にリポジトリも作成しました。
作成したプロジェクト直下のlibディレクトリ内にdartファイルを作成し、技術調査の内容を参考にしながら実装を進めました。

README作成

パッケージのプロジェクト直下にあるREADME.mdを編集してパッケージのREADMEを作成しました。
READMEとは、パッケージの取り扱い説明書のようなものであり、パッケージの使い方などが記載されています。
プロジェクト直下のREADME.mdに記入した内容がそのままpub.devで公開するパッケージのREADMEの内容になります。

example作成

パッケージのプロジェクト直下に新規Flutterプロジェクトexampleを作成しました。
exampleとは、実際にそのパッケージを利用したアプリのサンプルプロジェクトです。
exampleはFlutterプロジェクトなので、パッケージのリポジトリをクローンすればexampleのアプリをビルドして試しに動かしてみることができます。
プロジェクト直下にあるexampleのmain.dartがそのままpub.devで公開するパッケージのExampleの内容になります。


もう少しコメントを追加する必要がありそうですね…。

LICENSE作成

パッケージのプロジェクト直下のLICENSEを編集してパッケージのLICENSEを作成しました。
LICENSEにもいくつか種類がありますが、今回はこのパッケージにも使用しているgoogle_mlkit_text_recognitionに合わせてMITライセンスを採用しました。
LICENSEの追加はGitHub上のテンプレートを利用すると簡単に追加できます。
詳細はリポジトリへのライセンスの追加をご覧ください。
プロジェクト直下にあるLICENSEの内容がそのままpub.devで公開するパッケージのLICENSEの内容になります。


自分の名前が載るのはなんか嬉しいですね✨

pubspec.yamlの編集

パッケージが完成したのでpub.devに公開!
…と行きたいところだったのですが、パッケージのプロジェクト直下のpubspec.yamlに2つ問題があったため、アップロードできませんでした。
1つ目の理由は、pubspec.yamlのdescriptionがデフォルトの設定になっていたためです。適切なパッケージの説明を入れましょう。
2つ目の理由は、pubspec.yamlのrepositoryまたはhomepageが空欄になっていたためです。どちらかの項目のURLは必ず記載しておきましょう。
以下のように修正を行うことで、pub.devできるようになるはずです。

name: focused_area_ocr_flutter
description: This is a package to get text in the focused area on the camera. It is created based on OCR technology by https://pub.dev/packages/google_mlkit_text_recognition.
version: 0.0.1
repository: https://github.com/KobayashiYoh/focused_area_ocr_flutter

また、以下のコマンドを実行すると作成したパッケージをpub.devに公開するにあたって不備がないか確認することができます。

flutter pub publish --dry-run

作成したパッケージをpub.devに公開する

パッケージを公開ということで難しい手続きがあるのかと思いきや、コマンド1つでパッケージを公開することができちゃいます。(※ただし、一度公開したパッケージは削除できないのでご注意を)

flutter pub publish

コマンドの実行が終わると、パッケージがpub.devに公開されていました!やったー🙌


コード内のコメントが少ないので、コメントを追加して今後アップグレードをしていく必要がありそうですね。

公開したパッケージを使ってみる

公開したパッケージを利用してOCRのアプリを試しに作ってみます。
さっそく、新規Flutterプロジェクトを作成し、そのプロジェクトにパッケージfocused_area_ocr_flutterを導入してみましょう。
パッケージの導入方法についてはパッケージのREADMEInstallingに書いてあるので詳細は省略します。

ソースコード

import 'dart:async';

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

void main() {
  WidgetsFlutterBinding.ensureInitialized();
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Focused Area OCR Flutter',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
        useMaterial3: true,
      ),
      home: const MyHomePage(),
    );
  }
}

class MyHomePage extends StatefulWidget {
  const MyHomePage({Key? key}) : super(key: key);

  @override
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  final StreamController<String> controller = StreamController<String>();
  final double _textViewHeight = 80.0;

  @override
  Widget build(BuildContext context) {
    final double statusBarHeight = MediaQuery.of(context).viewPadding.top;
    final Offset focusedAreaCenter = Offset(
      0,
      (statusBarHeight + kToolbarHeight + _textViewHeight) / 2,
    );
    return Scaffold(
      backgroundColor: Colors.black,
      body: Stack(
        children: [
          FocusedAreaOCRView(
            onScanText: (text) {
              controller.add(text);
            },
            focusedAreaCenter: focusedAreaCenter,
          ),
          Column(
            children: [
              SizedBox(
                height: statusBarHeight + kToolbarHeight,
                child: AppBar(
                  title: const Text('Focused Area OCR Flutter'),
                  backgroundColor: Colors.blue,
                  foregroundColor: Colors.white,
                ),
              ),
              Container(
                padding: const EdgeInsets.all(16.0),
                width: double.infinity,
                height: _textViewHeight,
                color: Colors.black,
                child: StreamBuilder<String>(
                  stream: controller.stream,
                  builder:
                      (BuildContext context, AsyncSnapshot<String> snapshot) {
                    return Text(
                      snapshot.data != null ? snapshot.data! : '',
                      style: const TextStyle(color: Colors.white),
                    );
                  },
                ),
              ),
            ],
          ),
        ],
      ),
    );
  }
}

実行結果

Flutterプロジェクトのmain.dartを上記のソースコードに書き換え、アプリをビルドしてみます。
focused_area_ocr_flutterを使ってOCRアプリを作成することができました!

最後に

初めてのパッケージ制作でしたが、なんとか公開して他の人に使ってもらえるところまで進むことができました。
ソースコード内のコメント不足など、まだまだ課題はありますが、どんどんアップデートできるように引き続き頑張っていきたいと思います。

明日のアドベントカレンダー19日目の担当はデータサイエンスコースのYukariさんです!お楽しみに✨