Flutterでシャッター音なしで写真を撮る方法

Flutterでアプリを作っています。裏で定期的に写真を撮ります。
iOSだと定期的に「パシャ」「パシャ」と音が鳴りかなりかっこ悪いです。
その解決方法を紹介します。

経緯
- CameraController.takePicture() を使う。
  → Androidは無音ですがiOSで音が出るのでNG。
- Swift の captureOutput() を使う。
  → 実際Swiftで使ってます。Flutter向けにパッケージ化するのが面倒なのでNG。
- CameraController.startVideoRecording() で1秒録画してサムネイルを作る。
  → 結構試しました。10回に1回くらい失敗するのでNG。
- CameraController.startImageStream()を使う方法。
  → 今回の方法です。

# startImageStream() を使う

CameraController.startImageStream() を呼び出すと最新のカメラ画像を常に取得します。

```dart
  CameraImage? _cImage;

  cameraController.startImageStream((CameraImage cameraImage) async {
    _cImage = cameraImage; // ←最新のカメラ画像を取得し続ける
  });
```

問題点と改善点
1. 処理が重いです。カメラサイズを240pや480pにすると改善します。
2. 使うときだけ起動します。起動に400ミリ秒ほどかかります。

どちらの方法も長所と短所があります。
私の目的では使うときだけ起動する方法がよいです。

# ソースコード

```dart
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が流行るようにこれからも記事を書こう思います。

私はカメラと機械学習を組み合わせたアプリを作ろとしています。カメラアプリは今までなかったビジネス領域ですし、かなりの可能性を感じています。同じ目的を持っている人がいたら、是非一緒にやりましょう。