問題と対策

AndroidのMLKitでCode39バーコードをスキャンすると、誤読が発生しやすい。
N回連続で同じ値を読み取れた場合に初めて採用することで、誤読を減らすことを考える。

実装

ImageAnalysis.Analyzerを実装したクラスに、連続検出のカウントロジックを追加する。

private class BarcodeAnalyzer(
    private val requiredConsecutiveCount: Int,
    private val onBarcodeDetected: (String) -> Unit
) : ImageAnalysis.Analyzer {
    private val scanner = BarcodeScanning.getClient()
    private var pendingValue = ""
    private var consecutiveCount = 0 // 直前に読み取った値と同じ値を連続して読み取った回数
    private var confirmedValue = ""

    @ExperimentalGetImage
    override fun analyze(imageProxy: ImageProxy) {
        val mediaImage = imageProxy.image ?: run {
            imageProxy.close()
            return
        }
        val inputImage = InputImage.fromMediaImage(mediaImage, imageProxy.imageInfo.rotationDegrees)
        scanner.process(inputImage)
            .addOnSuccessListener { barcodes ->
                val value = barcodes.firstOrNull()?.rawValue
                if (value != null) {
                    if (value == pendingValue) {
                        consecutiveCount++
                    } else {
                        pendingValue = value
                        consecutiveCount = 1
                    }
                    // N回連続で同じ値を読み取れた場合に採用する
                    if (consecutiveCount >= requiredConsecutiveCount && value != confirmedValue) {
                        confirmedValue = value
                        onBarcodeDetected(value)
                    }
                } else {
                    pendingValue = ""
                    consecutiveCount = 0
                }
            }
            .addOnCompleteListener {
                imageProxy.close()
            }
    }
}

ポイントは3つの状態変数である。

  • pendingValue: 直前に読み取った値
  • consecutiveCount: 同じ値を連続して読み取った回数
  • confirmedValue: 採用済みの値(同じ値を重複してコールバックしないために保持)

バーコードが読み取れなかったフレームでは pendingValueconsecutiveCount をリセットする。読み取れた値が変わった場合も同様にカウントをリセットして、新しい値のカウントを1から始める。

confirmedValue と比較して、同一バーコードを連続スキャンしても重複してコールバックが呼ばれるのを防ぐ。

使い方

ImageAnalysisにアナライザーをセットする際にrequiredConsecutiveCountを指定する。

val imageAnalysis = ImageAnalysis.Builder()
    .setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
    .build()
    .apply {
        setAnalyzer(executor, BarcodeAnalyzer(
            requiredConsecutiveCount = 3,
            onBarcodeDetected = { value -> /* 採用された値を処理 */ }
        ))
    }

requiredConsecutiveCount = 3 の場合、同じ値を3フレーム連続で読み取ったときにコールバックが呼ばれる。値を大きくするほど誤読は減るが、読み取りに時間がかかるようになる。
精度と性能のトレードオフとなる。

1文字のバーコードについて

この方法でも、読み取り値が1文字の場合は精度が改善しにくい。1文字のバーコードはエンコードされる情報量が極端に少なく、誤認識に対する冗長性の低さが原因と考えられる。

ただし、Code39で1文字のバーコードは実用上ほぼ存在しない。
例えば、誤読による1文字の値が問題になる場合は、文字数でフィルタリングするとよい。

if (consecutiveCount >= requiredConsecutiveCount && value != confirmedValue) {
    if (value.length <= 1) return  // 1文字以下は無視
    confirmedValue = value
    onBarcodeDetected(value)
}