[Android] Firebase MLKitでTwitterのQRコードが読めない問題に対処する


# TwitterのQRコードを認識してくれない・・・

こんにちは．
これは僕のTwitterアカウントのQRコードですが，Firebase MLkitを使ってスキャンしようとしたところうんともすんとも認識してくれず，つらみが深かったのでどうにか認識させたお話です．

この記事を通して，以下の画像からFirebase MLKitを使って僕をフォローできるようになるのが目標です．

<img width="40%" alt="Image from iOS (1).png" src="https://qiita-image-store.s3.amazonaws.com/0/181933/e7fe5eab-baeb-d0d9-fdeb-560d0eada295.png">

結論からいうと，

**コード部分が背景に比べて白い（淡い）QRコードをFirebase MLKitが認識しない**

そんな感じです．そこはよしなにやってほしい，Googleさん．
ですので，通常のQRコードを反転させたものも認識してくれません．
とはいえ，QRコードの規格ではコード本体の輝度が高い事はあまり推奨されていないようで，それに従っただけの設計なのかもしれませんね．

本記事では，

**あるQRコードを認識できなかった場合には，画像の色を反転させて再度検出にかける**

という荒技で対処します．
今回は１回に画像１枚の処理なので検出時間は気になりませんが，カメラをつかったリアルタイム処理にはあまり向かないかもしれません．もっとスマートな方法があれば教えてください．


# Firebase MLKitによるバーコードスキャン
AndroidでQRコードをスキャンするといえば，zxingやMobile Vision API，最近だとFirebase MLKitもバーコードスキャンの機能を持っています．ちなみにQRコードはバーコードの一種です．

せっかくだから流行りのFirebaseを使って読んでみたいと思ったのと，zxingと違いQRコード部の座標取得などが比較的簡潔に実装できるため使おうと思ったのが事の発端です．


