Flutter で ML Kit for Firebase を使ってカメラと連動したアプリを開発するときには、次のような実装が多いと思います。
- camera パッケージの CameraPreview を使ってカメラのプレビューを表示させる
- 同時に、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.fromBytes
に planes[0]
しか渡していない点です。FirebaseVisionImage.fromBytes のドキュメント によれば Android では NV21 フォーマット(YUV_420_888 の plane を連結したフォーマット)のバイト列が渡されることを期待しているのですが、画像処理では輝度成分、すなわち YUV の Y 成分しか必要ないことも多いので planes[0]
だけ渡していると考えられます。こうなってくると planeData
も planes[0]
の分しか必要ないのではないかと思えてきましたが、検証していないのでわかりません。
まとめ
私が開発するときには以下の方針で進めようと思います。
- 基本的に bparrishMines/mlkit_demo を参考にする。
- 最近はカメラの orientation が取得できるようになっているので、ちゃんと処理する。
- カラー画像である必要が無いのであれば輝度成分だけでもいいかも。