Flutterでアプリを作っています。裏で定期的に写真を撮ります。
iOSだと定期的に「パシャ」「パシャ」と音が鳴りかなりかっこ悪いです。
その解決方法を紹介します。
経緯
- CameraController.takePicture() を使う。
→ Androidは無音ですがiOSで音が出るのでNG。 - Swift の captureOutput() を使う。
→ 実際Swiftで使ってます。Flutter向けにパッケージ化するのが面倒なのでNG。 - CameraController.startVideoRecording() で1秒録画してサムネイルを作る。
→ 結構試しました。10回に1回くらい失敗するのでNG。 - CameraController.startImageStream()を使う方法。
→ 今回の方法です。
startImageStream() を使う
CameraController.startImageStream() を呼び出すと最新のカメラ画像を常に取得します。
CameraImage? _cImage;
cameraController.startImageStream((CameraImage cameraImage) async {
_cImage = cameraImage; // ←最新のカメラ画像を取得し続ける
});
問題点と改善点
- 処理が重いです。カメラサイズを240pや480pにすると改善します。
- 使うときだけ起動します。起動に400ミリ秒ほどかかります。
どちらの方法も長所と短所があります。
私の目的では使うときだけ起動する方法がよいです。
ソースコード
import 'package:camera/camera.dart';
import 'package:image/image.dart' as imglib;
import 'dart:async';
// 実行例
func() async {
cameraController.startImageStream((CameraImage cameraImage) async {
controller.stopImageStream(); // 1回で停止
imglib.Image? img = await _toImage(controller, cameraImage); // 変換
});
}
// CameraImageからImageに変換
static Future<imglib.Image?> _toImage(CameraController? controller, CameraImage? cameraImage) async {
if(controller==null || cameraImage==null) {
return null;
}
imglib.Image? img;
if (cameraImage.format.group == ImageFormatGroup.yuv420) {
img = await _fromYuv(cameraImage);
if(img!=null){
int angle = controller.description.sensorOrientation;
if(angle>0)
img = imglib.copyRotate(img, angle); // 回転
}
} else if (cameraImage.format.group == ImageFormatGroup.bgra8888) {
img = _fromRgb(cameraImage);
}
return img;
}
// YUVからRGBに変換
static Future<imglib.Image?> _fromYuv(CameraImage? image) async {
if(image==null)
return null;
final int width = image.width;
final int height = image.height;
if(image.planes.length<3) {
print('err _fromYuv() planes.length=${image.planes.length}');
return null;
} else if(image.planes[1].bytesPerPixel==null) {
print('err _fromYuv() planes[1].bytesPerPixel=null');
return null;
} else if(image.planes[0].bytes.length<width*height) {
print('err _fromYuv() planes[0].bytes.length=${image.planes[0].bytes.length}');
return null;
}
try {
imglib.Image img = imglib.Image(width, height);
final int perRow = image.planes[1].bytesPerRow;
final int perPixel = image.planes[1].bytesPerPixel!;
for(int ay=0; ay < height; ay++) {
for(int ax=0; ax < width; ax++) {
final int index = ax + (ay * width);
final int uvIndex = perPixel * (ax / 2).floor() + perRow * (ay / 2).floor();
final y = image.planes[0].bytes[index];
final u = image.planes[1].bytes[uvIndex];
final v = image.planes[2].bytes[uvIndex];
int r = (y + v * 1436 / 1024 - 179).round().clamp(0, 255);
int g = (y - u * 46549 / 131072 + 44 - v * 93604 / 131072 + 91).round().clamp(0, 255);
int b = (y + u * 1814 / 1024 - 227).round().clamp(0, 255);
img.data[index] = (0xFF << 24) | (b << 16) | (g << 8) | r;
}
}
return img;
} catch (e) {
print("err _fromYuv() " + e.toString());
}
return null;
}
static imglib.Image _fromRgb(CameraImage image) {
return imglib.Image.fromBytes(
image.width, image.height,
image.planes[0].bytes,
format:imglib.Format.bgra,
);
}
解説
- YUVをRGBに変換するのがポイントです。
- 1回だけ実行して終了します。
- 回転も考慮します。
- 失敗するときはCameraControllerのimageFormatGroupをyuv420をbgra8888にして試してください。
まとめ
シャッター音が出ない方法を記事にしました。ネットで shutter sound で検索すると色々と出てきます。みんなも困ってるみたいです。
FlutterではAndroidでOKでもiOSでNGって場合があります。それでもSwiftとKotlinでやるよりもはるかに便利です。Flutterが流行るようにこれからも記事を書こう思います。
私はカメラと機械学習を組み合わせたアプリを作ろとしています。カメラアプリは今までなかったビジネス領域ですし、かなりの可能性を感じています。同じ目的を持っている人がいたら、是非一緒にやりましょう。