【Flutter】
cameraパッケージについてのTips

記事イメージ

当社で開発中に新アプリ(iOS/Android)にてcameraパッケージを使用しました。

従来はimage_pickerをよく利用していましたが、cameraパッケージの方が汎用的に利用できそうです。

せっかくなので今回得られた知見などを共有します。

cameraパッケージの概要

端末のカメラへのアクセスを環境ごとにラップしてくれるライブラリです。

プレビュー(カメラに写っているリアルタイムの映像をウィジェットに表示)する機能や、ストリーミング、写真撮影など汎用的な機能を備えています。

image_pickerとの違い

Flutterでカメラを扱える他のパッケージとしてはimage_pickerが存在します。

image_pickerは組み込みのカメラビューを呼び出しする一方、cameraパッケージは端末カメラからの入力をウィジェットに組み込んでプレビューやストリーミングが可能な点が異なります。

組み込みのカメラビューは安定的ですが、連続使用や機械学習モデルへのリアルタイム処理などカスタマイズ的な用途には対応できませんので、cameraパッケージを利用することになります。`

導入方法

公式ページの通りですが、次のコマンド・コードで導入します。


$ flutter pub add camera


import 'package:camera/camera.dart';

基本的な操作紹介

まずはカメラオブジェクトを初期化します。

以降の各操作はここで生成したcameraControllerオブジェクトを使用します。ローカル変数に格納するなどして使いまわしましょう。

カメラの解像度はCameraControllerオブジェクトのコストラクタでResolutionPreset列挙体を使用して指定します。

指定値によってはiOSとAndroidで縦横比やサイズが異なるので注意しましょう。ResolutionPreset enum(pub.dev)に詳細が記載されています。

Dart
WidgetsFlutterBinding.ensureInitialized();
final cameras = await availableCameras();
final camera = cameras.first;
//↓で解像度を指定
final cameraController = CameraController(camera, ResolutionPreset.high);
await cameraController.initialize();

UI上にプレビュー(カメラに写っているリアルタイム画像)を表示します。

CameraPreview関数がプレビューを表示するウィジェットを生成します。

Dart
@override
Widget build(BuildContext context) {
  return Scaffold(
    appBar: AppBar(
      title: Text("分類作業カメラ"),
    ),
    //カメラに映る映像をリアルタイムで表示
    body: CameraPreview(cameraController),
  );
}

そして、撮影は次のように行います。ボタンウィジェット等から呼び出しましょう。

Dart
//XFile型で結果を得られます。
final photo = await cameraController!.takePicture();

cameraパッケージいろいろテクニック

正方形の写真を撮りたい場合の実現方法

Cameraパッケージでは正方形の写真撮影には対応していません。

アスペクト比と伸縮を計算して実現する方法もありますが、ここはよりシンプルに「UI上は白色のブロックをプレビューに乗せて隠す」、保存処理は「座標計算で切り抜く」という形で実現してみます。

実現しようとする内容のイメージ。StackウィジェットでUI上で簡易的に正方形カメラを実現します。撮影後には当然ながらトリミング処理が必要です。
実現しようとする内容のイメージ

まずはウィジェットを組み立てます。

重ねて表示するには、Stackウィジェットを使用します。

childrenの要素にPositionedウィジェットを使用することで、Stack内の相対位置を指定できます。

Dart
    //サイズ計算(下半分くらいを隠してカメラが正方形となるように目隠しの縦横と位置を計算します。)
    final previewWidth = MediaQuery.of(context).size.width - 22.0;
    final previewHeight = previewWidth;
    final previewBottomCoverHeight = previewWidth * cameraController!.value.previewSize!.aspectRatio - previewWidth;

Dart
    //Widget組み立て
    Stack(
      children: [
        //プレビューウィジェット
        SizedBox(width: previewWidth, child: CameraPreview(cameraController!)),
        //目隠しウィジェット
        Positioned(
          top: previewHeight, //Stack内の目隠し位置を指定
          child: Container(
            width: previewWidth, 
            height: previewBottomCoverHeight,
            color: Colors.white)
          ),
        ),
        //撮影ボタン(目隠しのやや下に配置)
        Positioned(
          top: previewHeight + 24,
          child: SizedBox(
            width: previewWidth,cameraLock
            height: 44,
            child:ElevatedButton(
            child: Text("撮影"),
            onPressed: () async {
              final photo = await cameraController.takePicture();
            },
          )
        )
      ),
    ]
),

最後にこの方法で撮影した場合の画像処理(正方形に切り抜き)を行います。

上記の方法ではUIで目隠ししているだけなので、当然ながら撮影すると縦長の長方形画像が得られます。

UI上はプレビューの下側に目隠しを入れているので、左上座標(0,0)を起点に画像横幅と同じだけのwidth、heightで切り抜きすればOKです。

処理にはimageパッケージを利用します。

Dart
import 'package:path_provider/path_provider.dart';
import 'package:camera/camera.dart';
import 'package:image/image.dart';
import 'dart:io';

//保存する横幅
final saveImageWidth = 300;
//保存するパス
final path = join((await getTemporaryDirectory()).path, "IMG_1234.png");

//cameraパッケージで撮影(takePicture関数をコール)
final photo = await cameraController.takePicture();
//imageパッケージのImage型に変換
final image = decodeImage(await File(photo.path).readAsBytes())!;
//左上を起点に正方形(縦横同じ長さ)に切り抜き
final croppedImage = copyCrop(image, 0, 0, saveImageWidth, saveImageWidth);

//切り抜いた画像をdart:ioのFileオブジェクトに変換
final imageFile = await File(path).writeAsBytes(encodePng(croppedImage));

CameraExceptionへの対応

takePictureを連続して使用すると、以下のように「前の処理がまだ終わっていないよ」という例外が投げられます。

await句で同期処理を行なっているにもかかわらず、この例外は発生する場合があるので注意しましょう。

CameraException(Previous capture has not returned yet., takePicture was called before the previous capture returned.)

takePictureの戻り値を得ていることを確認して実行タイミングを制御すればOKです。

クラス変数で以下のようにして対処可能です。

Dart
try {
    //ローカル変数cameraLockで連続実行を制御します。
    if (!this.cameraLock) {
        return;
    }
    this.cameraLock = true;
    final photo = await cameraController!.takePicture();

    //do something

} finally {
    this.cameraLock = false;
}

フラッシュの制御が動作しない問題

次のコードでフラッシュのオンオフ自動を制御できるようなのですが、実際に試したところワークしませんでした。

Dart
cameraController.setFlashMode(FlashMode.off);

本件について、GitHubのイシューでも議論されている様子です。

当社でも研究中です。可能性は低いですが、もし打開策が見つければこちらで更新したいと思います。

記事筆者へのお問い合わせ、仕事のご依頼

当社では、IT活用をはじめ、業務効率化やM&A、管理会計など幅広い分野でコンサルティング事業・IT開発事業を行っております。

この記事をご覧になり、もし相談してみたい点などがあれば、ぜひ問い合わせフォームまでご連絡ください。

皆様のご投稿をお待ちしております。

記事筆者へ問い合わせする

※ご相談は無料でお受けいたします。
IT活用経営を実現する - 堺財経電算合同会社