CameraXとML KitでQRコード・バーコードリーダーを作る

何番煎じか分かりませんが、CameraXとML KitでQRコード・バーコードリーダーを作ったので、作り方を説明します。

ソースコードはこちら（MITライセンス）

https://github.com/ohmae/code-reader

プレイストアでも公開しています。

https://play.google.com/store/apps/details?id=net.mm2d.codereader

公開するアプリとしてきちんと作るにはいろいろ面倒なことをやらないといけないですが、QRコード・バーコードリーダーの機能の最低限のところを実現するところに絞って説明します。

# 必要なライブラリ

CameraXとML Kitを使う上で追加で必要になるのは以下のライブラリ群です。

```build.gradle.kts
implementation("androidx.camera:camera-camera2:1.3.1")
implementation("androidx.camera:camera-lifecycle:1.3.1")
implementation("androidx.camera:camera-view:1.3.1")
implementation("com.google.mlkit:barcode-scanning:17.2.0")
```

# AndroidManifestの記述

当然ながらカメラというハードウェアが必要であり、これにアクセスするにはパーミッションが必要です。

```AndroidManifest.xml
<uses-feature android:name="android.hardware.camera.any" />
<uses-permission android:name="android.permission.CAMERA" />
```

# パーミッションの獲得をする

カメラパーミッションはランタイムパーミッションなのでその処理が必要です。
ランタイムパーミッションの取得については本題から外れますので割愛。

# カメラの映像をViewに映す

カメラの映像を映すにはPreviewViewを使うと簡単にできます。

```xml:activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    >

    <androidx.camera.view.PreviewView
        android:id="@+id/preview_view"
        android:layout_width="0dp"
        android:layout_height="0dp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        />

</androidx.constraintlayout.widget.ConstraintLayout>
```

CameraXを使ってCameraを使用するには、`ProcessCameraProvider`を使用します。
`ProcessCameraProvider.getInstance(this)`の戻り値は`ListenableFuture`になっており、非同期でインスタンスが使えるようになります。

```MainActivity.kt
    private fun start() {
        val future = ProcessCameraProvider.getInstance(activity)
        future.addListener({
            setUp(future.get())
        }, ContextCompat.getMainExecutor(activity))
    }

    private fun setUp(provider: ProcessCameraProvider) {
        val resolutionSelector = ResolutionSelector.Builder()
            .setAspectRatioStrategy(AspectRatioStrategy.RATIO_16_9_FALLBACK_AUTO_STRATEGY)
            .build()

        val preview = Preview.Builder()
            .setResolutionSelector(resolutionSelector)
            .build()
        preview.setSurfaceProvider(previewView.surfaceProvider)

        runCatching {
            provider.unbindAll()
            provider.bindToLifecycle(
                this, CameraSelector.DEFAULT_BACK_CAMERA, preview, analysis
            )
        }
    }
```

`PreviewView`は`Preview`のインスタンスにsurfaceProviderを渡し、`ProcessCameraProvider`にbindします。
これだけで、カメラの映像がPreviewViewに表示されるようになります。

# カメラの映像を解析し、QRコード・バーコードを検出する

CameraXでは`ProcessCameraProvider`にPreviewと同様にAnalyzerをbindすることができ、映像の解析処理を実装することができます。ここでQRコード・バーコードの検出処理を実装します。

QRコード・バーコードの検出処理もML Kitを使うと簡単で、`BarcodeScanner`に`InputImage`を渡すだけです。

Analizerを以下のように実装しました。

```kotlin:CodeAnalizer.kt
class CodeAnalyzer(
    private val scanner: BarcodeScanner,
    private val callback: (List<Barcode>) -> Unit
) : Analyzer {
    @SuppressLint("UnsafeOptInUsageError")
    override fun analyze(imageProxy: ImageProxy) {
        val image = imageProxy.image
        if (image == null) {
            imageProxy.close()
            return
        }
        val inputImage = InputImage.fromMediaImage(image, imageProxy.imageInfo.rotationDegrees)
        scanner.process(inputImage)
            .addOnSuccessListener { callback(it) }
            .addOnCompleteListener { imageProxy.close() }
    }
}
```

