2
1

More than 1 year has passed since last update.

Android CameraX ImageAnalyzerをまとめる

Posted at

CameraXについてざっくり概要

呼び出し

val cameraProviderFuture = ProcessCameraProvider.getInstance(this)
cameraProviderFuture.let { cameraFuture ->
    cameraFuture.addListener(
        {
            /*
            Set below instances
            - selector
            - use case 
             */
            // preview imageCapture imageAnalyzer ...
            try {
                // Unbind use cases before rebinding
                cameraProvider.unbindAll()

                // Bind use cases to camera
                cameraProvider.bindToLifecycle(
                    this, // lifecycleOwner
                    cameraSelector,  // selector
                    preview, imageCapture, imageAnalyzer // use case
                )

            } catch(exc: Exception) {
                Log.e(TAG, "Use case binding failed", exc)
            }
        },
        ContextCompat.getMainExecutor(this) // Executor
    )

エントリーポイント

cameraProvider.bindToLifecycle(LifecycleOwner,CameraSelector,UseCase...)
  • LifecycleOwner : ライフサイクルオーナー 説明略

  • CameraSelector: フロントかバックカメラかを選択

  • UseCase: 以下の機能群、カメラを使って何をしたいかを指定するもの

    • Image Capture — 画像保存
    • Video Capture — 動画保存
    • Preview — 画像をディスプレイに表示
    • Image Analysis — カメラアルゴリズムにシームレスにアクセスできる

Image、Video, Previewは、概念としても難しくなく、ほぼほぼ以下の設定方法をしっておけば困らない気がする

// for binding camera display to layout
val preview = Preview.Builder().build()
    .also { it.setSurfaceProvider(viewBinding.viewFinder.surfaceProvider) }

// Which camera to use on the device
val cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA

// For taking a picture
imageCapture = ImageCapture.Builder().build()

Image Analysis はよくわからないので探索していく

Image Analysis

ImageAnalysisとは一般的に、デジタルデータとして取得した画像から特徴を取り出し、意味づけをすること。

Android では 以下説明

CPU からアクセスできる画像をアプリに提供し、それらの画像に対して画像処理、コンピュータ ビジョン、機械学習推論を実行します。

カメラからImageProxyというデータ構造を経由して、画像情報を取ってこれます。

すごくわかりやすい例として、カメラに映った以下情報を取得できたりします。

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

シンプルなやり方

エントリポイント

// Analyzerの作成
private class HeightAnalyzer(private val callback: (Int) -> Unit) : ImageAnalysis.Analyzer {
    override fun analyze(image: ImageProxy) {
        callback(image.height) // 単純にHeightを返す
        image.close()
    }
}

// ...Camera Provider Listener 
// ... set preview

// analyzer(userCase)の準備
val imageAnalyzer = ImageAnalysis.Builder()
    .build()
    .also {
        it.setAnalyzer(cameraExecutor, HeightAnalyzer { height ->
            Log.d(TAG, "ImageHeight: $height")
        })
    }
cameraProvider.bindToLifecycle(
    this, // lifecycleOwner
    cameraSelector,  // selector
    preview, imageAnalyzer // use case
)

アナライザ経由でカメラImageのHeightが取れる。

解析の深掘り

ImageProxyを経由するのでこれを深掘りしていく

  • close
    close処理。analyzer作るなら最後に呼ぶ必要あり、AutoCloseableを継承しているのでuseが使える
    /**
     * Closes the underlying {@link android.media.Image}.
     *
     * @see android.media.Image#close()
     */
    @Override
    void close();

// 自動で閉じてくれるように以下つかっておくのがよい
imageProxy.use { /* any process */ }
  • int getFormat();
    ユースケース作成時に指定した、画像の色情報を表現するためのフォーマット(デフォルト:YUV_420_888)を取得する。

このフォーマットは、画像を表現するのに必要なByteBufferの数と、各ByteBuffer内のピクセルデータの一般的なレイアウトを決定する(※詳細は後述。Planeの理解でなんとなく概念が掴める)

対応フォーマットはここ
ユースケース設定時↓

val imageAnalyzer = ImageAnalysis.Builder()
    .setOutputImageFormat(ImageAnalysis.OUTPUT_IMAGE_FORMAT_YUV_420_888)
    .build()
...
  • int getHeight();
  • int getWidth();
    いずれもPixelにて取得されるが、previewViewのサイズとはズレる。TODO ズレる理由が掴めてない。いずれもPixelだが、Imageとして取得すると小さくなる。
val imageAnalyzer = ImageAnalysis.Builder().build()
    .also {
        it.setAnalyzer(cameraExecutor, AnyAnalyzer { format, height, width ->
            Log.d("AnalyzerTag", "image height: $height")
            Log.d("AnalyzerTag", "layout viewFinder height : ${viewBinding.viewFinder.height}")
            Log.d("AnalyzerTag", "image widht: $width")
            Log.d("AnalyzerTag", "layout viewFinder height : ${viewBinding.viewFinder.width}")
        })
}

// log 
image height: 480
layout viewFinder height : 2756
image width: 640
layout viewFinder widht : 1440
  • Rect getCropRect();
  • void setCropRect(@Nullable Rect rect);

画像を切り抜くときに使用する? getImageの時にどう変わるかTODO 何に使うか

...
it.setAnalyzer(cameraExecutor, AnyAnalyzer { format, height, width, cropRect ->
            Log.d("AnalyzerTag", "cropRect: $cropRect")
        })
...

log 
cropRect: Rect(0, 0 - 0, 0)
  • PlaneProxy[] getPlanes();
    画像の各Planeを取得する。(Planeは後述)
    PlaneProxyを配列で取得。

モードによって取得するPlaneProxyのサイズが変わる。

val imageAnalyzer = ImageAnalysis.Builder()
    .setOutputImageFormat(OUTPUT_IMAGE_FORMAT_YUV_420_888)
    .build()
    .also {
        it.setAnalyzer(cameraExecutor, AnyAnalyzer { format, height, width, cropRect, planes ->
            Log.d("AnalyzerTag", "planes: ${planes.size}")
            Log.d("AnalyzerTag", "buffer: ${planes[0].buffer}")
            Log.d("AnalyzerTag", "buffer: ${planes[1].buffer}")
            Log.d("AnalyzerTag", "buffer: ${planes[2].buffer}")
        })
}
logs
planes: 3
buffer: java.nio.DirectByteBuffer[pos=0 lim=307200 cap=307200] // 「輝度(Y)」?
buffer: java.nio.DirectByteBuffer[pos=0 lim=76800 cap=76800] //「色差U(Cb)」?
buffer: java.nio.DirectByteBuffer[pos=0 lim=76800 cap=76800] // 「色差V(Cr)」?


val imageAnalyzer = ImageAnalysis.Builder()
    .setOutputImageFormat(OUTPUT_IMAGE_FORMAT_RGBA_8888)
    .build()
    .also {
        it.setAnalyzer(cameraExecutor, AnyAnalyzer { format, height, width, cropRect, planes ->
            Log.d("AnalyzerTag", "planes: ${planes.size}")
            Log.d("AnalyzerTag", "buffer: ${planes[0].buffer}")
            Log.d("AnalyzerTag", "pixelStride: ${planes[0].pixelStride}")
            Log.d("AnalyzerTag", "rowStride: ${planes[0].rowStride}")
        })
}

logs 
planes: 1
buffer: java.nio.DirectByteBuffer[pos=0 lim=1228800 cap=1228800]
pixelStride: 4
rowStride: 2560

画像解析を知らないと見慣れない用語ばかり

Stride: データ構造やアルゴリズムにおいて、次のデータ項目までの間隔や距離を表す数値

例えば、画像処理において畳み込み演算を行う場合、ストライドは畳み込み演算が行われる際のフィルタの移動量を示します。ストライドの値が1であれば、フィルタは隣接するピクセルに対して移動し、ストライドの値が2であれば、フィルタは2つ飛ばしでピクセルを移動することになります。

Plane : 輝度または色差など、画像の単一の側面を表す2次元の画像データ配列を指す

画像処理では、画像はしばしば複数の画像プレーンまたはチャンネルの集合として表現され、それぞれが画像の異なる側面を表す。

チャンネル:画像処理や音声処理などのデジタル信号処理において、情報を伝達するための個々のデータストリームを指す

画像処理において、一般的には3つのチャンネルが使用されます。これらは、RGBチャンネルと呼ばれます。RGBとは、赤(Red)、緑(Green)、青(Blue)の頭文字をとったもので、各チャンネルは赤、緑、青の成分を表します。各成分は、0から255の範囲の値を取り、画像の色を表現します。

○YUV_420_888について
「輝度(Y)」、「色差U(Cb)」、「色差V(Cr)」の3つのPlaneを特定の指定値を用いて画像を表現するフォーマット

YUV 420 888は、YUVという色空間を使用しており、画像を「輝度(Y)」、「色差U(Cb)」、「色差V(Cr)」の3つの要素に分割しています。YUV 420 888の数字は、この3つの要素をどのように表現するかを示しています。
「4:2:0」は、Y、U、Vの各成分をそれぞれ、水平方向に4つのサンプルのうち1つだけ、垂直方向に2つのサンプルのうち1つだけをサンプリングすることを意味しています。つまり、画像のサイズを1/2にするために、Y成分はフル解像度でサンプリングされ、UとVの成分はそれぞれ2つのピクセルにつき1つずつサンプリングされます。
888は、各成分を8ビットで表現することを示しています。つまり、Y、U、Vの各成分が0から255の範囲の値を取ります。
YUV 420 888は、主にビデオストリーミングや画像圧縮などのアプリケーションで使用されます。その理由は、YUV 420 888が情報量を削減し、同時に高品質の画像を保持するための最適なバランスを提供するためです。また、YUV 420 888は、色情報を分離して処理することができ、色を変更する、彩度を変更する、または画像の明るさを調整するなどの様々な画像処理アルゴリズムで使用されることができます。

ざっくりまとめると

PlaneProxyの各Planeは画像の一側面を表すデータであり、その数は画像フォーマットによって規格化されている。一側面をつなぎ合わせて画像を表現しているといったイメージ
デフォルトのYUV_420_888では「輝度(Y)」、「色差U(Cb)」、「色差V(Cr)」という3つの側面で画像を表現している。
Plane自体は、bufferやStrideといったより低次元なデータ構造によってPlaneを表現している。

  • ImageInfo getImageInfo();

Metadata for an image. とのこと。

...
it.setAnalyzer(cameraExecutor, AnyAnalyzer { format, height, width, cropRect, planes, imageInfo ->
            Log.d("AnalyzerTag", "imageInfo timestamp: ${imageInfo.timestamp}")
            Log.d("AnalyzerTag", "imageInfo rotationDegrees: ${imageInfo.rotationDegrees}")

        })
...
logs
imageInfo timestamp: 7468017665211
imageInfo rotationDegrees: 90
  • Image getImage();

android Image が返ってきます。
ImageProxyはImageのラッパーなので、Proxyで公開してないより低次元なデータの取得に使用する。

2
1
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
2
1