Androidプログラミング

【Android】CameraXの分析画像を表示する方法【OpenCV】

CameraXのAnalysis画像を表示する方法 Android

Androidアプリ開発において、自分でカメラ機能を作成しようとした際に使用されるCameraX。プレビュー(見せる用)とは別に画像分析用の画像が取得できます。今回は、それを画面に表示する方法をOpenCVを用いて紹介します。

スポンサーリンク

分析用画像表示の流れ

今回表示するのはいわゆる「プレビュー」ではございません。
CameraXが分析用に取得する画像を画面に表示します。

CameraXが分析用に取得する画像データはImageProxyという形式です。
どこで扱われるかというと、インターフェースImageAnalysis.Analyzeranalyze(ImageProxy image)が毎フレーム呼ばれます。

そのImageProxyを最終的にはBitmapにして、UIのImageViewに渡せば表示できます。

つまり画像データ変換の流れとしては以下のようになります。

ImageProxy型(→image型) → Mat型 → Bitmap型 → GUIへ表示

上の順序で問題はないですが、実用上は画像処理をすると思うので、それを加味すると以下のような流れになります。

ImageProxy型(→image型) → Mat型 → 画像処理 → Mat型 → Bitmap型 → GUIへ表示

画像データの変換や処理をするのでOpenCVを使用します。

スポンサーリンク

OpenCVの導入

以下記事が分かりやすいのでこちらを参考に導入してください。

Android StudioでOpenCVを使う - Qiita
はじめに AndroidStudioでOpenCVを導入する方法についてのメモです。 qiitaにもいくつか投稿がありますが、AndroidStudioのバージョンやOpenCVのバージョン等によって、 微妙に皆設定が違うような...
スポンサーリンク

ソースコード

実際に作成してみました。
(一番下にGitHubリンクあります)

CameraX関連

まずはカメラのセッティングをするsetUpCamera()。onCreate()などから呼びます。

変換後(画像処理後)のBitmap画像であるbmpImageを表示更新のクラス(UpdateUI)に渡しています。

CameraXで取得する画像形式は、「ImageAnalysis.OUTPUT_IMAGE_FORMAT_YUV_420_888」に指定しています。

private fun setUpCamera(){
    val cameraSelector = CameraSelector.Builder().requireLensFacing(CameraSelector.LENS_FACING_BACK).build()
    val cameraProviderFuture = ProcessCameraProvider.getInstance(this)

    cameraProviderFuture.addListener(Runnable {
        val cameraProvider: ProcessCameraProvider = cameraProviderFuture.get()

        // ◆Analysis
        imageAnalyzer = ImageAnalysis.Builder().setOutputImageFormat(ImageAnalysis.OUTPUT_IMAGE_FORMAT_YUV_420_888).build().also {
            it.setAnalyzer(cameraExecutor, MyAnalyzer{
                bmpImage ->

                // GUIの更新
                val postExecutor = UpdateUI(bmpImage)
                handler.post(postExecutor)

            })
        }

        var camera = cameraProvider.bindToLifecycle(customLifecycle, cameraSelector, imageAnalyzer)

    }, ContextCompat.getMainExecutor(this))
}

CameraXのImageAnalysis.Analyzer関連

次に画像を処理するAnalyzerについて。
fun analyse()にて画像データ(ImageProxy)をImageに変換後、Bitmap→Mat→(画像処理(今回は何もしない))→Bitmapと加工しています。