`Analyzer`の`analyze`メソッドに渡されるのは`ImageProxy`で、そこから`InputImage`への変換がちょっと面倒です。
`ImageProxy`に`getImage`メソッドがあるので、これでImageを取得すれば良いのですが、このメソッドは`@ExperimentalGetImage`がついているので、`@SuppressLint("UnsafeOptInUsageError")`をつけて使います。
また、`ImageProxy`は`analyze`の処理が終われば、`close`をコールする必要がありますが、`BarcodeScanner`の`process`は非同期に処理されるため、そのまま`imageProxy.close()`をコールしてしまうと、処理中にInputImageが閉じてしまって解析に失敗します。`OnCompleteListener`で処理が完了してから`close`をコールしましょう。

検出できたらコールバックで通知します。

この`CodeAnalyzer`を`ProcessCameraProvider`にbindすれば良いのですが、この辺の処理を別クラスに切り出しておきます。
Analyzerは実行するExecutorも指定する必要があり、そのライフサイクル管理も兼ねています。

```kotlin:CodeScanner.kt
class CodeScanner(
    private val activity: ComponentActivity,
    private val previewView: PreviewView,
    callback: (List<Barcode>) -> Unit
) {
    private val workerExecutor: ExecutorService = Executors.newSingleThreadExecutor()
    private val scanner: BarcodeScanner = BarcodeScanning.getClient()
    private val analyzer: CodeAnalyzer = CodeAnalyzer(scanner, callback)

    init {
        activity.lifecycle.addObserver(
            LifecycleEventObserver { _, event ->
                if (event == ON_DESTROY) {
                    workerExecutor.shutdown()
                    scanner.close()
                }
            }
        )
    }

    fun start() {
        val future = ProcessCameraProvider.getInstance(activity)
        future.addListener({
            setUp(future.get())
        }, ContextCompat.getMainExecutor(activity))
    }

    private fun setUp(provider: ProcessCameraProvider) {
        val resolutionSelector = ResolutionSelector.Builder()
            .setAspectRatioStrategy(AspectRatioStrategy.RATIO_16_9_FALLBACK_AUTO_STRATEGY)
            .build()

        val preview = Preview.Builder()
            .setResolutionSelector(resolutionSelector)
            .build()
        preview.setSurfaceProvider(previewView.surfaceProvider)

        val analysis = ImageAnalysis.Builder()
            .setResolutionSelector(resolutionSelector)
            .setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
            .build()
        analysis.setAnalyzer(workerExecutor, analyzer)

        runCatching {
            provider.unbindAll()
            provider.bindToLifecycle(
                activity, CameraSelector.DEFAULT_BACK_CAMERA, preview, analysis
            )
        }
    }
}
```

追加部分はここですね、コードリーダーとしては全フレーム処理をしたりする必要も無いため、`BackpressureStrategy`として`ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST`を指定しています。まあ、処理が滞ったら途中のは捨てて最新のフレームだけ処理するってことですね。

```kotlin
val analysis = ImageAnalysis.Builder()
    .setResolutionSelector(resolutionSelector)
    .setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
    .build()
analysis.setAnalyzer(workerExecutor, analyzer)
```

最後に、これをActivityから呼び出せば完了です。

```kotlin:MainActivity.kt
class MainActivity : AppCompatActivity() {
    private lateinit var binding: ActivityMainBinding
    private lateinit var codeScanner: CodeScanner
    private val launcher = registerForActivityResult(
        CameraPermission.RequestContract(), ::onPermissionResult
    )

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(binding.root)
        codeScanner = CodeScanner(this, binding.previewView, ::onDetectCode)
        if (CameraPermission.hasPermission(this)) {
            start()
        } else {
            launcher.launch(Unit)
        }
    }

    private fun onPermissionResult(granted: Boolean) {
        if (granted) {
            start()
        } else {
            finish()
        }
    }

    private fun start() {
        codeScanner.start()
    }

    private fun onDetectCode(codes: List<Barcode>) {
        codes.forEach {
            Toast.makeText(this, it.rawValue, Toast.LENGTH_LONG).show()
        }
    }
}
```

検出結果は横着してToastしているだけなので、この部分は適宜カスタマイズしていただければ。

ということで、CameraXとML Kitを使うとかなり簡単にコードリーダーアプリが作れました。
凝ったことをしようとするとまだ面倒ですが・・・

なお、ML Kitはオフラインでも使え、入力データを送信することはないそうですが、データ収集などのためクライアントの情報を送信する可能性があります。公開するアプリで使用する場合は、これらをユーザーに告知する必要があるのでご注意を。

https://developers.google.com/ml-kit/terms

以上です。