@MilosNaniwa

Firebase ML Visionを使ってリアルタイムにバーコードをスキャンする機能を自作した

この記事は最終更新日から1年以上が経過しています。

概要

FlutterではQRスキャン機能を持ったパッケージが複数あるが、スキャン部分がネイティブで書かれていたりして微妙に使い辛い。
また、Flutterのグレードアップに伴って利用できなくなるケースも多々ある。(本業では苦しめられた)
苦しめられたくないので、Google公式のパッケージであるcameraFirebaseMLを組み合わせてスキャン機能を自作した。
これを応用することで、リアルタイム顔認識機能やOCR機能をアプリに組み込むことも可能。
Flutterを実運用し始めてから約1年強、パッケージの依存関係には散々苦しめられたので、できる限り自作していこう(自戒)。

実際の挙動はこちら。
ezgif.com-gif-maker.gif

実装

FirebaseをFlutterに組み込む方法や、FirebaseMLの環境設定などは公式ドキュメントに載っているので割愛する。
今回必要なパッケージはcamerafirebase_ml_visionの2つだけ。

pubspec.yaml
name: simple_qr_scan_sample
description: A new Flutter application.

version: 1.0.0+1

environment:
  sdk: ">=2.1.0 <3.0.0"

dependencies:
  flutter:
    sdk: flutter

  camera: ^0.5.7+3 // 追加
  firebase_ml_vision: ^0.9.3+5 // 追加

dev_dependencies:
  flutter_test:
    sdk: flutter

flutter:
  uses-material-design: true

最小限?の実装は下記の通り。
特段難しいことはしていないが、camerastartImageStreamの使い方を書いたドキュメントがイマイチなかったので少々苦戦した。

main.dart
import 'package:camera/camera.dart';
import 'package:firebase_ml_vision/firebase_ml_vision.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Simple QR Scan',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: MyHomePage(),
    );
  }
}

class MyHomePage extends StatefulWidget {
  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  CameraController _cameraController;
  bool _shouldSkipScanning;
  String _scannedData;

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

    // 画面の向きをポートレートモード(縦向き)に固定する
    // 固定する必要性はないが、ランドスケープモードの比率の計算が面倒だったので。
    SystemChrome.setPreferredOrientations([
      DeviceOrientation.portraitUp,
      DeviceOrientation.portraitDown,
    ]);

    _shouldSkipScanning = false;
    _scannedData = 'スキャン中…';

    _cameraController = CameraController(
      null,
      null,
    );

    // この端末で利用可能なカメラを検出する
    availableCameras().then((value) {
      List<CameraDescription> cameraDescriptionList = value;

      _cameraController = CameraController(
        cameraDescriptionList[0], // 背面カメラを利用する。前面カメラならリスト1番目。
        ResolutionPreset.high, // 解像度高め。低すぎると認識精度が落ちる。
      );

      _cameraController.initialize().then((_) {
        if (!mounted) {
          return;
        }

        // スキャン処理を実行する
        _scanQrCode();

        setState(() {});
      });
    });
  }

  @override
  void dispose() {
    _cameraController.dispose();
    super.dispose();
  }

  Future<void> _scanQrCode() async {
    _cameraController.startImageStream(
      (CameraImage availableImage) async {
        if (_shouldSkipScanning) {
          return;
        }

        _shouldSkipScanning = true;

        // スキャンしたイメージをFirebaseMLで使える形式に変換する
        final FirebaseVisionImageMetadata metadata =
            FirebaseVisionImageMetadata(
          rawFormat: availableImage.format.raw,
          size: Size(
            availableImage.width.toDouble(),
            availableImage.height.toDouble(),
          ),
          planeData: availableImage.planes
              .map(
                (currentPlane) => FirebaseVisionImagePlaneMetadata(
                  bytesPerRow: currentPlane.bytesPerRow,
                  height: currentPlane.height,
                  width: currentPlane.width,
                ),
              )
              .toList(),
        );
        final FirebaseVisionImage visionImage = FirebaseVisionImage.fromBytes(
          availableImage.planes.first.bytes,
          metadata,
        );

        // バーコードを検出する
        final BarcodeDetector barcodeDetector =
            FirebaseVision.instance.barcodeDetector();
        final List<Barcode> barcodeList = await barcodeDetector.detectInImage(
          visionImage,
        );

        if (barcodeList.length != 0) {
          setState(() {
            _scannedData = barcodeList.first.rawValue;
          });
          _shouldSkipScanning = false;
        } else {
          _shouldSkipScanning = false;
        }
      },
    );
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: Colors.black87,
      body: _cameraController.value.isInitialized // 初期化が終わったらカメラ映像を描画
          ? Column(
              mainAxisSize: MainAxisSize.max,
              mainAxisAlignment: MainAxisAlignment.center,
              children: <Widget>[
                Stack(
                  alignment: Alignment.center,
                  children: <Widget>[
                    AspectRatio(
                      aspectRatio:
                          1 / _cameraController.value.previewSize.aspectRatio,
                      child: CameraPreview(
                        _cameraController,
                      ),
                    ),
                    Column(
                      children: <Widget>[
                        Container(
                          height: MediaQuery.of(context).size.height * 0.6,
                        ),
                        FittedBox( // スキャンしたデータが何文字になるか分からないのでFittedBoxで囲った
                          child: Text(
                            _scannedData,
                            style: TextStyle(
                              color: Colors.greenAccent,
                              fontSize: 40.0,
                            ),
                          ),
                        ),
                      ],
                    ),
                  ],
                ),
              ],
            )
          : Center(
              child: CircularProgressIndicator(),
            ),
    );
  }
}

参考

Camera document how to use ImageStream
https://github.com/flutter/flutter/issues/26348

ユーザー登録して、Qiitaをもっと便利に使ってみませんか。
  1. あなたにマッチした記事をお届けします
    ユーザーやタグをフォローすることで、あなたが興味を持つ技術分野の情報をまとめてキャッチアップできます
  2. 便利な情報をあとで効率的に読み返せます
    気に入った記事を「ストック」することで、あとからすぐに検索できます
MilosNaniwa
ギタリーマンエンジニア。1990年10月生。 【Skill】サービスを作ってリリースすること。 【Favorite】Flutter, Firebase, GCP, Blockchain 【Language】Dart, JavaScript, TypeScript, Bash, Java, C#

コメント

この記事にコメントはありません。
あなたもコメントしてみませんか :)
ユーザー登録
すでにアカウントを持っている方はログイン
記事投稿イベント開催中
競技プログラミング研究月間 - みんなでさらなる高みを目指そう
~
Azure AIを活用した機械学習に関する記事を投稿しよう!
~