【Flutter】
cameraパッケージで正方形カメラを構築

記事イメージ

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

その中で正方形カメラを作りたいと考えたのですが、cameraパッケージでは対応しておらず一工夫を加えて実現することとなりました。

今回はその内容を共有します。

※cameraパッケージについては、基本的なコードやTipsを別途公開しています。併せてご参考ください。

関連記事:【Flutter】cameraパッケージの基本とTips

背景と方向性

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

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

実現しようとする内容のイメージ。StackウィジェットでUI上で簡易的に正方形カメラを実現します。撮影後には当然ながらトリミング処理が必要です。
実現しようとする内容のイメージ
実現したUI(当社開発App「分類作業カメラ」のキャプチャ)
実現したUI(当社開発App「分類作業カメラ」のキャプチャ)

手順1:端末方向の固定

端末が回転すると計算のし直しが発生します。ここでは簡単のため、端末方向を縦に固定します。

main関数(エントリーポイントとなっている関数)に以下の通りコードを追加します。

Dart
void main() {
  WidgetsFlutterBinding.ensureInitialized();
  SystemChrome.setPreferredOrientations([
    DeviceOrientation.portraitUp,//縦固定
  ]);
  runApp(MyApp());
}

手順2:イニシャライズ時にカメラの方向を固定

cameraパッケージはデフォルト状態では端末の方向を検知してアスペクトを再計算しています。

今回は端末方向を固定するので、cameraパッケージ側にも縦に固定するよう指示を入れます。

Dart
//先に初期化
await _cameraController!.initialize();
//カメラの方向を縦方向に固定
await _cameraController!.lockCaptureOrientation(DeviceOrientation.portraitUp);

手順3:ウィジェット構築

続いてウィジェットを組み立てていきます。プレビューの上に目隠しを重ねて表示するため、Stackウィジェットを使用します。

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: [
        //Stack範囲全体を広げるために設置。これがないと、撮影ボタンが見切れる。
        SizedBox(height: previewHeight + 96,),
        //プレビューウィジェット
        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();

              //撮影後の処理

            },
          )
        )
      ),
    ]
),

手順4:得られた画像をUIに合わせてトリミング

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

上記の方法では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 resizedImage = copyResize(image, width: saveImageWidth);
//左上を起点に正方形(縦横同じ長さ)に切り抜き
final croppedImage = copyCrop(resizedImage, 0, 0, saveImageWidth, saveImageWidth);

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

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

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

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

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

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

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