まずは基本的な読み取り方から．基本的にすべてCodeLabに乗っています．
> [Detect Barcodes in an Image using Firebase MLKit](https://codelabs.developers.google.com/codelabs/mlkit-barcode-android/index.html?index=..%2F..index#0)

### 依存関係

```gradle:app/build.gradle
...
apply plugin: 'com.google.gms.google-services'

dependencies {
    ...
    implementation 'com.google.firebase:firebase-ml-vision:18.0.2'
}
```

### 検出

```kotlin:detect.kt
val options = FirebaseVisionBarcodeDetectorOptions.Builder()
            .setBarcodeFormats(
                FirebaseVisionBarcode.FORMAT_QR_CODE
            )
            .build()
val detector = FirebaseVision.getInstance().getVisionBarcodeDetector(options)

// bitmapが読み取りたい画像
val firebaseVisionImage = FirebaseVisionImage.fromBitmap(bitmap)

detector.detectInImage(firebaseVisionImage)
    .addOnSuccessListener { barcodes: List<FirebaseVisionBarcode> ->
        barcodes.forEach { barcode ->
            // 検出成功．煮るなり焼くなり．
            val bouｎdingBox = barcode.boundingBox // 座標をとったり
            val value = barcode.displayValue //　内容を読み出したりなど
        }
    }
    .addOnFailureListener {
        // do something
    }
```

たった2Stepで画像認識ができる良い時代です．

認識に成功すると`addOnSuccessListener`に登録した内容が呼ばれます．
**認識結果が0でも正常系と扱われるので，こっちに返ってきます．**

内部で何かしらの例外が発生したりした場合は，`.addOnFailureListener`に返ってきます．
肌感あんまり失敗はしませんが，渡したbitmapのカラーコードが`ARGB_8888`以外だと呼ばれたりします．
ちなみにiPhoneでスクショをとるとこのカラーコードがApple独自の`Display P3`とかになっていて死にます．ログで教えてくれないのでめちゃくちゃハマりました．そんな時はカラーコードを再定義しておきましょう．

```kotlin
val bitmapARGB8888 = bitmap.copy(Bitmap.Config.ARGB_8888, true)
```

# 認識しない
さて，先ほどのTwitterのQRコードが含まれた画像を渡すと，
`barcodes: List<FirebaseVisionBarcode>`
の中身が空の状態で返ってきます．

つまり，画像内にQRコードは無かったで？ということです．いや，あるんですがね．


# 認識させる
試行錯誤の結果，冒頭で述べた通りコード本体の輝度が高い場合に，MLKitの検出モデルは認識しないようです．カラーコードの件もそうですが，こういうサイレント仕様が多いですね．

では，無理やり反転させていきましょう．Androidにおける画像処理は，`ColorFilter`を使って行列変換を行うことで実現します．今回の処理程度ならOpenCVを入れるほどでは無いですね．

Bitmapクラスに反転画像を得る拡張関数を生やします．

```kotlin:BitmapExtensions.kt
fun Bitmap.negative(): Bitmap {
    val mat =
        floatArrayOf(
            -1.0f,  0.0f,  0.0f, 0.0f, 255f, // red
             0.0f, -1.0f,  0.0f, 0.0f, 255f, // green
             0.0f,  0.0f, -1.0f, 0.0f, 255f, // blue
             0.0f,  0.0f,  0.0f, 1.0f, 0.0f  // alpha
        )
    val paint = Paint().apply { colorFilter = ColorMatrixColorFilter(mat) }
    val bmp = Bitmap.createBitmap(this, 0, 0, this.width, this.height)
    val canvas = Canvas(bmp)
    canvas.drawBitmap(bmp, 0f, 0f, paint)

    return bmp
}
```

これで任意のBitmapオブジェクトに対して，

```kotlin
val negativeBitmap = bitmap.negative()
```

みたいな感じで呼び出すことができるようになりました．



# 検出部に適用する

めちゃくちゃネストが深くなるのでまず関数化しておきます．

```kotlin:detect.kt
fun detectOnNegativeImage(bitmap: Bitmap) {
    // 反転画像でFirebaseVisionImageオブジェクトを生成
    val firebaseVisionImage = FirebaseVisionImage.fromBitmap(bitmap.negative())

    detector.detectInImage(firebaseVisionImage)
        .addOnSuccessListener { barcodes: List<FirebaseVisionBarcode> ->
            // 反転画像で検出成功
            // do something
        }
        .addOnFailureListener {
            // 例外処理
        }
}
```

そして，元の処理部に組み合わせて呼び出します．

```kotlin:detect.kt
val firebaseVisionImage = FirebaseVisionImage.fromBitmap(bitmap)

detector.detectInImage(firebaseVisionImage)
    .addOnSuccessListener { barcodes: List<FirebaseVisionBarcode> ->
        if (barcodes.size == 0) {
            // 検出に失敗したので反転画像でトライ
            detectOnNegativeImage(bitmap)
        } else {
            // 元画像で検出成功
            // do something
        }
    }
    .addOnFailureListener {
        // 例外処理
    }
```


これで，無事`detectOnNegativeImage`内での`barcodes`の要素数が1以上になっているはずです．
これで無事Firebase MLKitを使って僕をフォローすることができるようになりました．

ただ，
**１枚の画像に通常のQRコードと，コード部が明るいコード双方が含まれている場合**
はこのコードだと動かないので，ご自分でmodifyしてくださいね．


# ちなみに反転後の画像は
こんな感じになってます．なんか毒々しいですね．
<img width="40%" alt="Image from iOS (1).png" src="https://qiita-image-store.s3.amazonaws.com/0/181933/e7fe5eab-baeb-d0d9-fdeb-560d0eada295.png"> <img width="40%" alt="Image from iOS (1).png" src="https://qiita-image-store.s3.amazonaws.com/0/181933/74be9a77-998a-3e5a-f63d-e9f9b9046cda.png">