【Flutter】google_mlkit_text_recognitionを使ってレシートを読み取る

# はじめに
今回は、google_mlkit_text_recognitionを使ってレシートを読み取る方法を紹介します。

# 開発環境
- Flutter 3.7.11
- Dart 2.19.6
- camera 0.10.5+5
- google_mlkit_text_recognition 0.11.0

# 開発手順
### 1. ライブラリのインストール

文字認識をするパッケージを追加します。

https://pub.dev/packages/google_mlkit_text_recognition/install

カメラを使うために、こちらのパッケージを追加します。

https://pub.dev/packages/camera/install

ターミナルに以下のコマンドを使い、ライブラリを追加する。
```terminal
flutter pub add google_mlkit_text_recognition camera
```

### 2. Androidの設定
まずは、Androidの設定をしていきます。
android/app/build.gradleを設定していきます。

```diff_gradle
android {

    defaultConfig {
        // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
        applicationId "com.example.flutter_ocr_app"
        // You can update the following values to match your application needs.
        // For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-gradle-build-configuration.
-       minSdkVersion flutter.minSdkVersion
-       targetSdkVersion flutter.minSdkVersion
+       minSdkVersion 22
+       targetSdkVersion 32
        versionCode flutterVersionCode.toInteger()
        versionName flutterVersionName
    }
    
}
```
google_mlkit_text_recognitionでは、ラテン文字の認識のみをサポートします。そのため以下を追加して日本語を認識できるようにする必要があります。その他言語も同様
```diff_gradle
dependencies {
    implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
+   implementation "com.google.mlkit:text-recognition-japanese:16.0.0"
}
```

Androidでカメラを使用するためにAndroidManifest.xmlに権限を追加します。(android/app/src/main/AndroidManifest.xml)

```diff_xml
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.flutter_ocr_app">

    <!-- この下の１行追加 -->
+  <uses-permission android:name="android.permission.CAMERA" />

   <application
        android:label="flutter_ocr_app"
        android:name="${applicationName}"
        android:icon="@mipmap/ic_launcher">
        
    ~~~~~~~~~~~ 中略 ~~~~~~~~~~~

        <meta-data
            android:name="flutterEmbedding"
            android:value="2" />
    </application>
</manifest>

```

### 3. iOSの設定
iOSでカメラを使うために、info.plistにアクセス権限許可の追加します。(ios/Runner/Info.plist)

```diff
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
	~~~~~~~~~~~ 中略 ~~~~~~~~~~~
+	<key>NSCameraUsageDescription</key>
+	<string>your usage description here</string>
+	<key>NSMicrophoneUsageDescription</key>
+	<string>your usage description here</string>
</dict>
</plist>

```
google_mlkit_text_recognitionの他言語の認識のために以下を`ios/Podfile`に追加

```ios/Podfile
pod 'GoogleMLKit/TextRecognitionJapanese', '~> 4.0.0'
```

### 4. 機能の実装
```main.dart
import 'package:camera/camera.dart';
import 'package:flutter/material.dart';
import 'package:flutter_ocr_app/camera_page.dart';

Future<void> main() async {
  WidgetsFlutterBinding.ensureInitialized();
  final cameras = await availableCameras();
  final firstCamera = cameras.first;

  runApp(MyApp(camera: firstCamera));
}

class MyApp extends StatelessWidget {
  const MyApp({
    super.key,
    required this.camera,
  });

  final CameraDescription camera;

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: CameraPage(camera: camera),
    );
  }
}
```

```camera_page.dart
import 'dart:io';

import 'package:camera/camera.dart';
import 'package:flutter/material.dart';
import 'package:flutter_ocr_app/result_page.dart';
import 'package:google_mlkit_text_recognition/google_mlkit_text_recognition.dart';

class CameraPage extends StatefulWidget {
  const CameraPage({
    super.key,
    required this.camera,
  });

  final CameraDescription camera;

  @override
  State<CameraPage> createState() => _CameraPageState();
}

class _CameraPageState extends State<CameraPage> {
  late CameraController _controller;
  late Future<void> _initializeControllerFuture;

  final textRecognizer = TextRecognizer(script: TextRecognitionScript.japanese);

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

    // カメラの設定
    _controller = CameraController(
      widget.camera,
      ResolutionPreset.max,
    );

    // コントローラーを初期化
    _initializeControllerFuture = _controller.initialize();
  }

  @override
  void dispose() {
    _controller.dispose();
    textRecognizer.close();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    // FutureBuilder で初期化を待ってからプレビューを表示（それまではインジケータを表示）
    return Scaffold(
      appBar: AppBar(
        title: const Text('レシートを読み取って下さい'),
      ),
      body: Padding(
        padding: const EdgeInsets.all(48.0),
        child: Center(
          child: FutureBuilder<void>(
            future: _initializeControllerFuture,
            builder: (context, snapshot) {
              if (snapshot.connectionState == ConnectionState.done) {
                return CameraPreview(_controller);
              } else {
                return const Center(child: CircularProgressIndicator());
              }
            },
          ),
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () async {
          try {
            // 写真を撮る
            final image = await _controller.takePicture();

            final file = File(image.path);

            final inputImage = InputImage.fromFile(file);

            final recognizedText =
                await textRecognizer.processImage(inputImage);

            await Navigator.of(context).push(
              MaterialPageRoute(
                builder: (context) => ResultPage(text: recognizedText.text),
                fullscreenDialog: true,
              ),
            );
          } catch (e) {
            ScaffoldMessenger.of(context).showSnackBar(
              const SnackBar(
                content: Text('失敗'),
              ),
            );
          }
        },
        child: const Icon(Icons.camera_alt),
      ),
    );
  }
}

```

レシート撮影後、画面遷移し抽出したTextを表示します。
```result_page.dart
import 'package:flutter/material.dart';
import 'package:google_mlkit_text_recognition/google_mlkit_text_recognition.dart';

class ResultPage extends StatelessWidget {
  final RecognizedText recognizedText;

  const ResultPage({super.key, required this.recognizedText});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('結果'),
      ),
      body: Container(
        padding: const EdgeInsets.all(30.0),
        child: Text(recognizedText.text),
      ),
    );
  }
}
```

# 実際に動かした時にスクショ
<img width="240" alt="Screenshot_20231224_021421_Gallery.jpg" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3510232/5229d6cc-15ed-a07d-584e-ab02bf9f6c6a.jpeg">

<img width="240" alt="Screenshot_20231224_021409_Gallery.jpg" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3510232/ec98442c-b340-ee1e-10ff-7e29c446d139.jpeg">



正しく抽出できていない箇所が少しありますが、基本的には認識できています。
レシートの情報としては、加工の過程を省いているので、これからの加工の仕方で、使えるデータになってくると思います。