private inner class MyAnalyzer(
    private val listener: MyListener
) : ImageAnalysis.Analyzer {

    // 画像格納用変数
    lateinit var bmp_ori: Bitmap
    lateinit var mat_ori: Mat
    lateinit var mat_output: Mat
    lateinit var bmp_output: Bitmap

    // 画像変換用(Image -> Bitmap)
    fun Image.toBitmap():Bitmap{

        val yBuffer = planes[0].buffer // Y
        val uvBuffer = planes[2].buffer // UV

        val ySize = yBuffer.remaining()
        val uvSize = uvBuffer.remaining()

        val nv21 = ByteArray(ySize + uvSize )

        yBuffer.get(nv21, 0, ySize)
        uvBuffer.get(nv21, ySize, uvSize)

        val yuvImage = YuvImage(nv21, ImageFormat.NV21, this.width, this.height, null)
        val out = ByteArrayOutputStream()
        yuvImage.compressToJpeg(Rect(0, 0, yuvImage.width, yuvImage.height), 50, out)
        val imageBytes = out.toByteArray()
        return BitmapFactory.decodeByteArray(imageBytes, 0, imageBytes.size)
    }

    // 毎フレーム呼ばれる
    override fun analyze(imageProxy: ImageProxy) {

        // バッファの読み取り位置の固定
        imageProxy.planes[0].buffer.rewind()
        imageProxy.planes[1].buffer.rewind()
        imageProxy.planes[2].buffer.rewind()

        // ImageProxy を Image に変換
        val image = imageProxy.image

        // Image をBitmapに変換
        bmp_ori = image!!.toBitmap()

        // BitmapをMatに変換
        mat_ori = Mat()
        Utils.bitmapToMat(bmp_ori, mat_ori)

        // Mat画像の回転(スマホタテ向きに合わせる。必要に応じて各自変更)
        Core.rotate(mat_ori, mat_ori, ROTATION_0)

        //画像処理など(今回はただの複製)
        mat_output = mat_ori.clone()

        // MatをBitmapに変換
        bmp_output = Bitmap.createBitmap(mat_ori.width(), mat_ori.height(), Bitmap.Config.ARGB_8888)
        Utils.matToBitmap(mat_output, bmp_output)

        // UI更新のためにリスナにBitmapを渡す
        listener(bmp_output)

        imageProxy.close()
    }
}

「バッファの読み取り位置の固定」をしないと表示が安定せず、ノイズ交じりの緑っぽい画像が表示されます(私の端末の場合)。

GUIへの画像表示関連

次にUIに表示するクラス。
xml側ImageViewのidである「ivAnalysisImage」は適宜変更してください。

private inner class UpdateUI(bmpImage: Bitmap ):Runnable{
    val bmpImg = bmpImage
    override fun run() {
        binding.ivAnalysisImage.setImageBitmap(bmpImg)
    }
}

とりあえずこれで表示できる。。。と思っていました。

スポンサーリンク

エラー(java.lang.UnsatisfiedLinkError)への対処

上記でカメラを稼働させるとアプリが落ちてしまいました。
エラーメッセージは以下の通り。

java.lang.UnsatisfiedLinkError: No implementation found for long org.opencv.core.Mat.n_Mat() (tried Java_org_opencv_core_Mat_n_1Mat and Java_org_opencv_core_Mat_n_1Mat__)

どこでエラーが発生しているか調べたところ、Matを扱うときでした。
以下記事(中ほど)でも同現象の言及があります。

AndroidアプリにOpenCVを導入してみる - Qiita
はじめに AndroidStudioのデフォルトだけで完結するアプリ作りにも飽きてきたので、 OpenCVを使って画像をあーだこーだしてアプリの幅を広げてみる。 手始めにインストールして試しに使ってみる。 Androidアプリ...

OpenCVを扱うためには、OpenCVManagerを使ってOpenCVを初期化する必要があり、その手順が抜けているとクラッシュするようです。

といわけで必要分(BaseLoaderCallback、onResume())を追記した全体像を以下に示します。

スポンサーリンク

ソースコード(全体像)

ソースコード(GitHub)はこちら。XMLとKotlinのファイルのみです。

GitHub - shinshingit/CameraXSample
Contribute to shinshingit/CameraXSample development by creating an account on GitHub.

一応ここにも一部抜粋を記載しておきます。

package com.example.CameraX01Blog

import androidx.appcompat.app.AppCompatActivity
// (略)


typealias MyListener = (bmpImage: Bitmap) -> Unit

class MainActivity : AppCompatActivity() {

    
    // ★OpenCVを使用するために追加:CallBack
    private val mLoaderCallback: BaseLoaderCallback = object : BaseLoaderCallback(this) {
        override fun onManagerConnected(status: Int) {
            when (status) {
                SUCCESS -> {
                    Log.i("OpenCV", "OpenCV loaded successfully")
                    Log.d("openCV", OpenCVLoader.OPENCV_VERSION)
                }
                else -> {
                    super.onManagerConnected(status)
                }
            }
        }
    }


    override fun onCreate(savedInstanceState: Bundle?) {
            <!-- 略     setUpCamera() を呼び出してください -->
            <!-- パーミッション関連などもここに記載する -->
    }

