画像解析

画像解析ユースケースは、CPU からアクセスできる画像をアプリに提供し、それらの画像に対して画像処理、コンピュータ ビジョン、機械学習推論を実行します。アプリには、各フレームで実行する analyze() メソッドを実装します。

動作モード

アプリの分析パイプラインが CameraX のフレームレート要件に対応できない場合、次のいずれかの方法でフレームをドロップするように CameraX を設定できます。

  • 非ブロッキング(デフォルト): このモードでは、エグゼキュータは常に最新の画像を画像バッファにキャッシュし(深度 1 のキューと同様)、その間にアプリは前の画像を分析します。アプリが処理を完了する前に CameraX が新しい画像を受信すると、新しい画像が同じバッファに保存され、前の画像が上書きされます。このシナリオでは ImageAnalysis.Builder.setImageQueueDepth() は効果がなく、バッファの内容は常に上書きされます。この非ブロッキング モードを有効にするには、STRATEGY_KEEP_ONLY_LATESTsetBackpressureStrategy() を呼び出します。 エグゼキュータの影響について詳しくは、STRATEGY_KEEP_ONLY_LATEST のリファレンス ドキュメントをご覧ください。

  • ブロッキング: このモードでは、内部エグゼキュータは内部画像キューに複数の画像を追加でき、キューがいっぱいになったときにのみフレームのドロップを開始します。ブロッキングはカメラデバイスのスコープ全体で発生します。カメラデバイスにバインドされたユースケースが複数ある場合、CameraX がこれらの画像を処理している間、ユースケースはすべてブロックされます。たとえば、プレビューと画像分析の両方がカメラデバイスにバインドされている場合、CameraX による画像の処理中もプレビューがブロックされます。ブロッキング モードを有効にするには、STRATEGY_BLOCK_PRODUCERsetBackpressureStrategy() に渡します。 ImageAnalysis.Builder.setImageQueueDepth() を使用して、画像のキュー深度を設定することもできます。

低レイテンシで高パフォーマンスのアナライザの場合、画像分析にかかる合計時間が CameraX のフレームの長さ(たとえば、60 fps では 16 ミリ秒)未満であれば、どちらの動作モードでも全体的にスムーズな動作が維持されます。ただし、非常に短いシステム ジッターを処理する場合などは、ブロッキング モードが役立つことがあります。

高レイテンシで高パフォーマンスのアナライザでは、ブロッキング モードと長いキューでレイテンシを補正する必要があります。ただし、この場合でもアプリはすべてのフレームを処理できます。

高レイテンシで時間のかかるアナライザ(アナライザはすべてのフレームを処理できません)では、分析パスのフレームをドロップする必要があるため、非ブロッキング モードの方が適している場合があります。ただし、同時にバインドされている他のユースケースでもすべてのフレームが表示されます。

実装

アプリで画像解析を使用する手順は次のとおりです。

バインドの直後、CameraX が登録済みのアナライザに画像を送信します。 分析が完了したら、ImageAnalysis.clearAnalyzer() を呼び出すか、ImageAnalysis ユースケースをアンバインドして分析を停止します。

ImageAnalysis ユースケースを作成する

ImageAnalysis は、アナライザ(画像コンシューマ)を、画像プロデューサーである CameraX に接続します。アプリは、ImageAnalysis.Builder を使用して ImageAnalysis オブジェクトを作成します。ImageAnalysis.Builder を使用すると、アプリで以下を設定できます。

アプリは、解像度またはアスペクト比のいずれかを設定できますが、両方を設定することはできません。正確な出力解像度は、アプリで要求されるサイズ(またはアスペクト比)とハードウェア機能に依存し、要求されるサイズや比率とは異なる場合があります。解像度マッチング アルゴリズムについては、setTargetResolution() のドキュメントをご覧ください。

アプリは、出力画像ピクセルを YUV 色空間(デフォルト)または RGBA 色空間の形式に設定します。RGBA 出力形式を設定すると、CameraX は内部で画像を YUV 色空間から RGBA 色空間に変換し、ImageProxy の最初のプレーン(他の 2 つのプレーンは不使用)の ByteBuffer に、次の順序で画像ビットを格納します。

ImageProxy.getPlanes()[0].buffer[0]: alpha
ImageProxy.getPlanes()[0].buffer[1]: red
ImageProxy.getPlanes()[0].buffer[2]: green
ImageProxy.getPlanes()[0].buffer[3]: blue
...

デバイスがフレームレート要件に対応できない複雑な画像解析を行う場合は、このトピックの動作モードセクションに記載されている戦略を使用してフレームをドロップするように CameraX を設定できます。

