Androidアプリ開発において、自分でカメラ機能を作成しようとした際に使用されるCameraX。プレビュー(見せる用)とは別に画像分析用の画像が取得できます。今回は、それを画面に表示する方法をOpenCVを用いて紹介します。
分析用画像表示の流れ
今回表示するのはいわゆる「プレビュー」ではございません。
CameraXが分析用に取得する画像を画面に表示します。
CameraXが分析用に取得する画像データはImageProxyという形式です。
どこで扱われるかというと、インターフェースImageAnalysis.Analyzerのanalyze(ImageProxy image)が毎フレーム呼ばれます。
そのImageProxyを最終的にはBitmapにして、UIのImageViewに渡せば表示できます。
つまり画像データ変換の流れとしては以下のようになります。
ImageProxy型(→image型) → Mat型 → Bitmap型 → GUIへ表示
上の順序で問題はないですが、実用上は画像処理をすると思うので、それを加味すると以下のような流れになります。
ImageProxy型(→image型) → Mat型 → 画像処理 → Mat型 → Bitmap型 → GUIへ表示
画像データの変換や処理をするのでOpenCVを使用します。
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を扱うときでした。
以下記事(中ほど)でも同現象の言及があります。
OpenCVを扱うためには、OpenCVManagerを使ってOpenCVを初期化する必要があり、その手順が抜けているとクラッシュするようです。
といわけで必要分(BaseLoaderCallback、onResume())を追記した全体像を以下に示します。
ソースコード(全体像)
ソースコード(GitHub)はこちら。XMLとKotlinのファイルのみです。
一応ここにも一部抜粋を記載しておきます。
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を組み合わせることで可能性が大きく広がると思います。
以下関連記事。
コメント