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を組み合わせることで可能性が大きく広がると思います。
以下関連記事。





コメント