アナライザを作成する

アプリは、ImageAnalysis.Analyzer インターフェースを実装して analyze(ImageProxy image) をオーバーライドすることで、アナライザを作成します。各アナライザで、アプリは ImageProxyMedia.Image のラッパー)を受け取ります。 画像形式は ImageProxy.getFormat() で確認できます。この形式は、アプリが ImageAnalysis.Builder で提供する次の値のいずれかです。

  • ImageFormat.RGBA_8888: アプリが OUTPUT_IMAGE_FORMAT_RGBA_8888 をリクエストした場合。
  • ImageFormat.YUV_420_888: アプリが OUTPUT_IMAGE_FORMAT_YUV_420_888 をリクエストした場合。

色空間の設定とピクセルバイトを取得できる場所については、BuildImageAnalysis ユースケースをご覧ください。

アナライザ内では、アプリケーションは次の処理を行う必要があります。

  1. 指定されたフレームをできるだけ早く、指定されたフレームレートの制限内(たとえば、30 fps の場合は 32 ミリ秒未満)で分析する。アプリがフレームを迅速に分析できない場合は、サポートされているフレーム ドロップの仕組みのいずれかを使用することをおすすめします。
  2. ImageProxy.close() を呼び出して、ImageProxy を CameraX にリリースします。ラップされた Media.Image のクローズ関数(Media.Image.close())は呼び出さないでください。

アプリは ImageProxy 内のラップされた Media.Image を直接使用します。ラップされた画像に対して Media.Image.close() を呼び出さないでください。CameraX 内の画像共有メカニズムが機能しなくなります。代わりに、ImageProxy.close() を使用して基盤となる Media.Image を CameraX にリリースします。

ImageAnalysis 向けにアナライザを設定する

アナライザを作成したら、ImageAnalysis.setAnalyzer() を使用してアナライザを登録し、分析を開始します。分析が完了したら、ImageAnalysis.clearAnalyer() を使用して、登録済みのアナライザを削除します。

画像を分析するように設定できるアクティブなアナライザは 1 つのみです。登録済みのアナライザがすでに存在する場合は、ImageAnalysis.setAnalyzer() を呼び出すと、そのアナライザが置き換えられます。アプリは、ユースケースのバインドの前でも後でも、随時新しいアナライザを設定できます。

ImageAnalysis をライフサイクルにバインドする

ProcessCameraProvider.bindToLifecycle() 関数を使用して、ImageAnalysis を既存の AndroidX ライフサイクルにバインドすることを強くおすすめします。bindToLifecycle() 関数は、選択した Camera デバイスを返します。このデバイスを使用して、露出などの詳細設定を微調整できます。カメラ出力の制御について詳しくは、こちらのガイドをご覧ください。

次の例では、上記の手順をすべて組み合わせて、CameraX の ImageAnalysis ユースケースと Preview ユースケースを lifeCycle オーナーにバインドします。

Kotlin

val imageAnalysis = ImageAnalysis.Builder()
    // enable the following line if RGBA output is needed.
    // .setOutputImageFormat(ImageAnalysis.OUTPUT_IMAGE_FORMAT_RGBA_8888)
    .setTargetResolution(Size(1280, 720))
    .setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
    .build()
imageAnalysis.setAnalyzer(executor, ImageAnalysis.Analyzer { imageProxy ->
    val rotationDegrees = imageProxy.imageInfo.rotationDegrees
    // insert your code here.
    ...
    // after done, release the ImageProxy object
    imageProxy.close()
})

cameraProvider.bindToLifecycle(this as LifecycleOwner, cameraSelector, imageAnalysis, preview)

Java

ImageAnalysis imageAnalysis =
    new ImageAnalysis.Builder()
        // enable the following line if RGBA output is needed.
        //.setOutputImageFormat(ImageAnalysis.OUTPUT_IMAGE_FORMAT_RGBA_8888)
        .setTargetResolution(new Size(1280, 720))
        .setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
        .build();

imageAnalysis.setAnalyzer(executor, new ImageAnalysis.Analyzer() {
    @Override
    public void analyze(@NonNull ImageProxy imageProxy) {
        int rotationDegrees = imageProxy.getImageInfo().getRotationDegrees();
            // insert your code here.
            ...
            // after done, release the ImageProxy object
            imageProxy.close();
        }
    });

cameraProvider.bindToLifecycle((LifecycleOwner) this, cameraSelector, imageAnalysis, preview);

参考情報

CameraX について詳しくは、以下の参考情報をご確認ください。

Codelab

  • CameraX のスタートガイド
  • コードサンプル

  • CameraX のサンプルアプリ