LoginSignup
6
5

More than 3 years have passed since last update.

Flutter で CameraImage から FirebaseVisionImage に変換する

Last updated at Posted at 2019-05-01

Flutter で ML Kit for Firebase を使ってカメラと連動したアプリを開発するときには、次のような実装が多いと思います。

  1. camera パッケージの CameraPreview を使ってカメラのプレビューを表示させる
  2. 同時に、startImageStream でカメラから画像の取得を連続して行い、取得した画像を ML Kit for Firebase で処理する

ここで、2. でのカメラからの画像の取得では、startImageStream の引数に渡したコールバック関数には CameraImage という型のデータが与えられます。この CameraImage という型は firebase_ml_vision(ML Kit for Firebase の公式ライブラリ)で使用される FirebaseVisionImage という型とは別の型なので、型を合わせる処理が必要です。

ところが、そのための方法は公式ドキュメントに載っていません。GitHub でサンプルコードを眺めてみても処理方法がバラバラでした。ドキュメント拡充の要望は Issue に上がっているので、困っている方は reaction をつけましょう。

とはいえドキュメントが拡充されるには時間がかかります。ML Kit for Firebase を利用するための最も無難な処理方法を見つけるため、GitHub 上で見つかるサンプルコードでの処理方法を眺めてみました。

調査方法

GitHub の全体検索で startImageStream を含む Dart のコードを検索して、調査時点で動作確認ができたものを列挙しました。

重複は個人の判断で排除しました。

調査結果

GoogleCloudPlatform/iot-smart-home-cloud

// Collect all planes into a single buffer
final WriteBuffer allBytesBuffer = WriteBuffer();
image.planes.forEach((Plane plane) => allBytesBuffer.putUint8List(plane.bytes));
final Uint8List allBytes = allBytesBuffer.done().buffer.asUint8List();

// Convert the image buffer into a Firebase detector frame
FirebaseVisionImage firebaseImage = FirebaseVisionImage.fromBytes(allBytes,
  FirebaseVisionImageMetadata(
    rawFormat: image.format.raw,
    size: Size(image.width.toDouble(), image.height.toDouble()),
    rotation: ImageRotation.rotation90,
    planeData: image.planes.map((plane) => FirebaseVisionImagePlaneMetadata(
      height: plane.height,
      width: plane.width,
      bytesPerRow: plane.bytesPerRow,
    )).toList(),
  ),
);

Google が出しているので、これを参考にしている人も多そうです。rotation: ImageRotation.rotation90 と決め打ちしてしまっているのが少し気になりますね。

bparrishMines/mlkit_demo

Uint8List concatenatePlanes(List<Plane> planes) {
  final WriteBuffer allBytes = WriteBuffer();
  planes.forEach((Plane plane) => allBytes.putUint8List(plane.bytes));
  return allBytes.done().buffer.asUint8List();
}

FirebaseVisionImageMetadata buildMetaData(
    CameraImage image,
    ImageRotation rotation,
    ) {
  return FirebaseVisionImageMetadata(
    rawFormat: image.format.raw,
    size: Size(image.width.toDouble(), image.height.toDouble()),
    rotation: rotation,
    planeData: image.planes.map(
          (Plane plane) {
        return FirebaseVisionImagePlaneMetadata(
          bytesPerRow: plane.bytesPerRow,
          height: plane.height,
          width: plane.width,
        );
      },
    ).toList(),
  );
}

Future<List<Face>> detect(
    CameraImage image,
    HandleDetection handleDetection,
    ImageRotation rotation,
    ) async {
  return handleDetection(
    FirebaseVisionImage.fromBytes(
      concatenatePlanes(image.planes),
      buildMetaData(image, rotation),
    ),
  );
}

ImageRotation rotationIntToImageRotation(int rotation) {
  switch (rotation) {
    case 0:
      return ImageRotation.rotation0;
    case 90:
      return ImageRotation.rotation90;
    case 180:
      return ImageRotation.rotation180;
    default:
      assert(rotation == 270);
      return ImageRotation.rotation270;
  }
}

公式とは関係のないリポジトリと思いきや、Flutter Live で登壇している Flutter チームのエンジニア のリポジトリなので、変なことはしていないと期待できます。

大まかな処理は GoogleCloudPlatform/iot-smart-home-cloud と同じですが、rotation をちゃんと計算しているところが違います。CameraDescription の sensorOrientation を rotationIntToImageRotation に渡して ImageRotation を取ってくるようです。

orientation 周りを処理しているサンプルコードがなかなか見つからないのですが、これは Flutter で orientation を取得できるようになったのが最近のことだからです。camera ライブラリでは orientation はバージョン 0.4.2 から取得できるようになりました。今から開発を行うのであれば、ちゃんと処理したほうがよいでしょう。

0.4.2
Add sensor orientation value to CameraDescription.

dshukertjr/snow

final FirebaseVisionImageMetadata metadata = FirebaseVisionImageMetadata(
  rawFormat: availableImage.format.raw,
  planeData: availableImage.planes
      .map(
        (currentPlane) => FirebaseVisionImagePlaneMetadata(
              bytesPerRow: currentPlane.bytesPerRow,
              height: currentPlane.height,
              width: currentPlane.width,
            ),
      )
      .toList(),
  size: Size(
      availableImage.width.toDouble(), availableImage.height.toDouble()),
  rotation: ImageRotation.rotation270,
);

final FirebaseVisionImage visionImage =
    FirebaseVisionImage.fromBytes(availableImage.planes[0].bytes, metadata);

rotation: ImageRotation.rotation270 を決め打ちしてしまっている点は GoogleCloudPlatform/iot-smart-home-cloud と同じです。

おもしろいのは FirebaseVisionImage.fromBytesplanes[0] しか渡していない点です。FirebaseVisionImage.fromBytes のドキュメント によれば Android では NV21 フォーマット(YUV_420_888 の plane を連結したフォーマット)のバイト列が渡されることを期待しているのですが、画像処理では輝度成分、すなわち YUV の Y 成分しか必要ないことも多いので planes[0] だけ渡していると考えられます。こうなってくると planeDataplanes[0] の分しか必要ないのではないかと思えてきましたが、検証していないのでわかりません。

まとめ

私が開発するときには以下の方針で進めようと思います。

  • 基本的に bparrishMines/mlkit_demo を参考にする。
  • 最近はカメラの orientation が取得できるようになっているので、ちゃんと処理する。
  • カラー画像である必要が無いのであれば輝度成分だけでもいいかも。
6
5
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
5