    // ★OpenCVを使用するために追加
    override fun onResume() {
        super.onResume()
        if (!OpenCVLoader.initDebug()) {
            OpenCVLoader.initAsync(OpenCVLoader.OPENCV_VERSION_3_0_0, this, mLoaderCallback)
        } else {
            mLoaderCallback.onManagerConnected(LoaderCallbackInterface.SUCCESS)
        }
    }


    private fun setUpCamera(){
        val cameraSelector = CameraSelector.Builder().requireLensFacing(CameraSelector.LENS_FACING_BACK).build()
        val cameraProviderFuture = ProcessCameraProvider.getInstance(this)

        cameraProviderFuture.addListener(Runnable {
            val cameraProvider: ProcessCameraProvider = cameraProviderFuture.get()

            // ◆Analysis
            imageAnalyzer = ImageAnalysis.Builder().setOutputImageFormat(ImageAnalysis.OUTPUT_IMAGE_FORMAT_YUV_420_888).build().also {
                it.setAnalyzer(cameraExecutor, MyAnalyzer{
                    bmpImage ->

                    // GUIの更新
                    val postExecutor = UpdateUI(bmpImage)
                    handler.post(postExecutor)
                })
            }

            var camera = cameraProvider.bindToLifecycle(customLifecycle, cameraSelector, imageAnalyzer)

        }, ContextCompat.getMainExecutor(this))
    }


    private inner class MyAnalyzer(
        private val listener: MyListener
    ) : ImageAnalysis.Analyzer {

        // 画像格納用変数
        lateinit var bmp_ori: Bitmap
        lateinit var mat_ori: Mat
        lateinit var mat_output: Mat
        lateinit var bmp_output: Bitmap

        // 画像変換用(Image -> Bitmap)
        fun Image.toBitmap():Bitmap{

            val yBuffer = planes[0].buffer // Y
            val uvBuffer = planes[2].buffer // UV

            val ySize = yBuffer.remaining()
            val uvSize = uvBuffer.remaining()

            val nv21 = ByteArray(ySize + uvSize )

            yBuffer.get(nv21, 0, ySize)
            uvBuffer.get(nv21, ySize, uvSize)

            val yuvImage = YuvImage(nv21, ImageFormat.NV21, this.width, this.height, null)
            val out = ByteArrayOutputStream()
            yuvImage.compressToJpeg(Rect(0, 0, yuvImage.width, yuvImage.height), 50, out)
            val imageBytes = out.toByteArray()
            return BitmapFactory.decodeByteArray(imageBytes, 0, imageBytes.size)
        }

        // 毎フレーム呼ばれる
        override fun analyze(imageProxy: ImageProxy) {

            // バッファの読み取り位置の固定
            imageProxy.planes[0].buffer.rewind()
            imageProxy.planes[1].buffer.rewind()
            imageProxy.planes[2].buffer.rewind()

            // ImageProxy を Image に変換
            val image = imageProxy.image

            // Image をBitmapに変換
            bmp_ori = image!!.toBitmap()

            // BitmapをMatに変換
            mat_ori = Mat()
            Utils.bitmapToMat(bmp_ori, mat_ori)

            // Mat画像の回転(スマホタテ向きに合わせる。必要に応じて各自変更)
            Core.rotate(mat_ori, mat_ori, ROTATION_0)

            //画像処理など(今回はただの複製)
            mat_output = mat_ori.clone()

            // MatをBitmapに変換
            bmp_output = Bitmap.createBitmap(mat_ori.width(), mat_ori.height(), Bitmap.Config.ARGB_8888)
            Utils.matToBitmap(mat_output, bmp_output)

            // UI更新のためにリスナにBitmapを渡す
            listener(bmp_output)

            imageProxy.close()
        }
    }

    // UI更新用
    private inner class UpdateUI(bmpImage: Bitmap ):Runnable{
        val bmpImg = bmpImage
        override fun run() {
            binding.ivAnalysisImage.setImageBitmap(bmpImg)
        }
    }
}
スポンサーリンク

さいごに

CameraXのプレビューではなく、実際に分析している画像(自分で加工している画像)を見てみたいと思い取り掛かりましたが、困難が多かったです。

CameraXとOpenCV、さらに今回は触れてませんがAIを組み合わせることで可能性が大きく広がると思います。

以下関連記事。

コメント

タイトルとURLをコピーしました